![]() |
![]() |
![]() ![]() ![]() |
Fades are an excellent way to cut to and from sections of your game. Fade into your intro screen, fade out after the player dies, and cross-fade from screen-to-screen. Well, how do you do it? I'm going to cover fades in DirectDraw 16-bit modes, this seems to be the most commonly used mode, and in subsequently is the most commonly asked mode to do fades in.
NOTE: This article assumes you have a decent knowledge of DirectDraw, such as setting modes, creating surfaces, locking/unlocking, etc.
Answer me this quick question, can you evenly divide 3 into 16? I hope you said no. My point is that in order to describe a single pixel, you need three components, red, green, and blue. In other words if your in a 16-bit mode each pixel is given 16 bits for it's RGB combo and since you can't evenly divide 3 (R,G,B) into 16 bits, your video card determines how it's going to split things up. Video cards put in 16-bit modes come in two flavors, the 5,6,5 format or the 5,5,5 format (# of bits for r,g,b respectively). How does this affect fading? Well we're going to need the red, green, and blue components of the pixels were going to be manipulating.
After that background info on these two 16-bit pixel formats, the next obvious step is to throw some code at you that demonstrates one technique of determining what the users video card format is in. The function is called DDGetRGB16() and i'll throw that in below but first, the function fills up a structure called RGB16 which holds information about pixel masks and bit positions.
// the structure which holds pixel RGB masks typedef struct _RGBMASK { unsigned long rgbRed; // red component unsigned long rgbGreen; // green component unsigned long rgbBlue; // blue component } RGBMASK; // the structure which holds screen format info (5,6,5 or 5,5,5 and masking) typedef struct _RGB16 { RGBQUAD depth; RGBQUAD amount; RGBQUAD position; RGBMASK mask; } RGB16;
Then of course, we're also going to need a couple globals. One being the RGB16 structure, and the integers being a couple of convienence variables so we don't constantly type out the entire structure. In case your wondering, the 'm' or 'p' preceding the variables below stand for mask and position variables respectively.
RGB16 rgb16; // Video Card RGB Information Structure int mRed, mGreen, // Faster values of above structure mBlue, pRed, // Faster values of above structure pGreen, pBlue; // Faster values of above structure
Ok, now we're ready to go ahead and see that DDGetRGB16() function. What it does is simply manipulate information returned from a query to ->GetSurfaceDesc() on the primary surface.
/* * DDGetRGB16: * Must run this function to fill the RGB16 struct with the information needed to plot a pixel * To call this, you must have rgb16 defined as a global (unless you want to modify this) variable * RGB16 rgb16; */ void DDGetRGB16(void) { DDSURFACEDESC ddsd; // DirectDraw Surface Description BYTE shiftcount; // Shift Counter // get a surface despriction ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_PIXELFORMAT; lpDDSPrimary->GetSurfaceDesc(&ddsd); // Fill in the masking values for extracting colors rgb16.mask.rgbRed = ddsd.ddpfPixelFormat.dwRBitMask; rgb16.mask.rgbGreen = ddsd.ddpfPixelFormat.dwGBitMask; rgb16.mask.rgbBlue = ddsd.ddpfPixelFormat.dwBBitMask; // get red surface information shiftcount = 0; while(!(ddsd.ddpfPixelFormat.dwRBitMask & 1)) { ddsd.ddpfPixelFormat.dwRBitMask >>= 1; shiftcount++; } rgb16.depth.rgbRed = (BYTE)ddsd.ddpfPixelFormat.dwRBitMask; rgb16.position.rgbRed = shiftcount; rgb16.amount.rgbRed = (ddsd.ddpfPixelFormat.dwRBitMask == 0x1f) ? 3 : 2; // get green surface information shiftcount = 0; while(!(ddsd.ddpfPixelFormat.dwGBitMask & 1)) { ddsd.ddpfPixelFormat.dwGBitMask >>= 1; shiftcount++; } rgb16.depth.rgbGreen =(BYTE)ddsd.ddpfPixelFormat.dwGBitMask; rgb16.position.rgbGreen = shiftcount; rgb16.amount.rgbGreen = (ddsd.ddpfPixelFormat.dwGBitMask == 0x1f) ? 3 : 2; // get Blue surface information shiftcount = 0; while(!(ddsd.ddpfPixelFormat.dwBBitMask & 1)) { ddsd.ddpfPixelFormat.dwBBitMask >>= 1; shiftcount++; } rgb16.depth.rgbBlue =(BYTE)ddsd.ddpfPixelFormat.dwBBitMask; rgb16.position.rgbBlue = shiftcount; rgb16.amount.rgbBlue = (ddsd.ddpfPixelFormat.dwBBitMask == 0x1f) ? 3 : 2; // fill in variables so we dont' have to access the structure anymore mRed = rgb16.mask.rgbRed; // Red Mask mGreen = rgb16.mask.rgbGreen; // Green Mask mBlue = rgb16.mask.rgbBlue; // Blue Mask pRed = rgb16.position.rgbRed; // Red Position pGreen = rgb16.position.rgbGreen; // Green Position pBlue = rgb16.position.rgbBlue; // Blue Position }
Since the implementation of fading that we use at the end involves manipulating each pixel, how do we go about blending one pixel into another? Well, let first establish an equation which will allow us to blend two pixel's color values for one of their RGB components.
// get pixels red,green,blue values red_final = (red1 * transparency1) + (red2 * transparency2); blue_final = (blue1 * transparency1) + (blue2 * transparency2); green_final = (green1 * transparency1) + (green2 * transparency2); // recombine red,green,blue to pixel value
Great, so Matt, how do we get the pixels red,green,blue values and then how to we create a pixel value from those new red,green,blue values we've calculated? Glad you asked, here's where we put into use that information we've obtained from our DDGetRGB16() function.
What follows are four macros which do exactly what they say, find out that information we need...
#define RED(p) (p >> pRed) // Extracts Red Component #define GREEN(p) ((p & mGreen) >> pGreen) // Extracts Green Component #define BLUE(p) (p & mBlue) // Extracts Blue Component #define RGB16(r, g, b) ((r << pRed) | (g << pGreen) | b) // Creates RGB Pixel Value
If you didn't know already, we also want this cross-fade to occur fast, so I took the liberty of making an important but commonly used optimization, a lookup table. I made the decision that for any given fade, 32 steps would be enough to create a smooth enough effect. So the lookup table is simply a huge array of all the shades of any given color in 32 steps.
WORD PixelShade[32][65536];
In our PixelShade initialization function, we're going to precompute the values for a given pixel in the given shade level, 0-31. The function should be self explanatory, the only interesting note is the use of our above macros, and the use of a constant array of the decimal percentage values for each shade level.
/* * InitPixelShade: * Fills the PixelShade array with precomputed shades of every possible pixel (32 shades) */ void InitPixelShade(void) { int i, j; int r,g,b; int dr,dg,db; const double alpha[32] = { 0.0, 0.03, 0.06, 0.09, 0.13, 0.17, 0.21, 0.24, 0.27, 0.31, 0.34, 0.37, 0.41, 0.44, 0.47, 0.49, 0.51, 0.53, 0.56, 0.59, 0.63, 0.66, 0.69, 0.73, 0.76, 0.79, 0.83, 0.87, 0.91, 0.94, 0.97, 1.0 }; for(i=0;i<32;i++) { for(j=0;j<65536;j++) { r = RED(j); g = GREEN(j); b = BLUE(j); dr = (int)(r*alpha[i]); dg = (int)(g*alpha[i]); db = (int)(b*alpha[i]); PixelShade[i][j] = RGB16(dr,dg,db); } } }
The first type of fade we'll implement is cross-fading, which is the basic fade directly from one image on screen to another. What your basically trying to do is combine the two desired images through blending. Where during each iteration the initial image becomes more transparent and the final image becomes more opaque (although, transparency_initial_img + transparency_final_img = 100%).
What the next function presented does is perform a cross-fade (Alpha Transition) on the two given surfaces.
/* * AlphaTransition: * Does an alpha transition from Src -> Des */ void AlphaTransition(LPDIRECTDRAWSURFACE Src, LPDIRECTDRAWSURFACE Des) { long i; // index into surfaces int alpha; // Holds current alpha value int dpitch, spitch, tpitch, ppitch; // surface pitch for destination, source, temp surfaces WORD *AlphaPTR; // pointer to the current AlphaMap level (Source) WORD *InvAlphaPTR; // the inverted pointer to the current AlphaMap level (Destination) WORD *src, *des, *tmp, *prm; WORD *fastsrc, *fastdes, *fasttmp; // Surface memory pointer for source, destination, and temporary surfaces RECT SrcRect, DesRect; // Source and destination rectangles // Set source and destination rectangles to the screen size SetRect(&SrcRect, 0, 0, 640, 480); SetRect(&DesRect, 0, 0, 640, 480); // Create the three surface we are going to use lpDDSTmp = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); // The temporary surface lpDDSSrc = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); // The source surface lpDDSDes = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); // The destination surface // Blit the transition surfaces into out newly created source/destination surfaces lpDDSSrc->Blt(&DesRect, Src, &SrcRect, DDBLT_WAIT, NULL); // Blit Src->lpDDSSrc lpDDSDes->Blt(&DesRect, Des, &SrcRect, DDBLT_WAIT, NULL); // Blit Des->lpDDSDes // lock all three surfaces temporary, source, and destination des = DDLockSurface(lpDDSDes, &dpitch); src = DDLockSurface(lpDDSSrc, &spitch); tmp = DDLockSurface(lpDDSTmp, &tpitch); prm = DDLockSurface(lpDDSPrimary, &ppitch); // for each alpha level for(alpha=31;alpha>=0;alpha--) { // set AlphaMap pointers to appropriate levels AlphaPTR = PixelShade[alpha]; InvAlphaPTR = PixelShade[31-alpha]; // "reset" the *fast* pointers to the locked surfaces fastsrc = src; fastdes = des; fasttmp = tmp; // loop through every pixel for(i=0;i<307200;i++,fasttmp++,fastsrc++,fastdes++) { // Set the new pixel value in temporary surface *fasttmp = AlphaPTR[*fastsrc] + InvAlphaPTR[*fastdes]; } // copy the temp surface to the primary surface // (640*480) = 307200 (words) * 2 = 614400 (bytes) memcpy(prm, tmp, 614400); } // Unlock our temporary, source, and destination surfaces DDUnlockSurface(lpDDSPrimary); DDUnlockSurface(lpDDSTmp); DDUnlockSurface(lpDDSDes); DDUnlockSurface(lpDDSSrc); // Release our temporary, source, and destination surfaces lpDDSTmp->Release(); lpDDSTmp = NULL; lpDDSSrc->Release(); lpDDSSrc = NULL; lpDDSDes->Release(); lpDDSDes = NULL; }
Ok, the next fade we are going to want to perform is a fade from black to a surface. Since all three fading methods use the same basic algorithm, and given the fact that PixelShade[0] would be black for any color, in each iteration we're simply going to fill the screen with that level pixel color until we hit PixelShade[31] which will be the desired image at full color.
Here's the code:
/* * FadeToSurface: * Fades into a surface from black */ void FadeToSurface(LPDIRECTDRAWSURFACE lpDDS) { int c; // counter variable long i; // incrementing variable WORD *tmp, *ref, *prm; WORD *fasttmp, *fastref; // temporary and destination surface mem pointers RECT SrcRect, DesRect; // Source and destination rectangles int tpitch, rpitch, ppitch; // temporary and destination surface pitch WORD *shade; // Set the source and destination rectangles to the size of the screen SetRect(&SrcRect, 0, 0, 640, 480); SetRect(&DesRect, 0, 0, 640, 480); // Create the surfaces lpDDSTmp = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); // the temporary surface lpDDSRef = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); // the temporary surface lpDDSRef->Blt(&DesRect, lpDDS, &SrcRect, DDBLT_WAIT, NULL); // blit the desired surface into our destination surface // Lock our surfaces temporary, and destination tmp = DDLockSurface(lpDDSTmp, &tpitch); prm = DDLockSurface(lpDDSPrimary, &ppitch); ref = DDLockSurface(lpDDSRef, &rpitch); // This can be changed, but it worx out nice to do 10 iterations for(c=1;c<=30;c++) { // get pointer indexed to the start of the current shade level shade = PixelShade[c]; // "reset" our *fast* surface pointers fasttmp = tmp; fastref = ref; // for every pixel on the screen (640*480=307200) for(i=0;i<307200;i++,fasttmp++,fastref++) { // new pixel please..... *fasttmp = shade[*fastref]; } // copy the temp surface to the primary surface // (640*480) = 307200 (words) * 2 = 614400 (bytes) systovid_memcpy(prm, tmp, 614400); } // unlock the temporary surface and destination surface DDUnlockSurface(lpDDSTmp); DDUnlockSurface(lpDDSPrimary); DDUnlockSurface(lpDDSRef); // blit the actual destination surface to the primary surface so we're sure // the screen is where it should be lpDDSPrimary->Blt(&DesRect, lpDDS, &SrcRect, DDBLT_WAIT, NULL); // release the temporary and destination surfaces lpDDSTmp->Release(); lpDDSTmp = NULL; lpDDSRef->Release(); lpDDSRef = NULL; }
And the final fade we're going to perform is the classic fade to black. What we want is to have a desired initial image already on the screen and simply do the opposite of what we did with the FadeToSurface(), starting with PixelShade[31] and ending with PixelShade[0].
Again, here's the implementation:
/* * FadeToBlack: * Fades a screen to black */ void FadeToBlack(void) { RECT SrcRect, DesRect; // Source and Destination Rectangles WORD *tmp; // temporary surface memory pointer WORD *ref; WORD *prm; WORD *fastref, *fasttmp; int c, tpitch, rpitch, ppitch; // incrementing variable, temporary surface pitch long i; // another incrementing variable WORD *shade; // Set source and destination rectangles to size of screen SetRect(&SrcRect, 0, 0, 640, 480); SetRect(&DesRect, 0, 0, 640, 480); // Create our temporary surface lpDDSTmp = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); lpDDSRef = DDCreateSurface(640, 480, DDSCAPS_SYSTEMMEMORY); // Blit our primary surface into our temporary SYSTEM MEMORY surface lpDDSRef->Blt(&DesRect, lpDDSPrimary, &SrcRect, DDBLT_WAIT, NULL); // Lock our temporary surface tmp = DDLockSurface(lpDDSTmp, &tpitch); ref = DDLockSurface(lpDDSRef, &rpitch); prm = DDLockSurface(lpDDSPrimary, &ppitch); for(c=30;c>=1;c--) { // get a pointer indexed to the start of the current shade level shade = PixelShade[c]; // "reset" our *fast* surface pointers fastref = ref; fasttmp = tmp; // for every pixel on the screen (640*480=307200) for(i=0;i<307200;i++,fasttmp++,fastref++) { // new pixel please.... *fasttmp = shade[*fastref]; } // copy the temp surface to the primary surface // (640*480) = 307200 (words) * 2 = 614400 (bytes) systovid_memcpy(prm, tmp, 614400); } // unlock our temporary surface DDUnlockSurface(lpDDSTmp); DDUnlockSurface(lpDDSRef); DDUnlockSurface(lpDDSPrimary); // just to make sure the screen is black when this routine is over, fill it with 0 DDFillSurface(lpDDSPrimary,0); // release our temporary surface lpDDSTmp->Release(); lpDDSTmp = NULL; lpDDSRef->Release(); lpDDSRef = NULL; }
Before I offer the download to the demo application, just a couple things to keep in mind. It's obvious that you should never attempt to do pixel manipulations directly on the primary (Video Memory) surface. That's the reason for the creation of system memory surfaces and then the copying of the working surfaces into them in each of the three functions. After that, you can do all the pixel manipulations you want and then blast it into the primary surface with a memcpy. The functions also attempt to minimize surface locking, another potential slowdown. But, as always, nothing is every "optimized" as new ways to speed things up can always be found...