Direct3D 绘制基础

发表于2017-10-30
评论0 3.5k浏览

12.1 顶点缓存的逆袭

12.1.1 引言

在计算机所描绘的3D 世界中,所有的物体模型(如树木,人物,山峦〉都是通过多边形网格来逼近表示的,这些多边形可以是三形,也可以是四边形。所以我们这样说,多边形网格是构成物体模型的基本单元。
下面我们先来看一组图片:

通过两幅图的对比我们可以明显地看到,这个可爱的萝莉模型其实是就是无数的三角形和四边形网格构成的。首先我们用这些网格勾勒出了可爱萝莉的轮廓,然后在网格轮廓的表面“依附”上相应的图片。这样, 栩栩如生的萝莉模型就完成了。
三角形作为最简单的多边形,在3D 世界中的地位不可小觑。在Direct3D 中,我们经常使用三角形来描述物体模型。需要指出的是,任何物体都可以用三角形网格来逼近表示, 三角形网格是构建物体模型的基本单元。
众所周知, 一个三角形有3 个顶点,为了能够使用大量的三角形组成三角形网格来描述物体,首先需要定义好三角形的
顶点(Vertex) , 3 个顶点确定一个三角形,而顶点除了定义每个顶点的坐标位置以外,还含有颜色等其他属性。
这样一说我们就可以发现,任何物体模型的最基本组成单元其实是顶点。

12.1.2 顶点缓存相关基础知识

在Direct3D 中, 顶点的具体表现形式是顶点缓存( Vertex Buffer ),顶点缓存保存了顶点数据的内存空间。顶点缓存的存储位置比较随意,既可以在内存中,也可以在显卡的显存中。
我们前面己经给大家灌输了这样一个概念,所有物体都可以用三角形来表示。这就表明了,如果想在Direct3D 之中手动创建物体的话,就需要创建构成物体的所有顶点结构。当Direct3D 绘制图形时,将根据这些顶点结构创建一个三角形列表,来描述物体的形状和轮廓。
为了讲解清楚顶点缓存的使用方法,下面我们来看一个例子:

在图中,我们用4 个顶点组成了一个正方形,这4 个顶点分别是V0, V1, V2, V3 。为了正确描述这个正方形,我们需要根据这4 个顶点创建两个三角形,而这两个三角形的顶点数据,会依次保存在顶点缓存中。
需要注意,在Direct3D 中我们一般是顺时针来给顶点赋值的,比如说要定义出一个三角形V0V1V2 ,我们就按图中的箭头方向,取3 个顶点分别为V[0]=(-8,-8,0), V[1]=(-8,8,0), V[2]=(8,8,0)。
另外,我们暂且是在x-y 的2 维空间中定义的两个三角形组成的正方形,所以z 坐标都取为0 。
下面我们开始讲解在Direct3D 中使用顶点缓存的具体步骤。

12.1.3 顶点缓存使用四步曲之一:设计顶点缓存

这一步的关键词是设计, Design 。

想要使用顶点缓存绘制图形,第一步的工作就是对顶点的类型进行设计。今天我们要介绍的一套顶点格式,它是固定功能流水线中使用频繁的一套顶点定义格式一一灵活顶点格式( Flexible Vertex Format,FVF ) 。 需要说明的是,与灵活顶点格式对应的是可编程渲染流水线中的“顶点声明”顶点定义套
路,今天我们暂时先介绍灵活顶点格式这套定义套路。
灵活顶点格式( Flexible Vertex Format, FVF )用来描述三角形网格的每个顶点。灵活顶点格式可以让我们随心所欲地自定义其中所包含的顶点属性信息。例如,指定顶点的三维坐标、颜色、顶点法线和纹理坐标等等。
创建自定义灵活顶点格式时,根据实际的需求,需要定义一个包含特定顶点信息的结构体。主动权在我们这里,我们可以随心所欲地定义顶点包含的属性。比如可以定义一个只包含顶点三维坐标和颜色的结构体。

  1. struct CUSTOMVERTEX  
  2. {  
  3.     FLOAT x, y, z;           //顶点的三维坐标值,x, y, z  
  4.     DWORD color;             //顶点的颜色值  
  5. };  
当然也可以定义一个复杂一点,包含很多属性的顶点:
  1. <span style="font-size: 14px;">struct CUSTOMVERTEX  
  2. {  
  3.     FLOAT x, y, z;        //顶点的三维坐标值,x, y, z  
  4.     float nx, ny , nz;   // 法线向量  
  5.     float u, v ;         // 纹理坐标  
  6. </span><span style="font-size:14px;">};</span>  
但仅定义出结构体, Direct3D 是不能理解我们想干什么,这时候,需要一个宏来传达我们定义的顶点有哪些属性。
比如刚刚定义的CUSTOMVERTEX 结构体就可以通过以下方式来描述:
  1. <span style="font-size:14px;">#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE)  //FVF灵活顶点格式</span>  
可以看到,结构体中有的属性, 在这个宏定义都有着一一对应。在Direct3D 中常用的FVF 格式可以取下表所示的这些值:


围绕着这个表格做一个说明:
其中D3DFVF_XYZ 和D3DFVF_XYZRHW 这两个属性的内容重复了,可谓水火不相容, 一山不容二虎,每次我们选择其中的一个写出来就可以了,其中D3DFVF_XYZ 表示未经过坐标变换的顶点,而D3DFVF_XYZRHW 表示经过坐标变换的顶点。
需要注意的是,我们在书写灵活顶点格式的宏定义的时候需要遵守一个顺序原则,顺序就是优先级需要这样来分:
顶点坐标位置〉RHW 值〉顶点混合权重值〉顶点法线向量〉漫反射颜色值〉镜面反射颜色值〉纹理坐标信息。

也就是说,在定义FVF 宏的时候,顶点坐标位置总是排着最前面的。然后依次是RHW 值,然后继续往后排。
在制作上面这个表格的时候专门为它们标好了序号,大家写的时候只要按照标号的顺序取自己想要的属性,按照顺序写就可以了。

为了加深理解,我们举两个例子。
顶点结构体定义好后,配套的宏定义需要遵守上面的约定, 于是我们按顺序这样写:

  1. <span style="font-size:14px;"> #define D3DFVF_CUSTOMVERTEX1 (D3DFVF_XYZ I D3DFVF_DIFFUSE I D3DFVF_TEX1)</span>  
即根据上面的顺序, D3DFVF_XYZ 序号为1 ,所以最先写,然后D3DFVF_DIFFUSE 序号为5,第二个写,D3DFVF_TEX1序号为7 ,所以最后写。

再举一个例子,依然是遵循表格中的顺序来写:

  1. #define D3DFVF_CUSTOMVERTEX2 (D3DFVF_XYZRHW I D3DFVF_XYZB1 l D3DFVF_DIFFUSE I D3DFVF_SPECULAR I D3DFVF_TEX1)  
对顶点进行描叙的结构体中,参数定义对应最好保持和宏定义中参数一样的l顺序。

最终定义顶点格式需要的代码就可以这样写:

  1. struct CUSTOMVERTEX  
  2. {  
  3.     FLOAT x, y, z, rhw;  
  4.     DWORD color;  
  5. };  
  6. #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)  //FVF灵活顶点格式  

12.1.4 顶点缓存使用四步曲之二:创建顶点缓存

这一步的关键词是创建, create 。

在Direct3D 中,顶点缓存由lDirect3DVertexBuffer9 接口对象来表示。
很显然,顶点属性的“设计书”出炉之后,下面要做的事就是拿着设计书依葫芦画瓢了。
这一步里面我们就是先定义一个指向IDirect3DVertexBuffer9 接口的指针变量,然后运用lDirect3DVertexBuffer9 接口的

CreateVertexBuffer 方法创建顶点缓存, 再把内存地址复制给我们创建的这个指针。

首先, 定义一下这个指向IDirect3DVertexBuffer9 接口的指针变量,即写上这么一句:

  1. LPDIRECT3DVERTEXBUFFER9 g_pVertexBuffer = NULL ;  // 顶点缓冲区对象  
然后就是用我们的Direct3D 设备调用CreateVertexBuffer 方法创建顶点缓存,把内存地址复制给我们创建的这个指针了。

下面我们详细讲一下这个方法,在DirectX SDK 中查到这个方法是这样的:

  1. HRESULT CreateVertexBuffer(  
  2.   [in]           UINT Length,  
  3.   [in]           DWORD Usage,  
  4.   [in]           DWORD FVF,  
  5.   [in]           D3DPOOL Pool,  
  6.   [out, retval]  IDirect3DVertexBuffer9 **ppVertexBuffer,  
  7.   [in]           HANDLE *pSharedHandle  
  8. );  

下而我们来看看这个方法各个参数的含义和使用方法:

  •  第一个参数, UINT 类型的Length , 表示顶点缓存的大小, 以字节为单位。
  •  第二个参数, DWORD 类型的Usage ,用于指定使用缓存的一些附加属性,这个参数的值可以取0 ,表示没有附加属性,或者取如下表格中的一个或者多个值,多个值之间用“|”连接。这些参数可以顾名思义,不用记忆。比如D3DUSAGE_WRITEONLY,显然表示的是只能写, 不能读。



  • 第三个参数, DWORD 类型的FVF ,指定将要存储在顶点缓存中的灵活顶点格式,显然就是我们在第一步里面定义的那个宏名称了。
  • 第四个参数, D3DPOOL 类型的Pool ,这是一个D3DPOOL 枚举类型,这里用来指定存储顶点缓存的内存位置是在内存中还是在显卡的显存中。默认情况下是在显卡的显存中的。这个枚举类型的原型是这样的:
  1. /* Pool types */  
  2. typedef enum _D3DPOOL {  
  3.     D3DPOOL_DEFAULT         = 0,  
  4.     D3DPOOL_MANAGED         = 1,  
  5.     D3DPOOL_SYSTEMMEM       = 2,  
  6.     D3DPOOL_SCRATCH         = 3,  
  7.     D3DPOOL_FORCE_DWORD     = 0x7fffffff  
  8. } D3DPOOL;  
其中每个值的含义如下:


  • 第五个参数,IDirect3DVertexBuffer9 类型的材ppVertexBuffer ,可以这样理解,调用CreateVertexBuffer 方法就是在对这个变量进行初始化,让它得到创建好的顶点缓存的地址。这个参数也是一把金钥匙,后面我们关于顶点缓存的操作,都是以它为媒的,拿着它做喜闻乐见的“->”操作就可以了。我们可以看到,它的类型有两个星号,为指针的指针。而我们之前定义的g_pVertexBuffer 参数骨子里只有一个星号,为单纯的指针而己,所以这里我们需要做一下取地址操作。
  • 第六个参数,HANDLE 类型的*pSharedHandle , 为保留参数, 一般不用去管它,设为NULL或者0 就万事大吉了。
另外需要注意的是,使用D3DUSAGE_DYNAMIC 参数创建的缓存叫做动态缓存,它被放在AGP (Accelerated Graphices Port,加速图形端口)的内存之中。AGP 内存中的数据能够很快被更新,但是除了动态缓存中的数据之外, AGP 内存中其余的数据更新都要比其他缓存中的数据慢,因为这些数据必须在渲染前被转移到显存之中。即这样理解, AGP 内存中其余的数据更新都要比其他缓存中的数据慢。
所以,这一步创建顶点缓存的代码合起来就是:
  1. LPDIRECT3DVERTEXBUFFER9     g_pVertexBuffer = NULL;    //顶点缓冲区对象  
  2. //--------------------------------------------------------------------------------------  
  3.     //创建顶点缓存  
  4.     if( FAILED( g_pd3dDevice->CreateVertexBuffer( 6*sizeof(CUSTOMVERTEX),  
  5.         0, D3DFVF_CUSTOMVERTEX,  
  6.         D3DPOOL_DEFAULT, &g_pVertexBuffer, NULL ) ) )  
  7.     {  
  8.         return E_FAIL;  
  9.     }  

12.1.5 顶点缓存使用四步曲之三:访问顶点缓存

这一步的关键词是访问, Access。

上一步里面我们创建好了顶点缓存的金钥匙指向IDirect3DVertexBuffer9 接口的指针。既然金钥匙得到了, 下面就比较好理解了。这一步我们需要做的, 就是利用这把金钥匙, “ 指” 一下IDirect3DVertexBuffer9 接口的Lock 以及Unlock 方法,然后在Lock 和Unlock 方法之间访问我们的顶点缓存就好了。
这里提到的Lock 和Unlock 方法是一对好基友, 一如前面我们讲到的BeginScene() 和EndScene() ,. 而我们需要进行的相关访问操作需要在这对好基友之间进行。
IDirect3DVertexBuffer9:: Lock()和IDirect3DV创exBuffer9 ::Unlock 是一对加锁、解锁函数, 对顶点缓存的内存操作必须通过Lock()和Unlock()来进行, 先用Lock 函数加锁,然后才能访问顶点缓存的内容,访问完成后再用Unlock 进行解锁,通知Direct3D 我们对顶点缓存的操作结束了。

下面就来讲一讲这对加锁/解锁函数,首先我们来看一下加锁函数IDirect3DVertexBuffer9::Lock,可以在DirectX SDK 中找到它的原型声明如下:

  1. HRESULT Lock(  
  2.   [in]   UINT OffsetToLock,  
  3.   [in]   UINT SizeToLock,  
  4.   [out]  VOID **ppbData,  
  5.   [in]   DWORD Flags  
  6. );  
第一个参数, UINT 类型的OffsetToLock,表示加锁区域自存储空间的起始位置到开始锁定位置的偏移量,单位为字节。
第二个参数, UINT 类型的SizeToLock,表示要锁定的字节数,也就是加锁区域的大小。
第三个参数, VOID 类型的**ppbData ,指向被锁定的存储区的起始地址的指针。
第四个参数, DWORD 类型的Flags , 表示锁定的方式, 我们可以把它设为0, 也可以为下面的之一或者组合:


我们在下面配了一副图,希望帮助大家对Lock 函数的各个参数有更深刻的认识:


讲完Lock 函数,对应地讲一下UnLock 函数,但Unlock 函数真的没什么好讲的, 因为它没有参数,简单的调用一下就可以了。就像这样:

  1. g_pVertexBuf->Unlock () ; // 解锁  
Lock 函数的Unlock 函数都讲完了,下面我们关注的内容就是在Lock 和UnLock 之间到底是如何访问顶点缓存内容的。这里主要有两种方式来访问缓存的内容,下面分别介绍。
第一种方式是直接在Lock 和UnLock 之间对每个顶点的数据进行赋值和修改,以Lock 方法中的ppbData 指针参数作为数组的首地址,例如这样写:
  1. g_pVertexBuf- >Lock (O, 0, (void**)&pVertices, 0) ; // 加锁  
  2. pVertices[O] = CUSTOMVERTEX(-8O.Of, -8O.Of, 0.0f, l.0f, D3DCOLOR_XRGB (255, 0, 0)); // VO  
  3. pVertices[1] = CUSTOMVERTEX(-8O.Of, -8O.Of, 80.Of, l.0f, D3DCOLOR_XRGB (0, 255, 0)); // V1  
  4. pVertices[2] = CUSTOMVERTEX(8O.Of, 8O.Of, 0.0f, l.Of, D3DCOLOR_XRGB (0, 255, 0)); // V2  
  5. pVertices[3] = CUSTOMVERTEX(8O.Of, -8O.Of, 0.0f, l.Of, D3DCOLOR_XRGB (255, 0, 255)); // V3  
  6. g_pVertexBuf->Unlock();  // 解锁  

第二种方式是事先准备好顶点数据的数组,然后在Lock 和Unlock 之间使用memcpy 函数,进行数组内容的拷贝就可以了。

例如这样写:

  1. //顶点数据的设置,  
  2.     CUSTOMVERTEX vertices[] =  
  3.     {  
  4.         //采用rand函数,给顶点以随机的颜色和位置  
  5.         { 300.0f, 100.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  6.         { 500.0f, 100.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },   
  7.         { 300.0f, 300.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  8.         { 300.0f, 300.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  9.         { (float)(800.0*rand()/(RAND_MAX 1.0)) , (float)(600.0*rand()/(RAND_MAX 1.0)) , 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  10.         { (float)(800.0*rand()/(RAND_MAX 1.0)) , (float)(600.0*rand()/(RAND_MAX 1.0)) , 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), }  
  11.   
  12.     };  
  13.   
  14.     //填充顶点缓冲区  
  15.     VOID* pVertices;  
  16.     if( FAILED( g_pVertexBuffer->Lock( 0, sizeof(vertices), (void**)&pVertices, 0 ) ) )  
  17.         return E_FAIL;  
  18.     memcpy( pVertices, vertices, sizeof(vertices) );  
  19.     g_pVertexBuffer->Unlock();  
其实这两种方式只是形式上不同,从本质上来看的话是完全相同的。

12.1.6 顶点缓存使用四步曲之四:图形的绘制

这一步的关键词是绘制, Draw.

前面我们做了那么多准备工作,无非就是想利用顶点缓存来绘制出图形来,这步就是最终的胜利果实了。
前面讲解过, Direct3D 的渲染操作都是在BeginScence 和EndScene 方法之间进行的,而要进行顶点缓存进行图形的绘制,如果采用灵活顶点格式来进行顶点缓存的定义的话,这一步中需要调用3 个函数,分别是IDirect3DDevice9: :SetStreamSource , IDirect3DDevice9::SetFVF ,IDirect3DDevice9: :DrawPrimitive。其中SetStreamSource 用于把包含的几何体信息的顶点缓存和渲
染流水线相关联,而SetFVF 用于指定我们使用的灵活顶点格式的宏名称(第一步中用#define 定义的那个名称,填宏后面的内容也可以,因为宏名称实际上就是等效代替后面的内容),也就是指定我们的顶点格式,而DrawPrimitive 用于完成最终的绘制操作,根据顶点缓存中的顶点来进行绘制。
下面我们就分别对这3 个方法进行详细介绍。
首先是IDirect3DDevice9: :SetStreamSource 方法,它用于把包含的几何体信息的顶点缓存和渲染流水线相关联。我们可以在DirectX SDK 中查到它的原型:

  1. HRESULT SetStreamSource(  
  2.   [in]  UINT StreamNumber,  
  3.   [in]  IDirect3DVertexBuffer9 *pStreamData,  
  4.   [in]  UINT OffsetInBytes,  
  5.   [in]  UINT Stride  
  6. );  

  • 第一个参数, UINT 类型的StreamNumber,用于指定与该顶点缓存建立连接的数据流,因为我们通常不涉及多个流,所以通常不管它,设为0 。
  • 第二个参数, IDirect3DVertexBuffer9 类型的*pStreamData,包含顶点数据的顶点缓存的指针。很显然就是我们定义的指向
  • IDirect3DVertexBuffer9 接口的指针变量g_pVertexBuffer 。
  • 第三个参数,UINT 类型的OffsetlnBytes,表示在数据流中以字节为单位的偏移量,通常也不管它,也设为0。
  • 第四个参数,UINT 类型的Stride ,表示在顶点缓存中存储的每个顶点结构的大小,单位为字节。

便于大家理解,我们举一个具体调用例子:

  1. g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX) );  
然后是调用IDirect3DDevice9::SetFVF方法,这个方法非常简单,只有一个参数:
  1. HRESULT SetFVF(  
  2.   [in]  DWORD FVF  
  3. );  
这个唯一的参数, DWORD 类型的FVF ,表示设置为当前需要使用的灵活顶点格式,第一步中用#define 定义的那个名称,填宏后面的内容也可以,因为宏名称实际上就是等效代替后面的内容,也就是指定我们的顶点格式。以后涉及到多个灵活顶点格式的时候,就靠这个方法来设置我们当前需要使用的了。
下面给出一个具体调用的例子,两种方式都可取:
  1. #define D3DFVF_CUSTOMVERTEX1(D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_SPECULAR)  //FVF 灵活顶点格式  
  2. // 方式一  
  3. g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEXl) ;  
  4. // 方式二  
  5. g_pd3dDevice->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_SPECULAR );  

最后是IDirect3DDevice9::DrawPrimitive,前面我们付出了那么多, 其实就是为了方便这个函数的调用,促成它的辉煌, 用它绘制出我们需要的用顶点构成的图形。下面我们就来讲讲这个作为本小节中的核心知识的DrawPrimitive 方法。我们可以在DirectX SDK
中查到这个方法的函数原型:
  1. HRESULT DrawPrimitive(  
  2.   [in]  D3DPRIMITIVETYPE PrimitiveType,  
  3.   [in]  UINT StartVertex,  
  4.   [in]  UINT PrimitiveCount  
  5. );  

第一个参数, D3DPRIMITIVETYPE 类型的PrimitiveType,表示将要绘制的图元类型, 在D3DPRIMITIVETYPE枚举中取值,这个枚举定义如下:
  1. // Primitives supported by draw-primitive API  
  2. typedef enum _D3DPRIMITIVETYPE {  
  3.     D3DPT_POINTLIST             = 1,  
  4.     D3DPT_LINELIST              = 2,  
  5.     D3DPT_LINESTRIP             = 3,  
  6.     D3DPT_TRIANGLELIST          = 4,  
  7.     D3DPT_TRIANGLESTRIP         = 5,  
  8.     D3DPT_TRIANGLEFAN           = 6,  
  9.     D3DPT_FORCE_DWORD           = 0x7fffffff, /* force 32-bit size enum */  
  10. } D3DPRIMITIVETYPE;  
其中D3DPT_POINTLIST 表示点列, D3DPT_LINELIST 表示线列,D3DPT_LINESTRIP 表示线带, D3DPT_TRIANGLELIST 表示三角形列, D3DPT_TRIANGLESTRIP 表示三角形带,D3DPT_TRIANGLEFAN 表示三角形扇元, 而最后一个

D3DPT_FORCE_DWORD 不用管它,表示将顶点缓存强制编译为32 位,这个参数目前不使用。
 第二个参数,UINT 类型的StartVertex,用于指定从顶点缓存中读取顶点数据的起始索引位置。
 第三个参数,UINT 类型的PrimitiveCount ,指定需要绘制的图元数量。通过StartVertex 和PrimitiveCount 这两个参数配合使用,可以对缓存中的某一部分进行绘制。
依然是写一个调用的实例:

  1. g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 2);  
