From Pixels to Images
In the various articles up to this point, we''ve learned a lot about pixels, color depth, color composition and memory manipulation. So long as we understand that different color depths require different color composition and memory requirements (per pixel), we should be able to draw pixels to the display in any screen mode and color depth we desire.
Sure, pixels can be fun, but the real excitement comes from image manipulation. In this article, we''re going to play around with DirectDraw surfaces in order to draw images onto our display. What we learn here will be the basis for all of our 2D games, so make sure that you''re following along with your own template code!
Video and System Memory
You''re probably aware that there''s physical memory on your video card. As a matter of fact, you most likely know how much memory your video card has because it''s an advertised feature of all current video cards. But how is this memory used?
We''ve already discussed the fact that the active display is really an area of video memory. How much memory is needed depends on the current screen resolution and color depth. For instance, a 1600x1200 resolution at 32-bit color requires at least 7,680,000 bytes of memory! If your video card doesn''t have the mimumum required memory for a video mode, it won''t be available to you. On the other hand, you usually have enough for your current display resolution with memory left over. It''s this ''extra'' available memory that we''re interested in.
The whole basis for animation involves using one or more backbuffers, preparing the image, and ''copying'' or ''flipping'' the backbuffer''s memory onto the active display memory. Traditionally, this was done by allocating some system memory for the backbuffer, and then when the time was right copying the entire backbuffer onto the display adapter. These days, where there is memory available on the video card, it is highly desirable to have this backbuffer on the video card''s memory, so that copying isn''t even necessary -- the video card is merely instructed to use the backbuffer''s memory as the new active display memory.
The beauty of DirectDraw is that it allows us to create DirectDrawSurface objects in either video or system memory. By default, DirectDraw attempts to use video memory if it''s available, falling back on system memory. Keep in mind that if our backbuffer is in system memory, this memory has to be copied into video memory which is a relatively slow operation. Either way, DirectDraw takes care of everything behind the scenes -- we just use our DirectDrawSurfaces normally.
The moral of the story here is that everything done within video memory is fast, and moving memory from the system to the video card is slower. DirectDraw will do its best to allocate video memory for our surfaces, but if it can''t system memory is used, and this is transparent to us.
DirectDrawSurface Objects
At a minimum, DirectDraw requires us to create a DirectDrawSurface object for the active display memory. For proper animation, it''s important to create at least one other DirectDrawSurface object to serve as a backbuffer. DirectDraw is aware of our needs, and provides a ''flipping chain'' for swapping primary and backbuffer memory. If both DirectDraw surfaces are in video memory, DirectDraw only needs to redirect the video card on where the current display''s memory is -- no copying is necessary.
DirectDraw also allows us to create DirectDrawSurface objects of any size. The trick here is to load any bitmap images we''d like onto DirectDrawSurface objects so that they can be transferred to the backbuffer surface quickly and effortlessly. In our game template code, a DirectDrawSurface is created for RESOURCE.BMP -- its size exactly matches the dimensions of the bitmap itself. You''ll notice that two functions inside of UTILS.CPP,
Utils_LoadBitmap() and
Utils_CopyBitmap() are responsible for the task, and the procedure involves loading a bitmap from disk, creating a DirectDrawSurface object with matching dimensions, and finally copying the bitmap image into the DirectDrawSurface object''s memory.
Here''s
Utils_LoadBitmap():
//////////////////////////////////////////////////////////////////////////////
// Utils_LoadBitmap
// Loads the requested bitmap from a file and creates a DirectDraw7
// surface for it.
//
// PARAMETERS:
// pDD - DirectDraw7 object
// szBitmap - Name of the bitmap file
// dx, dy - Bitmap size override; use 0, 0 for actual size
//
// RETURN VALUE: The bitmap surface
//
IDirectDrawSurface7* Utils_LoadBitmap(IDirectDraw7 *pDD, LPCSTR szBitmap,
int dx, int dy)
{
HBITMAP hbm;
BITMAP bm;
DDSURFACEDESC2 ddsd;
IDirectDrawSurface7 *pDDS;
// Load the bitmap from a file
hbm = (HBITMAP)LoadImage(NULL, szBitmap, IMAGE_BITMAP, dx, dy,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);
if (hbm == NULL) return NULL;
// Get the size of the bitmap
GetObject(hbm, sizeof(bm), &bm);
// Create a DirectDrawSurface for this bitmap
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS|DDSD_HEIGHT|DDSD_WIDTH;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = bm.bmWidth;
ddsd.dwHeight = bm.bmHeight;
if (pDD->CreateSurface(&ddsd, &pDDS, NULL) != DD_OK)
return NULL;
// Copy the bitmap into the surface
Utils_CopyBitmap(pDDS, hbm, 0, 0, 0, 0);
DeleteObject(hbm);
return pDDS;
}
LoadImage() is a Win32 API function that does the actual loading of the bitmap. From there, we need the bitmap structure itself so that we can ascertain its dimensions, and from there a DirectDrawSurface object is created. Next, we need to fill the DirectDrawSurface memory with the actual bitmap:
//////////////////////////////////////////////////////////////////////////////
// Utils_CopyBitmap
//
// Copies the bitmap to the given surface.
//
// PARAMETERS:
// pDDS - The surface to copy the bitmap onto
// hbm - The bitmap to copy
// x, y - Source position
// dx, dy - Width and height of the bitmap
//
// RETURN VALUE: DD_OK, E_FAIL or other DD-related error code
//
HRESULT Utils_CopyBitmap(IDirectDrawSurface7 * pDDS, HBITMAP hbm,
int x, int y, int dx, int dy)
{
HDC hdcImage;
HDC hdc;
BITMAP bm;
DDSURFACEDESC2 ddsd;
HRESULT hResult;
if (hbm == NULL || pDDS == NULL)
return E_FAIL;
// Make sure this surface is restored
pDDS->Restore();
// Select bitmap into a memoryDC so we can use it.
hdcImage = CreateCompatibleDC(NULL);
SelectObject(hdcImage, hbm);
// Get the size of the bitmap
GetObject(hbm, sizeof(bm), &bm);
// Use this size unless overridden
dx = dx == 0 ? bm.bmWidth : dx;
dy = dy == 0 ? bm.bmHeight : dy;
// Get the size of the surface
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_HEIGHT|DDSD_WIDTH;
pDDS->GetSurfaceDesc(&ddsd);
if ((hResult = pDDS->GetDC(&hdc)) == DD_OK)
{
StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, x, y,
dx, dy, SRCCOPY);
pDDS->ReleaseDC(hdc);
}
DeleteDC(hdcImage);
return hResult;
}
It can sometimes happen that a DirectDrawSurface''s memory gets freed by the operating system behind our backs (for instance when the user switches applications), so we start by reclaiming our memory with
Restore(). We''re ready now to bring up the bitmap image memory, so we create a device context for the image (don''t worry -- it''s Windows-specific procedure). The
StretchBlt() function copies an area of memory from a source to a destination, and requires the desired dimensions for each. By default, we don''t want the image stretched in any way, so the original dimensions of both the image and the DirectDrawSurface area (which are the same in this case) are used. Finally, we discard the Windows bitmap since we have a good copy in our DirectDrawSurface object.
Perhaps now you can see why functions such as these are tucked away in UTILS.CPP instead of GAMEMAIN.CPP -- they''re ugly and complicated, but rest-assured that so long as they''re working, you don''t need to worry about them any longer.
Now, imagine the situation where our primary surface, backbuffer and our image surfaces are all in video memory. Since we don''t need to do any memory copying from system memory, everything is going to be blazingly fast! And even if there isn''t enough video memory to go around, DirectDraw will do its best to generate the type of speed we''re looking for.
Moving Memory
Okay, so now we''re ready for action. The functions that we''re looking for are called
Blt() and
BltFast(). Their job is to copy DirectDrawSurface memory from one surface to another. These functions will allow us to copy our images from various DirectDrawSurface objects onto our backbuffer surface. As you can probably guess,
Blt() is the general-purpose memory copy function, and
BltFast() is faster but less-featured. Let''s take a look at each of them:
HRESULT Blt (
LPRECT lpDestRect,
LPDIRECTDRAWSURFACE7 lpDDSrcSurface,
LPRECT lpSrcRect,
DWORD dwFlags,
LPDDBLTFX lpDDBltFx );
The first three fields are fairly simple -- you can specify source and destination regions, and the source surface. You can use NULL for any of these RECTs to use the entire surface, but keep in mind that if the source and destination are different sizes,
Blt() will shrink or stretch the image to fit automatically. For flags, it''s best to consult the online SDK documentation -- we''ll be using some of them, so they''ll be explained at that time. And as if those flags weren''t enough, theres a large DDBLTFX structure that can be used for all kinds of strange and wonderful things. There''s no way we can discuss them all, but there''s one that we already use in
EraseBackground() (part of GAMEMAIN.CPP) -- a fill color for painting the entire screen.
And, here''s
BltFast():
HRESULT BltFast (
DWORD dwX,
DWORD dwY,
LPDIRECTDRAWSURFACE7 lpDDSrcSurface,
LPRECT lpSrcRect,
DWORD dwTrans );
For this function, we only need specify the x and y coordinates for the destination surface to copy to -- the source RECT is copied directly with its original size. As for the last field, it offers us a small subset of the many features
Blt() has, most notably color-keying (which we''ll be discussing shortly).
Aside from the limited features that
BltFast() has, there''s another limitation that we''ll be interested in -- clipping. We haven''t discussed clipping yet, but we will shortly.
Without further adieu, here''s a
Game_Main() routine that copies the lpDDSRes surface onto the backbuffer:
void Game_Main()
{
RECT rectSrc;
DDSURFACEDESC2 ddsd2;
// Get the description for the resource surface
memset(&ddsd2, 0, sizeof(ddsd2));
ddsd2.dwSize = sizeof(ddsd2);
G.lpDDSRes->GetSurfaceDesc(&ddsd2);
// Clear the backbuffer
EraseBackground();
// Prepare the destination rect for the entire resource surface
rectSrc.left = rectSrc.top = 0;
rectSrc.right = ddsd2.dwWidth;
rectSrc.bottom = ddsd2.dwHeight;
// Draw the resource bitmap to the background surface
G.lpDDSBack->BltFast(0, 0, G.lpDDSRes, &rectSrc,
DDBLTFAST_WAIT|DDBLTFAST_NOCOLORKEY);
// Flip the surfaces
G.lpDDSPrimary->Flip(NULL, 0);
}
Something that you''ll learn when coding is that sometimes it''s better to start with the actual function call that does all the work, and then work backwards to prepare the data the main function call needs. In this case, I start by realizing that I needed to copy surface memory, and without any fancy features, so I look up
BltFast() in the online SDK docs. This leads me to realize that I''m going to need the width and height of the source surface. Sure, I could have hard-coded these values, but it''s always better to generalize the code, so I used
GetSurfaceDesc() instead. Since
GetSurfaceDesc() requires a DDSURFACEDESC2 structure (with its dwSize field filled in), I went ahead and prepared it -- this is pretty common procedure in DirectX. Finally, I look through the possible flags for
BltFast() and found the two that I need.
You''ll notice that this code will work regardless of the screen resolution and color depth that you use in GLOBALS.H. However, for the remainder of this article, please set your template code to use 640x480 and 8-bit color. Naturally, this will incorporate palettes, so a DirectDrawPalette object will be created from the palette included in RESOURCE.BMP.
Palettes
Do you have an image editing tool?
No, I don''t mean PAINT. A decent image editing tool will allow you to manipulate an 8-bit bitmap''s palette. If you take a look at RESOURCE.BMP''s palette, you''ll see that the letters themselves use palette index 93, and the black background uses palette index 0. If you were to change the color in index 93, the color of the letters in the bitmap would instantly change -- same with index 0 and the background.
Our DirectDrawPalette works much the same way. You can use
GetEntries() to get palette index color information, and
SetEntries() to set these entries. At the exact moment the palette entries are set, all pixels on the screen that use those palette indicies are instantly changed. This is a fundamental and powerful feature of using palettes.
To demonstrate, try the following code:
void Game_Main()
{
RECT rectSrc;
DDSURFACEDESC2 ddsd2;
PALETTEENTRY Pal;
// Create a random color
Pal.peRed = rand()%256;
Pal.peGreen = rand()%256;
Pal.peBlue = rand()%256;
// Assign this random color to the palette index for the
// on-screen alpha-numeric text
G.lpDDPalette->SetEntries(0, 93, 1, &Pal);
// Get the description for the resource surface
memset(&ddsd2, 0, sizeof(ddsd2));
ddsd2.dwSize = sizeof(ddsd2);
G.lpDDSRes->GetSurfaceDesc(&ddsd2);
// Clear the backbuffer
EraseBackground();
// Prepare the destination rect for the entire resource surface
rectSrc.left = rectSrc.top = 0;
rectSrc.right = ddsd2.dwWidth;
rectSrc.bottom = ddsd2.dwHeight;
// Draw the resource bitmap to the background surface
G.lpDDSBack->BltFast(0, 0, G.lpDDSRes, &rectSrc,
DDBLTFAST_WAIT|DDBLTFAST_NOCOLORKEY);
// Flip the surfaces
G.lpDDSPrimary->Flip(NULL, 0);
}
Remember to set your GLOBALS.H for 8-bit color mode. Because the actual 24-bit color inside of palette index 93 is constantly being changed, the image on the screen will change as well wherever index 93 is being used (in this case everything). You must have seen effects like this in older 2D games -- changing the palette at run-time is fast and simple, and many great effects are accomplished using this technique...sprite and tile animation come to mind.
By playing with this code and making your own 8-bit images, you''ll solidify your understanding of palettes and color, and will be able to acheive undless effects as well.
Color Keying
To demonstrate what color keying is, take the BaseCode1 template that displays RESOURCE.BMP on the screen, and change the dwFillColor field in
EraseBackground() to 1 instead of 0. Make sure you''re in 8-bit color mode, and run the program.
What you''ll see is the image at the top-left of the display, and the rest of the background of the screen in a dark-red color. Since the background in the image was black (index 0), you can see it as black on the display. But what if you didn''t want to see the image background?
With color keying, you can tell DirectDraw not to copy over certain colors from an image. You''ve surely seen this kind of thing on television where actors stand in front of a blue screen, and then the blue area is replaced with other images. In our case, we''d like to exclude the black part of our image from the copy process so that only the letters and numbers themselves are copied over.
Excluding colors from the source image is called source color keying, and is a two-step process:
- Use the SetColorKey() method of the DirectDrawSurface object that holds the image to color key
- When using Blt() or BltFast(), indicate that you''d like to use source color keying
Since you only need to set the source color key once, add this code to the end of
DD_Init():
// Set the color key to zero (palette index 0)
DDCOLORKEY ck;
ck.dwColorSpaceLowValue = ck.dwColorSpaceHighValue = 0;
G.lpDDSRes->SetColorKey(DDCKEY_SRCBLT, &ck);
...and in the
BltFast() call during
Game_Main(), change DDBLTFAST_NOCOLORKEY to DDBLTFAST_SRCCOLORKEY. Run the program again, and you''ll see that the background color (dark red, index 1) can be seen behind the letters and numbers of the image.
For 8-bit colors, the DDCOLORKEY structure is used to specify the range of palette indicies to be keyed on. For higher color resolutions, you supply two RGB colors for the high and low ranges, and any image color that fits in that RGB range will be keyed. We''ll be making extensive use of color keying in the articles to come.
Clipping
When you take an image and copy it onto the backbuffer using
Blt() or
BltFast(), DirectDraw sincerely hopes that you don''t try anything silly like drawing the image past the end of the screen. If you know that you''ll always be drawing properly onto the display you don''t need clipping, but if you might (or aren''t sure), you need to use DirectDraw''s clipping feature.
Here''s an example of clipping in action: picture a boat drifting across the display from the left to the right. As the boat first appears on the far left, you can only see the front of the boat -- the rest is off-screen. In order to draw this you have to either only copy the visible portion of the image over, or use DirectDraw clipping (in which case you can just go ahead and copy the entire image -- DirectDraw will ''clip'' the edge off for you). After all, if you try copying to an area of memory that doesn''t even exist (and DirectDraw''s not clipping for you), it''s crash-time, baby.
To create a clipper, call DirectDraw''s
CreateClipper() method. After this, use the
SetHWnd() method to attach the clipper to the primary window, and finally call
SetClipper() on the backbuffer DirectDrawSurface to complete the initialization. Now, whenever
Blt() is used, anything copied onto the backbuffer surface will be properly clipped. Remember that
BltFast() won''t work when there''s a clipper attached, so if you''re going to play around, change
Blt() to
BltFast() in
Game_Main().
Touching the Surface
Of course, I''ve left a lot out in this article, but it''s simply not possible to go through absolutely everything that DirectDraw has to offer -- that''s what the SDK documentation is for. Let me reitirate: there are overviews for every DirectX component, so you should really do yourself a favor and read up on the components as we use them here.
My job, however, was to introduce you to DirectDraw surfaces, image loading, source color keying, palette manipulation, memory copying ala
Blt() and
BltFast() and clipping.
In the next article we''re going to start actually using this material for something useful...and trust me, once you get the hang of images and
Blt()ting, you''ll have one of the most powerful fundamental game development tools at your disposal.
Comments? Questions? Please reply to this topic.