Directdraw新手入门(二)

时间:2023-01-26 09:22:49

今天我们将分别使用调色板和RGB模式来熟悉DirectDraw的基本图形。它们有什么不同呢?如果你曾经在DOS下编程,你可能使用过调色板映射模式。调色板是个颜色查询表,为了绘制象素,你将一个单独的字节写入视频内存,通过这个字节你可以索引到一个拥有各种颜色的链表,这个颜色的链表,或查询表就叫作调色板。而RGB模式是不同的,因为它不需要颜色查询表。在RGB模式下绘制一个象素,你可以直接把红色、绿色和蓝色的值写入视频内存。任何色彩深度高于8位的调色板都可以用RGB模式取代。

  编写本文时,我假设你已经读过了前面几章,知道了怎样设置DirectDraw和创建表面。我们将使用DirectX7,它包含了最新的DirectDraw接口。实际上,DirectX 7 中的DirectDraw接口可能是最后的升级版本了!不用担心,未来的版本一定会兼容它的,但是未来可能是一个DirectDraw和Direct3D综合的产品,管它那,我们学的不会没有用的。

  在开始前我还有最后一件事要提醒你:在我的后续文章中关于调色板的部分可能再也用不到了,所以,如果你对于调色板模式不是很感兴趣,你可以跳过文章的前一部分,从象素格式开始看起。调色板的开发和使用是PC中使用的原始视频系统的内存限制带来的直接后果。现在由于充足的显存使调色板模式几乎废弃不用了。值得保留调色板模式的一个原因是,执行调色板操作可以实现一些有趣的动画效果。不罗嗦了,让我们开始吧!

  创建DirectDraw的调色板

  当你在色彩深度为8位或低于8位的模式下显示图形时,你必须创建调色板,也就是颜色查询表。更明确的讲,对于DirectX,调色板就是PALETTEENTRY结构。要建立一个调色板,我们要做如下三步:

  1、 创建颜色查询链表。

  2、 得到指向IDirectDrawPalette接口的指针。

  3、 把调色板链接到DirectDraw表面。

  我假设我们使用的是8位色彩深度。如果你要用16位或更高位的色彩深度编写游戏,你就不用继续看以下这段疯狂的Windows素材了。总之,8位色彩深度,我们可以有一个256个条目的调色板。所以,创建颜色查询链表,有256个条目在其中:

typedef struct tagPALETTEENTRY { 
 // pe
 BYTE peRed;
 BYTE peGreen;
 BYTE peBlue;
 BYTE peFlags;
} PALETTEENTRY;

  头三个参数很明显,分别是红色、绿色和蓝色的强度。每一个取值范围0-255,BYTE是无符号数据类型。最后一个参数是控制标志,应该设置为PC_NOCOLLAPSE。原因我就不说了。

  现在,我们需要把256个条目有秩序的排列好,也就是为了一下能找到,我们为链表设置一个数组,象这样:

PALETTEENTRY palette[256];

  Ok,我们有了数组了,你可以装载颜色了。当我工作在调色板模式下时,通常把颜色存储在一个外部文件里,然后用一些如下的东东装载颜色:

FILE* file_ptr;
int x;

if ((file_ptr = fopen("palette.dat", "rb")) != NULL)
{
 fread(palette, sizeof(PALETTEENTRY), 256, file_ptr);
 fclose(file_ptr);
}

  All right,第一步完成了。现在我们需要得到调色板的接口。交给IDirectDraw7::CreatePalette()函数就好了:

