|
★ 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的精华:使用位块传输。
☆ 使用位块传输
位块传输是显示卡操控位图数据的一部分,你同样可以用它来进行颜色填充。就像我们过一会儿看到的,随着硬件的性能提高,会有很多经典的技巧。DirectX有权使用硬件的加速功能,但要记住,如果DirectX使用的加速功能不被机器硬件支持,将自动启用硬件仿真层(HEL),但这也并非万无一失,因为有些功能靠硬件仿真层是无法实现的(否则谁还买3D加速卡^_^),所以你需要检测你的函数是否调用成功。 GDI位块传输可以在DirectDraw编程中使用,而且有时也的确是这样做的。然而DirectDraw具有其自身的位块传输函数,它们通常更加适合于编程环境,而且比GDI的相应的函数执行得更快。DirectDraw位块传输函数名为Blt()和BltFast(),都是有IDirectDrawSurface7接口提供的。两者不同处是BltFast()不处理剪切、放缩等其它Blt()做的有趣的事情。如果在硬件仿真层上,BltFast()要比Blt()快10%左右,但如果有硬件加速卡支持(硬件加速卡主要就是为位块传输服务的),二者的速度就差不多了,而且现在大多数的机器都有硬件加速卡,所以我总是使用Blt()。让我们仔细看看这个神奇的东东:
HRESULT Blt( LPRECT lpDestRect, LPDIRECTDRAWSURFACE7 lpDDSrcSurface, LPRECT lpSrcRect, DWORD dwFlags, LPDDBLTFX lpDDBltFx );
由于Blt()所拥有的最后一个参数,使其能做很多特殊的事儿。该参数配有一个标志常量列表,我将会向你介绍其中最有用的几个。另外,注意在把位图从一个表面向另一个表面传递时,你应该调用目的表面的Blt(),不是源表面的。好了吗?以下是函数的参数说明: ※ LPRECT lpDestRect:参数lpDestRect为指向结构RECT的指针,它给出了位块传输操作的目标表面的左上角和右下角的坐标。如果源表面和目标(目的)表面的大小不一致,Blt()将把源表面的图象自动按照比例适应目标表面的大小。如果此参数为NULL,则使用整个目标表面。 ※ LPDIRECTDRAWSURFACE7 lpDDSrcSurface:参数lpDDSrcSurface为指向DirectDraw表面的指针,该DirectDraw表面为位块传输之源表面。如果你只是要用颜色填充目的表面,你可以把它设置为NULL。 ※ LPRECT lpSrcRect:参数lpSrcRect为指向结构RECT的指针,它给出了位块传输(有的书上也叫作“位转换”)操作的源表面的左上角和右下角的坐标。如果此参数为NULL,则使用整个源表面。 ※ DWORD dwFlags:对于这个参数有一个巨大的标志常量列表,可以用“|”组合使用标志常量。其中一些是为Direct3D服务的,所以我将把我们常用的列出来:
◎ DDBLT_ASYNA:位块传输异步的以先入先出(FIFO)的顺序接收。如果没有空间可用于FIFO硬件,则该调用失败。 ◎ DDBLT_COLORFILL:使用DDBLTFX结构的数据成员dwFillColor作为RGB颜色填充目标表面的矩形。 ◎ DDBLT_DDFX:DDBLTFX结构的dwDDFX成员指定了位块传输的使用效果。 ◎ DDBLT_DDROPS:DDBLTFX结构的dwDDROP成员指定了光栅操作(ROPS),该操作不是Win32 API的一部分。 ◎ DDBLT_KEYDEST:颜色键与目标表面相关联。 ◎ DDBLT_KEYDESTOVERRIDE:DDBLTFX结构的dckDestColorkey成员是目标表面的颜色键。 ◎ DDBLT_KEYSRC:颜色键与源表面相关联。 ◎ DDBLT_KEYSRCOVERRIDE:DDBLTEX结构的dckSrcColorkey成员是源表面的颜色键。 ◎ DDBLT_ROP:DDBLTFX结构的dwROP成员是位块传输的ROP(光栅操作代码),这些ROP与Win32 API中定义的那些相同。 ◎ DDBLT_ROTATIONANGLE:DDBLTFX结构的dwRotationAngle成员是表面的旋转角度,其单位为1/100度。 ◎ DDBLT_WAIT:在位块传输器忙的情况下,推迟DDERR_WASSTILLDRAWING返回值(位块传输函数调用失败返回的值之一),而当位块传输开始或发生另一个错误时立即返回。
我几乎总是使用DDBLT_WAIT标志。颜色键标志也是很重要的,我们过一会儿再说它。现在,还有最后一个Blt()参数需要说一下: ※ LPDDBLTFX lpDDBltFX:这是一个指向DDBLTFX结构的指针,它可以包含各种特殊要求的信息。如果没有什么特殊要求,你就设置为NULL好了。让我们仔细看看这个结构。我警告你,它是很魁梧的:^_^
typedef struct _DDBLTFX{ DWORD dwSize; DWORD dwDDFX; DWORD dwROP; DWORD dwDDROP; DWORD dwRotationAngle; DWORD dwZBufferOpCode; DWORD dwZBufferLow; DWORD dwZBufferHigh; DWORD dwZBufferBaseDest; DWORD dwZDestConstBitDepth;
union { DWORD dwZDestConst; LPDIRECTDRAWSURFACE lpDDSZBufferDest; };
DWORD dwZSrcConstBitDepth;
union { DWORD dwZSrcConst; LPDIRECTDRAWSURFACE lpDDSZBufferSrc; };
DWORD dwAlphaEdgeBlendBitDepth; DWORD dwAlphaEdgeBlend; DWORD dwReserved; DWORD dwAlphaDestConstBitDepth;
union { DWORD dwAlphaDestConst; LPDIRECTDRAWSURFACE lpDDSAlphaDest; };
DWORD dwAlphaSrcConstBitDepth;
union { DWORD dwAlphaSrcConst; LPDIRECTDRAWSURFACE lpDDSAlphaSrc; };
union { DWORD dwFillColor; DWORD dwFillDepth; DWORD dwFillPixel; LPDIRECTDRAWSURFACE lpDDSPattern; };
DDCOLORKEY ddckDestColorkey; DDCOLORKEY ddckSrcColorkey; } DDBLTFX, FAR* LPDDBLTFX;
如果我整个详细的介绍这个结构,恐怕我们都会受不了的,并且也没有这个必要。所以我只告诉你一些重点的部分。谢天谢地,该结构的大部分都是为z缓冲区(z-buffers)和α消息服务的,我们不用理会它。嘻嘻,我的工作量变得很小了:^_^ ※ DWORD dwSize:象所有的DirectX的结构一样,当你初始化这个结构时,该成员放置结构的大小。 ※ DWORD dwDDFX:这些是位块传送所能接受的一些特殊操作。列表并不长,别担心喔!
◎ DDBLTFX_ARITHSTRETCHY:位块传输时,在Y轴算术拉伸位图。 ◎ DDBLTFX_MIRRORLEFTRIGHT:y轴上的镜像变换。表面从左到右完成镜像效果。 ◎ DDBLTFX_MIRRORUPDOWN:x轴上的镜像变换。表面从上到下完成镜像效果。 ◎ DDBLTFX_NOTEARING:把动画图像块转移到前段缓存时可以使用这个参数,这样位块传输操作的时间会与屏幕刷新率相一致,并使画面撕裂的可能性减小到最小。 ◎ DDBLTFX_ROTATE180:位块传输时,把表面顺时针旋转180度。 ◎ DDBLTFX_ROTATE270:位块传输时,把表面顺时针旋转270度。 ◎ DDBLTFX_ROTATE90:位块传输时,把表面顺时针旋转90度。
需要详细解释的可能只有DDBLTFX_NOTEARING。游戏离不开动画,动画制作者主要关心的通常是动画的速度和性能,速度太快会导致图象质量的恶化。光栅扫描显示系统(我们基本上用的都是这种显示器)利用电子束扫描每一条水平线上的屏幕象素点。象素行从屏幕的左上角开始更新,到屏幕的右下角结束。各象素行都被称为扫描线。电子束在每一行扫描线的末端被关掉,而电子枪重新瞄准下一行的起始点,这个过程成为水平回扫。当过程执行到屏幕扫描线的最后一行时,电子束再次被关掉,电子枪重新瞄准屏幕的左上角。电子枪从屏幕的右下角重新瞄准到左上角的过程所需的时间被称为垂直回归或者屏幕空白周期。如果在视频控制器显示视频数据的同时,视频数据被CPU做了更改,这时就会产生问题。在PC机中,屏幕的刷新率通常在60Hz到100Hz之间,而现在的CPU则可以在每秒处理成百上千的指令,这样就很可能导致位于视频内存区的图象在视频系统完成显示之前发生更改。图象断裂的结果被称为图象撕裂。就我的经验而言,使用了DDBLTFX结构的DDBLTFX_NOTEARING后,图象撕裂就不是什么问题了。^_^ ※ DWORD dwROP:使用这个标志来指定Win32模式的光栅操作代码。同GDI函数BitBlt()和StretchBlt()中的相对应参数的功能一样。可以通过IDirectDraw7::GetCaps()函数得到可能的光栅操作列表,可以通过“|”组合标志常量,确定源矩形表面和目标矩形表面是怎样结合的。 ※ DWORD dwRotationAngle:这是用来旋转位图角度的,可以旋转任意角度。这是非常棒的,但不幸的是,它只能在HAL层(硬件抽象层)上工作,这就意味着用户的显示卡要支持加速旋转,否则……,但不能保证每个用户都有这种高档的显示卡,所以你需要考虑周全。如果你真的需要旋转处理,你只好自己写这样的函数了,这可是一个大话题,需要另写一部指南了,所以我们就越过它。但请注意,如果是90度倍数的角度,你可以使用DDBLTFX_ROTATE90等。这将使你回避显示卡不干活的风险。^_^ ※ DWORD dwFillColor:如果你要使用位块传输来填充颜色,你必须把颜色放入到这个参数中。 ※ DDCOLORKEY ddckDestColorKey,ddckSrcColorKey:你要使用颜色键时必须要指定这些成员。这两个家伙是很重要的。但暂时我们还不讨论它们,因为我们过一会儿才讲颜色键。
以上这些就是DDBLTFX结构中比较有用的成员了,这就意味着你现在拥有足够的知识进行位块传输了!如果你现在感觉有些混乱,不要紧,你实践过一段时间就会好了。让我们看看几个例子。假设你已经有了一个叫作lpddsBack的后缓冲区,你想把它里面的内容传输到主表面,很简单的,你看:
lpddsPrimary->Blt(NULL, lpddsBack, NULL, DDBLT_WAIT, NULL);
再轻轻回忆一下,第一个参数和第三个参数分别是位块传输的目标矩形和源矩形。由于我把它们都设置为NULL,就说明是对全部的表面进行拷贝。现在,让我们在看看,假设你有一个在离屏表面里的名字叫作lpddsTileset的16×16大小的图形,你想把它传输到后缓冲区,变成32×32大小的,你需要这样做:
RECT dest, src; SetRect(&src, 0, 0, 16, 16); // the coordinates of the tile SetRect(&dest, 0, 0, 32, 32); // where you want it to end up on the back buffer lpddsBack->Blt(&dest, lpddsTileset, &src, DDBLT_WAIT, NULL);
这个例子同上一个例子不同处在与这个例子设置了位块传输的坐标。由于这两个矩形区的大小不同,Blt()依照比例适当的变更了图形的大小。最后,我们还得举例说明一下DDBLTFX结构,就做一个颜色填充的例子吧。假设你在16-bit色彩模式下,是565象素格式,你要把你的后缓冲区填充为蓝色。下面就是你应该做的:
DDBLTFX fx; INIT_DXSTRUCT(fx); // zero out the structure and set dwSize fx.dwFillColor = RGB_16BIT565(0, 0, 31); // set fill color to blue lpddsBack->Blt(NULL, NULL, NULL, DDBLT_WAIT | DDBLT_COLORFILL, &fx);
注意参数的设置,前三个都是NULL,你自己想想原因吧!^_^ 好了,让我们看看另一个位块传输函数BltFast()吧。它只是Blt()的简化版本,所以我们不需要太多的时间解释它。下面是它的原形:
HRESULT BltFast( DWORD dwX, DWORD dwY, LPDIRECTDRAWSURFACE7 lpDDSrcSurface, LPRECT lpSrcRect, DWORD dwTrans );
你可以看得出来,它同Blt()极其相似。它也是IDirectDrawSurface7接口的成员函数,被目标表面调用。来看看它的参数: ※ DWORD dwX,dwY:这是Blt()和BltFast()之间不同的地方。是目标表面上进行位块传输的x和y坐标。如果源矩形大于目标矩形,则调用失败,因为BltFaxt()不能干按比例变换的事儿及其它能够通过Blt()完成的工作。 ※ LPDIRECTDRAWSURFACE7 lpDDSrcSurface:这是源表面,同Blt()的一样。 ※ LPRECT lpSrcRect:这个也同Blt()的一样。是在源表面中定义了矩形左上角和右下角的RECT结构。 ※ DWORD dwTrans:定义了位块传输类型,它的标志列表很简单,只有四个标志:
◎ DDBLTFAST_DESTCOLORKEY:使用目标颜色键的透明位块传输。 ◎ DDBLTFAST_NOCOLORKEY:没有透明的普通复制位块传输。 ◎ DDBLTFAST_SRCCOLORKEY:使用源颜色键的透明位块传输。 ◎ DDBLTFAST_WAIT:如果位块传输忙的话,不产生DDERR_WASSTILLDRAWING消息。一旦位块传输能够开始或者发生另一个错误时返回。
就这些!BltFast()支持颜色键。下面让我们看一个简单的示例。把整个后缓冲区拷贝到主表面:
lpddsPrimary->BltFast(0, 0, lpddsBack, NULL, DDBLTFAST_WAIT);
到现在为止,你已经是一个位块传输的专家了,还有几件事情对于DirectX程序很重要:颜色键和剪裁板。你可能知道在某些情况下关于颜色键有上百万种标志,那么到底怎样使用它呢?让我们一起看看吧!
☆ 颜色键
作者的邮箱:ironblayde@aeon-software.com 要用英文给作者发信哦! 作者的ICQ:53210499 【傻马乱踢:howsee@163.com】 待续。。。。。 | | | |