作者 Lan Mader
译 冬草秋叶(Cocoaleaves)
DirectDraw总览
目录
DirectDraw总览
1. DirectX API 成员
2. 什么是DirectDraw
3. 与WinG的关系
4. DirectDraw页面 - 如何进入显存
5. 从翻页(page flipping)到动画
6. DirectDraw与COM
DirectDraw程序设计
1. 编译连接所需的东西
2. 初始化DirectDraw
3. 创建主页面并设置翻页链
4. 载入位图到显存
5. 载入调色板
6. 关键色
7. 组成场景
8. 清除
一些细节
1. 调试DirectDraw
2. 窗口模式运行
3. 页面丢失
4. 进一步学习
DirectDraw总览
本部分给出对DirectDraw的高度总览,解释需要理解的DirectDraw概念。
1.DirectX API 成员
微软DirectX API包括以下成员:
l DirectDraw – 直接操作显存
l DirectSound – 直接操作声音硬件
l DirectPlay – 对多人联机游戏的支持
l DirectInput – 对游戏输入设备如操纵杆的支持
它们被设计成一些API从而让程序设计师直接操作硬件。本文讲解DirectDraw技术,让你更快的编写高质量的程序。
2.什么是DirectDraw
DirectDraw从本质上讲就是显存的管理入口。它最重要的作用就是让程序设计师直接在显存中存储和操作位图。它让你能够利用显示硬件的加速器在显存中进行位图的区块拷贝(blit),这样比从内存拷贝到显存快多了。尤其对于今天64位显卡在显存中进行的64位运算有效。并且,硬件进行区块拷贝操作是独立于CPU的,从而减轻了CPU的负担。另外,DirectDraw支持显示硬件的其他加速功能,比如支持精灵动画和Z轴缓冲。DirectDraw1.0现在适用于Windows95,今年夏天将适用于WindowsNT。
3.与WinG的关系
到现在为止,程序设计师可能应用WinG或CreatDIBSection技术在Windows下创建动画。这种方法让程序设计师直接操作内存中位图从而有效的绘制位图。WinG比用GDI操作位图更好,因为GDI从来不能让游戏设计师编写出好程序。
当用位图组成WinG的场景时,是从内存复制到显存,从而显示出来的。这种技术没有DirectDraw快因为从内存到显存没有从显存到显存快。在复杂游戏或应用程序中DirectDraw和WinG技术都会被用到,因为显存资源是有限的。
4.DirectDraw页面 – 如何进入显存
在DirectDraw中,目标是尽可能多的往显存中存放位图。有了DirectDraw,所有显存对你都可用了。你可以将它用于存储各种各样的位图或当主页面和后缓冲页。这些一块块的显存区域在DirectDraw中称作页面(表面)。当你载入一个代表精灵的位图到显存中,首先要创建一个页面,即在显存中申请一块区域,然后将位图区块拷贝到页面中,这就有效的把位图载入到显存中,你可以使用它,用到什么时候都可。
屏幕上显示的内容在显存中也是一个页面,称为主页面。页面的大小与当前显示模式所需大小相适。如果显示模式是640x480与256色(8位每像素),主页面用了显存中307,200字节显存空间。你要同时创建至少一个与主页面大小相同的后缓冲页用于翻页(Flip)。这意味着你最初就需要614,400字节显存空间,在不载入位图的情况下。
所以,DirectDraw中最重要的硬件资源之一就是显存。当显存不够用时,DirectDraw的页面就要存到内存中,所有硬件加速的好处对内存页面都无效了。当今大多数显卡都带有至少1M的显存,但1M刚刚够用于初始化。事实上2M的显存是让DirectDraw发挥作用的,开发小程序的最低值。
DirectDraw提供在运行时检查显存剩余空间的函数,从而程序可以最优地使用显存。
5.从翻页到动画
场景是把存于显存块(页面)中的位图区块拷贝到另一块称为离屏缓冲区的显存块(页面)中而组成的。然后,通过硬件把可视的主页面翻转到后缓冲页,新组成的场景便显示出来了。这就是翻页(page flipping),这是非常快的,因为这个过程并没有数据的复制,而是简单的告诉显卡显存中哪一块是要刷新到显示器上的信号。DirectDraw保护硬件不被程序设计师操作,就提供一个简单的Flip()函数来做这些工作。用Flip()做动画:
1. 在后缓冲页中组合场景
2. 翻页,把后缓冲页显示出来
3. 之前显示的页面现在成了后缓冲页
4. 重复步骤1
翻页的帧频非常快,仅受限于显示器的刷新频率,即翻页发生于显示器的每次扫描。这意味着如果你的显示器设置为72Hz,你可以达到72帧每秒的帧频。翻页于扫描同步的好处是避免了撕裂DirectDraw页面的画面。撕裂是一种在扫描过程中移动屏幕上精灵所产生的现象。精灵移动到新位置时看起来是被撕裂的。比如,一个图象的上半截出现在某位置但下半截却向左或向右偏移了,看起来像撕开了一样。
图象程序师已经找到了有效的绘制精灵的技术,这些技术依然可用于DirectDraw程序。但是,学习那些技术对学习理解DirectDraw没有帮助。所以,我着眼于“从翻页到动画”。翻页是产生动画最简单的方法,也是最好的演示DirectDraw如何工作的方法。
6.DirectDraw和COM
DirectX是应用COM对象的工具。用DLL中的COM对象比简单的用DLL中的多态的API接口更有好处。DirectDraw设计师需要知道的主要一点就是使用COM对象不比用其他API难,尤其对于C++和面向对象Pascal程序。
你不必参与初始化COM,或调用QueryInterface来得到接口。DirectDraw头文件为不同的DirectX对象声明了C++类。你可以调用不同的函数实例化这些类。所以,关键在于学习不同的成员函数。
DirectDraw程序设计
本部分用示例代码演示DirectDraw如何工作。代码演示全部功能,并尽可能体现本质要点。
1.编译连接所需的东西
首先,你需要DirectX SDK。这目前仅能用于MSDN level II或更高、MS Visual C++4.1。SDK提供帮助文件,并有精彩的程序实例。
为了编译连接一个DirectDraw应用程序,你需要DDRAW.DLL、DDRAW.H,并且你需要用IMPLIB.EXE从DDRAW.DLL制作一个导入的库。DirectX是Win32下的技术,所以你需要一个能产生Win32应用程序的编译器。Borland C++4.52和Borland C++5.0提供一个很好的DirectX开发平台。
为了运行DirectX技术程序,你必须在系统里安装DirectX驱动。既然这是一个常用的游戏技术,你会发现DirectX已经安装在你的系统里了。到Window/系统目录下找找,是不是DDRAW.DLL已经存在了。(记住,DirectX技术不能用于Windows 3.x)
2. 初始化DirectDraw
学习DirectDraw的第一步是将其初始化.阅读下面代码对每一步操作的详细注释。
// 全局变量 (ugh)
LPDIRECTDRAW lpDD; // DDRAW.H中定义的DirectDraw对象
/*
* DirectDraw初始化函数
* 演示:
* 1) 创建DirectDraw对象
* 2) 设置协作级别
* 3) 设置显示模式
*
*/
bool DirectDrawInit(HWND hwnd)
{
HRESULT ddrval;
/*
* 创建主DirectDraw对象
*
* 此函数初始化COM并构造DirectDraw对象
*/
ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
if( ddrval != DD_OK )
{
return(false);
}
/*
* 协作级别决定我们使用屏幕权力的大小
* 这必须设置为DDSCL_EXCLUSIVE 或 DDSCL_NORMAL
*
* DDSCL_EXCLUSIVE 允许我们改变显示模式, 并且需要
* DDSCL_FULLSCREEN 标记, 从而把窗口设定为
* 全屏. 这是我们首选的DirectDraw模式因为它允许我们
* 控制整个屏幕而不用理会GDI
*
* DDSCL_NORMAL 允许DirectDraw程序在窗口下运行
*/
ddrval = lpDD->SetCooperativeLevel( hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN );
if( ddrval != DD_OK )
{
lpDD->Release();
return(false);
}
/*
* 设置显示模式为 640x480x8
* 设置显示模式可行,因为我们在上面已经选择了exclusive协作级别
*/
ddrval = lpDD->SetDisplayMode( 640, 480, 8);
if( ddrval != DD_OK )
{
lpDD->Release();
return(false);
}
return(true);
}
现在我们完成了初始化和设置显示模式。
3. 创建主页面并设置翻页链
下面我们创建带有一个后台缓冲的主页面。这在DirectDraw中称为复杂页(complex surface)。缓冲页是附属在主页面上的。当我们在程序结尾释放时,仅仅需要释放主页面指针,缓冲页也就随着释放了。
// 全局变量 (ugh)
LPDIRECTDRAWSURFACE lpDDSPrimary; // DirectDraw主页面
LPDIRECTDRAWSURFACE lpDDSBack; // DirectDraw缓冲区
/*
* 创建一个带有一个后缓冲页的可翻页的主页面
*/
bool CreatePrimarySurface()
{
DDSURFACEDESC ddsd;
DDSCAPS ddscaps;
HRESULT ddrval;
// 创建带有一个缓冲页的主页面
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
if( ddrval != DD_OK )
{
lpDD->Release();
return(false);
}
// 取得指向缓冲页的指针
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, &lpDDSBack);
if( ddrval != DD_OK )
{
lpDDSPrimary->Release();
lpDD->Release();
return(false);
}
return true;
}
现在我们创建好了双页面翻页环境。
4. 载入位图到显存
下一步是为游戏载入位图。理想情况下,有足够的显存,所有位图都可在显存中。如果不能, CreateSurface将在系统内存中创建页面。除了区块拷贝慢一些,其他没有任何影响。
/*
* 此函数创建离屏页面,从磁盘载入位图到页面
* 参数szBitmap是文件名或位图号
*/
IDirectDrawSurface * DDLoadBitmap(IDirectDraw *pdd, LPCSTR szBitmap)
{
HBITMAP hbm;
BITMAP bm;
IDirectDrawSurface *pdds;
DirectDraw总览
// LoadImage在Win95下有项补充的功能
// 允许你在文件载入位图
hbm = (HBITMAP)LoadImage(NULL, szBitmap, IMAGE_BITMAP, 0, 0,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);
if (hbm == NULL)
return NULL;
GetObject(hbm, sizeof(bm), &bm); // 取得位图尺寸
/*
* 为位图创建一个页面
* 下面准备函数CreateOffScreenSurface()
*/
pdds = CreateOffScreenSurface(pdd, bm.bmWidth, bm.bmHeight);
if (pdds) {
DDCopyBitmap(pdds, hbm, bm.bmWidth, bm.bmHeight);
}
DeleteObject(hbm);
return pdds;
}
/*
* 创建一个特殊尺寸的页面
* 如果显存足够就在显存中创建
* 否则在系统内存中创建
*/
IDirectDrawSurface * CreateOffScreenSurface(IDirectDraw *pdd, int dx, int dy)
{
DDSURFACEDESC ddsd;
IDirectDrawSurface *pdds;
//
// 为位图创建一个页面
//
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT |DDSD_WIDTH;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = dx;
ddsd.dwHeight = dy;
if (pdd->CreateSurface(&ddsd, &pdds, NULL) != DD_OK)
{
return NULL;
} else {
return pdds;
}
}
/*
* 此函数把先前载入过的位图复制到一个DirectDraw页面
* 注意到我们要为DirectDraw页面获得一个GDI的设备上下文(DC)
* 再把位图BitBlt到页面中
*/
HRESULT DDCopyBitmap(IDirectDrawSurface *pdds, HBITMAP hbm, int dx, int dy)
{
HDC hdcImage;
HDC hdc;
HRESULT hr;
HBITMAP hbmOld;
//
// 把位图选进DC,我们可以使用它了
//
hdcImage = CreateCompatibleDC(NULL);
hbmOld = (HBITMAP)SelectObject(hdcImage, hbm);
if ((hr = pdds->GetDC(&hdc)) == DD_OK)
{
BitBlt(hdc, 0, 0, dx, dy, hdcImage, 0, 0, SRCCOPY);
pdds->ReleaseDC(hdc);
}
SelectObject(hdcImage, hbmOld);
DeleteDC(hdcImage);
return hr;
}
现在我们有了一个已经载入了位图的DirectDraw页面。下面我们用它通过区块拷贝来组成场景。
5. 载入调色板
我们需要为主页面设置调色板。这意味着游戏中的所有位图被这同一个调色板创建。这有一个函数用于为给出的位图创建DirectDraw调色板。
/*
* 为已有的位图创建一个DirectDraw调色板
* 参数szBitmap是文件名或位图号
*/
IDirectDrawPalette * DDLoadPalette(IDirectDraw *pdd, LPCSTR szBitmap)
{
IDirectDrawPalette* ddpal;
int i;
int n;
int fh;
PALETTEENTRY ape[256];
if (szBitmap && (fh = _lopen(szBitmap, OF_READ)) != -1)
{
BITMAPFILEHEADER bf;
BITMAPINFOHEADER bi;
_lread(fh, &bf, sizeof(bf));
_lread(fh, &bi, sizeof(bi));
_lread(fh, ape, sizeof(ape));
_lclose(fh);
if (bi.biSize != sizeof(BITMAPINFOHEADER))
n = 0;
else if (bi.biBitCount > 8)
n = 0;
else if (bi.biClrUsed == 0)
n = 1 << bi.biBitCount;
else
n = bi.biClrUsed;
//
// 一个设备无关位图(DIB)颜色表存储颜色为BGR而不是RGB
// 所以要轮换一下
//
for(i=0; i<n; i++ )
{
BYTE r = ape[i].peRed;
ape[i].peRed = ape[i].peBlue;
ape[i].peBlue = r;
}
}
if (pdd->CreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL) != DD_OK)
{
return NULL;
} else {
return ddpal;
}
}
下面我们这样为主页面设置调色板:
lpDDPal = DDLoadPalette(lpDD, szBitmap); // 调用上面的函数载入调色板
if (lpDDPal)
lpDDSPrimary->SetPalette(lpDDPal); // 把调色板设置到主页面
6. 关键色
DirectDraw用叫做关键色的概念处理区块拷贝中的透明色。这就是说,你可以让你的位图在区块拷贝中一部分变透明,这在把精灵拷贝到背景上时是非常有用的。关键色指定了调色板中的一个颜色范围,源位图或目标位图的关键色在区块拷贝中不被复制。一般地,把调色板入口0或255设为关键色,同时位图中不想被拷贝的部分颜色应该为设置的关键色。
// 为这副位图设置关键色
//
// 无论什么颜色在调色板入口255处都将成为区块拷贝的关键色
// 颜色会是黑色,除非你创建调色板时用了DDPCAPS_ALLOW256标志
DDCOLORKEY ddck;
ddck.dwColorSpaceLowValue = 0xff;
ddck.dwColorSpaceHighValue = 0xff;
// lpDDSSomeBitmapSurface是一个已载入位图的表面,即所有表面都包含了一个位图
lpDDSSomeBitmapSurface->SetColorKey( DDCKEY_SRCBLT, &ddck );
7. 组成场景
现在我们已经准备好开始编写一个游戏,模拟器或其他基于图象的程序。简单的方法就是将一张背景位图区块拷贝到后缓冲页,然后把一些精灵(同样是位图)区块拷贝到后缓冲页合适的位置,然后翻页。重复上面的步骤,把精灵粘到新的位置。
// 此函数在消息循环的适当位置反复调用
void updateFrame( void )
{
RECT rcRect;
HRESULT ddrval;
int xpos, ypos;
// 把素材区块拷贝到下一帧
SetRect(&rcRect, 0, 0, 640, 480);
// 拷贝背景图。这是一张640x480的位图,
// 所以它将填满屏幕(记得我们把显示模式设为640x480x8
// 参数lpDDSOne是假定背景图装载进的页面。
// LpDDSBack是我们的后缓冲页
ddrval = lpDDSBack->BltFast( 0, 0, lpDDSOne, &rcRect,
DDBLTFAST_NOCOLORKEY | DDBLTFAST_WAIT);
DirectDraw总览
if( ddrval != DD_OK )