网页功能: 加入收藏 设为首页 网站搜索  
DDraw和D3D立即模式编程手册
发表日期:2007-01-16作者:[转贴] 出处:  


介绍
DirectX是微软开发并发布的游戏开发软件包,其中有一部分叫做Direct3D是三维图形立即模式绘演API。因为所有的人都说它将成为3D图形的标准,我决定去学习它,可它实在是难以琢磨,文档也写得很烂,总是出错,后来我逐渐习惯了这种痛苦,我意识到其他的人可以从我这里学会一些经验,这就是我写这篇文章的原因。 

请注意:此篇文章是免费的,所以不要设想它和一些杂志上的文章一样会那么有想象力,或者行文简洁、准确无误,但是这最起码要比微软用来哄骗老实的游戏程序员的那些废话要强得多。还有就是本篇文章还在继续撰写中,无论如何请原谅诸如排版、语法和例子程序等的错误。 

我不想总是打那么长的各种名称,请注意:Direct3D写作D3D,DirectDraw写作DD,DX则是DirectX的简写。这篇文章适用于DirectX 3,但极有可能也适用于更早的版本。 

在这篇文章中的例子程序代码是来自于我的一个程序,为了更容易读懂,在将其加入这篇文章时一些变量名已被更改了,可能还存在其它的明显的错误,我为此感到抱歉,如果您发现了一些错误请告诉我,我将努力的改正它。 

在已发行的关于DirectX的书中,我发现一本《DirectDraw Programming》很不错,作者是Bret Timmins(M&T Books, 1996)。这本书写得很好,有大量的很容易看懂的源程序帮助您掌握DirectDraw,对于想学习DirectX编程的程序员我推荐这本书。 


本文的组织 
本文分成若干小节,在每个小节中分别讲述了Direct3D编程的痛苦经历的一个部分。 

协定
本文假设您使用的是DirectX的C++ COM接口。在语法上,C与C++的接口只有细微的不同, C语言需要通过lpVtbl实现功能调用,还需要COM对象被传递到调用函数中。 

某些变量在本文中是统一的,如下: 

LPDIRECTDRAW        lpDD;
LPDIRECT3DDEVICE    lpD3DDevice; 
LPDIRECTDRAWSURFACE lpBackBuffer;
LPDIRECTDRAWSURFACE lpFrontBuffer;
LPDIRECTDRAWSURFACE lpZBuffer;
LPDIRECTDRAWCLIPPER lpClipper;
LPDIRECT3D          lpD3D;
LPDIRECT3DEXECUTEBUFFER lpExBuf;
D3DEXECUTEDATA      ExData;
LPVOID              lpBufStart, lpPointer, lpInsStart;
D3DMATRIXHANDLE     hModelWorldMatrix;
D3DMATRIXHANDLE     hWorldViewMatrix;
D3DMATRIXHANDLE     hProjectionMatrix;

修订历史
Release 1.00 (Not so soon!) 
将会有对于DirectX5立即模式的讨论。 

Release 0.50 (Coming Soon!) 
将会有关于纹理管理,材质管理,雾化,动态光源和重建表面的讨论。 

Release 0.46
修改了一处编译小错误。 

... ... 

Release 0.0
在网站发布第一个版本。 

致谢
让我高兴的是人们开始通过e-mail或其它方式帮助我,我要感谢他们,感谢他们的回应和错误报告,他们是: 

Mark Adami 
Chris Babcock 
James Bowman 
Mark Clouden 
Ray Gresko 
Andrei Voden 
Bryan Westhafer 
Dennis Ward 
Tim Wilson 
有关法律责任
本文和所有与本文的相关部分的版权归Brian Hook所有,授权Pigprince中文版版权所有,授权您可以通过电子手段(二进制形式)分发(e-mail、邮寄或存储)本文档的全部或部分,在没有对本文做任何改变的情况下授权容许打印拷贝。建议使用最新版本。 所有的信用和版权声明被保留,如果您要在其它站点加一个指向本文的连接请通知 Brian Hook(E文版)或Pigprince(中文版)取得授权。 

需要拥有其它发布形式权利的,如要在公司、组织、商业产品如:书籍、报刊杂志、CD-ROM、应用软件等中发布的,请尊重作者的权利(署名权,取得报酬权等)。 

本文没有任何明确的或者含蓄的表达说明本文是完全正确的,对于应用本文内容所产生的任何结果,本人概不负责。如果您在开发较高预算的工程,请您不要只依赖于本文的一家之言。 

所有的名称,商标,版权等属于该名称,商标,版权等的法定所有者。 


--------------------------------------------------------------------------------

Direct3D快速浏览 
DirectDraw 和 Direct3D
首先要明了一个基本事实:Direct3D是微软用来处理视频硬件的API--DirectDraw的一部分,既然D3D是DDraw的子接口,这就意味着你在使用Direct3D前必需掌握一些DDraw基础,一旦您清楚了这一点,思路会变得清晰一些,但是不要以为这就会使我们所面对的事情简单起来。 

表面
DirectDraw提出了表面(surface)的概念,表面是存储在内存中的一幅图形,IDirectDrawSurface 通常用来声明前、后绘演缓冲区、Z缓冲和f纹理映射的管理。 

运行缓冲区
所有的绘演数据和状态改变是通过运行缓冲区发送到D3D驱动模块的。运行缓冲是包含命令和数据的一大块内存,它在D3D中的作用被吹得天花乱坠,您将在后面看到对它的详细介绍。 


--------------------------------------------------------------------------------

令人心烦的初始化 
D3D程序员在开发D3D程序时遇到的第一个难关将是初始化。这一小节将介绍如何对DDraw和D3D进行初始化, 3D驱动程序和3D设备的异同,还有如何为您开发的程序选择最佳驱动程序和设备的方法。 

该死的GUIDs
首先我要解释的是GUID的问题,这是一个DirectX程序员在开发DirectX程序时始终要面对的烦人的东西,有时甚至感觉比教会狮子狗算算数题还难。DirectX通过GUIDs(Globally Unique Identifier) 来识别接口。如果GUIDs的文档说明能清楚一点那可真的是件值得庆幸的事!可惜没有。 

在使用GUIDs时您可能会遇到二个难题--目标代码连接出错和指针应用上的问题。 

使用INITGUID时目标代码连接出错
第一个难题是在试验着写一个D3D立即模式程序时常常会遇到一些诸如此类的出错信息: 

foo.obj : error LNK2001: unresolved external symbol _IID_IDirect3D
Debug/foo.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe
这就是臭名昭著的INITGUID bug! 

发生这个连接错误的原因是在您查寻某个接口时需要这个令人'振奋'的GUID,见下例: 

lpDD->QueryInterface( IID_IDirect3D, ( LPVOID * ) &lpD3D );
IID_IDirect3D是一个全局常量,一定要在程序中声明或者与DXGUID.LIB (缺省在dxsdk/sdk/lib路径下)编译连接,否则将会引发连接错误。 

在一个应用程序中实例化GUIDs
为了在一个程序中实例化必需的GUID(注意:您也可能在其他DirectX元件中遇到这种情况,他们同样需要他们自己的GUID),您一定要在包含(include)您的头文件之前定义(define)INITGUID,这可能是唯一正确的途径。见下例: 

FOO.C: 

#define INITGUID
#include <ddraw.h>
#include <d3d.h>
In file BAR.C: 

// Do not #define INITGUID in more than one file!!!!
#include <ddraw.h>
#include <d3d.h>
如果您实例化过多的GUIDs,您可能会遇到另一种连接错误(实际上是一堆错误信息),如下您可以看到这种让人头大的情况: 

foo.obj : error LNK2005: _IID_IDirect3DViewport already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirect3DExecuteBuffer already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirect3DMaterial already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirect3DLight already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirect3DTexture already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirect3D already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirectDrawClipper already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirectDrawPalette already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirectDrawSurface2 already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirectDrawSurface already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirectDraw2 already defined in bar.obj
foo.obj : error LNK2005: _IID_IDirectDraw already defined in bar.obj
foo.obj : error LNK2005: _CLSID_DirectDrawClipper already defined in bar.obj
foo.obj : error LNK2005: _CLSID_DirectDraw already defined in bar.obj
真正的让人觉得犯贱的是如若您是建立了一个静态库来实例化GUIDs,那么所有您要实例化GUIDs的地方都要连接到您的这个静态库,就又会得出和上面一样的连接错误。

真正圆满的解决办法是:与DXGUID.LIB连接。 

与DXGUID.LIB连接
作为一种选择,程序中也可以可以不用定义INITGUID,代替以直接与DXGUID.LIB进行编译连接的方法,在这个库中包含有所有的进行DirectX GUIDs实例化的代码,这可能是最简单的解决办法了。可是我却找不到关于只一点的任何的文档说明,在我明白这一点之前我不得不在Usenet中搜寻。如果您用这个方法不管是创建库还是一个完整的应用程序,都不会产生过多的实例化GUID的错误。唯一可能遇到的麻烦事就是使用Borland或Watcom编译器的程序员在连接DXGUID.LIB时会出现一些故障。 (DX5 SDK中包括有用于Borland编译器的库,译者注) 