HRESULT CreatePalette(
 DWORD dwFlags,
 LPPALETTEENTRY lpColorTable,
 LPDIRECTDRAWPALETTE FAR *lplpDDPalette,
 IUnknown FAR *pUnkOuter
);

  返回类型是HRESULT,你知道它的,所以可以用FAILED()和SUCCEEDED()这两个宏检测函数是否调用成功。参数的说明如下:

  ※ DWORD dwFlags:描述调色板对象的标志常量。当然,你可以用“|”组合它们:

   · DDPCAPS_1BIT:1位色彩,对应2色调色板。

   · DDPCAPS_2BIT:2位色彩,对应4色调色板。

   · DDPCAPS_4BIT:4位色彩,对应16色调色板。

   · DDPCAPS_8BIT:8为色彩,对应256色调色板。

   · DDPCAPS_8BITENTRIES:指出引用一个8位色彩索引。就是说,每个颜色条目是它本身的到目的表面8位调色板的索引。这叫作被变址的调色板。它必须同DDPCAPS_1BIT、DDPCAPS_2BIT,或者DDPCAPS_4BIT合用。乱套吧!^_^

   · DDPCAPS_ALPHA:每一个PALETTEENTRY的peFlags成员都应该被认为是阿尔发值。用这些标志创建的调色板可以被粘贴在Dierct3D纹理表面,因为DirectDraw本身并不支持阿尔发混合。

   · DDPCAPS_ALLOW256:允许8位调色板的全部256个条目被使用。通常,0指向黑色,255指向白色。

   · DDPCAPS_INITIALIZE:指出应该用PALETTEENTRY的数组初始化调色板。

   · DDPCAPS_PRIMARYSURFACE:调色板将链接到主表面,好快速更改显示颜色。

   · DDPCAPS_VSYNC:一般画圆时用到它。

  大多数情况,你将使用DDPCAPS_8BIT | DDPCAPS_INITIALIZE,如果你刚好想建立一个空的调色板,稍后再设置它,你可以去掉后者,就是DDPCAPS_INITIALIZE。当然,你还可以使用DDPCAPS_ALLOW256,如果你真的想改变这两个常用的条目。

  ※ LPPALETTEENTRY lpColorTable:这个指针指向我们创建的查询表,把数组的名称传递给它就好了。

  ※ LPDIRECTDRAWPALETTE FAR *lplpDDPalette:这是指向IDirectDrawPalette接口指针的地址。如果函数调用成功,它将被初始化。

  ※ IUnkown FAR *pUnkOuter:同以前一样,这总是为COM高级应用准备的。设置为NULL好了。

  不是太糟糕吧!现在我们可以建立我们的调色板对象了。最后一步是把调色板链接到一个表面,这只需要一个函数就好了——IDirectDrawSurface7::Setpalette()。它的原形如下:

HRESULT SetPalette(LPDIRECTDRAWPALETTE lpDDPalette);

  很简单,是不是?你只要把上一步得到的接口指针传递给它就可以了。那好,让我们把学到的综合到一起,下面我给你一个程序框架,我假设我们已经利用调色板的数组建立了一个索引链表,就像我们上一步做的。该框架是建立DirectDraw调色板来控制颜色,并且把它链接到主表面(当然,主表面是我们事先做好的):

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
}

  就是这么简单。一旦你的调色板建立完成,绘制象素部分同RGB模式就没有什么不同了。从此时开始,我将同时介绍RGB模式和调色板模式,在我们真正的显示图象前,我需要告诉你什么是RGB象素格式。

  象素格式

  象我前面说过的,当你把一个调色板模式的象素写入内存时,你同时分配了一个字节,每个字节表示一个到色彩查询表的索引。在RGB模式下,你只需要把颜色描述值写入内存,但每个颜色需要的字节数都要多于一个字节。字节的多少同色彩的深度相关。对于16-bit色彩,你要为每个象素准备两个字节(16位),以此类推,你可以猜到32-bit色彩是怎么回事了,这些都是很容易理解的。32-bit色彩对于一个象素来说,每一位的字符如下:

AAAA AAAA RRRR RRRR GGGG GGGG BBBB BBBB

  “A”是代表“alpha”(阿尔发),表示一个透明的值,这是为Direct3D准备的。我以前说过,DirectDraw不支持α混合,所以当你为DirectDraw创建32-bit色彩时,把高位都设置为0好了。下一个8位表示红色强度的值,再下一个8位表示绿色,最后8位表示蓝色。
一个32-bit色彩的象素需要32位,所以我们一般用UINT类型来定义相对应的变量类型,这是一个无符号实数类型。通常我用一个宏来把RGB数据转换成正确的象素格式。让我给你看看它的样子,希望这能更好的帮助你理解象素格式:

#define RGB_32BIT(r, g, b) ((r << 16) | (g << 8) | (b))

  就象你看到的,这个宏通过位移在相应的位置写入了相应的红、绿、蓝的强度值,并且完全符合正确的象素格式。是不是开始感觉有点儿入门了?要建立一个32-bit的象素,你就可以调用这个宏。红、绿、蓝每一个颜色的强度值都是8位,它们的取值范围都是从0——255。例如建立一个白色的象素,你可以这样:

UINT white_pixel = RGB_32BIT(255, 255, 255);

  24-bit色彩基本相同,道理实际上是一样的,只是24-bit没有关于α的描述,也就是少了α那8位。象素格式如下:

