If you need to find fast algorithms to draw lines, ellipses, or other simple
If you have a DirectX surface, you can grab a device context, or DC, and use the Windows GDI to draw to your surface. This is pretty
HRESULT DrawBitmap( LPDIRECTDRAWSURFACE7 pdds, HBITMAP hBMP, DWORD dwBMPOriginX, DWORD dwBMPOriginY, DWORD dwBMPWidth, DWORD dwBMPHeight, bool stretch) { HDC hDCImage; HDC hDC; BITMAP bmp; DDSURFACEDESC2 ddsd; HRESULT hr; if( hBMP == NULL pdds == NULL ) return E_INVALIDARG; // Make sure this surface is restored. if ( (pdds->IsLost() != DD_OK) ) { if( FAILED( hr = pdds->Restore() ) ) return hr; } // Get the surface.description ddsd.dwSize = sizeof(ddsd); m_pdds->GetSurfaceDesc( &ddsd ); if( ddsd.ddpfPixelFormat.dwFlags == DDPF_FOURCC ) return E_NOTIMPL; // Select bitmap into a memoryDC so we can use it. hDCImage = CreateCompatibleDC( NULL ); if( NULL == hDCImage ) return E_FAIL; SelectObject( hDCImage, hBMP ); // Get size of the bitmap GetObject( hBMP, sizeof(bmp), &bmp ); // Use the passed
size
, unless zero dwBMPWidth = ( dwBMPWidth == 0 ) ? bmp.bmWidth : dwBMPWidth; dwBMPHeight = ( dwBMPHeight == 0 ) ? bmp.bmHeight : dwBMPHeight; // Stretch the bitmap to cover this surface if( FAILED( hr = pdds->GetDC( &hDC ) ) )\ return hr; if(stretch) { StretchBlt( hDC, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hDCImage, dwBMPOriginX, dwBMPOriginY, dwBMPWidth, dwBMPHeight, SRCCOPY ); } else { BitBlt(hDC, dwBMPOriginX, dwBMPOriginY, dwBMPWidth, dwBMPHeight, hDCImage, 0, 0, SRCCOPY); } if( FAILED( hr = pdds->ReleaseDC( hDC ) ) ) return hr; DeleteDC( hDCImage ); return S_OK; }
Notice that the first thing this function does is check to see if the surface is lost and needs restoring. This is critical and failure to do it every time you access a DirectX surface is asking for trouble. The Win32 calls to StrectchBlt and BitBlt are inside the predicate that grabs a DC from the DirectX surface. You could replace these calls with any GDI function: LineTo , MoveTo , TextOut , whatever you want.
You should note one thing about
A color key, or as
When you set a color key for a surface, you use a pixel value that matches exactly with the pixel format of that surface (see Figure 6.3). Most colors are expressed in full are
Figure 6.3:
A Background, a Sprite with a Color Key, and the Intended Result.
First, your artists must be aware that any art with a color key must be stored in a convenient format that doesn't mess up the color key value. For example, a slightly lossy JPG will most
| Gotcha |
There's another insidious problem that artists will unknowingly create in their art. Many art packages like Photoshop and 3D Studio Max can antialias the art to a specific color. It might seem like antialiasing to the color key would smooth the edges of your art into the background, but it won't. Instead, you'll get a nasty colored outline that somewhat matches the color key color. If you want to have a smooth transition from your sprite to a background, the only solution is to use a separate alpha channel, something you'll learn shortly. |
Second, the programmer needs to find out what pixel value maps to the RGB color key definition, given the destination surface's pixel format. Under Win32, you get to use a GDI function, because you can't count on performing the conversion yourself. The resulting pixel must be exactly the same as what will be drawn by the video driver, and unless you've seen the video driver code, you can't predict its behavior. You only have to do this once for any destination surface. Here's a code example that
//------------------------------------------------- // Name: ConvertGDIColor() // Desc: Converts a GDI color (0x00bbggrr) into the equivalent color on a // DirectDrawSurface using its pixel format. //------------------------------------------------- DWORD ConvertGDIColor( LPDIRECTDRAWSURFACE7 pdds, COLORREF dwGDIColor ) { if( pdds == NULL ) return 0x00000000; COLORREF rgbT; HDC hdc; DWORD dw = CLR_INVALID; DDSURFACEDESC2 ddsd; HRESULT hr; // Use GDI SetPixel to color match for us if( pdds->GetDC(&hdc) == DD_OK) { rgbT = GetPixel(hdc, 0, 0); // Save current pixel value SetPixel(hdc, 0, 0, dwGDIColor); // Set our value pdds->ReleaseDC(hdc); } // Now lock the surface so we can read back the converted color ddsd.dwSize = sizeof(ddsd); hr = pdds->Lock( NULL, &ddsd, DDLOCK_WAIT, NULL ); if( hr == DD_OK) { dw = *(DWORD *) ddsd.lpSurface; if( ddsd.ddpfPixel Format.dwRGBBitCount < 32 ) // Mask it to bpp dw &= ( 1 << ddsd.ddpfPixelFormat.dwRGBBitCount ) - 1; pdds->Unlock(NULL); } // Now put the color that was there back. if( pdds->GetDC(&hdc) == DD_OK ) { SetPixel( hdc, 0, 0, rgbT ); pdds->ReleaseDC(hdc); } return dw; }
This code grabs a DC from the DirectX surface and uses the GDI's Get Pixel and SetPixel to save the current pixel and draw the target color to the upper left-hand pixel. The resulting pixel value is obtained using DirectX's Lock to get a pointer to the surface. Once this is done, the old pixel value is restored. Clearly, this function isn't very speedy so you should only call it once per destination surface. If every surface has the same pixel format, you should only call it once for your whole game. You'll have to call this function again if these surfaces are lost, because they usually get lost when the player does something crazy like changing their desktop settings or switching from fullscreen mode to windowed mode.
The source color key is set for the surface that has the transparent pixels. Here's how you do that:
HRESULT SetColorKey( LPDIRECTDRAWSURFACE7 pdds, DWORD dwColorKey, DDCOLORKEY &ddck ) { if( NULL == pdds ) return E_POINTER; ddck.dwColorSpaceLowValue = ddck.dwColorSpaceHighValue = ConvertGDIColor( pdds, dwColorKey ); return pdds->SetColorKey( DDCKEY_SRCBLT, &m_ddck ); }
Notice that the last value, DDCOLORKEY , is actually an output of this function. It's convenient to keep it around in case you have other surfaces with the same pixel format; you won't have to call ConvertGDIColor again.
How do you choose color key color? It should be a color that is not only rarely used, but located in the color space in a
Copying a surface should be straightforward. The hard part is figuring out what to do when the copy fails, or how to detect it:
HRESULT Copy(LPDIRECTDRAWSURFACE7 dest, LPDIRECTDRAWSURFACE7 src, CPoint &destPoint, CRect &srcRect, BOOL isColorKeyed) { if ( !dest !src ) { return DDERR_SURFACELOST; } HRESULT hr; for ( ;; ) { // Copy the surface hr = dest->BltFast( destPoint.x, destPoint.y, src, srcRect, isColorKeyed ? DDBLTFAST_SRCCOLORKEY : 0 ); if( hr != DDERR_WASSTILLDRAWING ) { /*** Note: Surfaces should not restore
themselves
because they don't know how to recreate their graphics. If they fail to draw, just return the result and let the caller figure it out. ***/ return hr; } }; return hr; }
The code enters a loop and attempts to copy the source surface to the destination. If it fails because the surface was busy, it
You might think that it would be a good idea to attempt a surface restore before bailing but that would be a mistake in this piece of code. A surface to surface copy will get used heavily in the screen render. Don't forget that the backbuffer is a surface also. If either the source or destination surface can't render, the most likely cause is an invalid surface. This would happen if the player
Color keys are convenient and cheap. Almost all video hardware accelerates surface to surface copies with a color key. The problem is that they don't look good. It's easy to pick out color keyed
Here's the basic formula for alpha blending two values in the same color channel:
Result = Destination + ( Alpha * ( Source - Destination ) )
It might not be obvious, but this formula works on color channels, not pixels. A pixel has three color channels: red, green, and blue. When you implement this formula, you must calculate each color separately. You have to perform a little
// rd is the destination pixel's red component // rs is the source pixel's red component // rd2 holds the result of the blending operation rd = (*lpDestPixel & REDMASK); rs = (*lpSrcPixel & REDMASK); rd2 = rd + ((alphaPixel * ( rs - rd ) ) >> 8); rd2 &= REDMASK;
The first two lines isolate the color channel from the pixel values of both the destination and source surface. The third line implements the formula, shifting the bit values back into place after the multiplication. The last line masks any overflow bits from the result.
Now you're ready to see the entire CopyAlpha function. There are three surfaces sent into CopyAlpha: the source and destination surfaces accompany a surface that stores the alpha channel. There are a number of ways to store the alpha channel values, even including it within a full 32-bit source surface. The code below stores alphas in a completely separate surface, encoded in the blue channel, which has exactly the same pixel format as the other two surfaces. Why? The blue channel doesn't require any shifting to grab the value. It also has to do with how the surface is created and cached into the game.
First, most 3D art tools like 3D Studio can create art with an alpha channel, but for it to have a reasonable resolution the whole piece of art will be stored as ARGB, each channel taking eight bits. It's a good idea to split this into two surfaces: one for RGB art and the other for the alpha channel, preferably stored in an 8-bit image. Splitting the art has the advantage of reducing its size and memory requirements, but more importantly it gives your game the ability to disable the alpha map entirely. A player with a slower machine might
No more stalling, here's CopyAlpha:
HRESULT CopyAlpha(const LPDIRECTDRAWSURFACE7 dest, const LPDIRECTDRAWSURFACE7 src, const LPDIRECTDRAWSURFACE7 alphaSurf, const LPPOINT destPoint, const LPRECT srcRect, const bool isColorKeyed ) { HRESULT hr = DDERR_UNSUPPORTED; // Check if it is colorKeyed int colorKey = 0; if ( isColorKeyed ) { DDCOLORKEY ddck; src->GetColorKey( DDCKEY_SRCBLT, &ddck ); colorKey = ddck.dwColorSpaceLowValue; } //Prepare the surface descriptors DDSURFACEDESC2 sdSrc, sdDest, sdAlpha; // Lock the source and obtain its descriptor ZeroMemory( &sdSrc, sizeof( sdSrc ) ); sdSrc.dwSize = sizeof( sdSrc ); hr = src->Lock( NULL, &sdSrc, DDLOCK_WAIT, NULL ); if ( (hr != DD_OK) ) { return hr; // Failed to lock } // Lock the source and obtain its descriptor ZeroMemory( &sdDest, sizeof( sdDest ) ); sdDest.dwSize = sizeof( sdDest ); hr = dest->Lock( NULL, &sdDest, DDLOCK_WAIT, NULL ); if ( (hr != DD_OK) ) { src->Unlock( NULL ); return hr; // Failed to lock } // Lock the source and obtain its descriptor ZeroMemory( &sdAlpha, sizeof( sdAlpha ) ); sdAlpha.dwSize = sizeof( sdAlpha ); hr = alphaSurf->Lock( NULL, &sdAlpha, DDLOCK_WAIT, NULL ); if ( hr != DD_OK ) { src->Unlock( NULL ); dest->Unlock( NULL ); return hr; // Failed to lock } // Note: // Alpha blitting requires that both the source and destination exist // in system memory, not video memory. It will freeze windows 98 machines // and slow down win2K machines if a source or destination // live in video memory if ((sdSrc.ddpfPixelFormat.dwRGBBitCount!=sdDest.ddpfPixelFormat.dwRGBBitCount) (sdSrc.ddpfPixelFormat.dwRBitMask!=sdDest.ddpfPixelFormat.dwRBitMask) (sdSrc.ddpfPixelFormat.dwGBitMask!=sdDest.ddpfPixelFormat.dwGBitMask) (SdSrc.ddpfPixelFormat.dwBBitMask!=sdDest.ddpfPixelFormat.dwBBitMask) ) { src->Unlock( NULL ); dest->Unlock( NULL ); alphaSurf->Unlock( NULL ); return DDERR_WRONGMODE; // Incompatible surfaces } // Grab the mask values const unsigned int REDMASK = sdSrc.ddpfPixelFormat.dwRBitMask; const unsigned int GREENMASK = sdSrc.ddpfPixelFormat.dwGBitMask; const unsigned int BLUEMASK = sdSrc.ddpfPixelFormat.dwBBitMask; // Determine how to blend based on source RGBBitCount switch( sdSrc.ddpfPixelFormat.dwRGBBitCount ) { // 16 bit blend case 16 : { const int iSrcWidth = sdSrc.lPitch / sizeof( short unsigned int ); const int iDstWidth = sdDest.lPitch / sizeof( short unsigned int ); // It will be assumed that the alpha surface shares // the same dimensions as the source short unsigned int * lpAlpha = &((short unsigned int *)sdAlpha.lpSurface)[ srcRect->left + srcRect->top * iSrcWidth ]; short unsigned int * lpSrc = &((short unsigned int *)sdSrc.lpSurface)[ srcRect->left + srcRect->top * iSrcWidth ]; short unsigned int * lpDest = &((short unsigned int *)sdDest.lpSurface)[ destPoint->x + destPoint->y * iDstWidth ]; assert(sdSrc.lPitch == sdAlpha.lPitch && "It is assumed that the alpha mask will have the same dimensions as the source sprite"); int rectH = srcRect->bottom - srcRect->top; int rectW = srcRect->right - srcRect->left; const int sskip = iSrcWidth - rectW; const int dskip = iDstWidth - rectW; int x,y; unsigned int alpha; //alpha pixel component unsigned int rs,gs,bs; //source pixel components unsigned int rd,gd,bd; //destination pixel components unsigned int rd2,gd2,bd2; //destination pixel components used to // avoid stalling due to read-write conflict for(y=0; y<rectH; y++) { for(x=0; x<rectW; x++, lpDest++, lpSrc++, lpAlpha++) { if ( colorKey && (*lpSrc)==(short unsigned int)colorKey ) { continue; } // Alpha channel will be based on the blue intensity because // reading blue requires no shifting alpha = *lpAlpha & BLUEMASK; if(BLUEMASK == alpha) { // Trivial case 1: full intensity, nothing but source shows *lpDest = *lpSrc; continue; } else if(0 == alpha) { // Trivial case 2: No intensity, background all the way continue; } rd = (*lpDest & REDMASK); gd = (*lpDest & GREENMASK); bd = (*lpDest & BLUEMASK); rs = (*lpSrc & REDMASK); gs = (*lpSrc & GREENMASK); bs = (*lpSrc & BLUEMASK); rd2 = rd + ((alpha*(rs-rd))>>5); gd2 = gd + ((alpha*(gs-gd))>>5); bd2 = bd + ((alpha*(bs-bd))>>5); rd2 &= REDMASK; gd2 &= GREENMASK; bd2 &= BLUEMASK; *lpDest = rd2 gd2 bd2; } lpDest += dskip; lpSrc += sskip; lpAlpha += sskip; } hr = DD_OK; } break; // 32 bit blend case 32 : { const int iSrcWidth = sdSrc.lPitch / sizeof( unsigned int ); const int iDstWidth = sdDest.lPitch / sizeof( unsigned int ); //It will be assumed that the alpha surface shares the same //dimensions as the source unsigned int * lpAlpha = &((unsigned int *)sdAlpha.lpSurface) [ srcRect->left + srcRect->top * iSrcWidth ]; unsigned int * lpSrc = &((unsigned int *)sdSrc.lpSurface) [ srcRect->left + srcRect->top * iSrcWidth ]; unsigned int * lpDest = &((unsigned int *)sdDest.lpSurface) [ destPoint->x + destPoint->y * iDstWidth ]; assert(sdSrc.lPitch == sdAlpha.lPitch && "It is assumed that the alpha mask will have the same dimensions as the source sprite"); int rectH = srcRect->bottom - srcRect->top; int rectW = srcRect->right - srcRect->left; const int sskip = iSrcWidth - rectW; const int dskip = iDstWidth - rectW; int x,y; unsigned int alpha; //alpha pixel component unsigned int rs,gs,bs; //source pixel components unsigned int rd,gd,bd; //destination pixel components unsigned int rd2,gd2,bd2; //destination pixel
components
used to //avoid stalling due to read-write conflict for(y=0; y<rectH; y++) { for(x=0; x<rectW; x++, lpDest++, lpSrc++, lpAlpha++) { if (colorKey && (*lpSrc)==(unsigned int)colorKey) { continue; } //Alpha channel will be based on the blue intensity //because reading blue requires no shifting alpha = *lpAlpha & BLUEMASK; if(BLUEMASK == alpha) { // Trivial case 1: full intensity, nothing but source shows *lpDest = *lpSrc; continue; } else if(0 == alpha) { // Trivial case 2: No intensity, background all the way continue; } rd = (*lpDest & REDMASK); gd = (*lpDest & GREENMASK); bd = (*lpDest & BLUEMASK); rs = (*lpSrc & REDMASK); gs = (*lpSrc & GREENMASK); bs = (*lpSrc & BLUEMASK); rd2 = rd + ((alpha*(rs-rd))>>8); gd2 = gd + ((alpha*(gs-gd))>>8); bd2 = bd + ((alpha*(bs-bd))>>8); rd2 &= REDMASK; gd2 &= GREENMASK; bd2 &= BLUEMASK; *lpDest = rd2 gd2 bd2; } lpDest += dskip; lpSrc += sskip; lpAlpha += sskip; } hr = DD_OK; } break; // no alpha blending on palettized surface case 8 : // unsupported default : hr = S_FALSE; break; } // Cleanup src->Unlock( NULL ); dest->Unlock( NULL ); alphaSurf->Unlock( NULL ); return hr; }
You should pay some special attention to a few things in this function. It distinguishes between 16-bit and 32-bit surfaces. Rather than writing one piece of code that handles both cases it's a better idea to write optimal code for each case. The function handles the case where the source surface has a color key. Any source pixel that matches the color key causes the loop to skip ahead to the
| Gotcha |
There's another significant warning in this code: It doesn't support video memory surfaces of any kind. While there's no theoretical
|
You can modify
CopyAlpha
to accept a single alpha value instead of an entire surface. This would be useful to create a
HRESULT CopyAlpha( const LPDIRECTDRAWSURFACE7 dest, const LPDIRECTDRAWSURFACE7 src, const unsigned char alpha, const LPPOINT destPoint, const LPRECT srcRect, const bool isColorKeyed ) { // Same code as before, just eliminate all references to alphaSurf }