Working with BitmapsThere are about a trillion different bitmap file formats, but I only use a few for game programming: .PCX (PC Paint), .TGA (Targa), and .BMP (Windows native format). They all have their pros and cons, but you're using Windows, so you might as well use the native format .BMP to make life easier. (I'm already in DirectX API revision hell, so I'm a bit unstable at this point. If I see one more Don Lapre commercial, I think I'm going to go postal!) The other formats all work in similar ways, so once you learn how to deal with one file format, figuring out another involves nothing more than getting hold of the header structure format and reading some bytes off the disk. For example, lots of people are really starting to like the .PNG (Portable Network Graphics) format. Loading .BMP FilesThere are a number of ways to read a .BMP file—you can write a reader yourself, use a Win32 API function, or a mixture of the two. Because figuring out Win32 functions is usually as hard as writing your own, you might as well write a .BMP loader yourself. A .BMP file consists of three parts, as shown in Figure 7.23. Figure 7.23. The structure of a .BMP file on disk.The three parts are as follows: Bitmap file header— This holds general information about the bitmap and is contained in the Win32 data structure BITMAPFILEHEADER: typedef struct tagBITMAPFILEHEADER { // bmfh WORD bfType; // Specifies the file type. // Must be 0x4D42 for .BMP DWORD bfSize; // Specifies the size in bytes of // the bitmap file. WORD bfReserved1; //Reserved; must be zero. WORD bfReserved2; // Reserved; must be zero. DWORD bfOffBits; // Specifies the offset, in // bytes, from the // BITMAPFILEHEADER structure // to the bitmap bits. } BITMAPFILEHEADER; Bitmap info section— This is composed of two other data structures, the BITMAPINFOHEADER section and the palette information (if there is one): typedef struct tagBITMAPINFO { // bmi BITMAPINFOHEADER bmiHeader; // the info header RGBQUAD bmiColors[1]; // palette (if there is one) } BITMAPINFO; And here's the BITMAPINFOHEADER structure: typedef struct tagBITMAPINFOHEADER{ // bmih DWORD biSize; // Specifies the number of // bytes required by the structure. LONG biWidth; // Specifies the width of the bitmap, in pixels. LONG biHeight; // Specifies the height of the bitmap, in pixels. // If biHeight is positive, the bitmap is a // bottom-up DIB and its // origin is the lower left corner // If biHeight is negative, the bitmap // is a top-down DIB and its origin is the upper left corner. WORD biPlanes; // Specifies the number of color planes, must be 1. WORD biBitCount // Specifies the number of bits per pixel. // This value must be 1, 4, 8, 16, 24, or 32. DWORD biCompression; // specifies type of compression (advanced) // it will always be // BI_RGB for uncompressed .BMPs // which is what we're going to use DWORD biSizeImage; // size of image in bytes LONG biXPelsPerMeter; // specifies the number of // pixels per meter in X-axis LONG biYPelsPerMeter; // specifies the number of // pixels per meter in Y-axis DWORD biClrUsed; // specifies the number of // colors used by the bitmap DWORD biClrImportant; // specifies the number of // colors that are important } BITMAPINFOHEADER; NOTE 8-bit images will usually have the biClrUsed and biClrImportant fields both set to 256, while 16 and 24-bit images will set them to 0. Hence, always test the biBitCount to find out how many bits per pixel are used and go from there. Bitmap data area— This is a byte stream that describes the pixels of the image (this may or may not be in compressed form) in 1-, 4-, 8-, 16-, or 24-bit format. The data is in line-by-line order, but it may be upside-down so that the first line of data is the last line of the image, as shown in Figure 7.24. You can detect this by looking at the sign of biHeight—a positive sign means the bitmap is upside-down, and a negative sign means the bitmap is normal. Figure 7.24. The image data in a .BMP file is sometimes inverted on the y-axis.To read a .BMP file manually, you first open the file (with any file I/O technique you like) and then read in the BITMAPFILEHEADER. Next, read in the BITMAPINFO section, which is really just a BITMAPINFOHEADER plus palette (if 256 color), so really you're just reading in the BITMAPINFOHEADER structure. From this, you determine the size of the bitmap (biWidth, biHeight) and its color depth (biBitCount, biClrUsed). Here you also read in the bitmap data along with the palette (if there is one). Of course, there are a lot of details, such as allocating buffers to read the data and moving file pointers around. Also, the palette entries are RGBQUAD, which are in reverse order of normal PALETTEENTRYs, so you have to convert them like this: typedef struct tagRGBQUAD { // rgbq BYTE rgbBlue; // blue BYTE rgbGreen; // green BYTE rgbRed; // red BYTE rgbReserved; // unused } RGBQUAD; Back in Chapter 4, "Windows GDI, Controls, and Last-Minute Gift Ideas," you may recall the LoadBitmap() function that's used to load bitmap resources from disk. You could use this function, but then you would always have to compile all your game bitmaps into your .EXE as a resource. Although this is cool for a complete product, it's not something you want to do when developing. Basically, you want to be able to tweak your graphics with a paint or modeling program, dump the bitmaps in a directory, and run your game to see what's up. Hence, you need a more general disk file-based bitmap reading function, which you'll write in a moment. Before you do, take a look at the Win32 API function to load bitmaps. Run LoadImage(): HANDLE LoadImage( HINSTANCE hinst, // handle of the instance that contains // the image LPCTSTR lpszName, // name or identifier of image UINT uType, // type of image int cxDesired, // desired width int cyDesired, // desired height UINT fuLoad ); // load flags This function is rather general, but you only want to use it to load .BMP files from the disk, so you don't have to worry about all the other stuff it does. Simply set the parameters to the following values to load a .BMP from disk: hinst— This is the instance handle. Set it to NULL. lpszName— This is the name of the .BMP file on disk. Send a standard NULL-terminated filename like ANDRE.BMP, C:/images/ship.bmp, and so forth. uType— This is the type of image to load. Set it to IMAGE_BITMAP. cxDesired, cyDesired— These are the desired width and height of the bitmap. If you set these to any number other than 0, LoadImage() will scale the bitmap to fit. Therefore, if you know the size of the image, set them. Otherwise, leave them at 0 and read the size of the image later. fuLoad— This is a load control flag. Set it to (LR_LOADFROMFILE | LR_CREATEDIBSECTION). This instructs LoadImage() to load the data from disk using the name in lpszName and to not translate the bitmap data to the current display device's color characteristics. The only problem with this function is that it's so general that getting to the damn data is difficult. You have to use more functions to access the header information, and if there's a palette, more trouble arises. Instead, I just created my own Load_Bitmap_File() function that loads a bitmap from disk in any format (including palettized) and stuffs the information into this structure: typedef struct BITMAP_FILE_TAG { BITMAPFILEHEADER bitmapfileheader; // this contains the // bitmapfile header BITMAPINFOHEADER bitmapinfoheader; // this is all the info // including the palette PALETTEENTRY palette[256];// we will store the palette here UCHAR *buffer; // this is a pointer to the data } BITMAP_FILE, *BITMAP_FILE_PTR; Notice that I've basically put the BITMAPINFOHEADER and the exploded BITMAPINFO all together in one structure. This is much easier to work with. Now for the Load_Bitmap_File() function: int Load_Bitmap_File(BITMAP_FILE_PTR bitmap, char *filename) { // this function opens a bitmap file and loads the data into bitmap int file_handle, // the file handle index; // looping index UCHAR *temp_buffer = NULL; // used to convert 24 bit images to 16 bit OFSTRUCT file_data; // the file data information // open the file if it exists if ((file_handle = OpenFile(filename,&file_data,OF_READ))==-1) return(0); // now load the bitmap file header _lread(file_handle, &bitmap->bitmapfileheader,sizeof(BITMAPFILEHEADER)); // test if this is a bitmap file if (bitmap->bitmapfileheader.bfType!=BITMAP_ID) { // close the file _lclose(file_handle); // return error return(0); } // end if // now we know this is a bitmap, so read in all the sections // first the bitmap infoheader // now load the bitmap file header _lread(file_handle, &bitmap->bitmapinfoheader,sizeof(BITMAPINFOHEADER)); // now load the color palette if there is one if (bitmap->bitmapinfoheader.biBitCount == 8) { _lread(file_handle, &bitmap->palette, MAX_COLORS_PALETTE*sizeof(PALETTEENTRY)); // now set all the flags in the palette correctly // and fix the reversed // BGR RGBQUAD data format for (index=0; index < MAX_COLORS_PALETTE; index++) { // reverse the red and green fields int temp_color = bitmap->palette[index].peRed; bitmap->palette[index].peRed = bitmap->palette[index].peBlue; bitmap->palette[index].peBlue = temp_color; // always set the flags word to this bitmap->palette[index].peFlags = PC_NOCOLLAPSE; } // end for index } // end if // finally the image data itself _lseek(file_handle, -(int)(bitmap->bitmapinfoheader.biSizeImage),SEEK_END); // now read in the image if (bitmap->bitmapinfoheader.biBitCount==8 || bitmap->bitmapinfoheader.biBitCount==16 || bitmap->bitmapinfoheader.biBitCount==24) { // delete the last image if there was one if (bitmap->buffer) free(bitmap->buffer); // allocate the memory for the image if (!(bitmap->buffer = (UCHAR *)malloc(bitmap->bitmapinfoheader.biSizeImage))) { // close the file _lclose(file_handle); // return error return(0); } // end if // now read it in _lread(file_handle,bitmap->buffer, bitmap->bitmapinfoheader.biSizeImage); } // end if else { // serious problem return(0); } // end else // close the file _lclose(file_handle); // flip the bitmap Flip_Bitmap(bitmap->buffer, bitmap->bitmapinfoheader.biWidth* (bitmap->bitmapinfoheader.biBitCount/8), bitmap->bitmapinfoheader.biHeight); // return success return(1); } // end Load_Bitmap_File The function isn't really that long or that complex; it's just a pain to write, that's all. NOTE There's a call to Flip_Bitmap() at the end of the function. This simply inverts the image because most .BMP files are in bottom-up format. Flip_Bitmap() is part of the library I'm building, and it's copied into the demos that will come shortly so you can review it at any time. It opens the bitmap file, loads in the headers, and then loads in the image and palette (if the image is a 256-color bitmap). The function works on 8-, 16-, and 24-bit color images. However, regardless of the image format, the buffer that holds the image UCHAR buffer is just a byte pointer, so you must do any casting or pointer arithmetic if the image is 16- or 24-bit. In addition, the function allocates a buffer for the image, so the buffer must be released back to the operating system when you're done mucking with the image bits. This is accomplished with a call to Unload_Bitmap_File(), shown here: int Unload_Bitmap_File(BITMAP_FILE_PTR bitmap) { // this function releases all memory associated with the bitmap if (bitmap->buffer) { // release memory free(bitmap->buffer); // reset pointer bitmap->buffer = NULL; } // end if // return success return(1); } // end Unload_Bitmap_File In a moment, I'll show you how to load bitmap files into memory and display them, but first I want to describe what you'll do with these images in the general context of a game. Working with BitmapsMost games have a lot of artwork, which consists of 2D sprites, 2D textures, 3D models, and so forth. In most cases, 2D art is loaded a frame at a time as single images (as shown in Figure 7.25), or as templates—that is, as a number of similar images all together in a rectangular matrix (as shown in Figure 7.26). Both methods have their pros and cons. The cool thing about loading single images, or one image per file, is that if you make a change to the image with an image processor, you can use the data immediately. However, there may be hundreds of frames of animation that make up a 2D game character. This means hundreds or thousands of separate image .BMP files! Figure 7.25. A standard set of bitmaps without templating.Figure 7.26. Bitmap images templated for easy access and extraction.Templated images, as shown in Figure 7.26, are great because the template holds all the animation for a single character, and hence all the data is in one file. The only downfall is that someone has to template the data! This can be very time-consuming, not to mention that there's an alignment problem because you must create a template of cells, where each cell is mxn (usually m and n are powers of 2) with a one-pixel border around each cell. Next, you write some software to extract an image from a particular cell, because you now know the size of the cell and so forth. You might use both techniques, depending on the type of game and how much artwork it uses. In any case, later you're going to have to write software that can extract images from loaded bitmaps in single-image or templated format, and then load the data into DirectDraw surfaces. This allows you to use the blitter, but I'll get to that later. For now, just use the Load_Bitmap_Function() to load an 8-, 16-, and 24-bit bitmap and display it in the primary buffer to get a feel for the function. NOTE I created most of the art for these demos myself, using various paint or 3D modeling programs and writing the art out as .BMP files (some of the art was done by other artists). This is where a good 2D paint program with support for lots of image formats comes in handy. I usually use Paint Shop Pro, the best all-around 2D paint program as far as price and performance go—it's on this book's CD, too! Loading an 8-Bit BitmapTo load an 8-bit image, this all you need to do: BITMAP_FILE bitmap; // this will hold the bitmap if (!(Load_Bitmap_File(&bitmap,"path:\filename.bmp"))) { /* error */ } // do what you will with the data stored in bitmap.buffer // in addition, the palette is stored in bitmap.palette // when done, you must release the buffer holding the bitmap Unload_Bitmap(&bitmap); WARNING You can have only 256 colors on the screen at once, so when you're generating your artwork, remember to find a 256-color palette that looks good when all the artwork is converted to 256 colors (Debabelizer is a great tool for this). In many cases you'll need multiple palettes—one for each level of the game—but no matter what, all the bitmaps that are going to be used for the same level and could be visible must be in the same palette! The only interesting thing about loading an 8-bit bitmap is that the palette information in the BITMAP_FILE structure is valid. You can use the data to change the DirectDraw palette, save it, or whatever. This brings us to a little detail about writing 8-bit games. Now is a good time to bring up something that I haven't had a good reason to show you yet—changing palette entries after the palette has already been created and attached to an 8-bit DirectDraw surface. As you know, most of the demos that you've written for 8-bit mode usually create a random or gradient palette and leave it at that. But this time, you're loading an image that has its own palette, and you want to update the DirectDraw palette object with the new palette entries. When you copy the image to the primary buffer, it looks right. To do this, all you need is the IDIRECTDRAWPALETTE:SetEntries() function, as shown here: BITMAP_FILE bitmap; // holds the 8-bit image // given that the 8-bit image has been loaded if (FAILED(lpddpal->SetEntries(0,0,MAX_COLORS_PALETTE, bitmap.palette))) { /* error */ } That's so easy it's sickening! For an illustration of loading an 8-bit image and displaying it, take a look at DEMO7_10.CPP|EXE. It loads an 8-bit image in 640x480 and dumps it to the primary buffer. Loading a 16-Bit BitmapLoading a 16-bit bitmap is almost identical to loading an 8-bit image. However, you don't need to worry about the palette because there isn't one. Also, very few paint programs can generate 16-bit .BMP files, so if you want to use a 16-bit DirectDraw mode, you may have to load a 24-bit bitmap and then manually convert the bits to 16-bit by using a color-scaling algorithm. In general, you would perform the following operations to convert a 24-bit image to a 16-bit image:
And then you write color into your destination 16-bit buffer. Later in the book, when you see all the library functions, I'll be sure to write a 24-bit to 16-bit bitmap converter for you (because I'm that kind of guy). Anyway, assuming that the bitmap is actually in 16-bit format and you don't need to do this operation, the bitmap load should be identical to the 8-bit load. For example, DEMO7_11.CPP|EXE loads a 24-bit image, converts it to 16-bit, and dumps it into the primary buffer. Loading a 24-Bit BitmapLoading a 24-bit bitmap is the simplest of all. Just create a 24-bit bitmap file and then load it with the Load_Bitmap_File() function. Then BITMAP_FILE.buffer[] will hold the data in 3-byte pixels left to right, row by row, but in BGR (blue, green, red) format. Remember this, because it matters when you extract the data. Furthermore, many graphics cards don't support 24-bit graphics; they support 32-bit because they don't like the odd byte addressing (multiples of 3). So an extra byte is used for padding or alpha channeling. In either case, when you read out each pixel from BITMAP_FILE.buffer[] and write it to the primary surface or an offscreen DirectDraw surface that's 32-bit, you'll have to do this padding yourself. Here's an example: // each pixel in BITMAP_FILE.buffer[] is encoded as 3-bytes // in BGR order, or BLUE, GREEN, RED // assuming index is pointing to the next pixel... UCHAR blue = (bitmap.buffer[index*3 + 0]), green = (bitmap.buffer[index*3 + 1]), red = (bitmap.buffer[index*3 + 2]); // this builds a 32 bit color value in A.8.8.8 format (8-bit alpha mode) _RGB32BIT(0,red,green,blue); And you've seen this macro, so don't freak out. Here it is again to refresh your memory: // this builds a 32 bit color value in A.8.8.8 format (8-bit alpha mode) #define _RGB32BIT(a,r,g,b) ((B) + ((g) << 8) + ((r) << 16) + ((a) << 24)) For an example of loading and displaying a nice 24-bit image, take a look at DEMO7_12.CPP|EXE. It loads a full 24-bit image, sets the display mode for 32-bit color, and copies the image to the primary surface. Looks sweet, huh? Last Word on BitmapsWell, that's it for loading bitmaps in 8-, 16-, or 24-bit format. However, you can see that a lot of utility functions will have to be written here! I'll do the dirty work, so don't worry. In addition, you might want to be able to load Targa files with the .TGA extension, because a number of 3D modelers can render animation sequences only to files with the name filenamennnn.tga, where nnnn varies from 0000 to 9999. You're probably going to need to be able to load animation sequences like this, so when I dump the library functions on you, I'll show you a .TGA load. It's much easier than the .BMP format. |