RRRR RRRR GGGG GGGG BBBB BBBB

  所以红色、绿色、蓝色仍然都分别是8位,这就意味着24-bit色彩和32-bit色彩实际上是有相同颜色深度的,只是32-bit多了个α混合。现在,你一定会想,24-bit比32-bit要好,真的是这样吗?否,因为使用24-bit有一些麻烦,事实上没有24-bit的数据类型,在你建立象素时,你不得不分三步写入红、绿、蓝的强度值,而不是象32-bit一次就完成。尽管32-bit色彩需要更多的内存,但在大多数的机器上,它要更快一些。实际上,很多显示卡不支持24-bit色彩模式,因为每一个象素占用3个字节是很不方便的。

  现在,轮到16-bit色彩了,它有一点儿小麻烦,因为对于16-bit色彩,不是每一种显示卡都使用相同的象素格式!有两种格式。其中一种,也是比较流行的,红色占据5位,绿色占据6位,蓝色占据剩下的5位。另一种格式是分别都占据5位,剩下的一位,也就是高位不使用,一些老的显示卡都使用这种格式。所以这两种格式看起来是这样的:

565 format: RRRR RGGG GGGB BBBB
555 format: 0RRR RRGG GGGB BBBB

  当你工作在16-bit色彩深度下,你首先需要检测显示卡是支持565格式还是555格式,然后使用适当的方式。这是很讨厌的,但你坚持用16-bit色彩,这是没有办法避免的。由于存在两种格式,你就需要两种宏:

#define RGB_16BIT565(r, g, b) ((r << 11) | (g << 5) | (b))
#define RGB_16BIT555(r, g, b) ((r << 10) | (g << 5) | (b))

  对于565格式,红色和蓝色的取值范围是0——31,绿色是0——63;对于555格式,取值范围都是0——31,所以当要创建一个白色象素时,就会有所不同:

USHORT white_pixel_565 = RGB_16BIT565(31, 63, 31);
USHORT white_pixel_555 = RGB_15BIT555(31, 31, 31);

  这个USHORT是无符号短实数类型,对应的变量只有16位。存在两种格式把事情搞得有些复杂,但在实际的游戏编程过程中,你将会感觉到这并没有你想象的那么讨厌。顺便说一下,有些时候555格式被称为15-bit色彩深度,所以在以后如果我这样谈到了它,你一定要心领神会哦!

  现在或许是告诉你在16-bit色彩深度模式下,怎样检测显示卡到底支持哪种格式的时机了,是555还是565呢?最简单的办法就是调用IDirectDrawSurface7接口下的GetPixelFormat()函数,它的原形如下:

HRESULT GetPixelFormat(LPDDPIXELFORMAT lpDDPixelFormat);

  参数是指向DDPIXELFORMAT结构的指针。你只要声明它,初始化它,然后传递它的地址就一切OK了。这个结构的本身是巨大的,所以我就不列举它了,但我得告诉你它的三个成员,都是DWORD类型的,它们是dwRBitMask、dwGBitMask、和dwBBitMask。你可以从dwRBitMask、dwGBitMask和dwBBitMask中获得掩码值(新东东,先不用太明白)。你也可以用它们检测显示卡支持的格式。如果显示卡支持565,dwGBitMask将为0x07E0。如果是555格式,dwGbitMask为0x03E0。

  现在,我们已经学习了所有我们可能用到的象素格式,可以进入在DirectX下显示图象的实际阶段了。你已经等待了很久了,不是吗?在把象素放到表面上前,我们需要锁定表面,至少是锁定表面的一部分。锁定表面返回一个指向表面在内存里位置的指针,然后,我们就可以为所欲为了。

  锁定表面

  没什么令人意外的东东,我们将使用的函数是IDirectDrawSurface7::Lock()。让我们仔细看看它:

HRESULT Lock(
 LPRECT lpDestRect,
 LPDDSURFACEDESC lpDDSurfaceDesc,
 DWORD dwFlags,
 HANDLE hEvent
);

  一定要检测函数的调用是否成功,否则可能会有大麻烦的:如果锁定失败,而返回的指针指向了一个不正确的内存区域,你若操控该区域,很有可能导致系统的混乱。函数的参数有以下这些组成:

  ※ LPRECT lpDestRect:是一个指向RECT结构的指针,它描述了将要被直接访问的表面上的矩形区。该参数被设置为NULL,以锁定整个表面。

  ※ LPDDSURFACEDESC2 lpDDSurfaceDesc:是DDSURFACEDESC2类型的结构变量的地址,它由直接访问表面内存所必需的全部信息进行填充。在该结构中返回的信息表面的基地址、间距和象素格式。

  ※ DWORD dwFlags:好像没有几个DirectX函数没有这个东东的。下面列出几个最有用的标志常量:

   · DDLOCK_READONLY:被锁定的表面为只读。(当然就不能写入了)

   · DDLOCK_SURFACEMEMORYPTR:表面返回一个指向锁定矩形左上角坐标的有效指针;如果没有指定矩形,那么返回表面左上角的坐标。此项为默认且无需显式的输入。

   · DDLOCK_WAIT:如果其它线程或进程正在进行位转换操作,不能锁定表面内存,则一直等到该表面可以锁定为止或返回错误信息。

   · DDLOCK_WRITEONLY:被锁定表面为可写。(当然就不能读取了)

  由于我们使用锁定去操控象素,你将总会用到DDLOCK_SURFACEMEMORYPTR。即使我们目前还没有学习位块操作,但使用DDLOCK_WAIT总是一个好主意。

  ※ HANDLE hEvent:没用的东东,设置为NULL好了。

  一旦我们锁定了表面,我们需要查看一下DDSURFACEDESC2结构来获取一些表面信息。我们以前介绍过这个结构,但在这里,针对现在的课题,我们只需要它的两个成员。由于它们都很重要,我就再重复一遍:

  ※ LONG lPitch:这个lPitch成员表示每个显示行的字节数,也就是行间距。例如,对于640×480×16模式,每一行有640个象素,每一个象素需要两个字节存放颜色信息,所以行间距应该为1280个字节,对不对?Well,对于一些显示卡,它的长度大于1280,每行上多于的内存不存放任何的图象数据,但你必须让它存在,因为这种显示卡在某种显示模式下不能创建线性内存模式。的确,这种显示卡的比例很小,但你需要考虑到它。

  ※ LPVOID lpSurface:这是指向内存中表面的指针。不管你使用何种显示模式,DirectDraw都创建一个线性地址模式,使你能够操控表面上的象素。 这个lpSurface指针是很容易理解的,而行间距是一个需要记住的重要值,因为你将必须使用它去计算特殊象素的偏移量。
我们过一会儿在细说,因为有一件事我们现在必须知道,当对锁定的表面操作完成后,你需要释放这个锁定表面,这个函数IDirectDrawSurface7::Unlock()的原形为:

HRESULT Unlock(LPRECT lpRect);

  参数同你传递给Lock()函数的要保持一致。都准备好了,让我们画一些象素吧!

  绘制象素

  首先是确定从Lock()函数得到的指针类型。逻辑上,我们希望指针的大小同象素的大小要保持一致。所以我们为8-bit色彩深度分配了UCHAR*类型,USHORT*是16-bit的,UINT*是32-bit的。但是24-bit怎么办呢?因为没有与之相对应的数据类型,我们还是使用UCHAR*类型,但具体操作有一些不同。

  我们也应该把lPitch成员转换成与指针相同的单位。记得吗,当我们第一次从DDSURFACEDESC2结构得到lPitch时,它是以字节为单位。对于16-bit模式,我们应该把它除以2以适应USHORT,对于32-bit我们应该把它除以4以适应UINT。

  在我们进行第二步前先看看实例代码。假设我们在32-bit模式锁定主表面来绘制象素。以下是代码:

// 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;
}

  All right,让我分别解说。首先,你可能已经注意到了我把它声明为一个inline函数,目的是消除传递所有参数时的辅助操作,例如每次我们想要做些简单的事情(如绘制一个象素)。在函数里,仅用了一行就定位了我们要绘制的点和设置了该点的颜色。注意,颜色仅仅是一个值,不是由红、绿、蓝分别组成的,所以我们需要使用宏RGB_32BIT()来设置这个颜色值。

  公式用来定位要绘制象素的具体位置——y*nPitch + x 。nPitch表示行间距,被y乘后就得到了正确的行数,再加上x,就得到了正确的位置。这就是你需要知道的,很简单吧!让我再告诉你在8-bit和16-bit模式下绘制象素的函数,它们都十分相象:

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;
}

  几个函数间唯一不同的就是参数数据类型的不同。应该还记得对于8-bit色彩深度,间距是以字节表示,对于16-bit,间距是以USHORT类型表示。现在,只剩下一个模式没有说了,就是24-bit模式。由于没有相应的数据类型,我们需要分别传递红、绿、蓝三个值,函数看起来应该如下:

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;
}

  如你所看到的,它将工作慢一些,因为它多了一次乘法运算,并且有三次内存写操作。你可以用其它方法替换x*3加快一些速度,如(x+x+x)或者(x<<1)+x,但是不会有太大效果的。当然,她还没有到应该放弃的地步。现在你就明白了为什么说24-bit色彩深度有点儿讨厌了吧!

  关注速度

  你应该采取一些行动使程序尽可能会的运行。

  首先,锁定一个表面并不是最快的,所以你要试图锁定表面上你要操作的最小矩形区域。对于很多操作,包括很简单的绘制象素演示程序,你都应该锁定最小的矩形区域。

  第二,让我们就640×480×16模式来说,间距总是1280个字节,你应该试图考虑有没有更好的办法表述它。当然,1280个字节你是不能改变的,但我们可以使公式最优化,用位移来替代乘法是一贯的加速方法。我们先前的公式是这样的:

buffer[y*nPitch + x] = color;

  如果我们知道nPitch将会是640(由于nPitch是USHORT类型,不是字节),我们就可以加速它(我们本来就知道它是640)。640不是一个理想的位移数字,但512是2的9次幂,128是2的7次幂,你猜到了吧,512+128=640。^_^ 很棒吧?我们就可以用下面这个更快的公式取代先前的公式:

buffer[(y<<9) + (y<<7) + x] = color;

  多数的解决办法都是分解成2的几次幂,有的需要动一点儿脑筋,如800×600(512+256+32=800),小菜一碟哦!位移是我们应用的最快的运算符。

  最后,如果你要使用两个函数—— 一个做乘法运算,一个做位移运算,要将比较判断放到循环的外部,不能象下面这样:

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++;
}

  你可能认为节省这么一点点时间意义不大,但当你进行千万次的循环时,意义就很大了。游戏程序员总是想办法提高游戏的速度的。
看看以前的文章,我们已经进行了好长时间的铺垫了。现在,我们知道了怎样绘制象素了,让我们看看能用现在学到的做些什么。

  淡出操作

  在游戏中最常用到的屏幕操作就是淡出成黑色,或者从黑色淡入。两种方式是同样的机理:你简单画出你的图象,然后申请一些屏幕转换来改变图象的亮度。对于淡出,你减少亮度从100%——0%;对于淡入,你增加亮度从0%——100%。如果你工作在调色板模式,这很容易做到,你只要改变你的调色板的颜色就可以了。如果你工作在RGB模式下,你得考虑一些其它方法。

  现在,我将说一说屏幕淡入、淡出相对好一些的方法。你可以使用Direct3D,它支持α混合,先设定每一帧的纹理,然后设置透明层;或者,更容易的方法,你可以使用DirectDraw的color/gamma控制。但是,如果你仅仅希望屏幕的一部分进行淡入或淡出的操作,或者淡入或淡出一种非黑色的颜色,而且你又不是一个Direct3D的高手——我本人就不是!——那么具体做法的手册就在你眼前。现在,你所需要做的最基本的就是读取每一个你需要控制的象素,然后把它分解成红色、绿色和蓝色,然后你把三个值分别乘以要淡出或淡入的级别,再合成RGB值,把新的颜色值写回缓冲区。听起来很复杂?别害怕,没有想象的那么坏。看看下面这段演示代码,它演示了屏幕左上角200×200区域的淡出效果,是16-bit色彩深度和565格式:

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);
  }
 }
}

  现在,这个函数有很多不稳妥的地方。首先,计算象素的位置公式不但包含在循环中,而且还出现了两次!你可以在整个程序中只计算它一次,但现在的代码计算了它80000次!下面是你应该做的:在函数的开始部分,你应该声明一个USHORT*的变量,让它等于buffer(如USHORT* temp = buffer;)。在内部循环里,增加一个指针使其能得到下一个象素;在外部循环,增加一行(temp+=jump;),使其能转入下一行。下面是修改后的代码:

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;
 }
}

  这就好一些了吧!jump值是USHORT类型,是表示从200个象素宽的末尾(200个象素没有占满一行)到下一行开始的值。尽管如此,对于浮点运算和提取/还原颜色计算并没有提高速度。应该有办法的,看看这个:

USHORT clut[65536][20];

  如果你要求一个DOS程序员把这么大的数组放入他的程序中,他可能痛苦的会哭出声来,甚至当场昏死过去,起码也要加速自然死亡。但在Windows中,如果你需要这样做,不会遇到什么麻烦的。因为你拥有整个系统的可利用内存。如果把整个的内循环替换成下面这一行,是不是很美妙的一件事呢?

*temp = clut[*temp][index];

  这样做,又快了一些!^_^ 你可以传递一个0——100间的整数来替代浮点数传递给函数。如果为100,就不需要淡出的操作了,所以就返回“什么事儿也不用做”;如果为0,就用ZeroMemory()函数处理所有的工作好了。另外,把传递的数除以5,作为数组的第二个下标。

  如果你对于我知道查询表的尺寸感到好奇,我就告诉你好了,65536是2的16次幂,所以在16-bit模式下,就有65536种颜色。既然我们的颜色值是无符号的值,它们的范围从0——65535,那么我们就用20作为淡出的增量值好了,反正考虑到相关的内存,我觉得挺合适的。
对于24-bit和32-bit模式,你显然不能直接使用颜色查询表,因为数组太巨大了,所以你只有使用三个小一点的数组:

UCHAR red[256];
UCHAR green[256];
UCHAR blue[256];

  然后,每当你读取一个象素,就把它分解出的颜色值放入相应的数组,使其形成自己的查询表,经过变化,再组合到一起,得到RGB色彩值。有很多办法可以实现程序的优化,最好的办法是根据你的目的不断地测试哪一种是最适合你的程序的,然后总结经验,记住它。我下面将简单的介绍一下你可能用得着的其它的转换。

  透明操作

  把一个透明的图象覆盖在非透明的图象上,你就不能使用颜色查询表了,因为它总共需要有65536个查询表,一台普通的电脑就需要8.6GB的内存来处理这个庞然大物。^_^ 所以你不得不计算每一个象素。我将给你一个基本的思路。假设你要用图象A覆盖图象B,图象A的透明百分比为pct,这是一个0——1之间的浮点数,当为0时是完全不可见的,当为1时是完全可见的。那么,让我们把图象A的象素称作pixelA,相对应,图象B的象素称作pixelB。你将应用下面这个公式:

color = (pixelA * pct) + (pixelB * (1-pct));

  基本上,这是一个两个象素颜色的平均值。所以,你实际上看到每个象素有6个浮点乘法运算。你可以用一些小型的查询表降低你的工作量。你真的应该试一试!

  你或许想做的另一件事情是建立一个部分透明的纯色窗口。那种效果用一个颜色查询表完全可以达到。因为对于“地球人”,我只需要为屏幕上可能出现的颜色提供蓝色。实际上,我就是用查询表完成的。我将告诉你我实际的意思:

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));
 }
}

  这段代码来源于“地球人”,用查询表创建了一个文本框。为了安全起见,随处都使用了类型修饰。这段代码还能再快一些,但我没有很认真的优化,因为我只在游戏的最开始的部分调用了它一次。首先,红、绿、蓝的亮度值被提取出来,由于是16-bit模式,注意我们用了一个color_depth变量检测了显示卡是555还是565格式。然后,用下面公式计算了象素的亮度:

y = r + g + b;
brightness = r*(r/y) + g*(g/y) + b*(b/y);

  这是一个理想的平均值。我不能确定是否颜色亮度值这样得到就正确,但它看起来符合逻辑,并且实际效果很好。在公式的最后我加了一个.5,因为当你把浮点数变为整数时,小数部分被去掉,加上.5使其凑整。最后,我把亮度除以2再加上1,这样不会使文本框太亮,加1使文本框不会全黑。由于16-bit模式的低位是蓝色,我可以只把颜色设置为蓝色,就不用宏了。理解了吗?最后,结束之前,我给你演示怎样创建文本框:

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<ibox.bottom; y++)
 {
  for (x=ibox.left; x<ibox.right; x++, ptr++)
   *ptr = clut[*ptr];
   ptr += jump;
 }
 return(TRUE);
}

  这就是一个查询表,看起来更象淡出操作的代码了,就是查询表的控制值与前面的不一样了。这里用一个计算代替了20。 顺便说一下,对于查询表的一个声明,象下面这个:

USHORT clut[65536];

  总结

  本文是为以象素为基础的图形服务的。下一章,我们将学习位图的知识。不管你信不信,使用位图要比象素简单多了,以后你就知道了。下一篇文章将是学习DirectX基础知识的最后一章,在此之后,我们将编写一个RPG游戏。细节到时候你就知道了。