所以,第四步整体来看, 代码就是以下这些:
  1. // 【Direct3D渲染五步曲之二】:开始绘制  
  2.     //--------------------------------------------------------------------------------------  
  3.     g_pd3dDevice->BeginScene();                     // 开始绘制  
  4.     //--------------------------------------------------------------------------------------  
  5.     // 【Direct3D渲染五步曲之三】:正式绘制  
  6.     //--------------------------------------------------------------------------------------  
  7.     g_pd3dDevice->SetRenderState(D3DRS_SHADEMODE,D3DSHADE_GOURAUD);//设置渲染状态  
  8.     //------------------------------------------------------------  
  9.     // 【顶点缓存使用四步曲之四】:绘制图形  
  10.     //------------------------------------------------------------  
  11.     g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX) );  
  12.     g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );  
  13.     g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 2);  
  14.     //--------------------------------------------------------------------------------------  
  15.     // 【Direct3D渲染五步曲之四】:结束绘制  
  16.     //--------------------------------------------------------------------------------------  
  17.     g_pd3dDevice->EndScene();                       // 结束绘制  
最后我们总结一下,使用顶点缓存来绘制图形,简明扼要的四步曲,八个字: 设计,创建,访问, 绘制。

12.1 .7 示例程序D3Ddemo3

