您的位置:首页 > 其它

游戏开发新手入门之位图化图形

2011-10-09 17:54 239 查看
  简介

   终于,你已经掌握了制作一个完整游戏的基础知识了,只不过你现在还只能使用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程序很重要:颜色键和剪裁板。你可能知道在某些情况下关于颜色键有上百万种标志,那么到底怎样使用它呢?让我们一起看看吧!

  颜色键

   颜色键使一个位图被拷贝到另一个位图上时,不使所有的象素都显现。例如:当你把一个精灵(游戏中会动的对象一般都称作精灵)拷贝到地图上(背景上)时,这个精灵位图一般不会是一个精灵形状的位图,它通常都是一个矩形位图,位图里包含你所需要的精灵(除非你的精灵就是一个矩形机器人):

   游戏中,地图是先于精灵显示的,那么精灵走到树后时,还应有相应被遮挡的部分,这个先不讨论,下一节再说。现在,对我们更重要的是,如果不应用颜色键,这个精灵将永远带着这个黑色底框,这是绝对不能容忍的。

   为了解决这个问题,我们使用源颜色键。这个源颜色键告诉你精灵矩形的哪些颜色将不被拷贝(当然我们是让黑色不被拷了)。一个颜色键由两个值组成:一个低位颜色值,一个高位颜色值。当一个颜色键被申请使用时,在两个值之间的颜色,包括这两个值的颜色都将不会被拷贝。在DirectX中有一个结构用来处理它,叫作DDCOLORKEY,看看吧:

typedef struct _DDCOLORKEY{

DWORD dwColorSpaceLowValue;

DWORD dwColorSpaceHighValue;

} DDCOLORKEY, FAR* LPDDCOLORKEY;


   很简单的结构,我就不解释了。我将展示给你使用了颜色键之后的效果。我使用颜色键的高位和低位两个值仅仅把黑色包括在它们之间。因此,黑色是唯一不会被拷贝的颜色。

   好多了,是不是?这就是我们想得到的结果!现在,在我告诉你怎样建立和使用颜色键之前,我还有说一说目标颜色键,尽管我们的确我们不常用到它(我们常用的是源颜色键)。鉴于源颜色键定义了哪些颜色键不能被拷贝,目标颜色键定义了哪些颜色不能被写入(覆盖)。听起来很怪异,是不是?我也有同感。举个实例你就明白了。当你要把A位图拷贝到B位图的下面,意思就是把A位图作为背景,例如由于某种理由,需要把一个文本框拷贝到空的后缓冲区,然后再把背景画面拷贝到这个后缓冲区,但你又不能覆盖先前的文本框。因此,在后缓冲区里除了文本框的那些黑色的部分才能被写入象素。

   我也不清楚你什么时候需要处理这种情况,但是你的确可能用到(一旦你用到了,可千万要告诉我哦,我一直没有遇到这种情况呢^_^)。现在,你已经知道什么是颜色键了,让我们看看怎样使用它们吧!

  置颜色键

   在DirectDraw中有两种方法使用颜色键。第一种,你可以链接一个颜色键(或者两个,如果你同时使用源和目标颜色键)到表面,然后在位块传输时定义DDBLT_KEYSRC,DDBLT_KEYDEST,DDBLTFAST_SRCCOLORKEY或DDBLTFAST_DESTCOLORKEY标志,具体使用哪个标志,取决于你使用哪个位块传输函数和使用哪种颜色键。第二种,你可以创建一个颜色键,然后通过DDBLTFX结构传送给位块传输操作。当你不断地需要使用颜色键时,我向你推荐第一种方法;反之,当你偶然要使用一次颜色键,就用第二种方法吧!

   你可以把颜色键链接到已经建立好了的表面,也可以在建立表面的同时建立颜色键。两种方法我都将详细告诉你。假设你工作在16-bit显示模式下,是565象素格式,你要在后缓冲区使用一个仅包含黑色的源颜色键。如果你的后缓冲区已经建立好了,你就可以简单建立一个DDCOLORKEY结构,然后把它传递给IDirectDrawSurface7::SetColorKey()函数,如下所示:

HRESULT SetColorKey(

  DWORD dwFlags,

  LPDDCOLORKEY lpDDColorKey

);


   记住要用FAILED()宏检测这个函数的返回值,保证一切都在计划之中。函数的参数很简单:

   ※ DWORD dwFlags:决定所使用颜色键类型的标志。以下三个是你将用到的:

   · DDCKEY_COLORSPACE:该结构包含一个颜色范围,如果结构包含的是单独的颜色键则不作设置。

   · DDCKEY_DESTBLT:该结构指定颜色键或者颜色范围作为用于位块传输操作的目标颜色键。

   · DDCKEY_SRCBLT:该结构指定颜色键或者颜色范围作为用于覆盖操作的颜色键。

   ※ LPDDCOLORKEY lpDDColorKey:这是指向DDCOLORKEY结构的指针。

   就这么多。你可以根据你所需要使用的颜色键适当地定义位块传输的标志。注意,一个颜色键链接到表面,并不意味着你每一次必须使用它。如果你只定义了DDBLT-WAIT或DDBLTFAST_WAIT标志,颜色键将被忽略。下面是设置颜色键的方法:

