Delphi中使用DirectDraw技术进行图形处理
DirectDraw是一套名为DirectX复杂工具的一部分,DirectX是由许多不同的技术组成,比如:DirectDraw、Direct3D、DirectSound、DirectPlay、DirectInput和DirectSetup等等。这其中的每一种技术都是集中了几种处多媒体的技术或游戏的技术,像声音播放、3D图形、网络播放、硬件设备如鼠标和强制反馈等等。不过,在本章中将只介绍DirectDraw,并且这个主题很容易就会占用一章或更多的章节关于DirectX的其他技术内容,读者可以去参阅其他关于DirectX的书籍。
DirectDraw程序要求用户的系统必须有DirectDraw运行时的DLL,这些运行时文件(实际只是DLL的集合,可能许多机器已经安装了),还可以从Micorsoft的Web站点获取;该站点有各种各样的产品,包括游戏、Windows 98、将来的操作系统Windows NT 5等。如果读者正在使用的是Windows NT 4,那么至少要用Service Pack 3(SP3)去升级,之后才能够访问作为SP3一部分的DirectDraw 3。不要试图直接在Windows NT系统下安装运行时的DirectDraw,而应该安装最新的补丁(Service Pack),直接安装运行时的DirectDraw是针对Windows 95/98系统而言的。确定一个系统是否安装了DirectDraw的一个方法是查看Windows/System或Winnt/System32目录是否存在DDRAW.DLL和DSOUND.DLL,如果有,则说明系统已经安装了DirectDraw。
在可能的情况下,读者应该从Microsoft获取DirectDraw SDK,通常它可以从Microsoft的Web站点下载得到,不过请注意,它至少有30M。安装了SDK后,它在硬盘上创建一个名为DXSDK的目录,在这个目录之下是SDK目录,其中包含有各种各样的文档、用C/C++编写的示例文件和帮助文件。另外,还可以直接从Microsoft得到SDK,在有些特殊情况下,SDK可能是作为MSDN的一个部分而打包发行的;Microsoft出版的Inside DirectX一书也有DirectX SDK。
说得详细点儿,程序员所做的是先在屏幕偏移缓冲区中构造一幅图,然后把它显示在观众面前;当观众全神贯注于当前这幅图时,程序员接着构造另外一幅场景图,然后用这幅场景图取代先前的那幅图显示到屏幕上。比如,开始用第一帧在屏幕的最左端显示一个球;然后,只要把球从左到右每一帧移动几个像素就可以产生运动的效果,位置上的每一个小的改变,只需要用一幅新图去更新屏幕;程序员实际只是显示球的一些的静止图片,但观众却得到"受骗"后的感觉:看到了动画。
刷新屏幕的平均速度是每秒大约25帧,这个速率已经足够快,以至人眼根本感觉不到单独一帧的存在,相反地,只是觉得看到的是真正的动画。如果以25帧/秒的速率从屏幕左边移动球到屏幕右边,观众就会认为它们看到的不是球的一系列静止画面,而是球真正地在空间运动。根据程序员编写的代码的质量的好坏,DirectX是能够实现高于25帧/秒的速率的。
我们也许该说这么件事:当我们最初开始创建动画时认为,建立一个完全的缓冲区然后再把缓冲区的内容显示到屏幕上是愚蠢的作法,这完全是不必要的操作;我们那时决定想办法直接写屏,修改前后两帧不同的地方,避免资源的无谓开销。
后来证明,我们的方法不一定完全行得通,尤其是在想创建出相对复杂一点的效果时。很多情况下,直接写屏所作的修改很难瞒过用户的眼光;剔除屏幕上的某一块区域、然后绘制上下一帧的显示内容,这个操作产生的是显而易见的蹩脚的拼凑效果,即使使用了尽可能有效的作法,效果也得不到改善。这样的操作应该使用用户看不见的后台缓冲区(back buffer),然后用整个缓冲区的内容取代原来屏幕的显示,这时,用户看到的是就是一幅完整的构图而不会是拼凑的效果。
DirectDraw子系统会确保在屏幕刷新时同步执行页面交换操作。我们还从来没碰上谁真正看到了72MHz的屏幕刷新率。如果一幅画以那种速率刷新的话,那么在向屏幕刷新显示时,用户根本不能觉察到两次刷新的片刻闪烁。
我们还有些担心:不管我们说什么,一些读者还是想尝试在用户的目光注视下直接写屏修改屏幕上的某些区域。尽管在某些情况下这可能行得通,但是,更多的时候还只能先在缓冲区里构图,然后直接"贴"到屏幕显示给用户,这样才能够获得好的效果。如果读者真要尝试直接写屏,尽管去尝试;不过,到走进死胡同时,不妨回过头来尝试双缓冲,你会惊喜地发现它在解决看似棘手问题的快速性和有效性。
DirectDraw允许程序员创建一个后台缓冲区(back buffer),向缓冲区绘制图形,然后把它交换到可见视频内存,再由可见视频内存显示到屏幕上。假定是在独占模式下,拥有足够的视频内存来把主表面(primary surface)和后台表面(back surface)都装入视频RAM,那么页面交换操作就不再是一个复制的过程;相反的,它只要简单地改变一下被视频卡可见内存引用的内存块的地址就可以了。也就是说,当执行页面交换操作时,内存中只有四个字节的内容需要改变,而其它内容都不变;这唯一需要做的改变就是:
改变指向当前活动视频页的指针,该指针存放在内存的四个字节中。因此,该操作速度很快;另外,它还能保证与显示器的刷新操作同步发生。这样,使用DirectDraw就可以实现非常平滑的动画。
DirectDraw和别的DirectX技术尽最大的努力为程序员提供一套高级的、完善的视频例程;比如3D例程尽可能利用硬件来实现超快速的操作。然而,许多视频卡或别的硬件设备并不能提供程序员所希望的全部功能,在这种情况下,DirectX将试图从软件上仿真硬件缺少的功能。
到底软件能提供何种功能、硬件又能提供何种功能,这整个的问题是一个相当技术化的主题,大大超出了本书的范围。但是,可以在特定的系统上运行Microsoft SDK所带的DirectX Viewer(DirectX浏览器)来查看DirectDraw的状态,该浏览器通过执行一系列的DirectX的功能例程来报告特定系统上各种各样多媒体硬件的状态;还可以在运行时刻亲自查询这些子系统,不过这个内容在本书中我们只是稍微介绍一点儿。
到现在为止,所应清楚的一点是:DirectX的一些功能在硬件中执行,而其余部分由软件仿真完成。仿真功能是由一个名称看起来有些奇怪的子系统来处理的,它就是HEL,即Hardware Emulation Layer (硬件仿真层);而程序中非仿真的部分由另一个名称看起来更奇怪的子系统处理:HAL,即Hardware Abstraction Layer(硬件抽象层)。HEL和HAL处理所有DirectDraw的各种操作;它们的驱动程序使得DirectDraw成为可能。
可以应用HEL和HAL中的任意一个来初始化DirectDraw;不过,这是一个非常高级的功能,即使在以后的课程中用到很苛刻的性能测试时都可能用不着调用它。
在初始化DirectDraw的实例后,下一步就是设置合作层次(cooperative level):
hr : = FDirectDraw.SetCooperativeLevel(Handle,
DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN);
这些代码把CooperativeLevel设置成全屏独占模式,这样,编写的程序将占据整个屏幕,不许别的窗口覆盖它,除非通过按Ctrl+Alt+Tab组合键从程序中显式地把该窗口从视频缓冲区移走。也可以把合作层次初始化成DDSCL_NORMAL,它是一个窗口模式。我们现时将略过该模式,因为它在某些方面比独占模式复杂得多。
在独占模式下,应用程序可以选择分辨率及色深,像下面这样:
hr : = FDirectDraw.SetDisplayMode(640, 480, 8);
这行代码把屏幕的分辨率设置为640×480,8位色深(就是说至多可以显示256色)。直到最近,选择比这更高的分辨率和色深还是没有结果的。要保持一幅640×480的屏幕的单个拷贝需要大约300KB的空间,也就是说,需要600KB的空间来保持一个屏幕拷贝及其后台缓冲区;这是期望从大多数系统得到的最大空间了,所以要获取更高分辨率通常是不可行的。现在,事实上有些视频卡在更高的分辨率下能够提供相当好的性能,但对大多数人来说,还得指望一阵子。正因为这样,所以我们一直使用这相对简单的分辨率,尽管它意味着要学会使用调色板。
设置了合作层次和屏幕分辨率后,下一步是获取两个表面,以便在表面之间进行页面交换。换句话说,要创建一个指针指向用户注视的视频内存,也指向后台缓冲区(以便为双缓冲或者说页面交换使用)。可以如下进行:
SurfaceDesc.dwSize := sizeof(SurfaceDesc);
SurfaceDesc.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
SurfaceDesc.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or
DDSCAPS_FLIP or
DDSCAPS_COMPLEX;
SurfaceDesc.dwBackBufferCount := 1;
hr := FDirectDraw.CreateSurface(SurfaceDesc, FPrimarySurface, nil);
if(hr = DD_OK) then begin
ddscaps.dwCaps := DDSCAPS_BACKBUFFER;
hr := FPrimarySurface.GetAttachedSurface(ddscaps, FBackSurface);
if(hr = DD_OK) then begin
PaintSurfaces;
Exit;
end;
end;
这段显然非常不直接的代码创建了主表面和后台表面,主表面存放显示给用户看的内容,后台表面存放的是当调用DirectDraw的Flip函数来实现双缓冲时要通过页面交换操作显示到主表面的内容。
下面是DDSCaps的结构:
TDDSCaps = record
dwCaps: DWORD;
end;
这个结构初看起来相当简单,但稍微观察一下就会发现它实际上很复杂。事实上,DDSCaps和SurfaceDesc结构都有几乎令人绝望的复杂的结构,这些结构来处理一大堆常量。
为了避免在描述它们时让我们和读者发疯,我们提示只要用DirectX SDK的在线帮助查看一下这些常量参数即可。比如,DDSCaps结构的一个名为dwCaps的字段就有30来个可能的常量,其中的很多解释起来都很复杂。
下面是TDDSurfaceDesc结构的定义:
TDDSurfaceDesc = record
dwSize: DWORD;
dwFlags: DWORD;
dwHeight: DWORD;
dwWidth: DWORD;
lPitch: Longint;
dwBackBufferCount: DWORD;
case Integer of
0: (
dwMipMapCount: DWORD;
dwAlphaBitDepth: DWORD;
dwReserved: DWORD;
lpSurface: Pointer;
ddckCKDestOverlay: TDDColorKey;
ddckCKDestBlt: TDDColorKey;
ddckCKSrcOverlay: TDDColorKey;
ddckCKSrcBlt: TDDColorKey;
ddpfPixelFormat: DDPIXELFORMAT;
DDSCaps: TDDSCaps;
);
1: (
dwZBufferBitDepth: DWORD;
);
2: (
dwRefreshRate: DWORD;
);
end;
该结构也是非常复杂的。我们没有必要逐个字段逐个字段地说明这个结构,相反,只是指出:该记录可用来获取一个表面的大小、高度、宽度、像素深度以及一个指向表面实际占用字节的指针。其中更复杂的一个字段叫做Pitch,它描述到表面下一条线起始位置的距离,这未必就是表面的宽度;所以,要获取表面的实际宽度,还需要特殊的函数调用。在直接处理一个DirectDraw表面的字节(或位)时,请特别注意字段pitch。
创建了主表面后,很容易就能获取先创建的后台表面,它是用来完成页面交换操作的:
ddscaps.dwDaps : = DDSCAPS_BACKBUFFER;
hr : = FPrimarySurface.GetAttachedSurface(ddscaps, FBackSurface);
这些代码得到后台表面;我们总是让一个指针指向后台表面,以便需要时访问它。
在可能的情况下,读者应该从Microsoft获取DirectDraw SDK,通常它可以从Microsoft的Web站点下载得到,不过请注意,它至少有30M。安装了SDK后,它在硬盘上创建一个名为DXSDK的目录,在这个目录之下是SDK目录,其中包含有各种各样的文档、用C/C++编写的示例文件和帮助文件。另外,还可以直接从Microsoft得到SDK,在有些特殊情况下,SDK可能是作为MSDN的一个部分而打包发行的;Microsoft出版的Inside DirectX一书也有DirectX SDK。
在介绍代码的运作方式之前,我们将花一点时间说说双缓冲的有关内容。这项技术的思路是尽可能模仿胶片电影的基于帧的技术,即双缓冲只是以足够快的速度将一系列的静止画面显示出来从而使用户产生流畅动画的感觉。