我们略去了一些宏定义、全局变量声明、库文件的包含,只贴出程序中的核心代码,也略去了和在之前章节中讲解过或者贴出过的代码,如函数的全局声明、WinMain 函数、WndProc 、Get_FPS 函数等,以节省篇幅。后面我们不再说明,贴出的代码都是这样,只贴重点部分。
  1. //------------------------------------------------------------------------------------------------  
  2. // 【顶点缓存使用四步曲之一】:设计顶点格式  
  3. //------------------------------------------------------------------------------------------------  
  4. struct CUSTOMVERTEX  
  5. {  
  6.     FLOAT x, y, z, rhw;  
  7.     DWORD color;  
  8. };  
  9. #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)  //FVF灵活顶点格式  
  10.   
  11. //-----------------------------------【全局变量声明部分】-------------------------------------  
  12. //  描述:全局变量的声明  
  13. //------------------------------------------------------------------------------------------------  
  14. LPDIRECT3DDEVICE9                   g_pd3dDevice = NULL; //Direct3D设备对象  
  15. ID3DXFont*                              g_pFont=NULL;    //字体COM接口  
  16. float                                           g_FPS = 0.0f;       //一个浮点型的变量,代表帧速率  
  17. wchar_t                                     g_strFPS[50];    //包含帧速率的字符数组  
  18. LPDIRECT3DVERTEXBUFFER9     g_pVertexBuffer = NULL;    //顶点缓冲区对象  
  19.   
  20. //-----------------------------------【Object_Init( )函数】--------------------------------------  
  21. //  描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化  
  22. //--------------------------------------------------------------------------------------------------  
  23. HRESULT Objects_Init(HWND hwnd)  
  24. {  
  25.     //创建字体  
  26.     if(FAILED(D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1, false, DEFAULT_CHARSET,   
  27.         OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("微软雅黑"), &g_pFont)))  
  28.         return E_FAIL;  
  29.     srand(timeGetTime());      //用系统时间初始化随机种子   
  30.   
  31.     //--------------------------------------------------------------------------------------  
  32.     // 【顶点缓存使用四步曲之二】:创建顶点缓存  
  33.     //--------------------------------------------------------------------------------------  
  34.     //创建顶点缓存  
  35.     if( FAILED( g_pd3dDevice->CreateVertexBuffer( 6*sizeof(CUSTOMVERTEX),  
  36.         0, D3DFVF_CUSTOMVERTEX,  
  37.         D3DPOOL_DEFAULT, &g_pVertexBuffer, NULL ) ) )  
  38.     {  
  39.         return E_FAIL;  
  40.     }  
  41.     //--------------------------------------------------------------------------------------  
  42.     // 【顶点缓存使用四步曲之三】:访问顶点缓存  
  43.     //--------------------------------------------------------------------------------------  
  44.     //顶点数据的设置,  
  45.     CUSTOMVERTEX vertices[] =  
  46.     {  
  47.         //采用rand函数,给顶点以随机的颜色和位置  
  48.         { 300.0f, 100.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  49.         { 500.0f, 100.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },   
  50.         { 300.0f, 300.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  51.         { 300.0f, 300.0f, 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  52.         { (float)(800.0*rand()/(RAND_MAX 1.0)) , (float)(600.0*rand()/(RAND_MAX 1.0)) , 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), },  
  53.         { (float)(800.0*rand()/(RAND_MAX 1.0)) , (float)(600.0*rand()/(RAND_MAX 1.0)) , 0.0f, 1.0f,  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256), }  
  54.   
  55.     };  
  56.   
  57.     //填充顶点缓冲区  
  58.     VOID* pVertices;  
  59.     if( FAILED( g_pVertexBuffer->Lock( 0, sizeof(vertices), (void**)&pVertices, 0 ) ) )  
  60.         return E_FAIL;  
  61.     memcpy( pVertices, vertices, sizeof(vertices) );  
  62.     g_pVertexBuffer->Unlock();  
  63.   
  64.   
  65.     g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, false);   //关掉背面消隐,无论是否顺时针,随机的那个三角形都会显示。   
  66.     return S_OK;  
  67. }  
  68.   
  69. //-----------------------------------【Direct3D_Render( )函数】-------------------------------  
  70. //  描述:使用Direct3D进行渲染  
  71. //--------------------------------------------------------------------------------------------------  
  72. void Direct3D_Render(HWND hwnd)  
  73. {  
  74.     //--------------------------------------------------------------------------------------  
  75.     // 【Direct3D渲染五步曲之一】:清屏操作  
  76.     //--------------------------------------------------------------------------------------  
  77.     g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);  
  78.   
  79.     //定义一个矩形,用于获取主窗口矩形  
  80.     RECT formatRect;  
  81.     GetClientRect(hwnd, &formatRect);  
  82.   
  83.     //--------------------------------------------------------------------------------------  
  84.     // 【Direct3D渲染五步曲之二】:开始绘制  
  85.     //--------------------------------------------------------------------------------------  
  86.     g_pd3dDevice->BeginScene();                     // 开始绘制  
  87.     //--------------------------------------------------------------------------------------  
  88.     // 【Direct3D渲染五步曲之三】:正式绘制  
  89.     //--------------------------------------------------------------------------------------  
  90.     g_pd3dDevice->SetRenderState(D3DRS_SHADEMODE,D3DSHADE_GOURAUD);//设置渲染状态  
  91.     //------------------------------------------------------------  
  92.     // 【顶点缓存使用四步曲之四】:绘制图形  
  93.     //------------------------------------------------------------  
  94.     g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX) );  
  95.     g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );  
  96.     g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 2);  
  97.   
  98.     //在窗口右上角处,显示每秒帧数  
  99.     int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() );  
  100.     g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(255,39,136));  
  101.   
  102.     //--------------------------------------------------------------------------------------  
  103.     // 【Direct3D渲染五步曲之四】:结束绘制  
  104.     //--------------------------------------------------------------------------------------  
  105.     g_pd3dDevice->EndScene();                       // 结束绘制  
  106.     //--------------------------------------------------------------------------------------  
  107.     // 【Direct3D渲染五步曲之五】:显示翻转  
  108.     //--------------------------------------------------------------------------------------  
  109.     g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻转与显示  
  110. }  
  111.   
  112. //-----------------------------------【Direct3D_CleanUp( )函数】--------------------------------  
  113. //  描述:资源清理函数,在此函数中进行程序退出前资源的清理工作  
  114. //---------------------------------------------------------------------------------------------------  
  115. void Direct3D_CleanUp()  
  116. {  
  117.     //释放COM接口对象  
  118.     SAFE_RELEASE(g_pVertexBuffer)  
  119.     SAFE_RELEASE(g_pFont)  
  120.     SAFE_RELEASE(g_pd3dDevice)  
  121. }  
