★ DirectDraw的位图化图形
☆ 简介
终于,你已经掌握了制作一个完整游戏的基础知识了,只不过你现在还只能使用GDI。今天,我们就学习使用DirectX来执行每一件你以前用GDI完成的工作,以及一些关于DirectX其它的东东。具体内容是:装载(调用)位图,使用位块传输,填充表面,使用剪裁板、颜色键等拷贝位图。 你可以在不了解前一章内容的基础上学习本章,但象素格式是很重要的,我将经常直接或间接的提到它,所以你至少应该看看上一章关于象素格式的部分!^_^ 另外,我假设你已经本系列的第一、二、三、四章,并且拥有一个DirectX SDK游戏开发平台。准备好了吗?发动引擎吧,女士们、先生们!
☆ 装载位图
不管你信不信,你的确已经知道了把位图装载到DirectDraw表面的大部分知识。怎么会这样呢?Well,在Windows GDI下装载位图同在DirectDraw下极其相似,只是有一点点不同。轻轻的回忆一下,我们曾经使用LoadImage()函数得到位图的句柄,然后把位图选入到内存设备上下文中,最后利用BitBlt()函数把图形从内存设备上下文中拷贝到显示设备上下文中,设备上下文可以用GetDC()函数得到。如果这个承担显示任务的就是DirectDraw表面(现在我们就是要用它),我们就可以针对性的得到DirectDraw表面的设备上下文!感谢上帝,IDirectDrawSurface7接口提供了一个极其简单的函数来得到这个设备上下文:
HRESULT GetDC(HDC FAR *lphDC);
该函数的返回类型同所有DirectDraw函数的返回类型相同。如果函数调用成功,参数就是一个HDC类型的设备上下文的指针,很简单吧!本章就是从把一个位图装载到DirectDraw表面讲起的。千万要记住使用完了表面设备上下文后,你一定要释放它哦!你可能已经想到了,用表面接口函数ReleaseDC()完成: HRESULT ReleaseDC(HDC hDC); 你不用回头去看关于GDI部分的位图调用,我将把适合于DirectDraw的位图调用展现给你。唯一不同的是:不是直接把设备上下文作为一个参数,而是用一个DirectDraw表面指针取代了它,然后函数从表面得到设备上下文,用它来拷贝图形,最终释放设备上下文。(可能这里我说的有些混乱,但你看一下下面的程序代码就都明白了^_^):
int LoadBitmapResource(LPDIRECTDRAWSURFACE7 lpdds, int xDest, int yDest, int nResID) { HDC hSrcDC; // source DC - memory device context HDC hDestDC; // destination DC - surface device context HBITMAP hbitmap; // handle to the bitmap resource BITMAP bmp; // structure for bitmap info int nHeight, nWidth; // bitmap dimensions
// first load the bitmap resource if ((hbitmap = (HBITMAP)LoadImage(hinstance, MAKEINTRESOURCE(nResID), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION)) == NULL) return(FALSE);
// create a DC for the bitmap to use if ((hSrcDC = CreateCompatibleDC(NULL)) == NULL) return(FALSE);
// select the bitmap into the DC if (SelectObject(hSrcDC, hbitmap) == NULL) { DeleteDC(hSrcDC); return(FALSE); }
// get image dimensions if (GetObject(hbitmap, sizeof(BITMAP), &bmp) == 0) { DeleteDC(hSrcDC); return(FALSE); }
nWidth = bmp.bmWidth; nHeight = bmp.bmHeight;
// retrieve surface DC if (FAILED(lpdds->GetDC(&hDestDC))) { DeleteDC(hSrcDC); return(FALSE); }
// copy image from one DC to the other if (BitBlt(hDestDC, xDest, yDest, nWidth, nHeight, hSrcDC, 0, 0, SRCCOPY) == NULL) { lpdds->ReleaseDC(hDestDC); DeleteDC(hSrcDC); return(FALSE); }
// kill the device contexts lpdds->ReleaseDC(hDestDC); DeleteDC(hSrcDC);
// return success return(TRUE); }
上面这段代码被设计成从资源调用位图,但你可以很容易就把它修改成从外部文件调用位图,或者更理想的是,首先你从资源调用位图,如果失败,再试图从外部文件调用位图。从外部调用,需要记住的是调用LoadImage()函数时加上LR_LOADFROMFILE标志。最美妙的事情是,函数BitBlt()自动完成象素格式的转换。举例说,当我们把24-bit的位图放入内存设备上下文,再把它传送(拷贝)到16-bit色彩深度的表面,所有的颜色将得到正确的显示,不用顾忌象素格式是555还是565,很方便吧,哦? 如果你要控制位图传递的实际过程,而不是使用BitBlt()这样简单的函数,你有两个选择。第一个,你可以修改这个函数,需要利用BITMAP结构的bmBits成员,它是一个组成图象的位的LPVOID指针变量。第二种方法,如果你真的想控制图象的调用过程,你可以自己编写函数,思路是使用标准的I/O函数来打开图象文件,然后读取它。要这样做,你需要了解位图文件的结构。我们将不涉及这种函数的编写,因为目前的对我们来说已经足够了,但我还是要为你将来的大展鸿图做一点点铺垫。^_^
☆ 位图格式
令人高兴的是,要自己写一个调用位图的函数,有一个Win32结构的位图头文件可以利用。读取这个头文件的信息,用fread()这样简单的函数就可以了。所有的位图文件都有这样一个头文件,它包含了位图的全部信息。BITMAPFILEHEADER就是这个头文件结构的名字,下面是它的原形:
typedef struct tagBITMAPFILEHEADER { // bmfh WORD bfType; // file type - must be "BM" for bitmap DWORD bfSize; // size in bytes of the bitmap file WORD bfReserved1; // must be zero WORD bfReserved2; // must be zero DWORD bfOffBits; // offset in bytes from the BITMAPFILEHEADER // structure to the bitmap bits } BITMAPFILEHEADER;
我就不详细介绍这些成员了,因为注释里已经说得很清楚了,只要使用fread()读取它们就可以了。注意要检测bfType成员是否等于字符“BM”,若是,说明你正在处理一个有效的位图。在此之后,有另一个头文件需要读取,它包含位图的尺寸、压缩类型等图象信息。以下是它的结构:
typedef struct tagBITMAPINFOHEADER{ // bmih DWORD biSize; // number of bytes required by the structure LONG biWidth; // width of the image in pixels LONG biHeight; // height of the image in pixels WORD biPlanes; // number of planes for target device - must be 1 WORD biBitCount; // bits per pixel - 1, 4, 8, 16, 24, or 32 DWORD biCompression; // type of compression - BI_RGB for uncompressed DWORD biSizeImage; // size in bytes of the image LONG biXPelsPerMeter; // horizontal resolution in pixels per meter LONG biYPelsPerMeter; // vertical resolution in pixels per meter DWORD biClrUsed; // number of colors used DWORD biClrImportant; // number of colors that are important } BITMAPINFOHEADER;
只有几个成员需要解说一下。第一个,注意压缩格式。大多数的位图你都需要做解压缩的操作。最普通的位图压缩格式是run-length编码(RLE),但只能应用于4-bit或8-bit图象,在此情况时,成员biCompression将分别是BI_RLE4和BI_RLE8,我们就不讨论这种压缩格式了,但它真的很简单,很容易理解,你如果要了解它是不会有任何麻烦的。^_^ 第二个,对于高色彩的位图,biClrUsed和biClrImportant这两个成员通常设置为0,所以不用太在意它们。对于BI_RGB这种未压缩格式的位图,成员biSizeImage也将被设置为0。最后,针对我们的目的,其它的结构成员都不是很重要的,我们只需要注意位图的长、宽和色彩的深度(biWidth、biHeight、biBitCount)。 读取完了这些头文件的信息后,如果位图是8-bit或者以下色彩深度的(也就是调色板模式),调色板的信息会紧跟在这些信息之后。也许出乎你的意料,调色板的信息不是存储在PALETTEENTRY结构中,而是在RGBQUAD结构中。RGBQUAD结构如下:
typedef struct tagRGBQUAD { // rgbq BYTE rgbBlue; BYTE rgbGreen; BYTE rgbRed; BYTE rgbReserved; } RGBQUAD;
不要问我为什么红、绿、蓝以倒序方式排列,事实就是这样!读取RGBQUAD中的数据,把数据传递给DirectDraw调色板的数组。记得要把每个PALETTEENTRY的peFlag设置成PC_NOCOLLAPSE。 之后呢(调色板信息不一定存在,因为高彩模式下就没有),你将发现图象位(image bits),你可能会想到建立一个指针,在内存中分配足够的空间来控制这些图象位数据,然后读取它们。对极了,我正要这样干。假设把存储在BITMAPINFOHEADER结构中的信息头文件称作info,你的图象位指针称作fptr,实施的代码如下:
UCHAR* buffer = (UCHAR*)malloc(info.biSizeImage); fread(buffer, sizeof(UCHAR), info.biSizeImage, fptr);
要记住,在一些情况下,biSizeImage的值可能为0,所以有必要在上面的代码运行前检测它一下。如果它被设置为0,你将不得不计算图象由多少个象素构成,每个象素需要多少个字节。 写你自己的位图调用函数,并非什么难事儿。但你觉得不需要,就用我们开始介绍的方法好了。这个话题告一段落,下面让我们看看DirectDraw的精华:使用位块传输。