Special Effects

In the early years of 2D gaming, titles were simple sprite renderers with little or no visual enhancements. These were the years of very restrictive hardware, where every call to PutPixel was counted in clock cycles. As years passed and hardware evolved, programmers began implementing special effects (sometimes referred to as eye candy), so games looked better than the competition. In this section, I will provide a list of common effects that have become classic through the years. They are all very easy to code and will greatly enhance any 2D game.

Palette Effects

Most graphics displays from the old days were palletized, offering between 16 and 256 colors. Those systems didn't have very good access time to the frame buffer (or did not have a frame buffer), so sometimes effects had to be coded without directly accessing the screen memory. The most popular of those effects were implemented by manipulating, not the screen itself, but the hardware color palette that controlled how colors were displayed onscreen. A palette was usually composed of 256 entries; each consisting of three bytes (red, green, and blue). So altering the palette was much faster than having to write to the frame buffer.

A good example of this family of effects can be found in the popular fade-in and fade-out routines present in many games. To ease the transitions, these games progressively illuminated (or switched off) the screen, so colors appeared smoothly. This would be a really simple effect in a true-color game. Take a look at the following fade-out routine, which works on a fictitious true-color screen. Each pixel represents an RGB triplet in byte format, thus representing the full 24-bit spectrum:

 for (ix=0;ix<SCREENX;ix++)       {       for (iy=0;iy<SCREENX;iy++)             {             color col=screenarray[ix][iy];             if (col.r>0) col.r--;             if (col.g>0) col.g--;             if (col.b>0) col.b--;             screenarray[ix][iy]=col;             }       } 

Notice that this loop is computationally expensive. It depends on the screen resolution; and even on a low-resolution setting like 320x200 pixels, it would take 64,000 loop iterations to achieve the effect. Now, assume the screen is palletized, and each pixel is represented by a single byte that points to a hardware palette. Again, the palette will be composed of RGB byte triplets. Then, we could save some precious clock cycles by using an alternative loop such as this one:

 for (ip=0;ip<NUMCOLORS;ip++)       {       color col=palette[ip];       if (col.r>0) col.r--;       if (col.g>0) col.g--;       if (col.b>0) col.b--;       palette[ip]=col;       } 

This loop is much faster and does not depend on the screen resolution. Because the palette is implemented in hardware, changing it will immediately affect all the pixels on screen regardless of how many pixels are affected. Here is the full source code of a fade-out routine written in C++, using the PC-VGA graphics architecture as a starting point:

 void GetPaletteEntry(unsigned char id, unsigned char &r, unsigned char &g, unsigned char  graphics/ccc.gif&b) { outp(0x03C7,id); r=inp(0x03C9); g=inp(0x03C9); b=inp(0x03C9); } void SetPaletteEntry(unsigned char id, unsigned char r, unsigned char g, unsigned char b) { outp(0x03C8,id); inp(0x03C9,r); inp(0x03C9,g); inp(0x03C9,b); } void FadeOut() { unsigned char r,g,b; for (int isteps=0;isteps<64;isteps++)       {       WaitVSync();       for (int ipal=0;ipal<256;ipal++)             {             GetPaletteEntry(ipal,r,g,b);             if (r>0) r--;             if (g>0) g--;             if (b>0) b--;             SetPaletteEntry(ipal,r,g,b);             }       } } 

Notice how the VGA frame buffer was accessed by reading and writing to specific ports. Port 0x3C7 was used to request a palette read, and port 0x3C8 was used for palette writes. These routines, which I have presented here in C++, were usually hard-coded in assembly language for improved performance. In addition, I call the routine WaitVSync to make sure we change palette colors only when the screen is not being accessed by the electron beam. Failing to do so would result in flicker. Finally, the outer loop runs for 64 iterations because 64 was the maximum value of R, G, and B values in the VGA palette.

A fade-in routine could be implemented in a similar way. We only need a persistent palette in main memory, and the routine will increment the hardware palette progressively until it matches the persistent version. Here is the full source code:

 void FadeIn() { unsigned char r,g,b; for (int isteps=0;isteps<64;isteps++)       {       WaitVSync();       for (int ipal=0;ipal<256;ipal++)             {             GetPaletteEntry(ipal,r,g,b);             if (r<palette[ipal].r) r++;             if (g<palette[ipal].g) g++;             if (b<palette[ipal].b) b++;             SetPaletteEntry(ipal,r,g,b);             }       } } 