我们先看一下运行截图,多次编译并运行这个demo ,我们会得到如下千奇百怪的图形, 也就是两个相连的三角形,他们的顶点颜色是完全随机的, 其中一个永远3 个顶点固定, 另外一个三角形一个顶点固定,另外两个顶点随机:


当然,有时候我们会只看到其中那个永远固定在这里的三角形, 而看不到随机的那个三角形,因为那个随机的三角形有两个顶点是完全随机的,有可能组成这个三角形的三个顶点缩成一团了,或者成了一条极细的线,我们看不到等等情况,导致我们最终只能看那个固定的三角形单独出现在窗口当中。当然,这样还涉及到了统序的问题,有的时候如果这个三角形顶点的排列顺序为逆时针,
那么也不会显示出来。


12.2 索引缓存——顶点缓存的红颜知己

12.2.1 引言

第一个例子。比如,我们现在要用顶点缓存绘制一个正方形。首先我们知道, 三角形是Direct3D中绘制图形的基本单元,我们绘制任何图形,说白了, 就是用大量的三角形组合起来,堆砌完成的。而正方形,显然是由两个大小相同的三角形结合起来组成的。所以要绘制一个正方形,我们用顶点缓存写两个三角形, 然后进行绘制就可以了。而一个三角形有3 个顶点,两个三角形就有6 个顶点。所以,用顶点缓存绘制一个正方形的话,需要用6 个顶点缓存。而众所周知一个正方形也就是4个顶点。也就是说我们单用顶点缓存来绘制一个正方形,多用了2 个顶点。下面就是我们在上一节中用到的配图:

其实单单用顶点缓存来绘制图形的话,需要的个数是非常好算的,我们只要计算图形中三角形的个数, 然后把这个数乘以3 就可以了。第二个例子,八边形的绘制,如下图。

单单用顶点缓存来绘制八边形的话,需要3 × 8=24 个顶点缓存。
代码方面,我们就可以这样写:
  1. Vertex circle[24) = {  
  2.     vO , vl , v2 ,  // 三角形0  
  3.     vO , v2 , v3 ,  // 三角形l  
  4.     vO , v3 ,v4,  // 三角形2  
  5.     vO , v4 , v5 ,  // 三角形3  
  6.     vO , v5 , v6 ,  // 三角形4  
  7.     vO, v6 ,  v7 ,  // 三角形5  
  8.     vO , v7 ,v8,  //三角形6  
  9.     vO , v8 , vl    // 三角形7  
  10. } ;  
提醒大家一点,看图写顶点的时候依然是顺时针来绕圈进行书写。
第三个例子,立方体的绘制,如下图。


我们知道, 一个立方体六个面,每个面都为一个正方形,所以一共有2 × 6= 12 个三角形,所以就有12 × 3=36 个顶点。则单单用顶点缓存来完成这个立方体的话,就需要36 个顶点缓存。
一个立方体只有8 个顶点,我们的想法是每个顶点只要记录一次,这样就可以节约我们的资源开销。但是我们采用的这种方式,却需要36 个顶点, 每个顶点都多存储了3. 5 次,这不科学。
通过上面的几个引例我们可以发现,这种只用顶点缓存来绘制图形的方法在应对复杂图形的时候非常不科学,显得复杂而力不从心。也就是说,当物体模型很复杂、顶点数量很大时, 仅使用顶点缓存绘制图形会使重复的顶点大大增加,并且Direct3D 仍需要对这些重复的顶点进行计算,因此需要更多的存储空间和更大的开销。
这时候,我们顶点缓存的红颜知己一一索引缓存是时候出场了。
索引缓存( Index Buffers ),人如其名,它就是一个索引,用于记录顶点缓存中每一个顶点的索引位置。我们可以把索引缓存理解为是一本书(这本书就是顶点缓存)的目录, 一本书有了目录,我们才能更快更高效地检索到所需要的内容。索引缓存作为顶点缓存的目录和提纲,能让顶点缓存发挥出内在的潜力,更高效更容易地绘制出一个3D 图形来。
另外提一点,索引缓存能够加速显存中快速存取顶点数据位置的能力。

12.2.2 索引缓存的使用思路

顶点缓存保存了物体模型所有的顶点数据,而这些数据可以是唯一的。索引缓存保存了构成物体的顶点在顶点缓存的索引值,通过索引查找对应的顶点,以完成图形的绘制。
关于缓存和缓冲区. 其实它们是同一个概念, 都是Buffer 这个词的英文翻译。有时候根据语境.我们会译为缓存; 有时候缓冲区更合语境, 我们便译为缓冲区。
下面我们看看上一节中介绍顶点缓存时贴出来过的、绘制一个正方形需要的思路图的升级版,这幅图是采用顶点缓存和索引缓存双剑合璧的方法来绘制的:

可以看到,在这幅图中我们用4 个顶点和6 个索引来描述一个正方形。
我们在顶点缓存中只需要保存这4 个顶点就可以了,而绘制正方形的两个三角形,则通过索引缓存来表示。
关于上面讲到的知识,我们再不庆其烦地总结一遍:
当物体模型很复杂、顶点数量很大时,仅使用顶点缓存绘制图形会使重复的顶点大大增加,并且Direct3D 仍需要对这些重复的顶点进行计算,因此需要更多的存储空间和更大的开销。但是如果我们把顶点缓存和他的好兄弟索引缓存配合起来使用的话,就可以化繁为简,化腐朽为神奇,不仅代码敲起来轻松了很多,也让我们写出来的程序性能提高。越复杂的图形,越体现了索引缓存的价值。不过需要注意的是,我们后面使用Direct3D 绘制的物体,大多数情况下是从3D 模型文件中载入的,而不是在Direct3D 中用代码敲出来的。

12.2.3 相濡以沫的顶点缓存与索引缓存

顶点缓存与索引缓存之间的关系,宛如俞伯牙与钟子期那种高山流水般的知己情谊,也如千里马与伯乐的那种难得的知遇之恩。
为什么这样说呢,那得看一看在使用顶点缓存配合索引缓存绘制图形时所用到的IDirect3DDevice9::DrawlndexedPrimitive 函数了。首先, 我们得注意一下这个函数的拼写,作者发觉经常有朋友会错写成DrawlndexPrimitive,注意其中Index 后面有个ed,千万不要忘了。正确写法是DrawlndexedPrimitive 。这个函数其实和使用顶点缓存绘制图形时用到的IDirect3DDevice9: :Draw Primitive 函数用法非常相似,有个别参数基本上一样。我们可以在MSDN中查到这个函数的原型:
  1. HRESULT DrawIndexedPrimitive(  
  2.   [in]  D3DPRIMITIVETYPE Type,  
  3.   [in]  INT BaseVertexIndex,  
  4.   [in]  UINT MinIndex,  
  5.   [in]  UINT NumVertices,  
  6.   [in]  UINT StartIndex,  
  7.   [in]  UINT PrimitiveCount  
  8. );  
  • 第二个参数, INT 类型的BaseVertexindex , 表示将要进行绘制的索引缓存的起始顶点的索引位置,也就是我们从哪个顶点开始做我们的索引目录,或者说是索引缓存区引用起始位置对应的顶点位置。
  • 第三个参数,UINT 类型的Minlndex,表示索引数组中最小的索引值,通常都设为0,这样我们的索引就是0,1,2,3,4,5 这样往后排的。
  • 第四个参数, UINT 类型的NumVertices ,表示我们这次调用DrawlndexedPrimitive 方法所需要的顶点个数, 也就是为多少个顶点做索引,或者说是索引缓存中使用的顶点数目。
  • 第五个参数, UINT 类型的Startlndex , 表示从索引中的第几个索引处开始绘制我们的图元,或者说是索引缓存区开始读索引值的起始位置。
  • 第六个参数, UINT 类型的PrimitiveCount , 显然就是要绘制的图元个数了。
有些知识点真的是很难用文字表达出来的, 正所谓有图有真相。为了便于大家理解,下面依旧配了一副关于DrawlndexedPrimitive 方法、顶点缓存和索引缓存之间的关系图。


在DrawlndexedPrimitive 中,参数NumVertices 指定索引缓存中使用的顶点数目,以及参数Startlndex 指定起始索引位置, 使我们在使用顶点缓存和索引缓存时更加如鱼得水。比如我们在顶点缓存区保存了多个物体模型的顶点数据, 那么就可以在每次调用DrawlndexedPrimitive 方法时,通过制定顶点、顶点数和起始索引, 分别绘制出顶点缓存与索引缓存中存储着的不同物体的模型。
这样,我们就可以用一段顶点缓存区配合一段索引缓存区,应付多样化的物体顶点的保存和绘制。
通过上面的演绎我们可以发现, 索引缓存是最懂顶点缓存的知己了,它可以无限挖掘顶点缓存的潜力,让顶点缓存可以用最少的“能量”, 迸发出最大的“光亮” 来。
最后, 我们来做个总结。索引缓存, 就是为了辅佐顶点缓存更好、更简洁、更高效、更有序地绘制图形而存在的。没有顶点缓存, 单单存在索引缓存是没有意义的。顶点缓存和索引缓存有点如影随行的感觉,需要一起使用,才能迸发出无穷无尽的能量。