C and C++ GUID 接口是不同的
另一个与此有关的错误是在C语言中,您应该传递一个指针到GUID再到IDirectDraw::QueryInterface,而在C++中您应该传递一个参考参数(reference)到GUID再到IDirectDraw::QueryInterface 这就是说C语言的实现应为: 

// pass address of IID_IDirect3D to QueryInterface
lpDD->lpVtbl->QueryInterface( lpDD, &IID_IDirect3D, ( LPVOID * ) &lpD3D );
而C++的实现应为: 

// pass reference to IID_IDirect3D
lpDD->QueryInterface( IID_IDirect3D, ( LPVOID * ) &lpD3D ) ;
设备与驱动程序的异同现在GUID的话题告一段落,下面我们要明确的是设备与驱动这二者的定义。设备是在计算机中安装的硬件。在一台计算机中可能装有一个或几个DirectDraw设备,对每一个设备将由唯一的DDraw驱动程序用来完成硬件和软件间的通信,使二者紧密地结合起来。每一个DirectDraw设备的驱动程序将轮流有几个Direct3D驱动程序隶属于它,那么一旦您选定了一个具体要使用的Direct3D驱动程序,你就要从中创建一
个IDirect3DDevice,是的,真的没有想象的那样容易。 

举一个例子,我的一台计算机,已安装了如下一些DirectDraw设备: 

Primary device
3Dfx DirectDraw device
上述系统有一个主要设备--使用Rendition Verite芯片的Intergraph Reacter卡, 还有第二个设备是使用3Dfx Interactive Voodoo图形加速器的Diamond Monster3D卡。对每一个设备都由唯一的设备驱动程序,但是每一个DDraw设备驱动程序可以有多个Direct3D驱动程序(保持这种差别是很重要的)。 

在微软的文档中对驱动程序和设备这两个名词的定义是不同的,在上一段的叙述中我采用了微软的说法,虽然与微软的定义保持完全一致并没有什么必要,但至少我觉得应该这样做。 

好吧,现在我们已经知道了在一台计算机内可以有一个或多个DDraw设备(还有设备驱动程序),每一个设备至少有两个微软的D3D软件驱动程序,至少有一个硬件支持厂商提供的D3D硬件HAL(硬件支持层,Hardware Abstraction Layer), 其中两个软件驱动程序用来支持绘演能力,如果设备存在硬件加速能力的话, HAL用来提供处理加速能力的途径。 

在我的计算机中的主要设备(Intergraph Reactor)有如下几种驱动程序: 

Ramp Emulation
RGB Emulation
Direct3D HAL
一旦您决定了您要使用的D3D驱动程序,您必须创建用来做绘演操作的D3D设备。 

好,我们来概括一下: 

DirectDraw设备:一个用来在计算机系统中提供DirectDraw(可能包括D3D硬件加速能力)能力的硬件设备。在一个计算机系统中至少要安装有一个DirectDraw设备(标准显示适配器)。 

DirectDraw设备驱动程序:是一个DirectDraw设备在系统中的软件接口,对每一个DirectDraw设备使用唯一的DirectDraw设备驱动程序对其进行驱动操作。 

Direct3D驱动程序:是一个已指明的DirectDraw设备驱动程序的子接口。一个DirectDraw设备驱动程序可能有多于一个的Direct3D设备驱动程序存在。微软已在其中封装了两个缺省的D3D设备驱动模块,Ramp Emulation和RGB Emulation驱动。如果D3D设备具有硬件加速设备支持能力,则还应具有另外一个驱动模块--HAL。 

IDirect3DDevice:这是您从由您指明要使用的D3D设备驱动中创建的软对象。 IDirect3DDevice是用来取得你创建的运行缓冲并将其传递到驱动模块中。 

初始化D3D要走的笨拙的11个步骤
初始化D3D的确是一件使人心烦的事,但是如果您对要完成的目标有了较全面的了解,公平的讲还是比较清晰的。为了取得“准备完毕”的状态信息,首先要初始化DDraw(因为D3D是DDraw的一部分),然后再来初始化D3D,步骤如下: 

例举DDraw系统设备 
选择并创建IDirectDraw对象 
例举显示模式(可选) 
创建IDirect3D对象 
为已选择的DDraw设备实例化D3D驱动程序 
选择D3D驱动程序 
设置坐标系级别 
创建前、后缓冲区,剪裁器是必需的要创建的 
创建Z缓冲(可选) 
创建 IDirect3DDevice 
创建 IDirect3DViewport并与IDirect3DDevice关联 
第一步:Step 1: 例举DDraw系统设备?/A> 
应用程序要做的第一件事就是例举系统中所有的DDraw设备,这部分工作由DirectDrawEnumerate完成。 DirectDrawEnumerate执行一次您传递至该函数的对每一个设备的回叫。您可以将回叫的值存储在一个表单中,以便于在您或者使用者来改变要使用的设备时进行处理。下面的
例子就是一个DirectDrawEnumerate回叫函数: 

BOOL FAR PASCAL DDEnumCallback( GUID FAR* lpGUID, 
                                LPSTR lpDriverDesc,
                                LPSTR lpDriverName, 
                                LPVOID lpContext )
{
    LPDIRECTDRAW lpDD;
    DDCAPS DDcaps, HELcaps;
    // once again, my own data structure
    DDDeviceList *dl = ( DDeviceList *) lpContext;

    /*
    ** 使用指明的GUID试着创建DD设备
    */
    if ( DirectDrawCreate( lpGUID, &lpDD, NULL ) != DD_OK )
    {
        // 失败,忽略该设备
        return DDENUMRET_OK;
    }

    /*
    ** 取得这个DD驱动的头,如果未成功,转移至一个驱动
    */
    memset( &DDcaps, 0, sizeof( DDcaps ) );
    DDcaps.dwSize = sizeof( DDcaps );
    memset( &HELcaps, 0, sizeof( HELcaps ) );
    HELcaps.dwSize = sizeof( HELcaps );
    if ( lpDD->GetCaps( &DDcaps, &HELcaps ) != DD_OK )
    {
        lpDD->Release();
        lpDD = 0;
        return DDENUMRET_OK;
    }

    /*
    ** 是有效设备,现在将有关信息存储至设备列表
    */
    dl->AddDevice( &DDcaps, lpGUID, lpDriverName, lpDriverDesc );
    lpDD->Release();

    if ( dl->IsFull() )
        return DDENUMRET_CANCEL;

    return DDENUMRET_OK;
}

上面的代码应该可以说明问题了,但是还有几个小窍门,只有对主要设备时lpGUID的值为NULL,其它所有的设备的lpGUID都应为非空,这个您可能不会立刻发现。 

另外要特别注意到不要直接的存储lpGUID,而且您的设备信息表结构应该类似于: 

struct DriverInfo
{
    LPGUID lpGUID;
    GUID guid;
    // … other stuff
};

当你在表中增加一个设备时,代码应该如此: 

struct DriverInfo *current;
if ( lpGUID == NULL )
{
    current->lpGUID = 0;
}
else
{
    current->lpGUID = &current->guid;
    memcpy( &current->guid, lpGUID, sizeof( GUID ) );
}

这会使您免于将lpGUID指针乱指一气。其实lpGUID并不重要,真正麻烦的是GUID,我们应该尽可能的直接存储GUID。 

好吧,现在我们已经有了一个DDraw设备列表,其中的一个是主要设备(lpGUID为空),有0个或更多的非主要设备(lpGUID为非空)。 

第二步:选择并创建IDirectDraw对象
好了您现在已有了系统中的DDraw设备的列表,但是如果只有一个DDraw设备,请直接到下一步。如果系统中有几个DDraw设备,您就应该让使用者来选择使用哪一个设备,不要试着确定或使用一些复杂的判断来确定合适的设备。应该由用户来决定哪一个更适合并保存他们的选择。 

一旦您决定了使用哪一个设备,接下来您要创建一个与DDraw设备的驱动的接口,通过DirectDrawCreate传送一个指针到您要使用的设备的GUID中来实现,从而取得一个指向IDirectDraw对象的指针。 

if ( DirectDrawCreate( d_selected_dd_device.lpGUID, &lpDD, NULL ) != DD_OK )
goto fail;
第三步:例举显示模式(可选)
如果您计划要使用满屏模式,您就需要使用IDirectDraw::EnumDisplayModes 函数来例举系统所支持的所有显示模式。这个函数通过回叫来实现所有的显示模式的例举,同上所述您也应该将其存储在一个列表中提供给用户进行选择。 