We have seen how some clever palette handling can implement fade-ins and fade-outs. But there is much more we can achieve using palettes. Now, I will focus on one of the most popular effects used in countless games, technology demos, and so on in the 1980s. The effect is generally called a palette rotation and can be used in many scenarios, from drawing flowing water to creating animated textures.

The trick is to realize that, if we change some palette entries, we can produce color changes in sprites that look like real animation. I will begin with a simple palette effect and afterward show a general-case palette rotation trick. Imagine that we want to produce a semaphore with a red and green light. In order to render the right color, depending on the state of the semaphore, we could simply paint a colored sprite on top of the semaphore graphic, which represents either a green walking character or a red stop sign. But we can achieve the same effect with only one sprite and some clever palette tricks. To better understand this technique, take a look at Figure 11.4. Here, we reserve four palette entries for the semaphore sprite. The first color (labeled 1) will be used for the semaphore and will be a yellow hue. The second (2) will be the background color for the semaphore, generally black. The third color will be used to render the green walking character, and the fourth color will be used for the red stop sign. Instead of repainting the screen with sprites, we only paint the semaphore once, and then manipulate palette entries 3 and 4 to modify the semaphore. To make the semaphore appear in the green state, we put green as the third palette color and leave the fourth color black. Inversely, turning the traffic light red only requires putting red as the fourth color, leaving the third one black.

Figure 11.4. Traffic light.

graphics/11fig04.gif

We have saved precious clock cycles, because we only need to render the semaphore light. But there is a downside to all this. We are manipulating palette entries 3 and 4, which will need to be reserved for this traffic light. If we have two semaphores, we will need more palette entries, or the semaphores will be in sync because they will share the palette.

Now, let's extend this core idea to a more complex situation. Let's imagine that we want to paint a river with animated water. I am not thinking about real-time, reflective water. All we need is some animated water such as the kind found in many 8-bit games. We will use the same technique as we did for the semaphore, but extend it a bit. For example, let's reserve six palette entries, which we will use to store different hues of water color. Initially, the entry labeled 1 will contain very deep blue, and entry number 6 will hold a light blue color. Then, if we rotate the colors in these six palette entries, all sprites using those colors will keep on changing. If we draw them carefully, water animation will seem to appear. It is all a matter of artistic talent: being able to imagine how a sprite will evolve as we rotate its colors. In Figure 11.5, you can see a larger-than-life version of the sprite I will be using. You can also see a waterfall as rendered using this technique. Although these effects look simplistic in today's cutting edge graphics cards, some of these concepts might prove useful even today, because they provide a simple, cheap way of cheating the eye and conveying a sense of motion.

Figure 11.5. Water sprite.

graphics/11fig05.gif

Palette rotation tricks were used for water, lava, fire, neon glows, and countless other tricks. Once you master the core technique, it is really easy to adapt it to many situations. Surely, the next time you play an old-school game you will discover many of its incarnations.

Stippling Effects

Another interesting technique you can use in 2D games is stippling. A stipple is just a simple patterned texture that combines one color (generally black or grey) with the transparency index. This way, rendering a stipple on top of another sprite covers it in part, creating a rather convincing illusion of shadow. All you have to do is

  1. Render the background

  2. Using the transparency index, render the stipple

  3. Render the character

If you follow these simple steps, the shadow of the character will effectively look as if it is blended with the background: Some areas will look darker than others, and so on. Stippling looks especially good in high resolutions. The pattern is almost indistinguishable, and the blending becomes smoother.

There are many other uses for stippling. For example, you can implement varying-density fog by using several stippling patterns (with different densities). To achieve this, you will need to reverse the rendering order, so you actually do the following:

  1. Render the background.

  2. Render the character.

  3. Using the transparency index, render the stipple.

This way the character will also be immersed in the fog.

Another interesting use of stippling is to illuminate parts of the scene, for example, the trail of an explosion or a torch in the ground. Here, the stippling pattern must be colored as the light source (yellow, orange). By painting the stipple, the background will seem to illuminate, with more light at the center of the stippling pattern.