12.2.4 双剑合璧:顶点缓存、索引缓存使用四步曲

上面我们讲过,索引缓存单独用起来是没有意义的,需要配合顶点缓存一起使用。本小节下面我们就来一起学习顶点缓存和索引缓存是如何搭配起来使用的。其实顶点缓存和索引缓存的创建和访问以及绘制过程是非常相似的,相信有了上篇文章中的讲解,理解下面这些知识点是毫无问题的。
1 . 四步曲之一: 设计顶点格式
这一步的关键词是设计, Design.
这一步和“顶点缓存使用四步曲之一:设计顶点缓存”中的做法完全一致,就可以设计出合适的顶点格式,下面贴出这步的实现代码:
  1. // 【顶点缓存使用四步曲之一】:设计顶点格式  
  2. //------------------------------------------------------------------------------------------------  
  3. struct CUSTOMVERTEX  
  4. {  
  5.     FLOAT x, y, z, rhw;  
  6.     DWORD color;  
  7. };  
  8. #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)  //FVF灵活顶点格式  
2 . 四步曲之二:创建顶点缓存以及索引缓存
这一步的关键词是创建, create 。
在12.1 节中我们讲过,在Direct3D 中, 顶点缓存由IDirect3DVertexBuffer9 接口对象来表示的。而本篇文章的主角索引缓存的对象,则由IDirect3DlndexBuffer9 来表示的。我们可以看到这两个接口命名规则非常的相似。其实,这两个接口中的方法也基本相同,也是GetDesc、Lock 和Unlock 三个。
这一步我们要做的就是先分别定义指向IDirect3DVertexBuffer9 接口和IDirect3DlndexBuffer9接口的指针变量,然后分别运用
IDirect3DVertexBuffer9 接口的CreateVertexBuffer 方法和CreatelndexBuffer 方法创建顶点缓存和索引缓存,把内存地址分别复制给我们创建的这两个指针。
首先,分别定义指向IDirect3DVertexBuffer9 接口和IDirect3DIndexBuffer9 接口的指针变量。即写上这么两句:
  1. LPDIRECT3DVERTEXBUFFER9     g_pVertexBuffer = NULL;    //顶点缓冲区对象  
  2. LPDIRECT3DINDEXBUFFER9      g_pIndexBuffer  = NULL;    // 索引缓存对象  
然后就是分别运用IDirect3DVertexBuffer9 接口的CreateVertexBuffer 方法和CreatelndexBuffer方法创建顶点缓存和索引缓存,把内存地址分别复制给我们创建的这两个指针。
这两个方法参数和使用方法大致都是一样的, 只有一点细微的差别。这里我们贴出两者的原型,然后对今天的主场作战的CreatelndexBuffer 做一下讲解。首先是贴出创建顶点缓存的方法CreateVertexBuffer 的原型(参数讲解参看12 . 1 节) :
  1. HRESULT CreateVertexBuffer(  
  2.   [in]           UINT Length,  
  3.   [in]           DWORD Usage,  
  4.   [in]           DWORD FVF,  
  5.   [in]           D3DPOOL Pool,  
  6.   [out, retval]  IDirect3DVertexBuffer9 **ppVertexBuffer,  
  7.   [in]           HANDLE *pSharedHandle  
  8. );  
然后是本小节主场作战的CreatelndexBuffer 的原型和参数讲解:
  1. HRESULT CreateIndexBuffer(  
  2.   [in]           UINT Length,  
  3.   [in]           DWORD Usage,  
  4.   [in]           D3DFORMAT Format,  
  5.   [in]           D3DPOOL Pool,  
  6.   [out, retval]  IDirect3DIndexBuffer9 **ppIndexBuffer,  
  7.   [in]           HANDLE *pSharedHandle  
  8. );  
  • 第三个参数,D3DFORMAT 类型的Format , 我们可以发现这个参数是CreatelndexBuffer和CreateVertexBuffer 最大的不同点。这个参数用于指定索引缓存中存储每个索引的大小,可以看到它为D3DFORMAT 枚举类型。D3DFORMAT 这个枚举类型中内容有些多, 在这里就不贴出来了,具体可以去查查MSDN,我们在这里只讲一下常常会用到的值。取值为D3DFMT_INDEX16 表示为16 位的索引,取值为D3DFMT_INDEX32则表示为32 位的索引。通常我们都用的是16 位的索引,也就是取值D3DFMT_INDEX16 。
所以这一步综合起来,就是这样写:
  1. // 【顶点缓存、索引缓存绘图四步曲之二】:创建顶点缓存和索引缓存  
  2. //--------------------------------------------------------------------------------------  
  3. //创建顶点缓存  
  4. if( FAILED( g_pd3dDevice->CreateVertexBuffer( 18*sizeof(CUSTOMVERTEX),  
  5.     0, D3DFVF_CUSTOMVERTEX,  
  6.     D3DPOOL_DEFAULT, &g_pVertexBuffer, NULL ) ) )  
  7. {  
  8.     return E_FAIL;  
  9. }  
  10. // 创建索引缓存  
  11. if( FAILED(     g_pd3dDevice->CreateIndexBuffer(48 * sizeof(WORD), 0,   
  12.     D3DFMT_INDEX16, D3DPOOL_DEFAULT, &g_pIndexBuffer, NULL)) )  
  13. {  
  14.     return E_FAIL;  
  15. }  
3. 四步曲之三:访问顶点缓存以及索引缓存
这一步的关键词是访问, Access 。
与访问顶点缓存的方式相似,在访问索引缓存时需要调用IDirect3DlndexBuffer 接口的Lock方法对缓存进行加锁, 并取得加锁后的缓存首地址, 然后通过该地址向索引缓存中写入索引信息,最后调用Unlock 方法对缓存进行解锁。
IDirect3DlndexBuffer 接口中的加锁和解锁函数和IDirect3DVertexBuffer 接口中的加锁和解锁函数原型和用法完全一致:
这里主要有两种方式来访问缓存的内容,下面我们依旧分别介绍:
第一种方式是直接在Lock 和UnLock 之间对每个索引的数据进行赋值和修改,以Lock方法中的ppbData 指针参数作为数组的首址,例如这样写:
  1. // 填充索引数据  
  2. WORD *pindices = NULL;  
  3. g_pindexBuf->Lock (0, 0, (void**) &pindices , 0) ;  
  4. pindices [O] = O, pindices [1] = 2 , pindices [2] = 3 ;  
  5. pindices[3] = 2 , pindices[4] = 1 , pindices[5] = 3;  
  6. pindices[6] = 1 , pindices[7] = 0 , pindices[8]= 3 ;  
  7. pindices[9] = 2 , pindices[lO] = 0 , pindices[ll] = 1  
  8. g_pindexBuf- >Unlock ();  
第二种方式是事先准备好索引数据的数组,然后在Lock 和Unlock 之间用下memcpy 函数,进行数组内容的拷贝就可以了
例如这样写:
  1. //索引数组的设置  
  2.     WORD Indices[] ={ 0,1,2, 0,2,3, 0,3,4, 0,4,5, 0,5,6, 0,6,7, 0,7,8, 0,8,9, 0,9,10, 0,10,11 ,0,11,12, 0,12,13 ,0,13,14 ,0,14,15 ,0,15,16, 0, 16,1 };  
  3.   
  4.     // 填充索引数据  
  5.     WORD *pIndices = NULL;  
  6.     g_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);  
  7.     memcpy( pIndices, Indices, sizeof(Indices) );  
  8.     g_pIndexBuffer->Unlock();  
所以,这一步代码整体来看就是下面这样的,用于绘制一个彩色的十六边形:
  1. //--------------------------------------------------------------------------------------  
  2. // 【顶点缓存、索引缓存绘图四步曲之三】:访问顶点缓存和索引缓存  
  3. //--------------------------------------------------------------------------------------  
  4. //顶点数据的设置,  
  5. CUSTOMVERTEX Vertices[17];  
  6. Vertices[0].x = 400;  
  7. Vertices[0].y = 300;  
  8. Vertices[0].z = 0.0f;  
  9. Vertices[0].rhw = 1.0f;  
  10. Vertices[0].color = D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256);  
  11. for(int i=0; i<16; i )  
  12. {  
  13.     Vertices[i 1].x =  (float)(250*sin(i*3.14159/8.0))   400;  
  14.     Vertices[i 1].y = -(float)(250*cos(i*3.14159/8.0))   300;  
  15.     Vertices[i 1].z = 0.0f;  
  16.     Vertices[i 1].rhw = 1.0f;  
  17.     Vertices[i 1].color =  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256);  
  18. }  
  19.   
  20. //填充顶点缓冲区  
  21. VOID* pVertices;  
  22. if( FAILED( g_pVertexBuffer->Lock( 0, sizeof(Vertices), (void**)&pVertices, 0 ) ) )  
  23.     return E_FAIL;  
  24. memcpy( pVertices, Vertices, sizeof(Vertices) );  
  25. g_pVertexBuffer->Unlock();  
  26.   
  27. //索引数组的设置  
  28. WORD Indices[] ={ 0,1,2, 0,2,3, 0,3,4, 0,4,5, 0,5,6, 0,6,7, 0,7,8, 0,8,9, 0,9,10, 0,10,11 ,0,11,12, 0,12,13 ,0,13,14 ,0,14,15 ,0,15,16, 0, 16,1 };  
  29.   
  30. // 填充索引数据  
  31. WORD *pIndices = NULL;  
  32. g_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);  
  33. memcpy( pIndices, Indices, sizeof(Indices) );  
  34. g_pIndexBuffer->Unlock();  
4. 四步曲之四:绘制图形
最后一步的关键词是绘制, Draw 。
因为我们的顶点缓存和索引缓存是配合使用的,这里就和单用顶点缓存绘制不太一样了。单用顶点缓存绘制,需要用到IDirect3DDevice9::SetStreamSource 、IDirect3DDevice9::SetFVF 、IDirect3DDevice9::DrawPrimitive 这3 个方法,也就是这样写(单用顶点缓存绘制):
  1. g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX) );  
  2. g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );  
  3. g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 2);  

而使用索引缓存绘制图形时,在用完IDirect3DDevice9 : :SetStreamSource 方法把包含的几何体信息的顶点缓存和渲染流水线相关联,以及IDirect3DDevice9 ::SetFVF 方法指定我们使用的灵活顶点格式的宏名称之后,还要调用IDirect3DDevice9 接口的一个Setlndices 方法设置索引缓存,最后才是调用绘制函数。而这里的绘制函数不是以前使用的DrawPrimitive了,而是我们在12.2.3 节中重点讲过的DrawlndexedPrimitive 。下面重点讲解下Setlndices方法。
  1. HRESULT SetIndices(  
  2.   [in]  IDirect3DIndexBuffer9 *pIndexData  
  3. );  
这个函数唯一的参数是1Direct3D IndexB uffer9 类型的*plndexData,指向我们设置的索引缓冲区的地址,就是点名要我们的那把金钥匙, 第二步里我们定义的并初始化的那个g_plndexBuf。
最后是绘制函数DrawlndexedPrimitivelndex 方法,原型如下:
  1. HRESULT DrawIndexedPrimitive(  
  2.   [in]  D3DPRIMITIVETYPE Type,  
  3.   [in]  INT BaseVertexIndex,  
  4.   [in]  UINT MinIndex,  
  5.   [in]  UINT NumVertices,  
  6.   [in]  UINT StartIndex,  
  7.   [in]  UINT PrimitiveCount  
  8. );  
所以,这一步综合起来看,就是如下的代码:
  1. //--------------------------------------------------------------------------------------  
  2. // 【Direct3D渲染五步曲之二】:开始绘制  
  3. //--------------------------------------------------------------------------------------  
  4. g_pd3dDevice->BeginScene();                     // 开始绘制  
  5. //--------------------------------------------------------------------------------------  
  6. // 【Direct3D渲染五步曲之三】:正式绘制  
  7. //--------------------------------------------------------------------------------------  
  8.     // 设置渲染状态  
  9. g_pd3dDevice->SetRenderState(D3DRS_SHADEMODE,D3DSHADE_GOURAUD);//这句代码可省略,因为高洛德着色模式为D3D默认的着色模式  
  10. //-------------------------------------------------------------------  
  11. // 【顶点缓存、索引缓存绘图四步曲之四】:绘制图形  
  12. //--------------------------------------------------------------------  
  13. g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX) );//把包含的几何体信息的顶点缓存和渲染流水线相关联  
  14. g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );//指定我们使用的灵活顶点格式的宏名称  
  15. g_pd3dDevice->SetIndices(g_pIndexBuffer);//设置索引缓存  
  16. g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 17, 0, 16);//利用索引缓存配合顶点缓存绘制图形  
  17.   
  18. //在窗口右上角处,显示每秒帧数  
  19. int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() );  
  20. g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(255,39,136));  
  21.   
  22. //--------------------------------------------------------------------------------------  
  23. // 【Direct3D渲染五步曲之四】:结束绘制  
  24. //--------------------------------------------------------------------------------------  
  25. g_pd3dDevice->EndScene();                       // 结束绘制  
  26. //--------------------------------------------------------------------------------------  
5 总结
最后总结一下,使用索引缓存配合顶点缓存来绘制图形,核心思想没变,大体思路没变,总结起来,简明扼要,依旧是四步曲,依旧是八个字:设计,创建,访问, 绘制。
关于Direct3D 中的绕序, 在这里简明扼要地说明一下, 由于Direct3D 用的是左手坐标系,我们用左手除了大拇指的四个手指按顺时针方向对某个三角形绕圈,大拇指指的方向就是这个三角形的正面了。

12.2.5 示例程序D3Ddemo4

我们将讲解利用顶点缓存的红颜知己索引缓存配合顶点缓存一起来绘制图形。下面就是一个索引缓存配合顶点缓存绘制一个彩色的随机顶点颜色的十六边形示例程序的详细注释源代码(它略去了一些无关紧要的代码):
  1. //------------------------------------------------------------------------------------------------  
  2. // 【顶点缓存使用四步曲之一】:设计顶点格式  
  3. //------------------------------------------------------------------------------------------------  
  4. struct CUSTOMVERTEX  
  5. {  
  6.     FLOAT x, y, z, rhw;  
  7.     DWORD color;  
  8. };  
  9. #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)  //FVF灵活顶点格式  
  10.   
  11. //-----------------------------------【全局变量声明部分】-------------------------------------  
  12. //  描述:全局变量的声明  
  13. //------------------------------------------------------------------------------------------------  
  14. LPDIRECT3DDEVICE9                   g_pd3dDevice = NULL; //Direct3D设备对象  
  15. ID3DXFont*                              g_pFont=NULL;    //字体COM接口  
  16. float                                           g_FPS = 0.0f;       //一个浮点型的变量,代表帧速率  
  17. wchar_t                                     g_strFPS[50];    //包含帧速率的字符数组  
  18. LPDIRECT3DVERTEXBUFFER9     g_pVertexBuffer = NULL;    //顶点缓冲区对象  
  19. LPDIRECT3DINDEXBUFFER9      g_pIndexBuffer  = NULL;    // 索引缓存对象  
  20.   
  21. //-----------------------------------【Object_Init( )函数】--------------------------------------  
  22. //  描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化  
  23. //--------------------------------------------------------------------------------------------------  
  24. HRESULT Objects_Init(HWND hwnd)  
  25. {  
  26.     //创建字体  
  27.     if(FAILED(D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1, false, DEFAULT_CHARSET,   
  28.         OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("微软雅黑"), &g_pFont)))  
  29.         return E_FAIL;  
  30.     srand(timeGetTime());      //用系统时间初始化随机种子   
  31.   
  32.     //--------------------------------------------------------------------------------------  
  33.     // 【顶点缓存、索引缓存绘图四步曲之二】:创建顶点缓存和索引缓存  
  34.     //--------------------------------------------------------------------------------------  
  35.     //创建顶点缓存  
  36.     if( FAILED( g_pd3dDevice->CreateVertexBuffer( 18*sizeof(CUSTOMVERTEX),  
  37.         0, D3DFVF_CUSTOMVERTEX,  
  38.         D3DPOOL_DEFAULT, &g_pVertexBuffer, NULL ) ) )  
  39.     {  
  40.         return E_FAIL;  
  41.     }  
  42.     // 创建索引缓存  
  43.     if( FAILED(     g_pd3dDevice->CreateIndexBuffer(48 * sizeof(WORD), 0,   
  44.         D3DFMT_INDEX16, D3DPOOL_DEFAULT, &g_pIndexBuffer, NULL)) )  
  45.     {  
  46.         return E_FAIL;  
  47.     }  
  48.     //--------------------------------------------------------------------------------------  
  49.     // 【顶点缓存、索引缓存绘图四步曲之三】:访问顶点缓存和索引缓存  
  50.     //--------------------------------------------------------------------------------------  
  51.     //顶点数据的设置,  
  52.     CUSTOMVERTEX Vertices[17];  
  53.     Vertices[0].x = 400;  
  54.     Vertices[0].y = 300;  
  55.     Vertices[0].z = 0.0f;  
  56.     Vertices[0].rhw = 1.0f;  
  57.     Vertices[0].color = D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256);  
  58.     for(int i=0; i<16; i )  
  59.     {  
  60.         Vertices[i 1].x =  (float)(250*sin(i*3.14159/8.0))   400;  
  61.         Vertices[i 1].y = -(float)(250*cos(i*3.14159/8.0))   300;  
  62.         Vertices[i 1].z = 0.0f;  
  63.         Vertices[i 1].rhw = 1.0f;  
  64.         Vertices[i 1].color =  D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256);  
  65.     }  
  66.   
  67.     //填充顶点缓冲区  
  68.     VOID* pVertices;  
  69.     if( FAILED( g_pVertexBuffer->Lock( 0, sizeof(Vertices), (void**)&pVertices, 0 ) ) )  
  70.         return E_FAIL;  
  71.     memcpy( pVertices, Vertices, sizeof(Vertices) );  
  72.     g_pVertexBuffer->Unlock();  
  73.   
  74.     //索引数组的设置  
  75.     WORD Indices[] ={ 0,1,2, 0,2,3, 0,3,4, 0,4,5, 0,5,6, 0,6,7, 0,7,8, 0,8,9, 0,9,10, 0,10,11 ,0,11,12, 0,12,13 ,0,13,14 ,0,14,15 ,0,15,16, 0, 16,1 };  
  76.   
  77.     // 填充索引数据  
  78.     WORD *pIndices = NULL;  
  79.     g_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);  
  80.     memcpy( pIndices, Indices, sizeof(Indices) );  
  81.     g_pIndexBuffer->Unlock();  
  82.   
  83.     return S_OK;  
  84. }  
  85.   
  86. //-----------------------------------【Direct3D_Render( )函数】-------------------------------  
  87. //  描述:使用Direct3D进行渲染  
  88. //--------------------------------------------------------------------------------------------------  
  89. void Direct3D_Render(HWND hwnd)  
  90. {  
  91.     //--------------------------------------------------------------------------------------  
  92.     // 【Direct3D渲染五步曲之一】:清屏操作  
  93.     //--------------------------------------------------------------------------------------  
  94.     g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);  
  95.   
  96.     //定义一个矩形,用于获取主窗口矩形  
  97.     RECT formatRect;  
  98.     GetClientRect(hwnd, &formatRect);  
  99.   
  100.     //--------------------------------------------------------------------------------------  
  101.     // 【Direct3D渲染五步曲之二】:开始绘制  
  102.     //--------------------------------------------------------------------------------------  
  103.     g_pd3dDevice->BeginScene();                     // 开始绘制  
  104.     //--------------------------------------------------------------------------------------  
  105.     // 【Direct3D渲染五步曲之三】:正式绘制  
  106.     //--------------------------------------------------------------------------------------  
  107.         // 设置渲染状态  
  108.     g_pd3dDevice->SetRenderState(D3DRS_SHADEMODE,D3DSHADE_GOURAUD);//这句代码可省略,因为高洛德着色模式为D3D默认的着色模式  
  109.     //-------------------------------------------------------------------  
  110.     // 【顶点缓存、索引缓存绘图四步曲之四】:绘制图形  
  111.     //--------------------------------------------------------------------  
  112.     g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX) );//把包含的几何体信息的顶点缓存和渲染流水线相关联  
  113.     g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );//指定我们使用的灵活顶点格式的宏名称  
  114.     g_pd3dDevice->SetIndices(g_pIndexBuffer);//设置索引缓存  
  115.     g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 17, 0, 16);//利用索引缓存配合顶点缓存绘制图形  
  116.   
  117.     //在窗口右上角处,显示每秒帧数  
  118.     int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() );  
  119.     g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(255,39,136));  
  120.   
  121.     //--------------------------------------------------------------------------------------  
  122.     // 【Direct3D渲染五步曲之四】:结束绘制  
  123.     //--------------------------------------------------------------------------------------  
  124.     g_pd3dDevice->EndScene();                       // 结束绘制  
  125.     //--------------------------------------------------------------------------------------  
  126.     // 【Direct3D渲染五步曲之五】:显示翻转  
  127.     //--------------------------------------------------------------------------------------  
  128.     g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻转与显示  
  129. }  
  130.   
  131. //-----------------------------------【Direct3D_CleanUp( )函数】--------------------------------  
  132. //  描述:资源清理函数,在此函数中进行程序退出前资源的清理工作  
  133. //---------------------------------------------------------------------------------------------------  
  134. void Direct3D_CleanUp()  
  135. {  
  136.     //释放COM接口对象  
  137.     SAFE_RELEASE(g_pIndexBuffer)  
  138.     SAFE_RELEASE(g_pVertexBuffer)  
  139.     SAFE_RELEASE(g_pFont)  
  140.     SAFE_RELEASE(g_pd3dDevice)  
  141. }  

多次运行这个demo ,我们会得到万花筒般绚丽的十六边形的效果,因为构成这个十六边形的每个顶点的颜色都是随机的, 配合Direct3D 中默认的高洛德( GOURAUD )着色模式,就会得到这样颜色平滑过渡的绚丽的十六边形来。
这个demo 的运行截图如下:


最后我们讲解一下索引缓存数组的设置。最好结合本章开头的第2 个引例来进行书写,一个为八边形, 一个为十六边形,基本思路一致。其实非常简单,就是0,1,2 为一组,做为第1个三角形。然后顺时针往后,0,2,3 为一组,表示第2 个三角形。然后就是0,3,4 …… 到0, 15 ,16 ,最后是0,16, 1 ,转了一圈,回到起点了。
另外提一点,在渲染五步曲第一步Clear 的时候, 要加个D3DCLEAR_ZBUFFER 选项,用于清除Z 缓存, 不然可能显示不出图形来。因为我们涉及到了Z 坐标,定义的点却依然是在二维坐标中。如果Z 坐标设值不为0 的话,就要加上了,不过我们这个程序中顶点的Z 坐标都是0 ,所以此时不加也无妨。

12.3 章节小憩

嗯,渐渐领略到Direct3D 的魅力了吧。也许书本前的你还是觉得不满足,觉得画这些几何体一点意思都没有,而是想要绘制出酷酷的游戏人物出来。好吧,继续往下一章节进发吧,更多惊喜还在后面呢。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引