Color EffectsThe next subject I want to discuss is color animation and tricks. In the past, 256-color palettized modes were the only bit depths available, and a lot of tricks and techniques were invented to take advantage of the instantaneous nature of color changes—that is, a change to one or more of the palette registers is instantly visible on the screen. These days 256-color modes are fading away due to faster hardware and acceleration. However, learning these techniques is still crucial to understanding other related concepts, not to mention that it will be many years until all games are totally RGB. There are a lot of 486 machines and even slower-MHz Pentiums still around that can handle only 256-color modes at any sort of reasonable speed! Color Animation in 256-Color ModesColor animation basically refers to any operation of modifying or shifting around color palette entries on-the-fly. For example, glowing objects, blinking lights, and many other effects can be created simply by manipulating the entries of the color table on-the-fly. The cool thing is that any object on the screen that has pixels with the values that you're manipulating in the color table will be affected. Imagine how hard it would be to do this with bitmaps. For example, let's say you had a little ship with running lights on it, and you wanted them to blink. You would need a bitmap for each frame of animation. But with color animation, all you need to do is draw a single bitmap, make the lights a specific color index, and then animate the color index. Figure 7.36 illustrates this indirection graphically. Figure 7.36. Color animation using palette indirection.Two of my favorite effects are blinking and glowing colors. Let's begin with a blinking light function. Here's the functionality you want:
This is a perfect example for showing some persistent data techniques and showing how to update DirectDraw palette entries. My strategy will be to create a single function that's called to create and destroy the lights as well as to perform the animation. The function will use static data arrays that are local and the function will have a number of operation modes: BLINKER_ADD— Used to add a blinking color to the database. When called, the function returns an ID number used to reference the blinking light. System holds up to 256 lights. BLINKER_DELETE— Deletes a blinking light of the sent ID. BLINKER_UPDATE— Updates the on/off parameters of the sent ID's light. BLINKER_RUN— Processes all the lights through one cycle. The data structure used to hold a single light and also to create one is called BLINKER and is shown here: // blinking light structure typedef struct BLINKER_TYP { // user sets these int color_index; // index of color to blink PALETTEENTRY on_color; // RGB value of "on" color PALETTEENTRY off_color; // RGB value of "off" color int on_time; // number of frames to keep "on" int off_time; // number of frames to keep "off" // internal member int counter; // counter for state transitions int state; // state of light, // -1 off, 1 on, 0 dead } BLINKER, *BLINKER_PTR; Basically, you fill in the "user" fields and then call the function with a BLINKER_ADD command. Anyway, the general operation is as follows: You call the function at any time to add, delete, or update, but only once per frame with the run command. Here's the code for the function: int Blink_Colors(int command, BLINKER_PTR new_light, int id) { // this function blinks a set of lights static BLINKER lights[256]; // supports up to 256 blinking lights static int initialized = 0; // tracks if function has initialized // test if this is the first time function has run if (!initialized) { // set initialized initialized = 1; // clear out all structures memset((void *)lights,0, sizeof(lights)); } // end if // now test what command user is sending switch (command) { // add a light to the database { // run thru database and find an open light for (int index=0; index < 256; index++) { // is this light available? if (lights[index].state == 0) { // set light up lights[index] = *new_light; // set internal fields up lights[index].counter = 0; lights[index].state = -1; // off // update palette entry lpddpal->SetEntries(0,lights[index].color_index, 1,&lights[index].off_color); // return id to caller return(index); } // end if } // end for index } break; case BLINKER_DELETE: // delete the light indicated by id { // delete the light sent in id if (lights[id].state != 0) { // kill the light memset((void *)&lights[id],0,sizeof(BLINKER)); // return id return(id); } // end if else return(-1); // problem } break; case BLINKER_UPDATE: // update the light indicated by id { // make sure light is active if (lights[id].state != 0) { // update on/off parms only lights[id].on_color = new_light->on_color; lights[id].off_color = new_light->off_color; lights[id].on_time = new_light->on_time; lights[id].off_time = new_light->off_time; // update palette entry if (lights[id].state == -1) lpddpal->SetEntries(0,lights[id].color_index, 1,&lights[id].off_color); else lpddpal->SetEntries(0,lights[id].color_index, 1,&lights[id].on_color); // return id return(id); } // end if else return(-1); // problem } break; case BLINKER_RUN: // run the algorithm { // run thru database and process each light for (int index=0; index < 256; index++) { // is this active? if (lights[index].state == -1) { // update counter if (++lights[index].counter >= lights[index].off_time) { // reset counter lights[index].counter = 0; // change states lights[index].state = -lights[index].state; // update color lpddpal->SetEntries(0,lights[index].color_index, 1,&lights[index].on_color); } // end if } // end if else if (lights[index].state == 1) { // update counter if (++lights[index].counter >= lights[index].on_time) { // reset counter lights[index].counter = 0; // change states lights[index].state = -lights[index].state; // update color lpddpal->SetEntries(0,lights[index].color_index, 1,&lights[index].off_color); } // end if } // end else if } // end for index } break; default: break; } // end switch // return success return(1); } // end Blink_Colors NOTE I've boldfaced the sections that update the DirectDraw palette entries. I assume that there's a global palette interface lpddpal. The function has three main sections: initialization, updating, and run logic. When the function is called for the first time, it initializes itself. Then the next code segment tests for updating commands or the run command. If an update-type command is requested, logic is performed to add, delete, or update a blinking light. If the run mode is requested, the lights are all processed through one cycle. In general, you would use the function after first adding one or more lights, which you'd do by setting up a generic BLINKER structure and then passing the structure to the function with the BLINKER_ADD command. The function would then return the ID of your blinking light, which you'd save—you'll need the ID if you want to delete or update a blinking light. After you've created all the lights you want, you can call the function with all NULLs except for the command, which is BLINKER_RUN. You do this for each frame of your game loop. For example, let's say you have a game that runs at 30fps, and you want a red light to blink with a 50-50 duty cycle—one second on, one second off—along with a green light with a 50-50 duty cycle of two seconds on and two seconds off. Furthermore, you want to use palette entries 250 and 251 for the red and green light, respectively. Here's the code you need: BLINKER temp; // used to hold temp info PALETTEENTRY red = {255,0,0,PC_NOCOLLAPSE}; PALETTEENTRY green = {0,255,0,PC_NOCOLLAPSE}; PALETTEENTRY black = {0,0,0,PC_NOCOLLAPSE}; // add red light temp.color_index = 250; temp.on_color = red; temp.off_color = black; temp.on_time = 30; // 30 cycles at 30fps = 1 sec temp.off_time = 30; // make call int red_id = Blink_Colors(BLINKER_ADD, &temp, 0); // now create green light temp.color_index = 251; temp.on_color = green; temp.off_color = black; temp.on_time = 60; // 30 cycles at 30fps = 2 secs temp.off_time = 60; // make call int green_id = Blink_Colors(BLINKER_ADD, &temp, 0); Now you're ready to rock and roll! In the main part of your game loop, you would make a call to Blink_Colors() each cycle, something like this: // enter main event loop while(TRUE) { // test if there is a message in queue, if so get it if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { // test if this is a quit if (msg.message == WM_QUIT) break; // translate any accelerator keys TranslateMessage(&msg); // send the message to the window proc DispatchMessage(&msg); } // end if // main game processing goes here Game_Main(); // blink all the colors // could put this into Game_Main() also – better idea Blink_Colors(BLINKER_RUN, NULL, 0); } // end while Of course, you can delete a blinker with its ID at any time, and it won't be processed anymore. For example, if you want to kill the red light: Blink_Colors(BLINKER_DELETE, NULL, red_id); It's as simple as that. And of course, you can update a blinker's on/off time and color values by setting up another BLINKER structure and then making the call with BLINKER_UPDATE. For example, if you want to alter the green blinker's parameters: // set new parms temp.on_time = 100; temp_off_time = 200; temp.on_color = {255,255,0,PC_NOCOLLAPSE}; temp.off_color = {0,0,0,PC_NOCOLLAPSE}; // update blinker Blink_Colors(BLINKER_UPDATE, temp, green_id); That's enough of that! Check out DEMO7_15.CPP|EXE, which uses the Blink_Colors() function to make some of the lights on the starship image blink. Color Rotation in 256-Color ModesThe next interesting color animation effect is called color rotation or color shifting. Basically, it's the process of taking a collection of adjacent color entries or registers and shifting them in a circular manner, as shown in Figure 7.37. Using this technique, you can make objects seem as if they're moving or shifting without writing a single pixel to the screen. It's great for simulating water or the motion of fluids. In addition, you can draw a number of images at different positions, each with a different color index. Then, if you rotate the colors, it will look like the object is moving. Great 3D Star Wars trenches can be created like this. Figure 7.37. Color rotation.The code for color rotation is fairly trivial. Algorithmically, to rotate color[c1] to color[c2], use the following code: temp = color[c1]; for (int index = c1; index < c2; index++) color[c1] = color[c1+1]; // finish the cycle, close the loop color[index] = temp; Here's a function that implements the algorithm that I'm using for our library: int Rotate_Colors(int start_index, int end_index) { // this function rotates the color between start and end int colors = end_index - start_index + 1; PALETTEENTRY work_pal[MAX_COLORS_PALETTE]; // working palette // get the color palette lpddpal->GetEntries(0,start_index,colors,work_pal); // shift the colors lpddpal->SetEntries(0,start_index+1,colors-1,work_pal); // fix up the last color lpddpal->SetEntries(0,start_index,1,&work_pal[colors - 1]); // update shadow palette lpddpal->GetEntries(0,0,MAX_COLORS_PALETTE,palette); // return success return(1); } // end Rotate_Colors Basically, the algorithm takes the starting and ending color index that you want to rotate and performs the rotation. Don't worry about the "shadow palettes" stuff; this is a library thing, so just focus on the logic. The interesting thing is how the algorithm works. It does the same thing as the FOR loop version, but in a different way. This is possible via an in-place shift. Anyway, for a demo of the function, take a look at DEMO7_16.CPP|EXE. It uses the function to create a moving stream of acid—water is for wimps! Tricks with RGB ModesThe problem with RGB modes is that there isn't any color indirection. In other words, every pixel on the screen has its own RGB values, so there's no way to make a single change that affects the entire image. However, there are two ways to perform color-related processing:
TRICK Gamma correction deals with the nonlinear response of a computer monitor to the input drive. In most cases, gamma correction allows you to modify the intensity or the brightness of the video signal. However, gamma correction can be performed separately on each red, green, and blue channel to obtain interesting effects. |