DDCOLORKEY ckey;

ckey.dwColorSpaceLowValue = RGB_16BIT565(0, 0, 0); // or we could just say '0'

ckey.dwColorSpaceHighValue = RGB_16BIT565(0, 0, 0);

if (FAILED(lpddsBack->SetColorKey(DDCKEY_SRCBLT, &ckey)))

{

  // error-handling code here

}


   如果你要为已经建立的颜色键链接一个表面,有几件事情你需要做。首先,当你定义DDSURFACEDESC2结构的有效成员时,你需要使dwFlags成员包含DDSD_CKSRCBLT或者DDSD_CKDESTBLT标志,具体使用哪个标志,取决于你要使用哪种颜色键。回头再看看DDSURFACEDESC2结构,它包含两种DDCOLORKEY结构。一种称为ddcdCKSrcBlt,另一种称为ddcdCKDestBlt,填写适当的结构来创建表面。你就需要干这么多!以下是关于640×480大小的离屏表面的实例代码:

// set up surface description structure

INIT_DXSTRUCT(ddsd);

ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_CKSRCBLT;

ddsd.dwWidth = 640; // width of the surface

ddsd.dwHeight = 480; // and its height

ddsd.ddckCKSrcBlt.dwColorSpaceLowValue = RGB_16BIT(0,0,0); // color key low value

ddsd.ddckCKSrcBlt.dwColorSpaceHighValue = RGB_16BIT(0,0,0); // color key high value

ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // type of surface

// now create the surface

if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsBack, NULL)))

{

// error-handling code here

}


   关于颜色键的部分到此结束。现在我们可以进行本章最后一项了——剪切。

  IDirectDrawClipper接口

   假设你有一个图形,而你却只想把它的一部分显示在屏幕上。你应该怎样做呢?如果你曾经在DOS下编写过游戏,你可能对剪切望而生畏。Well,在DirectX下,这只是小菜一碟!首先的首先,这的确是很容易做到的,因为DirectX用矩形来做位块传输,改变矩形的坐标要比指定内存中的哪一部分图形被拷贝(就像在DOS下所做的)要容易的多。其次,DirectDraw还为此提供了一个接口——IDirectDrawClipper。

DirectDraw的剪切性能完全可以满足你的要求,你不但可以剪切矩形区域,你还可以剪切任意多边形区域!真的是很棒!如果你在屏幕上同时要显示一个主窗口,在屏幕的一边显示一个状态栏,在屏幕的底部显示一个文字提示栏,并且用黑色分隔开这些区域,你可以用DirectDraw的剪切功能完成它,非常容易的!

   做这样的操作你需要分几步走。首先你得得到一个指向IDirectDrawClipper接口的指针。没什么难的,只需要调用IDirectDraw7::CreateClipper(),如下:

HRESULT CreateClipper(

  DWORD dwFlags,

  LPDIRECTDRAWCLIPPER FAR *lplpDDClipper,

  IUnknown FAR *pUnkOuter

);


   在你调用这个函数前,你应该首先声明一个LPDIRECTDRAWCLIPPER类型的指针,这样你才能把它的地址传递给这个函数。记着要检测函数的调用是否成功哦!以下是函数参数的解释:

   ※ DWORD dwFlags:简直就是幸福——这个参数还没有使用过,设置为0。

   ※ LPDIRECTDRAWCLIPPER FAR *lplpDDClipper:把你的LPDIRECTDRAWCLIPPER指针的地址传递给它。

   ※ IUnknown FAR *pUnkOuter:你知道怎么做,设置为NULL。^_^

   一旦你有了自己的接口指针,下一件事就是创建剪切列表(clip list)。多个剪切的矩形组成了剪切列表。需要使用到RGNDATA结构,它包含了足够的信息来定义一个任意的区域,看看它的原形吧:

typedef struct _RGNDATA {

RGNDATAHEADER rdh;

char Buffer[1];

} RGNDATA;


   我需要详细解说一下它的参数。

   ※ RGNDATAHEADER rdh:它是RGNDATA结构中嵌套的一个结构。它包含了第二个参数——Buffer的所有信息。它定义了需要剪切区域里的矩形的数目,整个区域的形状等信息。我们过一会儿再具体讨论它。

   ※ char Buffer[1]:这并不意味着是只有一个值得数组;它将是在内存中任意大小的区域来控制着实际的剪切区域数据。同样的,对于RGNDATA结构,我们要声明一个指向该结构的指针,然后使用malloc()函数为RGNDATAHEADER设置足够的内存空间,也就是为剪切列表设置足够的空间。有一件事我要提醒你:剪切列表里的矩形按从上到下,然后从左到右排列,不能交迭。

   我已经意识到你有些胡涂了,不要紧,继续学习,一切会好起来的。下面是RGNDATAHEADER结构的原形,它比较好理解:

typedef struct _RGNDATAHEADER {

  DWORD dwSize;

  DWORD iType;

  DWORD nCount;

  DWORD nRgnSize;

  RECT rcBound;

} RGNDATAHEADER;


   ※ DWORD dwSize:结构的大小。简单的使用sizeof(RGNDATAHEADER)好了。

   ※ DWORD iType:它描述了每个区域的外形。它是另有玄机的,以后我们再把它扩展开来讨论。现在,你只要把它设置为RDH_RECTANGLES就好了,这也正是我们需要的。

   ※ DWORD nCount:这是组成该区域的矩形的数量。换句话说,就是你的剪切列表理的矩形数。

   ※ DWORD nRgnSize:为缓冲区的大小设置它,将得到自身的区域数据。由于我们使用n个矩形组成了剪切区域,所以它的大小应该是sizeof(RECT) *nCount。

   ※ DWORD rcBound:这是一个矩形类型,包含了剪切列表里的所有矩形。通常你把它设置成表面上需要剪切部分的尺寸。

   现在,我们可以建立一个剪切列表了。首先我们声明一个LPRGNDATA的指针,分配给剪切列表足够的内存空间;然后根据上面我们所学的设置每个成员。让我们看看最简单的实例,你可能经常要用到它哦!它只有一个剪切区域,并且,就是整个屏幕,是640×480显示模式的。以下就是代码:

// first set up the pointer -- we allocate enough memory for the RGNDATAHEADER

// along with one RECT. If we were using multiple clipping area, we would have

// to allocate more memory.

LPRGNDATA lpClipList = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER) + sizeof(RECT));

// this is the RECT we want to clip t the whole display area

RECT rcClipRect = {0, 0, 640, 480};

// now fill out all the structure fields

memcpy(lpClipList->Buffer, &rcClipRect, sizeof(RECT)); // copy the actual clip region

lpClipList->rdh.dwSize = sizeof(RGNDATAHEADER); // size of header structure

lpClipList->rdh.iType = RDH_RECTANGLES; // type of clip region

lpClipList->rdh.nCount = 1; // number of clip regions

lpClipList->rdh.nRgnSize = sizeof(RECT); // size of lpClipList->Buffer

lpClipList->rdh.rcBound = rcClipRect; // the bounding RECT


   一旦有了剪切列表,你需要把它作为你的剪裁板。你将调用IDirectDrawClipper接口的函数SetClipList()。就是下面这个东东:

HRESULT SetClipList(

  LPRGNDATA lpClipList,

  DWORD dwFlags

);


   你所要做的就是把RGNDATA的指针传递给它。参数dwFlags没有用,设置为0。现在,剪切列表设置好了,还需要一步,就是把剪切列表链接到你所要控制的表面上,这需要调用SetClipper()函数,它将表面指针作为其唯一的参数:

HRESULT SetClipper(LPDIRECTDRAWCLIPPER lpDDClipper);


   你知道应该怎样做:就是把你已经设置好的接口的指针传递给它。任何时候,你要位块传输一个有剪裁板相关联的表面,剪裁板将做所有的工作。所以如果你要在屏幕上显示一个贴片的一部分,例如传输一个矩形坐标为{-10,-10,6,6},或者类似的矩形贴图,都不会有麻烦的。很不错吧,嗯?

   关于剪切的最后一件事情是必须要用free()函数释放你用malloc()设置的内存空间。还有,就是由于某种原因在调用SetClipList()或SetClipper()失败后,在返回错误代码前或你要根据失败的结果进行操作前,要释放内存空间。在你完成用LPRGNDATA的指针设置剪切列表后,这个指针就没有存在的意义了,所以它占用的内存空间将被立即释放。

  总结

   到此,关于DirectDraw的部分就讨论完了!你真的从这六章里学到了很多的知识,如果你坚持到现在,那么祝贺你,你真的已经走了很长一段路了。对于这一章我们所学习的,我将把它们组合在一起,给你一个Demo程序,它将从资源调用位图,使用位块传输位图,颜色填充,放缩位图比例,使用剪切功能。

   仍然有些东东我们应该讨论,例如页面的切换(flipping),双缓冲区的应用等,无论如何,我们还将继续,所以不必担心我们会遗漏重要的内容。

   现在,最初的原始积累已经结束,我们以后的焦点会从创建Windows程序转移到创建一个贴图基础的RPG游戏。以后的章节将包括用DirectInput建立一个好的输入机制,写一个基础的引擎脚本,播放背景音乐和配音,等等。下一章,我们将学习为贴图游戏制作一个简单的卷轴游戏引擎,很容易的,没有你想象的那么难哦!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: