Home This Article Is Taken From The Game Programming MegaSite, A Definitive Resource For Game Developers!

DirectDraw Fading Mania
By: Matt Reiferson

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.

16-bit Pixel Formats

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
	}

Fading Technique

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...

Download: ddfade.zip (Executable, Source C++)



The Game Programming MegaSite - ⌐1996- Matt Reiferson.