In addition, stippling has been broadly used in fog-of-war techniques. These techniques are used in real-time strategy games, where only the part of the map where the player has actually explored is shown, and the rest is covered in fog the closer the area, the less dense the fog. Stippling patterns have been used in games such as Warcraft and Age of Empires for the PC to implement fog-of-war. As a summary, you can see a variety of stippling effects in Figure 11.6.

Figure 11.6. Stipples.

graphics/11fig06.gif

Glenzing

Stippling is nothing but a poor man's transparency. But we can do more to simulate transparency effects, whether it's a shadow, light, or fog-of-war. The technique we will be using is called glenzing, and it allows us to really mix colors as if we were painting a partially transparent object. In the end, it all boils down to some palette handling tricks, but the results will be quite convincing, usually better than those achieved by simple stippling.

Basically, glenzing converts a color interpolation, such as the ones required to compute semitransparent objects, into a palette value interpolation. If you wanted to paint a semitransparent object on top of another, opaque object, you would mix both objects' colors according to an opacity index. The resulting color would be

 Color=Color_transparent*opacity + Color_opaque*(1-opacity) 

Now, we simply cannot do that in a palletized world. But we can do something similar. Imagine that we lay out our palette so that for each semitransparent object, we have some extra palette positions that are reserved for transparency effects. If the transparent object is blue, and the background is red, we will make sure we have a position with some sort of magenta to be used when both objects collide.

Several glenzing methods exist. The most popular substitutes the sprite or triangle-rendering core with one that looks up the current frame buffer value and increments it by the new fragment's palette value. The code would be

 set the palette paint the base object paint the semi-transparent object      for each pixel, read the frame-buffer value      add the second object's palette value      write the pixel back 

Then, we must make sure the palette is cleverly laid out to guarantee that proper transparency colors are generated.

Fire

Fire can be simulated using a variety of techniques. It can be an animated sprite painted by hand, so it loops properly. Or, it can use some kind of 2D particle system (see Chapter 19, "Particle Systems," for more on particle systems). But in the 2D era, the word "fire" didn't really refer to the previous options. Fire was extensively used in menus, special effects, and so on using a cellular automata on the frame buffer. As you might already know, a cellular automata is an automata consisting of a number of cells running in parallel whose behavior is governed by neighboring cells. They have been used to simulate life, and in the '80s, they were the best way to create fire.

Essentially, the effect consists of defining an area that will work as the fire emitter. This could be the bottom of the screen, some text, a drawing, and so on. Then, each frame pixel belonging to the emitter is initialized to the pure white fire color. This made each pixel in the emitter glow and flicker. All that was needed were some flames, which is what the cellular automata did. Let's consider each pixel as a cell in our automata, continuously executing the following algorithm (assume we are working on pixel (x,y)):

 color(x,y) = (color (x,y+1) + color (x+1,y+1) + color (x-1,y+1))/3 

This rather minimal routine simply computes a pixel's new color as the average of the colors of the three pixels below it. This makes fire rise above the emitter, and because the emitter's colors are somewhat random, the averages inherit this randomness and produce a somewhat chaotic living effect.

Fire was usually mapped on palletized modes. The palette was occupied entirely or in part with a gradient of colors suitable for fire, from whites to reds and oranges. Thus, averaging colors was really a process of averaging color indices. Here is a full example of a fire effect that is emitted by the bottom of the screen (y=SCREENY-1) and climbs vertically. I assume a 256-color palette, with the first entry being used for pure white colors, which fade to yellow, orange, red, and finally black as we traverse the palette.

 // generate new sparks for (int i=0;i<SCREENX/2;i++)          {          int x=rand()%SCREENX;          int col=rand()%25;          PutPixel(x,SCREENY-1,col);          } // recompute fire for (int ix=0;ix<SCREENX;ix++)          {          for (int iy=0;iy<SCREENY;iy++)                   {                   unsigned char col=(GetPixel(ix-1,iy+1) + GetPixel(ix,iy+1) + GetPixel( graphics/ccc.gifix+1,iy+1)) / 3;                   PutPixel (ix,iy,col);                   }          } 

As you might have guessed, fire was an expensive effect, because it needed the whole screen to be recalculated at each frame. Sometimes the effect was confined to a specific area to avoid recalculating the whole screen and keep the focus on the fire-accessible area only.



Core Techniques and Algorithms in Game Programming2003
Core Techniques and Algorithms in Game Programming2003
ISBN: N/A
EAN: N/A
Year: 2004
Pages: 261

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net