第四步:创建IDirect3D对象
说真的,我们等这一步等了这么久了!但是我们现在还是没有直接处理D3D的途径。所以我们不得不在IDirectDraw对象(先前使用DirectDrawCreate创建的)中查询它是否支持D3D,如果其支持D3D那么同时这个操作也会初始化一个指向D3D对象的指针,请看下面的
例子: 

BOOL CreateD3D( LPDIRECTDRAW lpDD )
{
    if ( lpDD->QueryInterface( IID_IDirect3D, ( LPVOID *) &lpD3D ) != DD_OK )
        return FALSE;
    else
        return TRUE;
}

第五步:例举D3D驱动程序
现在桀骜不逊的Direct3D已被我们理顺了一些,但还有较长的路要走。我们已经有了指向IDirectDraw对象(LPDIRECTDRAW)和IDirect3D对象(LPDIRECT3D)的指针。下一步我们要做的是例举所有关联到DDraw设备的D3D驱动。这一步是由名字容易引起误解的IDirect3D::EnumDevices方法调用实现的(因为我们要例举的是驱动,而不是设备)。我们将再一次的在这个方法中使用回叫来查询所有的D3D驱动,要注意我们同时也传递了一个指向程序应用数据的指针,也就是说我们将传递一个指向我们想要存储器动程序信息的存储结构的指针。 

但是在我们取得进步之前,还是要脚踏实地的一步一步的来。进行到这里稍微有一点棘手,我们使用的回叫并不需要完整的存储对D3D驱动的整个描述,而应该使用一个小技巧--回叫应只存储那些有用的信息,举例来讲:如有我们需要的标准的硬件驱动程序。 

下面是D3D例举回叫的例子: 


HRESULT WINAPI EnumD3DDriversCallback( LPGUID lpGuid,
                                    LPSTR lpDeviceDescription,
                                    LPSTR lpDeviceName,
                                    LPD3DDEVICEDESC lpHWDesc,
                                    LPD3DDEVICEDESC lpHELDesc, 
                                    LPVOID lpContext )
{
    // D3D 驱动管理
    D3DDriverList *r = ( D3DDriverList * ) lpContext;
    int should_keep = 0;

    /*
    ** 这是HAL吗? 检查lpHWDesc->dwFlags。我不知道此方法是否正确,
    ** 但是它工作得很好,而且我没有发现有任何其他的方法可以做得更好。
    */
    if ( lpHWDesc && lpHWDesc->dwFlags != 0 )
    {
        // 这是一个HAL!检查参数,如果我们要使用它,置should_keep为1。
        // 如:我们应该让具有16位Z缓冲的硬件来完成透明纹理映射。
        // 注意:如果我们要进行程序调试,就不应该使用硬件加速(表面存储在内存中)。
        if ( ( lpHWDesc->dpcTriCaps.dwTextureCaps & D3DPTEXTURECAPS_PERSPECTIVE ) && ( lpHWDesc->dwDeviceZBufferBitDepth & DDBD_16 ) && !debugging )
        {
            should_keep = 1;
        }
    }
    else if ( lpHELDesc )
    {
        // 这是一个HEL!检查参数,如果我们要使用它,置should_keep为1。
        // 如:我们只想保留RGB模式的驱动。
        if ( lpHELDesc->dcmColorModel & D3DCOLOR_RGB )
            should_keep = 1;
    }

    /*
    ** 如果我们需要它,记录D3D驱动的信息。
    */
    if ( should_keep )
    {
        r->Add( lpGuid, lpDeviceDescription, lpDeviceName );
        if ( r->IsFull() )
            return D3DENUMRET_CANCEL;
    }
    return D3DENUMRET_OK;
}

我希望上面的代码就足以说明问题,但我还是要在这里画蛇添足一番:做为一个一般的人人都可以完成的编程任务,我并不是在告诉您如何实现一个用来存储D3D驱动的堆。我要作解释的是lpHELDesc->dcmColorModel方法看起来应该是一个域,而不是一个enum,就是说在这里应该用您的聪明才智来解决它,而不要直接看一下系统是否支持RGB模式。 

还有就是我所知道的:这个回叫总是在lpHELDesc和lpHWDesc指针中得到非空的值,这使您不可能知晓这个驱动到底是硬件还是软件的,我知道的唯一获得这个信息的方法是检查lpHWDesc中的dwFlags参数,这是由经验总结出来的,我尚未发现更好的解决办法。 

我早期的关于代替存储lpGUID而存储GUID的解释也在这里了。 

第六步:选择D3D驱动程序
那么好吧,我们现在应该已经有了LPDIRECTDRAW、LPDIRECT3D和一个用于选择的D3D驱动列表。再次重复:我们应该要用户来选择要使用哪一个驱动!在我们开始认为我们知道哪一个更好时,我们就已经开始给自己找麻烦了,那不是一个好主意。不要去做使我们的程序变得过大的事。经典一些的做法是弹出一个包含所用可以使用的D3D驱动的对话窗让用户来选择一个,并存储他们的选择,这就象吃饼干一样简单。在本文后面将有关于这种做法的解释,如存储结构:d_selected_driver,这个结构通常由程序来定义,我将提供给您,它通常包括如:驱动程序名、描述、能力和GUID等表项。 

一旦用户选择了一个驱动,在我们创建可以在上面绘演三角形的上帝---D3D设备之前,我们还要做一些穷极无聊的事。 

第七步:设置坐标系级别
这里并没有过多的处理,只是选择是运行在窗口模式还是在满屏模式。唯一要注意的是有一些DDraw设备不支持窗口模式(想起了3Dfx Interactive Voodoo芯片)。如果要转换到满屏模式则在这里就要用到您在前面例举的显示模式之一了。 

Anyway, you should have a short chunk of code that does something like this: 总之:我们应该由大块的代码来完成一些我们要做的事情,如下: 

if ( fullscreen )
{
    if ( lpDD->SetCooperativeLevel( hWnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN ) != DD_OK )
        return 0; // 错误引发
    /*
    ** 设置满屏显示模式
    */
    if ( lpDD->SetDisplayMode( width, height, bpp ) != DD_OK )
        return 0;
}
else
{
    if ( lpDD->SetCooperativeLevel( hWnd, DDSCL_NORMAL ) != DD_OK ) 
        return 0; // 错误引发
}

第八步:创建前、后缓冲区 (包括剪裁器)
我们现在要创建的是绘演缓冲,这包括一个前缓冲和一个后缓冲,我将在后面谈到纹理表面。 

第一个您要创建的缓冲是前缓冲---也被称为首要显示外表,然后您还要创建后缓冲,而且如果您的应用程序试运行在窗口模式的,您就还需要创建一个剪裁器,并把它关联到应用程序的窗口上。 

下面的代码是用来创建满屏模式绘演的缓冲的例子: 

LPDIRECTDRAWSURFACE lpFrontBuffer, lpBackBuffer;

