Basic 2D Drawing Concepts


If you need to find fast algorithms to draw lines, ellipses, or other simple shapes there's no better reference series than Graphics Gems. Every programmer should have all but one on their bookshelf. The one not on your bookshelf will be open on your desk. I hope you'll forgive me for de-referencing those algorithms instead of regurgitating them here, but I simply can't add anything new to that venerable body of knowledge and I'd like to spend time and trees on other things. This is not going to be one of those books where I reinvent the wheel.

Windows Bitmaps and Other GDI Doodads

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 convenient if you want to use the GDI for one shot stuff, but don't forget that it's way too slow for things you'll be doing every game loop. Here's an example of how you would go about drawing a bitmap:

 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 grabbing a DC from any DirectDraw surface. This effectively locks the surface until the device context is released. If the surface happens to be a texture or otherwise inserted in the draw pipeline, it can't be used to draw anything until the DC is released. If you really want to use GDI functions to draw to a DirectDraw surface, the best thing to do is create it in off-screen memory, perform the GDI drawing, and copy the surface to something in the draw pipeline like a surface in video RAM. This seems like going out of your way, but your game will run faster.

Color Key or Chroma Key

A color key, or as artists call it "chroma key" is a special pixel value that means, "Don't copy me." Actually there's a more specific name for that kind of color key—it's a source color key. There are destination color keys, too, which are neat for creating interesting effects but they are only rarely used. The source color key, on the other hand, is used all the time. In case you were wondering, this technology is exactly the same as the technology used to make weather forecasters appear on weather maps. They actually stand in front of a green background, watching their own image on a monitor after it has been blended with the weather map. Have you ever noticed that they never wear anything green? If they did, that part of their wardrobe would appear completely transparent.

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 expressed in full 8-bit RGB values, which you can only use if the surface's pixel format happens to be 32-bit. Artists need to know the exact RGB value so they can fill the areas that should be transparent, but when the art is converted to the screen's pixel format, the bits that map to the color key don't match the original definition! You solve this problem in one of two ways.

click to expand
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 certainly tweak some of your transparent pixel values to something other than the exact RGB value you expect. This will create weird colored halos around your art at best or completely disable the transparency at worst.

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 performs this task:

 //------------------------------------------------- // 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 group of rarely used colors. This will avoid any unintentional misuse of the color key. After years of study and many opinions of artists, it seems that the first prize for the "ugliest color contest" goes to a setting of RGB (255, 0, 255). I think the only development group in the industry that can't use it is Mattel. This color is just too close to Barbie's favorite color. Maybe they use "vomit green;" I have no idea.

Copying Surfaces

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 tries again. If the failure was for any other reason, such as an invalid surface, the code exits with an error.

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 flipped screen modes, which can happen at any time. It is likely that every other surface will fail a BltFast call, so the best course of action is to exit the draw entirely and restore every surface at once. After you do that you can try to draw everything again. This time it will work.

Copying Surfaces Using an Alpha Channel

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 sprites because their edges are harsh and aliased. I mentioned before that you could use an alpha channel to fix this problem, but be aware that there's no hardware acceleration. That should surprise you; it has certainly confused me. Video cards clearly have the hardware to blend pixel values. This is evident whenever you see a semi-transparent 3D object. This same technology though has never been exposed in DirectDraw, even though the flags are there! We are left to compose our alpha blended surfaces with nothing more than stone knifes and bear skins.

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 trickery to keep masked integer values from overflowing their boundaries. Here's the code that implements this formula for an 8-bit color channel:

 // 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 turn off the alpha map to speed up their game.

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 next pixel. You'll also notice that this code doesn't support 8-bit surfaces. You're free to write that code yourself!.

Gotcha

There's another significant warning in this code: It doesn't support video memory surfaces of any kind. While there's no theoretical barrier to performing blends of video memory surfaces, it simply won't work well under WinXP or Win98. This is most likely a driver related issue, so take the warning seriously. Reading and writing individual pixels from video memory is painfully slow anyway, so this limitation shouldn't be a huge burden. If you want to blend to a surface in video RAM, like your backbuffer, make a temporary copy of the target area in system RAM, perform the blend, and copy it back when you're done.

You can modify CopyAlpha to accept a single alpha value instead of an entire surface. This would be useful to create a fade-in or fade-out effect over multiple frames. You can write this function yourself; just take the CopyAlpha code you saw a moment ago and remove all references to alphaSurf. Recompile, fix the errors, and you'll have what you need:

 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 } 




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

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