☆ 锁定表面
没什么令人意外的东东,我们将使用的函数是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++; }
你可能认为节省这么一点点时间意义不大,但当你进行千万次的循环时,意义就很大了。游戏程序员总是想办法提高游戏的速度的。 看看以前的文章,我们已经进行了好长时间的铺垫了。现在,我们知道了怎样绘制象素了,让我们看看能用现在学到的做些什么。