BOOL CreateFullScreenSurfaces( LPDIRECTDRAW lpDD )
{
    DDSURFACEDESC ddsd;
    DDSCAPS ddscaps;

    memset( &ddsd, 0, sizeof( ddsd );

    ddsd.dwSize = sizeof( ddsd );
    ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
    ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_3DDEVICE | DDSCAPS_COMPLEX;
    ddsd.dwBackBufferCount = 1;

    if ( lpDD->CreateSurface( &ddsd, &lpFrontBuffer, NULL ) != DD_OK )
    {
        goto fail;
    }
    ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
    if ( lpFrontBuffer->GetAttachedSurface( &ddscaps, &lpBackBuffer ) != DD_OK )
    {
        goto fail;
    }
    return TRUE;
fail:
    RELEASE( lpFrontBuffer );
    return FALSE;
}

下面的代码是用来创建窗口模式绘演的缓冲的例子: 

BOOL CreateWindowedSurfaces( LPDIRECTDRAW lpDD )
{
    DDSURFACEDESC ddsd;
    DDSCAPS ddscaps;

    memset( &ddsd, 0, sizeof( ddsd );

    ddsd.dwSize = sizeof( ddsd );
    ddsd.dwFlags = DDSD_CAPS;
    ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

    if ( lpDD->CreateSurface( &ddsd, &lpFrontBuffer, NULL ) != DD_OK )
    {
        goto fail;
    }

    ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_CAPS;
    ddsd.dwWidth = window_width;
    ddsd.dwHeight = window_height;

    ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_3DDEVICE;
    /*
    ** 在调试时表面要创建在系统内存中,
    ** 所一旦锁定了表面时,调试器将不工作。
    */
    if ( debugging || !using_hardware )
        ddsd.ddsCaps.dwCaps |= DDSCAPS_SYSTEMMEMORY;
    else
        ddsd.ddsCaps.dwCaps |= DDSCAPS_VIDEOMEMORY;

    // 创建后缓冲
    if ( lpDD->CreateSurface( &ddsd, &lpBackBuffer , NULL ) != DD_OK )
    {
        goto fail;
    }

    // 创建剪裁器并关联到窗口
    if (lpDD->CreateClipper( 0, &lpClipper, NULL ) != DD_OK )
    {
        goto fail;
    }

    if ( lpClipper->SetHWnd( 0, hWnd ) != DD_OK )
    {
        goto fail;
    }

    if ( lpFrontBuffer->SetClipper( lpClipper ) != DD_OK )
    {
        goto fail;
    }

    // 释放剪裁器,在我们使用SetClipper设置时,会自动的获得操作。
    if ( lpClipper->Release() != DD_OK )
        goto fail;

    return TRUE;
fail:
    RELEASE( lpFrontBuffer );
    RELEASE( lpBackBuffer );
    RELEASE( lpClipper );
    return FALSE;
}

第九步:创建Z缓冲
目前我们只是创建了前、后缓冲区,如果我们还要在程序中进行更深缓冲的表面移动,那么就还要创建Z缓冲,如果您不需要,您可以忽略这一小节和在本文中所有与Z缓冲有关的内容。 

创建Z缓冲是很麻烦的,如果您要使用它的话,您一定要在创建 IDirect3DDevice(在后面描述)之前创建Z缓冲,这一点很重要!否则会引发某些稀奇古怪的错误。我所用的方法见下面代码,在满屏方式或窗口方式绘演中都工作得很好。 

/*
** CreateZBuffer
*/
BOOL CreateZBuffer( void )
{
    DDSURFACEDESC ddsd;

    memset( &ddsd, 0, sizeof( ddsd ) );
    ddsd.dwSize = sizeof( ddsd );

    /*
    ** create the Z-buffer
    */
    memset( &ddsd, 0 ,sizeof(DDSURFACEDESC));
    ddsd.dwSize = sizeof( ddsd );
    ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_CAPS | DDSD_ZBUFFERBITDEPTH;
    ddsd.ddsCaps.dwCaps = DDSCAPS_ZBUFFER;
    ddsd.dwWidth = screen_width;
    ddsd.dwHeight = screen_height;

    if ( !debugging || !using_hardware )
        ddsd.ddsCaps.dwCaps |= DDSCAPS_SYSTEMMEMORY;
    else
        ddsd.ddsCaps.dwCaps |= DDSCAPS_VIDEOMEMORY;

    /*
    ** choose a 16-bit Z-buffer depth, at least that's what I do
    */
    if ( !( d_selected_driver.d_desc.dwDeviceZBufferBitDepth & DDBD_16 ) )
    {
        return FALSE;
    }
    ddsd.dwZBufferBitDepth = 16;

    if ( lpDD->CreateSurface( &ddsd, &lpZBuffer, NULL ) != DD_OK )
        return FALSE;

    /*
    ** attach the Z-buffer to the back buffer
    */
    if (lpBackBuffer->AddAttachedSurface( lpZBuffer ) != DD_OK )
    {
        RELEASE( lpZbuffer );
        return FALSE;
    }

    return TRUE;
}

第十步:创建 IDirect3DDevice
呜呼,终于到这里了!我们可以创建D3D设备,还可以试着在屏幕上画一些东西了。在能够做这些之前我们要创建缓冲区的原因是因为D3D设备是由IDirectDrawSurface 创建的,不要问我为什么,我也不知道。下面的一大段代码是用来由用户选定的D3D设备驱动,并存储起来的GUID,来创建IDirect3DDevice。 

BOOL CreateD3DDevice( void ) 
{
    HRESULT hresult;

    hresult = lpBackBuffer->QueryInterface( d3d_driver.guid, ( LPVOID * ) &lpD3DDevice );
    if ( hresult != DD_OK )
        return FALSE;
    return TRUE;
}


第十一步:创建IDirect3DViewport并关联到D3D设备
最后一步是:创建IDirect3DViewport并关联到在上一步中创建的D3D设备。这里的唯一诀窍是在给dvMinZ和dvMaxZ赋值时要搞清楚Z顺序。因为一些原因,D3D例子程序中并未对这些值进行赋值。它好象也可以工作,下面是代码: 


BOOL CreateViewport( void )
{
    /*
    ** Create and add viewport
    */
    if ( lpD3D->CreateViewport( &lpViewport, NULL ) != DD_OK )
    {
        return FALSE;
    }
    if ( lpD3DDevice->AddViewport( lpViewport ) != DD_OK )
    {
        RELEASE( lpViewport );
        return FALSE;
    }

    /*
    ** setup the viewport for a reasonable viewing area
    */
    D3DVIEWPORT viewdata;
    DWORD largest_side;

    memset( &viewdata, 0, sizeof( viewdata ) );

    /*
    ** this compensates for aspect ratio
    */
    if ( display_width > display_height )
        largest_side = display_width;
    else
        largest_side = display_height;

    viewdata.dwSize = sizeof( viewdata );
    viewdata.dwX = viewdata.dwY = 0;
    viewdata.dwWidth = display_width;
    viewdata.dwHeight = display_height;
    viewdata.dvScaleX = largest_side / 2.0F;
    viewdata.dvScaleY = largest_side / 2.0F;
    viewdata.dvMaxX = ( float ) ( viewdata.dwWidth / ( 2.0F * viewdata.dvScaleX ) );
    viewdata.dvMaxY = ( float ) ( viewdata.dwHeight / ( 2.0F * viewdata.dvScaleY ) );
    viewdata.dvMinZ = 1.0F;
    viewdata.dvMaxZ = 1000.0F; // choose something appropriate here!

    if (lpViewport->SetViewport( &viewdata ) != DD_OK )
    {
        RELEASE( lpViewport );
        return FALSE;
    }
    return TRUE;
}


第十二步:初始化完成
现在我们终于有了D3D设备,我已经疲惫不堪了,您还成吗?估计也和我一样了吧!进行到这里,我们已看到了曙光。

目前你应该已经有了lpViewport,lpD3DDevice, lpD3D, lpDD, lpClipper,lpZBuffer, lpBackBuffer, 和lpFrontBuffer等指针给您带来作为程序员的苦与甜。作为您的第一个绘演程序我建议您:用某种颜色填充后缓冲(提示∶IDirectDrawSurface::Blt通过DDBLT_COPYFILL),然后显示它。在本文的其它部分是关于DX的噩梦的延续。 


--------------------------------------------------------------------------------

糊里糊涂的缓存管理
DirectDraw全是关于缓冲管理的内容,包括表面、和颜色、深度和纹理数据等的缓冲管理。在这一节中对颜色缓冲和Z缓冲进行了一些探讨,关于纹理部分的内容放在让人心碎的材质影射部分。 

缓冲操作:Flipping和Blitting
把您绘演的场景显示在屏幕上有两种方法,flipping和blitting,如果您的应用程序工作在满屏方式下,您应该使用整页弹出方法--flip;如果是在窗口模式下,是在前、后缓冲间切换的方法--Blitting。见下例: 


if ( fullscreen )
{
    if ( lpFrontBuffer->Flip( NULL, 1 ) != DD_OK )
    // something bad happened;
}
else
{
    RECT src_rect, dst_rect;
    POINT pt;

    /*
    ** src_rect is relative to offscreen buffer
    */
    GetClientRect( hWnd, &src_rect );

    /*
    ** dst_rect is relative to screen space so needs translation
    */
    pt.x = pt.y = 0;
    ClientToScreen( hWnd, &pt );
    dst_rect = src_rect;
    dst_rect.left += pt.x;
    dst_rect.right += pt.x;
    dst_rect.top += pt.y;
    dst_rect.bottom += pt.y;
   
    /*
    ** perform the blit from backbuffer to primary, using
    ** src_rect and dst_rect
    */
    if ( lpFrontBuffer->Blt( &dst_rect, lpBackBuffer, &src_rect, DDBLT_WAIT, 0 ) != DD_OK )
    {
        // something bad happened
    }
}

清除缓冲区
在某些情况下需要清除表面并将其置为某个颜色,下面的例子足以说明: 


void ClearSurface( LPDIRECTDRAWSURFACE lpDDS, float r, float g, float b )
{
    RECT dst;
    DDBLTFX ddbltfx;
    DWORD fillcolor;
    DDSURFACEDESC ddsd;

    /*
    ** compute the fill color
    */
    fillcolor = MakeSurfaceRGB( lpDDS, r, g, b );

    /*
    ** get the surface desc
    */
    ddsd.dwSize = sizeof(ddsd);
    lpDDS->GetSurfaceDesc(&ddsd); 

    memset(&ddbltfx, 0, sizeof(ddbltfx));
    ddbltfx.dwSize = sizeof(DDBLTFX);
    ddbltfx.dwFillColor = fillcolor;
    dst.left = dst.top = 0;
    dst.right = ddsd.dwWidth;
    dst.bottom = ddsd.dwHeight;

    if ( lpBackBuffer->Blt( &dst, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx ) != DD_OK )
    {
        // something bad happened
    }
}

这里有个计算填充颜色的小技巧,不知道是什么原因,填充颜色不能以独立的模式设定于一个桢缓冲中,所以在每一个应用程序中您不得不指明如何将一个RGB颜色值影射到桢缓冲中。这可以有两种方法来完成:直接读取有颜色的表面的RGB值,或者写一个RGB888颜色值到桢缓冲再读回来。 

前一方法也许是最‘正确’的,最起码听起来比后面的强。但是可悲的是在这里还是需要一些十分乏味的步骤。基本上您进行以上步骤后可以得到对表面的描述和表面上的点的RGB值,有了这个描述您就可以决定在RGB888和桢缓冲模式变换的值得大小,不幸的是这样的出来的结果可能不会在必须指定颜色的时候与色度值协同工作。 

那么,我们来看第二种方法:read-back方法,可能更加可取,虽然使用这种方法可能使运行慢得象老牛拉破车,但您也要想到计算桢缓冲的颜色并不是经常要进行的。 

在例子代码中的MakeSurfaceRGB是从DirectX SDK中‘偷来的’,使用Win32 API的SetPixel函数实现向桢缓冲写一个RGB值,然后使用DirectDraw对桢缓冲的直接处理能力把它读回来。 

/*
** this assumes that R, G, and B are passed as floats in the range [0,1]
*/
DWORD MakeSurfaceRGB( LPDIRECTDRAWSURFACE lpDDS, float r, float g, float b )
{
    unsigned long dw = 0;
    COLORREF cref = RGB( r * 255, g * 255, b * 255 );
    COLORREF tmpCref;
    DDSURFACEDESC ddsd;
    HDC hdc = NULL;

    /*
    ** Get a DC from the surface
    */
    if ( lpDDS->GetDC( &hdc ) != DD_OK )
        // something bad happened
        return 0;

    /*
    ** save pixel in surface then store a pixel into the surface
    */
    tmpCref = GetPixel( hdc, 0, 0 );
    SetPixel( hdc, 0, 0, cref );
    lpDDS->ReleaseDC( hdc );

    memset( &ddsd, 0, sizeof( ddsd ) );
    ddsd.dwSize = sizeof( ddsd );

    /*
    ** lock the back buffer so that we can read back the value
    ** we just wrote with SetPixel()
    */
    if ( lpDDS->Lock( NULL, &ddsd, DDLOCK_WAIT, NULL ) != DD_OK )
    {
        // something bad happened
        // should probably restore the color we wrote out
        // earlier, but I'm too lazy to write that code
        return 0;
    }

    /*
    ** read back the color
    */
    dw = * ( DWORD * ) ddsd.lpSurface;

    /*
    ** mask off high bits if the bit count is not 32
    */
    if ( ddsd.ddpfPixelFormat.dwRGBBitCount != 32 ) 
        dw &= ( ( 1 << ddsd.ddpfPixelFormat.dwRGBBitCount ) - 1 );


    /*
    ** unlock the back buffer
    */
    lpDDS->Unlock( NULL );
   
    /*
    ** restore the pixel we overwrote
    */
    if ( lpDDS->GetDC( &hdc ) == DD_OK )
    {
        SetPixel( hdc, 0, 0, tmpCref );
        lpDDS->ReleaseDC( hdc );
    }

    return dw;
}


--------------------------------------------------------------------------------

使人生厌的表面管理 
Direct3D和DirectDraw的讨论是紧紧的围绕着表面的概念展开的,它实际上包含大量的系统方法和存放在现实内存中的显示数据。举例来讲有:前缓冲、后缓冲、Z缓冲和材质。 

因为Direct3D应用程序是运行在多任务环境中的,那么程序正在使用的一部分内存完全有可能"丢失",也就是说被其它应用程序使用了。
既然我们的Direct3D有如此古怪的特性,我们一定要检查是否发生这种情况,并把它恢复。 

Checking for Lost Surfaces
(To Be Continued) 
Restoring Lost Surfaces
(To Be Continued)
--------------------------------------------------------------------------------

使人发疯的状态管理
与Direct3D通信的实现几乎总是通过运行缓冲实现的,应用程序在其中存储命令(操作码,op-codes)和数据,然后将整个的运行缓冲传递到D3D驱动程序中,由驱动程序解析运行缓冲的内容,转换驱动程序运行的实际指令并运行。 

不管在任何图形支持库中,画三角形可能是最基础的能力了,但也是在D3D的帮助中最少提到的一部分。提供的例子代码也没有清楚的说明是怎样做的,并且在帮助文件中几乎没有提到如何建立运行缓冲。 

运行缓冲快速浏览
运行缓冲是一块内存,它存储了一系列的DWORD变量组成的的命令和紧随其后的顶点序列,他将被传递到D3D驱动程序中,并被D3D驱动程序解析、执行。 

下面举例说明在三角形绘演中的运行缓冲的结构: 

数据 长度
+------------------------+----------+ <- lpBufStart (起始地址)
| 定点数据 | 可变 |
+------------------------+----------+ <- lpInsStart
| 操作码 | 固定 |
+------------------------+----------+
| 操作码参数 | (可变) |
+------------------------+----------+
\ * * \
/ ( 可 变 数 量 的 操 作 码 ) /
\ * * \
+------------------------+----------+
| (QWORD UNALIGNER) | (固定) | (conditionally inserted)
+------------------------+----------+
| OP_TRIANGLE_LIST | 固定 |
+------------------------+----------+
| 三角形数据 | 可变 |
+------------------------+----------+
+ OP_EXIT | 固定 +
+------------------------+----------+

这个格式由一串顶点和紧随其后的操作码组成(包括三角形信息),最后是一个结束符OP_EXIT操作码。要注意其它的格式也是有可能的,重要的是要在调用IDirect3DExecuteBuffer::SetExecuteData方法时设置D3DEXECUTEDATA。 

顶点数据
顶点数据描述了的位置,法线和顶点的颜色信息(可选)。在D3D中有三种不同的定点,分别是: D3DVERTEX,D3DLVERTEX和D3DTLVERTEX。
D3DVERTEX存储的是位置(模型坐标),法线和材质坐标信息; D3DLVERTEX存储的是位置(模型坐标),漫反射颜色,镜反射颜色和材质坐标信息; D3DTLVERTEX存储的是位置(屏幕坐标), 漫反射颜色,镜反射颜色和材质坐标信息。 

D3D提供了一个用来向运行缓冲拷贝数据的宏,VERTEX_DATA,这里要注意的是虽然VERTEX_DATA宏使用了sizeof( D3DVERTEX )声明,但是看起来所有的D3D顶点结构的大小都是相同的, 可以得出结论:VERTEX_DATA宏可以用于所有的三种D3D顶点结构。 

处理顶点
然后您需要给定点数据增加指令来“处理定点”,意思就是:“给数据增加它应该做的动作的指令”。这个指令就
是:OP_PROCESS_VERTICES,仅更着它的是PROCESSVERTICES_DATA,至于OP_xxx指令只是简单的把D3D指令加入到运行缓冲中,PROCESSVERTICES_DATA宏是用来添加操作码参数的,顶点处理的形式取决于所使用的定点类型。 

顶点类型 处理形式 
D3DVERTEX D3DPROCESSVERTICES_TRANSFORMLIGHT 
D3DLVERTEX D3DPROCESSVERTICES_TRANSFORM 
D3DTLVERTEX D3DPROCESSVERTICES_COPY 


QWORD unalignment
三角形数据需要在QWORD(8位)边界进行结合,因此针对三角形数据的指令需要被分开,代码如下所示: 

if ( QWORD_ALIGNED( lpPointer ) ) {
    OP_NOP( lpPointer );
}
OP_TRIANGLE_LIST( 1, lpPointer );

警告:您必须在执行OP_NOP指令前面加一个判断,否则编译将出错!这是因为OP_NOP宏的执行错误,千万不要将判断移走。 

三角形数据
三角形数据是使用OP_TRIANGLE_LIST在n个三角形数据之后插入在一个表中的,在运行缓冲的开始部分,三角形是由三个顶点数据索引指明的。注意:采用正确的顶点顺序,否则你的三角形将把它的背面呈现出来,把3D图形整个弄拧。 

OP_EXIT
OP_EXIT指令告诉D3D什么时候停止处理数据并关闭运行缓冲。 

创建运行缓冲
现在我们已经知道了关于运行缓冲的核心秘密,现在我们试着来创建一个,这应该还算是简单的,比较困难的部分是计算运行缓冲要消耗多少内存,这样将不得不计算所有的被存放在缓冲中的操作码和数据的所占内存的大小,这实际容易出错的部分。 

还要注意一些驱动程序只支持有限的运行缓冲大小,这种有限的空间大小可能是运行缓冲的全部大小,或者只是在运行缓冲中的顶点的最大数值。驱动程序的描述标志可以告诉您在当前的驱动模式下运行缓冲是否有字节大小限制或定点限制。 

// make sure driver specifies a max buffer size
if ( d_selected_driver.d_desc.dwFlags & D3DDD_MAXBUFFERSIZE )
{
    // if max buffer size == 0 then it's unlimited
    if ( d_selected_driver.d_desc.dwMaxBufferSize )
        d_max_execute_buffer_size = d_selected_driver.d_desc.dwMaxBufferSize;
    else
        d_max_execute_buffer_size = MY_DEFAULT_BUFFER_SIZE;
}
else
{
    d_max_execute_buffer_size = MY_DEFAULT_BUFFER_SIZE;
}
// make sure driver specified a max vertex count
if ( d_selected_driver.d_desc.dwFlags & D3DDD_MAXVERTEXCOUNT )
{
    // if max vertex count == 0 then it's unlimited
    if ( d_selected_driver.d_desc.dwMaxVertexCount )
        d_max_vertex_count = d_selected_driver.d_desc.dwMaxVertexCount;
    else
    d_max_vertex_count = MY_DEFAULT_VERTEX_COUNT;
}
else
{
    d_max_vertex_count = MY_DEFAULT_VERTEX_COUNT;
}

上面的代码很直接的说明了问题:如果驱动程序返回在顶点数目或缓冲大小上有限制,是复杂而且容易出现错误,则我们使用该函数。如果没有,则使用我们的缺省方法,并确定在当前驱动模式下没有上述两种限制。我发现很直接的方法是创建一个运行缓冲并在程序中所有使用运行缓冲的部分一直使用它,而不要在每次要进行绘演时创建一个运行缓冲。我不知道这样做对还是不对,但是这样可以减少错误发生的可能。 

计算运行缓冲的大小 
计算我们需要的运行缓冲大小是复杂而且容易出错的,请注意如下代码: 

sizeof( D3DINSTRUCTION) * num_opcodes +
sizeof( D3DVERTEX ) * num_vertices +
sizeof( D3DTRIANGLE ) * num_triangles +
sizeof( all parameters to opcodes );

不用我多说上面的程序有多狗屎,您可能也看出来了! 

举例来讲,假定要绘演10个独立的三角形,这就意味着你需要下述操作码: OP_EXIT,OP_PROCESS_VERTICES,和OP_TRIANGLE_LIST。这样就需要30个定点和10个三角形的存储空间,还潜在的需要强制QWORD unalignment时的OP_NOP操作码的空间。还有D3DPROCESSVERTICES和OP_PROCESS_VERTICES 的参数的空间。 

size = sizeof( D3DINSTRUCTION ) * 4 // OP_EXIT, OP_PROCESSVERTICES, OP_TRIANGELIST, OP_NOP
sizeof( D3DPROCESSVERTICES ) +
sizeof( D3DTRIANGLE ) * 10 +
sizeof( D3DSTATE ) * 0 + 
sizeof( D3DVERTEX ) * 30;

在这里并没有很完整的思路,所以请原谅我不能在这里提供有帮助的代码,我把D3DSTATE指令的代码放在这里作为结束。] (我是完全不知所云!真是不明白。by 译者) 

分配运行缓冲
现在我们对运行缓冲进行分配,下面的函数进行指定大小的运行缓冲的分配。注意:如果我们试着分配大于驱动程序(和我们的软件)可提供的空间,该函数调用会失败。 

LPDIRECT3DEXECUTEBUFFER AllocateExecuteBuffer( int size )
{
    D3DEXECUTEBUFFERDESC debDesc;
    LPDIRECT3DEXECUTEBUFFER exBuf;

    // create a D3DEXECUTEBUFFERDESC
    memset( &debDesc, 0, sizeof( debDesc ) );
    debDesc.dwSize = sizeof( debDesc );
    debDesc.dwFlags = D3DDEB_BUFSIZE;
    debDesc.dwBufferSize = size;

    if ( size > d_max_buffer_size ) return 0;

    // create the buffer
    if ( lpD3DDevice->CreateExecuteBuffer( &debDesc, &exBuf, NULL ) != DD_OK )
    {
        return 0;
    }
    return exBuf;
}

填充运行缓冲
现在我们知道了如何进行运行缓冲分配,下面就是如何使用一些指令进行填充。我先要再次的因为这些即该死的又难看的代码请求您原谅,但不是为我,我发誓,这就是D3D的设计。 

锁定运行缓冲
在能修改运行缓冲之前我们还要锁定它,就是我们要告诉驱动程序我们要开始填充了,还要依照顺序不能把他弄乱。
用IDirect3DExecuteBuffer::Lock方法完成此项功能。下面的代码说明了如何锁定运行缓冲。 

D3DEXECUTEBUFFERDESC debDesc;

memset( &debDesc, 0, sizeof( debDesc ) );
debDesc.dwSize = sizeof( debDesc );
if ( lpExBuf->Lock( &debDesc ) != DD_OK )
    fail();

运行了IDirect3DExecuteBuffer::Lock方法后,在debDesc结构中的成员lpData指针被置为运行缓冲的地址。我们使用三个神奇的指针变量(在后面说明)存储这个地址。 

lpPointer = lpBufStart = lpInsStart = debDesc.lpData;
我已经提到了一些程序使用memset来使运行缓冲没有冗余, 但是我认为好象不是很有用,因此我没有用它,它不会对程序产生什么坏的影响,所以如果你想使用它就用,例子如下: 

memset( debDesc.lpData, 0, d_max_execute_buffer_size );
三个神奇的指针变量
在创建运行缓冲时您需要管理如下三个指针:缓冲起始地址、指令区起始地址和缓冲结束地址。一般来讲,在您锁定运行缓冲时将起始地址存储在lpBufStart中,在以后的地址运算中您将会使用它,lpInsStart也只是在你插入第一个操作码时设定一次,最后lpPointer指针是指向当前地址的漫游指针,用在写入运行缓冲时,当你对运行缓冲写入完毕,该指针将指向您所写入运行缓冲的指令和数据的结束地址。 

这三个指针在以后的计算中是必须的,如在计算矩阵偏移量,指令偏移量和计算整个运行缓冲大小时。 

The Direct3D Helper Macros
填充运行缓冲包括写入数据、操作码和操作码参数。您可以自己写一个宏或函数来对运行缓冲进行填充,或者使用由DirectX SDK提供的例子中的代码(/dxsdk/sdk/samples/misc/d3dmacs.h)。使用D3DMACS.H中得宏时要小心,因为他写的不是很完美(微软的一贯作风 By 译者)。 

填充运行缓冲的例子
在这里假定你已经所定了运行缓冲,并且已对上面所谈到的三个神奇指针进行了赋值 -- 指向debDesc.lpData。下面的代码向运行缓冲填充了进行单个三角形绘演的数据。 

void FillBuffer( D3DVERTEX *vertices, int num_vertices, 
D3DTRIANGLE triangles[], int num_tris )
{
    int i;

    VERTEX_DATA( vertices, num_vertices, lpPointer );
    lpInsStart = lpPointer;
    OP_PROCESS_VERTICES( 1, lpPointer );
    PROCESSVERTICES_DATA( D3DPROCESSVERTICES_TRANSFORMLIGHT, 0, num_vertices, lpPointer );
    // triangle data must be QWORD aligned, so we need to make sure
    // that the OP_TRIANGLE_LIST is unaligned! Note that you MUST have
    // the braces {} around the OP_NOP since the macro in D3DMACS.H will
    // fail if you remove them.
    if ( QWORD_ALIGNED( lpPointer ) ) {
        OP_NOP( lpPointer );
    }
    OP_TRIANGLE_LIST( num_tris, lpPointer );
    for ( i = 0; i < num_tris; i++ ) {
        LPD3DTRIANGLE tri = ( LPD3DTRIANGLE ) lpPointer;

        tri->v1 = tris[i].v1;
        tri->v2 = tris[i].v2;
        tri->v3 = tris[i].v3;
        tri->wFlags = D3DTRIFLAG_EDGEENABLETRIANGLE;
        tri++;

        lpPointer = ( LPVOID ) tri;
    }
    OP_EXIT( lpPointer );
}

将以上代码段改为使用D3DTLVERTEX或者D3DLVERTEX顶点类型,只要简单的改掉在函数声明部分中的D3DVERTEX,并把D3DPROCESSVERTICES_TRANSFORMLIGHT分别改为D3DPROCESSVERTICES_COPY或 D3DPROCESSVERTICES_TRANSFORM。 

执行运行缓冲
在向运行缓冲填充了有意义的数据后,下面最后的一步是运行其中的指令。 

对运行缓冲进行解锁
执行运行缓冲的第一步是对运行缓冲进行解锁,通知驱动程序已准备好。 

lpExBuf->Unlock();
设置运行缓冲数据
现在运行缓冲已经被解锁,我们还要使用IDirect3DExecuteBuffer::SetExecuteData 方法来通知驱动程序所有的我们已创建的指令。 

D3DEXECUTEDATA d3dExData;
memset(&d3dExData, 0, sizeof(D3DEXECUTEDATA));
d3dExData.dwSize = sizeof(D3DEXECUTEDATA);
d3dExData.dwVertexCount = num_vertices;
d3dExData.dwInstructionOffset = (ULONG)((char*)lpInsStart - (char*)lpBufStart);
d3dExData.dwInstructionLength = (ULONG)((char*)lpPointer - (char*)lpInsStart);
lpExBuf->SetExecuteData(&d3dExData);
上述代码创建了D3DEXECUTEDATA结构,置其冗余为0,并适当的设定它的成员。其中dwInstructionOffset成员是从运行缓冲开始到第一个指令数据的的字节偏移量。 dwInstructionLength成员是指令数据的字节长度。然后调用IDirect3DExecuteBuffer::SetExecuteData设置这些数据。 

然后我们将得到一个运行缓冲可以被执行的状态。 

执行缓冲内容
执行当前的运行缓冲是比较简单的,只是简单的调用IDirect3DDevice::Execute方法,并把当前运行缓冲传递给它。需要注意的是被调用IDirect3DDevice::Execute 方法一定要被IDirect3DDevice::BeginScene和IDirect3DDevice::EndScene方法括起来,就是说IDirect3DDevice::Execute放在IDirect3DDevice::BeginScene/IDirect3DDevice::EndScene 之间,不能放在这个函数对的外面。 

if ( lpD3DDevice->Execute( lpExBuf, lpViewport, D3DEXECUTE_CLIPPED ) != DD_OK )
    fail();

检查运行时的返回值是十分重要的,因为在运行这个方法时你可能遇到无数的bug。注意D3DEXECUTE_CLIPPED只有使用D3DVERTEX或D3DLVERTEX顶点类型才可使用(在使用上述两种顶点的情况下,如果你知道要进行的绘演不需要剪裁算法,将其指定为 D3DEXECUTE_UNCLIPPED可以获得一小点的性能提高)。如果使用D3DTLVERTEX就必须自己写剪裁部分的代码,并指明D3DEXECUTE_UNCLIPPED。 

清除运行缓冲 
清除运行缓冲的代码如下: 

lpExBuf->Release();
lpExBuf = 0;

另外一种选择是使用D3DMACS.H中提供的RELEASE宏。 

RELEASE( lpExBuf );

--------------------------------------------------------------------------------

纯粹该死的多边形绘演
既然所有的绘演是由运行缓冲执行的,在进行处理之前你应该对运行缓冲有一定的理解,如果您还没有, 请参考乱七八糟的运行缓冲。 

绘演单独的三角形
绘演带状三角形组
绘演扇面状三角形组
绘演凸多边形
(上述定义可参考M$的DirectX SDK help中的"Direct3D Immediate Mode Overview",By 译者)
--------------------------------------------------------------------------------

使人发疯的状态管理
状态管理由在程序中控制绘演输出的动作组成,如:选择当前纹理、当前阴影模式、雾化模式、雾化颜色等。绝大部分的状态很少改变,如阴影模式或雾化颜色。 

状态的改变可以是全局的或局部的,也可能二者兼顾。全局状态的改变通常用来影响所有的绘演操作,而局部状态的改变使用来控制一部分特殊的绘演任务。改变全局状态的的例子如:选择纹理的filter模式,这往往是您因为审美情趣和性能表现等原因想要使用者控制的部分,而对于局部状态通常是纹理的选择,因为物体或网格通常有其特有的影射纹理。 

改变全局状态
全局状态的改变通常是通过创建一个单独的用来管理状态的运行缓冲来实现的。在这个运行缓冲中将没有顶点数据,只有指令序列。在下面的例子中通过创建一个单独的运行缓冲并执行它实现了改变Z缓冲状态。 

D3DEXECUTEBUFFERDESC debDesc;
D3DEXECUTEDATA d3dExData;
LPDIRECT3DEXECUTEBUFFER lpD3DExCmdBuf = NULL;
LPVOID lpBuffer, lpInsStart;
size_t size = 0;

/*
** create an execute buffer of the required size
*/
size = 0;
size += sizeof(D3DSTATE) * 25; // bigger than I need, but it doesn't matter
memset(&debDesc, 0, sizeof(D3DEXECUTEBUFFERDESC));
debDesc.dwSize = sizeof(D3DEXECUTEBUFFERDESC);
debDesc.dwFlags = D3DDEB_BUFSIZE;
debDesc.dwBufferSize = size;

if ( lpD3DDevice->CreateExecuteBuffer( &debDesc, &lpD3DExCmdBuf, NULL ) != DD_OK )
{
    fail();
}
/*
** lock the execute buffer
*/
if ( lpD3DExCmdBuf->Lock( &debDesc ) != DD_OK )
{
    fail();
}

/*
** zero out execute buffer memory
*/
memset( debDesc.lpData, 0, size );

lpInsStart = debDesc.lpData;
lpBuffer = lpInsStart;

/*
** set the render state
*/
OP_STATE_RENDER( 3, lpBuffer);
STATE_DATA( D3DRENDERSTATE_ZENABLE, TRUE, lpBuffer );
STATE_DATA( D3DRENDERSTATE_ZFUNC, D3DCMP_LESSEQUAL, lpBuffer );
STATE_DATA( D3DRENDERSTATE_ZWRITEENABLE, TRUE, lpBuffer );
OP_EXIT( lpBuffer );

/*
** unlock the buffer
*/
if ( lpD3DExCmdBuf->Unlock() != DD_OK )
{
    fail();
}

/*
** set the execute data and execute the buffer
*/
memset( &d3dExData, 0, sizeof(D3DEXECUTEDATA) );
d3dExData.dwSize = sizeof(D3DEXECUTEDATA);
d3dExData.dwInstructionOffset = (ULONG) 0;
d3dExData.dwInstructionLength = (ULONG) ( (char*)lpBuffer - (char*)lpInsStart );

if ( lpD3DExCmdBuf->SetExecuteData( &d3dExData ) != DD_OK )
{
    fail();
}

if ( lpD3DDevice->Execute( lpD3DExCmdBuf, lpViewport, D3DEXECUTE_UNCLIPPED ) != DD_OK )
{
    fail();
}
RELEASE( lpD3DExCmdBuf );

就向您已经看到的,要使用了这么多的代码来完成这个动作,不过这里关键的一点是减少不得不做的创建、填充和执行状态管理运行缓冲等等动作的时间。详细的讨论见软件/硬件状态体系, 在上面的代码说明了在改变状态时得到了什么。 

改变局部状态
改变局部状态和改变全局状态很相象,但是它是直接插入到三角形/顶点运行缓冲中作为命令流的一部分。举例来讲,在绘演多边形时可以通过如上所述的手段控制当前的纹理。 

软/硬状态机制
我采用了软/硬范例的机制来控制D3D状态管理,在这种机制下状态的改变被认为是‘软’的,举例来讲,当程序被调入了内存并实际存在后,它们并不是立刻运行起来的,这是他们冻结了或者说是锁定了当前状态,这就需要进行状态更新才可以进行进一步的动作。 

所以软状态就是程序处于“傲慢的”并实际存在的当前状态(但不要指望它会响应什么),而硬状态才是D3D实际控制下的状态。软状态向硬状态的转变是在程序要开始进行绘演时由一些清楚明了的命令来完成的。 

为什么这么大惊小怪的?就象我说过的那样,D3D状态的改变是十分的麻烦的,而且开销很大。我使用了这样的方法,创建一个全局状态运行缓冲,它只是在状态需要被冻结时才被刷新和执行,这样做可以使状态运行缓冲的创建/填充/执行/释放循环的开销降为最低。 

可见实现软/硬状态机制的实现是设计一些管理软状态的一套函数。比如我们可以用一个全局结构来容纳我们认为相关的D3D状态,于是我们就会需要一些独立的函数来控制Z缓冲。见下例: 

void APISetZFunc( D3DCMPFUNC cmp )
{
    app_state.d_zfunc = cmp;
}

使用了软/硬状态机制,程序可以任意的改变它的状态,不必担心改变状态时的性能改变。唯一需要注意的是在状态被冻结时,性能有可能受影响,不过一般在绘演动作前进行不会有什么问题。 

一旦软状态管理函数设计好了,我们只需要用一个函数取得程序的全局状态并插入到运行缓冲中,就可以有效的将状态冻结。这看起来几乎与全局状态改变的例子的效果相同,而实际上主要的不同点在于有更多的状态变量被更新,并且它们的值来自与全局状态,见下例: 

/*
** set the render state
*/
OP_STATE_RENDER( NUM_STATE_VARIABLES, lpBuffer);
STATE_DATA( D3DRENDERSTATE_ZENABLE, app_state.d_zenable, lpBuffer );
STATE_DATA( D3DRENDERSTATE_ZFUNC, app_state.d_zfunc, lpBuffer );
STATE_DATA( D3DRENDERSTATE_ZWRITEENABLE, app_state.d_zwriteenable, lpBuffer );
// etc. etc.
OP_EXIT( lpBuffer );


--------------------------------------------------------------------------------

纯属添乱的转换矩阵
D3D中定义了三个不同的转换矩阵,转换矩阵是在调用IDirect3DDevice::Execute方法时进行了设置 D3DPROCESSVERTICES_TRANSFORM或
D3DPROCESSVERTICES_TRANSFORMLIGHT后使用。 

这三个矩阵分别为:modelworld,worldview和projection。 modelworld矩阵用来将模型或物体的坐标转换为虚拟3D世界(就是在计算机屏幕中的那个世界)的坐标系统; worldview矩阵用来转换虚拟3D坐标到视角坐标(3D to 2D,by 译者);最后是projection矩阵转换视角坐标到屏幕坐标,这个动作是通过依照比例影射到屏幕的视口变换实现的。 

创建矩阵
矩阵是使用IDirect3DDevice::CreateMatrix方法创建的,创建完毕后返回一个由D3DMATRIXHANDLE类型表示的矩阵句柄。 

D3DMATRIXHANDLE hMatrix;
if ( lpD3DDevice->CreateMatrix( &hMatrix ) != DD_OK )
    fail();

这个矩阵句柄可以简单的表示D3D中的任何类型的矩阵。 

设置矩阵
下面进行矩阵的设置,这项工作是通过向D3DMATRIX结构赋值并调用IDirect3DDevice::SetMatrix。 D3DMATRIX结构并不是想像中的那样每一个元素都有明确的命名,正相反是使用一维或二维数组的浮点数。 

D3DMATRIX m;

// the following code sets "m" to an identity matrix
m._11 = 1.0F;
m._12 = 0.0F;
m._13 = 0.0F;
m._14 = 0.0F;
m._21 = 0.0F;
m._22 = 1.0F;
m._23 = 0.0F;
m._24 = 0.0F;
m._31 = 0.0F;
m._32 = 0.0F;
m._33 = 1.0F;
m._34 = 0.0F;
m._41 = 0.0F;
m._42 = 0.0F;
m._43 = 0.0F;
m._44 = 1.0F;
if ( lpD3DDevice->SetMatrix( hMatrix, &m ) != DD_OK )
    fail();

设置全局矩阵
这三个全局矩阵(modelworld,worldview和projection)是在创建状态运行缓冲时使用适当的操作马进行设置的。如下代码所示: 

OP_STATE_TRANSFORM( 3, lpBuffer );
STATE_DATA( D3DTRANSFORMSTATE_WORLD, hModelWorldMatrix, lpBuffer );
STATE_DATA( D3DTRANSFORMSTATE_VIEW, hWorldViewMatrix, lpBuffer );
STATE_DATA( D3DTRANSFORMSTATE_PROJECTION, hProjectionMatrix, lpBuffer );

除非您需要多个矩阵再绘演时进行变换,否则您只需要设置这三个转换矩阵一次,它们在调用IDirect3DDevice::SetMatrix方法时将被自动
设置。 

For more information on state management, refer to The Hell of State Management. 想要更多的状态管理的有关信息,请到:使人发
疯的状态管理 

清除矩阵
当局真不再使用时,您应该使用IDirect3DDevice::DeleteMatrix方法把它清除。 

if ( hMatrix )
{
    lpD3DDevice->DeleteMatrix( hMatrix );
    hMatrix = NULL;
}

直接向运行缓冲插入矩阵
直接向运行缓冲插入矩阵也是有可能的,我在编程时没有用到这种情况,也没有自己亲手试一下,所以也没有发言权。 

D3D矩阵说明
D3D的矩阵和顶点变换系统分别由4X4矩阵和1X4向量,D3D的坐标系统是左手坐标系,并且假定使用的是行向量,同样D3DMATRIX矩阵也是行向量组成的。 

在这一点上是与OpenGL系统相反的,在OpenGL系统中使用的是列向量(矩阵由各个空间排列组成),而且OpenGL系统使用的是右手坐标系,并使用列向量。 

我还没有发现D3D的projection据真是如何工作的,所以我只好使用了DX文档的内容。 


--------------------------------------------------------------------------------

让人心碎的材质影射
(To Be Continued)
--------------------------------------------------------------------------------

您将诅咒的调试
调试D3D程序真的十分象是在吃碎玻璃片!为了使这种玻璃片变得好吃一点在这一节中我们探讨一些调试D3D程序的基本规律。 

规律 #1: 检查返回值
这是最重要的一条规律,一定要检查您使用的每一个DirectX调用的返回值,甚至看起来绝对没有毛病的调用都有可能有问题!举例来讲有一些驱动是不能工作在一些显示分辨率下的, 1152X864就是一个例子,如够您使用了这个分辨率则不会有什么明显的原因就会失败。检查调用返回值可以大大节省你浪费在调试中的时间。 

规律 #2: 使用调试库(Debugging Lib)
就因为这一个原因您就因该使用调试库:如果您使用VC的话,他们将把调试信息,出错状态信息打印在Message窗口内(borland程序员的悲哀!,by 译者),不用您手工去找。 

规律 #3: 在每一个您能找到的硬件设备上测试
并不是象SGI的OpenGL那样,D3D程序在某一个硬件设备上可以运行并不意味着就可以在其它的设备上运行。因此你要在尽可能多的硬件设备上测试你的D3D应用程序。 

规律 #4: 使用OutputDebugString来帮助测试
也许OutputDebugString函数是世界上最好的函数,它可以把调试信息输出到系统调试信息中。如果你使用的是MSVC++编译器你就会感到他的诸多好处(又是Borland程序元的悲哀! by 译者)。 

规律 #5: 支持软加速和窗口模式的绘演
D3D程序的调试简直就是一场恶梦,但这个规律也许可以使你脱离梦魇。第一要注意的是让你的程序在窗口模式下工作,只可以使你可以进行单步跟踪,使调试变得容易一些;第二是关闭硬件加速,只使用软件加速,而且在尽可能的情况下使用系统内存进行绘演,这可以使你免于由于Win16锁定而导致的down机(见规律 #6)。 

规律 #6: 记住锁定对调试有害
正像在规律 #5中所说的,可能在执行一个缓冲或表面时有可能不经意的导致Win16锁定。 Win16锁定一般会使系统挂起,而在调试时发生这种状况往往是你认为是代码有问题,而代码则可能是正确的,为了避免这一切的发生,您在调试时应该尽可能的使用软件加速和系统内存表面(见规律 #5),并且千万不要把条使用的断点放在Lock/Unlock函数对的中间行中。 

规律 #7: 在尽可能多的硬件设备上验证你的D3D程序因为D3D电汇眼的规范是够不错的了(几乎没有),所以在每一个你可一找的到的D3D加速器上策是你的程序是十分重要的。如果需要加上软件加速驱动,但是在大多数情况下这种方法被证明是不可行的,因为即使是最基本的功能也会大打折扣。 

规律 #8: 如果可能就是用双机调试
把两台机器连接起来,在一台机器上运行程序,在另一台上运行调试程序,这样做有很多的优点,比如可以调试locks/unlocks函数对之间的代码,还有在Down机的情况下保持稳定的能力。不过这就意味着你需要两台机器(钱哪!)而且十分的慢。 
 
我来说两句】 【加入收藏】 【返加顶部】 【打印本页】 【关闭窗口
中搜索 DDraw和D3D立即模式编程手册
本类热点文章
  DDraw和D3D立即模式编程手册
  矩阵求逆的快速算法
  本地化支持:OGRE+CEGUI中文输入:OGRE方..
  Direct3D中实现图元的鼠标拾取
  3D场景中的圆形天空顶
  OpenGL显卡编程
  一种高效的基于大规模地形场景的OCCLUS..
  一个完善的读取3DS文件例子
  如何制作一个可控制的人体骨骼模型
  Direct3D 入门之我见
  Slope(斜坡) 法线生成算法,在地形渲染..
  在Direct3D中渲染文字
最新分类信息我要发布 
最新招聘信息

关于我们 / 合作推广 / 给我留言 / 版权举报 / 意见建议 / 广告投放  
Copyright ©2003-2024 Lihuasoft.net webmaster(at)lihuasoft.net
网站编程QQ群   京ICP备05001064号 页面生成时间:0.00568