Today we're going to go over basic DirectDraw graphics using both palettized and RGB modes. What's the difference? Well, a palettized screen mode is probably what you're used to if you've been programming in DOS. To plot pixels, you write a single byte to the video memory. That byte is an index into a lookup table full of colors, which is called a palette. RGB modes are different, because there's no lookup table. To plot a pixel in an RGB display mode, you write the values for red, green, and blue directly into the video memory. Any color depth higher than 8-bit is RGB instead of palettized. But now we're getting ahead of ourselves!
In writing this article, I assume you have either read my previous entries in the series, or are otherwise familiar with setting up DirectDraw and creating surfaces. We'll be using version 7 of DirectX, which contains the most recent DirectDraw interfaces. Actually, on a side note, the DirectDraw interfaces in DirectX 7 will be the last ones released! Don't worry, future versions of DirectX will still be backwards-compatible, but future implementations will have DirectDraw and Direct3D integrated into one component. But let's not worry about that now.
One last thing I will mention before we get started: the information on palettes is not necessary for anything I'll be covering later in the series, so if you're not interested in palettized modes, feel free to skip over the first part of this article and pick up when we get to the Pixel Formats section. Now that my disclaimers are at an end, let's get going!
[size="5"]Creating a DirectDraw Palette
Before working with graphics in a color depth of 8-bit or less, you must create a palette, which is simply a table full of colors. More specifically, in DirectX, a palette is a table full of [font="Courier New"][color="#000080"]PALETTEENTRY[/color][/font] structures. To create one, there are three steps we need to take:
- Create the color lookup table.
- Get a pointer to an [font="Courier New"][color="#000080"]IDirectDrawPalette[/color][/font] interface.
- Attach the palette to a DirectDraw surface.
typedef struct tagPALETTEENTRY { // pe
BYTE peRed;
BYTE peGreen;
BYTE peBlue;
BYTE peFlags;
} PALETTEENTRY;
Now, as I said, we're going to need a bunch of these structures, so the logical thing to do is to declare an array that we'll use for the lookup table. It would look like this:
PALETTEENTRY palette[256];
FILE* file_ptr;
int x;
if ((file_ptr = fopen("palette.dat", "rb")) != NULL)
{
fread(palette, sizeof(PALETTEENTRY), 256, file_ptr);
fclose(file_ptr);
}
HRESULT CreatePalette(
DWORD dwFlags,
LPPALETTEENTRY lpColorTable,
LPDIRECTDRAWPALETTE FAR *lplpDDPalette,
IUnknown FAR *pUnkOuter
);
[font="Courier New"][color="#000080"]DWORD dwFlags[/color][/font]: Any number of a series of flags describing the palette object. As usual, you can combine the values with the | operator. The valid flags are:
DDPCAPS_1BIT1-bit color; this corresponds to a 2-color palette.DDPCAPS_2BIT2-bit color; this corresponds to a 4-color palette.DDPCAPS_4BIT4-bit color; this corresponds to a 16-color palette.DDPCAPS_8BIT8-bit color; this corresponds to a 256-color palette.DDPCAPS_8BITENTRIESIndicates that the index refers to an 8-bit color index. That is, each color entry is itself an index to a destination surface's 8-bit palette. It must be combined with DDPCAPS_1BIT, DDPCAPS_2BIT, or DDPCAPS_4BIT. This is called an indexed palette. Weird, hey?DDPCAPS_ALPHAIndicates that the peFlags member of each PALETTEENTRY should be interpreted as an alpha value. Palettes created with this flag can only be attached to a Direct3D texture surface, since DirectDraw itself doesn't support alpha-blending.DDPCAPS_ALLOW256Allows all 256 entries of an 8-bit palette to be used. Normally, index 0 is reserved for black, and index 255 is reserved for white.DDPCAPS_INITIALIZEIndicates that the palette should be initialized with the values of an array of PALETTEENTRYs that is passed in another parameter of CreatePalette().DDPCAPS_PRIMARYSURFACEIndicates that the palette will be attached to the primary surface, so that altering it immediately changes the colors of the display.DDPCAPS_VSYNCForces palette changes to take place only during the vertical blank, minimizing the odd effects normally associated with palette-changing during a drawing cycle. This is not fully supported.For the most part, you'll want to use [font="Courier New"][color="#000080"]DDPCAPS_8BIT | DDPCAPS_INITIALIZE[/color][/font], though you can take out the latter if you just want to create an empty palette and set it up later. Or you may want to add [font="Courier New"][color="#000080"]DDPCAPS_ALLOW256[/color][/font] if you really want to change the two normally reserved entries.
[font="Courier New"][color="#000080"]LPPALETTEENTRY lpColorTable[/color][/font]: This is a pointer to the lookup table we created, so just pass the name of the array.
[font="Courier New"][color="#000080"]LPDIRECTDRAWPALETTE FAR *lplpDDPalette[/color][/font]: This is the address of a pointer to an [font="Courier New"][color="#000080"]IDirectDrawPalette[/color][/font] interface, which will be initialized if the call succeeds.
[font="Courier New"][color="#000080"]IUnkown FAR *pUnkOuter[/color][/font]: As always, this is for advanced COM stuff and should be set to [font="Courier New"][color="#000080"]NULL[/color][/font].
That's not too bad, so now we can go ahead and create our palette object. The last step is to attach that palette to a surface, which requires only a simple function call to [font="Courier New"][color="#000080"]IDirectDrawSurface7::SetPalette()[/color][/font]. The function is shown below:
HRESULT SetPalette(LPDIRECTDRAWPALETTE lpDDPalette);
LPDIRECTDRAWPALETTE lpddpal;
// create the palette object
if (FAILED(lpdd7->CreatePalette(DDPCAPS_8BIT | DDPCAPS_INITIALIZE,
palette, &lpddpal, NULL)))
{
// error-handling code here
}
// attach to primary surface
if (FAILED(lpddsPrimary->SetPalette(lpddpal)))
{
// error-handling code here
}
[size="5"]Pixel Formats
As I said earlier, hen you're writing a pixel into memory in a palettized mode, you write one byte at a time, and each byte represents an index into the color lookup table. In RGB modes, however, you write the actual color descriptors right into memory, and you need more than one byte for each color. The size of the memory write is equivalent to the color depth; that is, for 16-bit color, you write two bytes (16 bits) for each pixel, and so on. Let's start out at the top, because it's easiest to understand. 32-bit color uses a pixel format like this, where each letter is one bit:
AAAA AAAA RRRR RRRR GGGG GGGG BBBB BBBB
A pixel in 32-bit color needs to be 32 bits in size, and so the variable type we use to hold one is a UINT, which is an unsigned integer. Usually I use macros to convert RGB data into the correct pixel format, so let me show you one here. Hopefully if you're a little confused at this point, this will clear things up a bit:
#define RGB_32BIT(r, g, b) ((r << 16) | (g << 8) | (b))
UINT white_pixel = RGB_32BIT(255, 255, 255);
RRRR RRRR GGGG GGGG BBBB BBBB
Now, 16-bit color is a bit tricky, because not every video card uses the same pixel format! There are two formats supported. One of them, which is by far more common, has five bits for red, six bits for green, and five bits for blue. The other format has five bits for each, and the high bit is unused. This is used mostly on older video cards. So the two formats look like this:
565 format: [font="Courier New"][color="#000080"]RRRR RGGG GGGB BBBB[/color][/font]
555 format: [font="Courier New"][color="#000080"]0RRR RRGG GGGB BBBB[/color][/font]
When you're working in a 16-bit color depth, you'll need to determine whether the video card uses 565 or 555 format, and then apply the appropriate technique. It's kind of a pain, but there's no way around it if you're going to use 16-bit color. Since there are two different formats, you'd write two separate macros:
#define RGB_16BIT565(r, g, b) ((r << 11) | (g << 5) | (b))
#define RGB_16BIT555(r, g, b) ((r << 10) | (g << 5) | (b))
USHORT white_pixel_565 = RGB_16BIT565(31, 63, 31);
USHORT white_pixel_555 = RGB_15BIT555(31, 31, 31);
Here is probably a good place to show you just how exactly how determine whether a machine is using the 555 or 565 format when you're running in 16-bit color. The easiest way to do it is to call the [font="Courier New"][color="#000080"]GetPixelFormat()[/color][/font] method of the [font="Courier New"][color="#000080"]IDirectDrawSurface7[/color][/font] interface. Its prototype looks like this:
HRESULT GetPixelFormat(LPDDPIXELFORMAT lpDDPixelFormat);
Now that we've seen all the pixel formats you can encounter, we can get into actually showing graphics in DirectX. About time, wouldn't you say? Before we can manipulate the actual pixels on a surface, though, we need to lock the surface, or at least a part of it. Locking the surface will return a pointer to the memory the surface represents, so then we can do whatever we want with it.
[size="5"]Locking Surfaces
Not surprisingly, the function we'll use to do this is [font="Courier New"][color="#000080"]IDirectDrawSurface7::Lock()[/color][/font]. Let's take a look at it.
HRESULT Lock(
LPRECT lpDestRect,
LPDDSURFACEDESC lpDDSurfaceDesc,
DWORD dwFlags,
HANDLE hEvent
);
[font="Courier New"][color="#000080"]LPRECT lpDestRect[/color][/font]: This is a [font="Courier New"][color="#000080"]RECT[/color][/font] that represents the area on the surface we want to lock. If you want to lock the entire surface, simply pass [font="Courier New"][color="#000080"]NULL[/color][/font].
[font="Courier New"][color="#000080"]LPDDSURFACEDESC2 lpDDSurfaceDesc[/color][/font]: This is a pointer to a [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font] structure, which is the big baddie we covered last time. All you need to do is initialize the structure, then pass the pointer. If [font="Courier New"][color="#000080"]Lock()[/color][/font] succeeds, it fills in some of the members of the structure for you to use.
[font="Courier New"][color="#000080"]DWORD dwFlags[/color][/font]: What kind of DirectX function would this be if it didn't have a list of flags do go along with it? Here are the most useful ones, which you can combine in the usual way:
DDLOCK_READONLYIndicates that the surface being locked will only be read from, not written to.DDLOCK_SURFACEMEMORYPTRIndicates that a valid memory pointer to the upper-left corner of the specified RECT should be returned in the DDSURFACEDESC2 structure. Again, if no RECT is specified, the pointer will be to the upper-left corner of the surface.DDLOCK_WAITIf a lock cannot be obtained because a blit is in progress, this flag indicates to keep retrying until a lock is obtained, or a different error occurs.DDLOCK_WRITEONLYIndicates that the surface being locked will only be written to, not read from.Since we'll be using the lock to manipulate pixels, you'll always want to use [font="Courier New"][color="#000080"]DDLOCK_SURFACEMEMORYPTR[/color][/font]. And even though we haven't gotten to using the blitter yet, it's usually a good idea to include [font="Courier New"][color="#000080"]DDLOCK_WAIT[/color][/font] as well.
[font="Courier New"][color="#000080"]HANDLE hEvent[/color][/font]: This parameter is not used and should be set to [font="Courier New"][color="#000080"]NULL[/color][/font].
Once we lock the surface, we need to take a look at the [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font] structure to get some information about the surface. We went over all of the members of this structure last time, but there are only two that we need to concern ourselves with at this point. Since both are very important, I'll list those two here again.
[font="Courier New"][color="#000080"]LONG lPitch[/color][/font]: The [font="Courier New"][color="#000080"]lPitch[/color][/font] member represents the number of bytes in each display line. You'd think this would be obvious. For example, in 640x480x16, there are 640 pixels in each line, and each one requires 2 bytes for color information, so the pitch (also called the "stride") should be 1280 bytes, right? Well, on some video cards, it will be greater than 1280. The extra memory on each line doesn't hold any graphical data, but sometimes it's there because the video card can't create a perfectly linear memory mode for the display mode. This will happen on a very small percentage of video cards, but you need to take it into account.
[font="Courier New"][color="#000080"]LPVOID lpSurface[/color][/font]: This is a pointer to the memory represented by the surface. No matter what display mode you're using, DirectDraw creates a linear addressing mode you can use to manipulate the pixels of the surface.
The [font="Courier New"][color="#000080"]lpSurface[/color][/font] pointer is pretty easy to understand, but are you following me on this whole pitch thing? It's an important value to remember, because you'll have to use it to calculate the offset to a particular pixel. We'll get back to it in just a minute. There's one thing I want to get out of the way first. When you're done plotting pixels, you need to unlock the surface that you locked. The prototype for [font="Courier New"][color="#000080"]IDirectDrawSurface7::Unlock()[/color][/font] is this:
HRESULT Unlock(LPRECT lpRect);
[size="5"]Plotting Pixels
The first thing is to typecast the pointer we got from the [font="Courier New"][color="#000080"]Lock()[/color][/font] function. Logically, we're going to want a pointer that's the same size as the pixels we're writing. So we want a [font="Courier New"][color="#000080"]UCHAR*[/color][/font] for 8-bit color depth, a [font="Courier New"][color="#000080"]USHORT*[/color][/font] for 16-bit, and a [font="Courier New"][color="#000080"]UINT*[/color][/font] for 32-bit. But what about 24-bit? Since there's no 24-bit data type, we'll need to use a [font="Courier New"][color="#000080"]UCHAR*[/color][/font] for this also, and do things a little differently.
We should also convert the [font="Courier New"][color="#000080"]lPitch[/color][/font] member so it's in the same units as our pointer. Remember, when we first retrieve [font="Courier New"][color="#000080"]lPitch[/color][/font] from the [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font] structure, it's in bytes. For 16-bit mode, we should divide it by 2 to get the pitch in terms of [font="Courier New"][color="#000080"]USHORTs[/color][/font], and for 32-bit mode, we divide by 4 to get it in terms of [font="Courier New"][color="#000080"]UINT[/color][/font]s.
Let's stop for a second and look at some example code. Suppose we are in 32-bit mode and want to lock the primary surface for plotting pixels. Here's what we would do:
// declare and initialize structure
DDSURFACEDESC2 ddsd;
INIT_DXSTRUCT(ddsd);
// lock the surface
lpddsPrimary->Lock(NULL, &ddsd, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
// now convert the pointer and the pitch
UINT* buffer = (UINT*)ddsd.lpSurface;
UINT nPitch = ddsd.lPitch >> 2;
inline void PlotPixel32(int x, int y, UINT color, UINT *buffer, int nPitch)
{
buffer[y*nPitch + x] = color;
}
The formula used to locate the pixel at location ([font="Courier New"][color="#000080"]x[/color][/font], [font="Courier New"][color="#000080"]y[/color][/font]) is [font="Courier New"][color="#000080"]y*nPitch + x[/color][/font]. This makes sense because [font="Courier New"][color="#000080"]nPitch[/color][/font] is the number of [font="Courier New"][color="#000080"]UINT[/color][/font]s in a line. Multiplying that by the [font="Courier New"][color="#000080"]y[/color][/font] value yields the correct row, and then we just need to add [font="Courier New"][color="#000080"]x[/color][/font] to locate the correct column. That's all you need to know! Pretty simple, hey? Let me show you a couple of functions for plotting pixels in 16-bit and 8-bit modes, because they're very similar:
inline void PlotPixel8(int x, int y, UCHAR color, UCHAR* buffer, int byPitch)
{
buffer[y*byPitch + x] = color;
}
inline void PlotPixel16(int x, int y, USHORT color, USHORT* buffer, int nPitch)
{
buffer[y*nPitch + x] = color;
}
inline void PlotPixel24(int x, int y, UCHAR r, UCHAR g, UCHAR b, UCHAR* buffer, int byPitch)
{
int index = y*byPitch + x*3;
buffer[index] = r;
buffer[index+1] = g;
buffer[index+2] = b;
}
[size="5"]Notes on Speed
There are a few things you should do to make sure this all goes as quickly as possible. First of all, locking a surface is not the fastest thing in the world, so you should try to keep the number of times you lock a surface per frame to a minimum. For many things, including a simple pixel-plotting demo, one lock per frame is all you'll need.
Second, let's say you're working in 640x480x16. The pitch will almost always be 1,280 bytes. You still need to take into consideration that it might not always be that way, but you may want to add some optimized code to take advantage of the fact that the pitch will almost always be 1,280. More specifically, you can use a pixel-plotting function that uses bit shifts instead of multiplication to locate the correct pixel. The function we were using earlier used this line of code:
buffer[y*nPitch + x] = color;
buffer[(y<<9) + (y<<7) + x] = color;
Finally, if you are going to be using two functions -- one using multiplication, and the other using bit-shifts -- keep the comparison outside of the plotting loop. That is, don't do this:
for (x=0; x<1000; x++)
{
if (nPitch == 640)
PlotPixelFast16();
else
PlotPixel16();
}
if (nPitch == 640)
{
for (x=0; x<1000; x++)
PlotPixelFast16( parameters );
}
else
{
for (x=0; x<1000; x++)
PlotPixel16( parameters );
}
for (x=0; x<640; x++)
PlotPixel16(x, 50, color, buffer, pitch);
// get the address of the line
USHORT* temp = &buffer[50*pitch];
// plot the pixels
for (x=0; x<640; x++)
{
*temp = color;
temp++;
}
Considering how long the previous articles in this series have been, stopping this one here would seem like kind of a rip-off, wouldn't it? Now that we know how to plot pixels, let's take a look at some of the things you can use it for.
[size="5"]Fading Out
One of the most commonly used screen transitions in games is the fade to black, or the fade in from black. Each one is achieved in the same manner. You simply draw your frame, then apply some sort of transformation that alters the brightness of the image. To fade out, you decrease the brightness from 100% to 0%, and to fade in, you increase it from 0% to 100%. If you're working in a palettized mode, you've got it easy! You just change the colors in your palette, and the result shows up onscreen. If you're working in RGB mode, you have to consider other methods.
Now, I'll say right here that manually fading the screen has some better alternatives. You can use Direct3D, which supports alpha-blending, to set each frame as a texture, then set the transparency level. Or, even easier, you can use the DirectDraw color/gamma controls. But if you only want to fade part of the screen, or fade to a color besides black, and you're not a Direct3D expert -- I'm certainly not! -- then knowing how to do the manual transform might come in handy.
Basically all you do is to read each pixel and break it up into red, green, and blue. Then you multiply each of the three values by the level of fading to apply, recombine the RGB values, and write the new color back out to the buffer. Sound complicated? It's not too bad. Take a look at this (highly unoptimized) sample code. It applies a fade to a viewport of size 200x200 in the upper-left corner of the display, in a 16-bit color depth using the 565 pixel format.
void ApplyFade16_565(float pct, USHORT* buffer, int pitch)
{
int x, y;
UCHAR r, g, b;
USHORT color;
for (y=0; y<200; y++)
{
for (x=0; x<200; x++)
{
// first, get the pixel
color = buffer[y*pitch + x];
// now extract red, green, and blue
r = (color & 0xF800) >> 11;
g = (color & 0x0730) >> 5;
b = (color & 0x001F);
// apply the fade
r = (UCHAR)((float)r * pct);
g = (UCHAR)((float)g * pct);
b = (UCHAR)((float)b * pct);
// write the new color back to the buffer
buffer[y*pitch + x] = RGB_16BIT565(r, g, b);
}
}
}
void ApplyFade16_565(float pct, USHORT* buffer, int pitch)
{
int x, y;
UCHAR r, g, b;
USHORT color;
USHORT* temp = buffer;
int jump = pitch - 200;
for (y=0; y<200; y++)
{
for (x=0; x<200; x++, temp++) // move pointer to next pixel each time
{
// first, get the pixel
color = *temp;
// now extract red, green, and blue
r = (color & 0xF800) >> 11;
g = (color & 0x0730) >> 5;
b = (color & 0x001F);
// apply the fade
r = (UCHAR)((float)r * pct);
g = (UCHAR)((float)g * pct);
b = (UCHAR)((float)b * pct);
// write the new color back to the buffer
*temp = RGB_16BIT565(r, g, b);
}
// move pointer to beginning of next line
temp+=jump;
}
}
USHORT clut[65536][20];
*temp = clut[*temp][index];
If you're wondering where I got the dimensions for the lookup table, 65536 is 2^16, so that's how many colors there are in a 16-bit color depth. Since our color values are unsigned values, they range from 0 to 65535. The 20 is just the number of increments you'd be using for a fade. It seemed a decent choice to me, considering the memory involved.
For 24-bit and 32-bit color, you obviously can't create a lookup table to span every color combination, so what you could do is use three smaller tables:
UCHAR red[256];
UCHAR green[256];
UCHAR blue[256];
[size="5"]Basic Transparency
Overlaying a transparent image on an opaque one is not something you can use a lookup table for, because it would have to have 65,536 entries in both dimensions. It's going to be awhile before the average computer has the 8.6 GB of RAM it takes to hold that monster. So you'll have to do all the calculations for each pixel. I'll give you the basic idea. Suppose you want to place image A on top of image B, and image A is to have a transparency percentage of [font="Courier New"][color="#000080"]pct[/color][/font], which is a floating-point number between 0 and 1, where 0 is fully transparent (invisible) and 1 is fully opaque. Then, let's call a pixel in image A [font="Courier New"][color="#000080"]pixelA[/color][/font], and its counterpart in image B we'll call [font="Courier New"][color="#000080"]pixelB[/color][/font]. You would apply the following equation:
color = (pixelA * pct) + (pixelB * (1-pct));
The other thing you might want to do is to create a window of a solid color that's partially transparent. If you've seen a demo or screenshots of my upcoming RPG, Terran, you know what I mean. An effect like that can be done entirely with a lookup table, because in Terran's case, I just needed to provide a color of blue for every possible color on the screen. In fact, a lookup table is exactly how I do the effect. I'll show you exactly what I mean.
void Init_CLUT(void)
{
int x, y, bright;
UCHAR r, g, b;
// calculate textbox transparency CLUT
for (x=0; x<65536; x++)
{
// transform RGB data
if (color_depth == 15)
{
r = (UCHAR)((x & 0x7C00) >> 10);
g = (UCHAR)((x & 0x03E0) >> 5);
b = (UCHAR)(x & 0x001F);
}
else // color_depth must be 16
{
r = (UCHAR)((x & 0xF800) >> 11);
g = (UCHAR)((x & 0x07E0) >> 6); // shifting 6 bits instead of 5 to put green
b = (UCHAR)(x & 0x001F); // on a 0-31 scale instead of 0-63
}
// find brightness as a weighted average
y = (int)r + (int)g + (int)b;
bright = (int)((float)r * ((float)r/(float)y) +
(float)g * ((float)g/(float)y) +
(float)b * ((float)b/(float)y) + .5f);
// write CLUT entry as 1 + one half of brightness
clut[x] = (USHORT)(1 + (bright>>1));
}
}
y = r + g + b;
brightness = r*(r/y) + g*(g/y) + b*(b/y);
int Text_Box(USHORT *ptr, int pitch, LPRECT box)
{
int x, y, jump;
RECT ibox;
// leave room for the border
SetRect(&ibox, box->left+3, box->top+3, box->right-3, box->bottom-3);
// update surface pointer and jump distance
ptr += (ibox.top * pitch + ibox.left);
jump = pitch - (ibox.right - ibox.left);
// use CLUT to apply transparency
for (y=ibox.top; y
{
for (x=ibox.left; x
*ptr = clut[*ptr];
ptr += jump;
}
return(TRUE);
}
Now that it's just a lookup table, this looks a lot like the code for fading. The only difference is that the lookup table holds different values. And it's only one column instead of 20. The declaration for the lookup table, by the way, looks like this:USHORT clut[65536];
With that, you should be able to produce some rather interesting effects. To get you started, check out the sample code that comes with this article. It's available via the attached resource file. You might try modifying it so that it fills the screen with pixels, then plots a transparent box over the top of them.[size="5"]Closing
That does it for pixel-based graphics. Next time around, we'll be working with bitmaps! Believe it or not, working with bitmaps is easier than all this pixel stuff. Seems a little backwards, doesn't it? You'll find out. In the meantime, send me any questions you might have and I'll be happy to help you out. My E-mail address is [email="ironblayde@aeon-software.com"]ironblayde@aeon-software.com[/email], and my ICQ UIN is 53210499. Oh, one other thing... the next article will be the last one covering general DirectX techniques. After that, we'll get into specific applications that you can use for developing an RPG. More details to follow. Later!
Copyright (C) 2000 by [email="ironblayde@aeon-software.com"]Joseph D. Farrell[/email]. All rights reserved.