一.顶点缓存与索引缓存
3D中,各种图形一般都是由多边形来逼近的,一般采用三角形来逼近。例如像下图展示的那样:
这个蓝色的球体是由大量的三角形来组成,当然三角形的数量越多球体就会显得更加的逼真。需要指出的是,任何物体都可以用三角形网格来逼近表示,三角形网格是构建物体模型的基本单元。而一个三角形是由三个顶点组成,所以顶点就可以说是组成物体模型的基本单位。这里的顶点并不像我平常所说的点一样,它不仅仅只保存了位置信息,还有可以保存颜色,法线,纹理坐标等信息。
1.顶点缓存
在D3D中,顶点的具体表现形式是顶点缓存(Vertex Buffer),顶点缓存保存了顶点数据的内存空间。我们可以创建很多的顶点,将其保存在顶点缓存对应的内存空间,然后就可以调用相应的函数渲染出顶点对应的图形。使用顶点缓存的具体步骤如下:
(1)定义顶点结构
前面说过,顶点中可以保存的内容不限于位置信息,还可以有很多其他的内容。那我们需要顶点保存什么信息呢,所以我们必须先定义顶点结构,确定顶点中需要保存那些信息。顶点的定义很简单,就是一个简单的结构体定义。例如下面的顶点结构保存了顶点的位置信息和颜色信息:
struct Vertex { float x, y, z; D3DCOLOR color; };
也可以包含其他信息,例如下面的包含了位置信息和纹理坐标:
struct Vertex { float x, y, z; lloat u, v; };
顶点结构定义好了,这个还不够,毕竟只是你知道这个顶点结构包含了什么信息。D3D并不知道你这个结构所包含的信息,所以你还必须指定顶点结构的格式。灵活顶点格式(Flexible Vertex Format,FVF)用来描述三角形网格的每个顶点。灵活顶点格式可以让我们随心所欲地自定义其中所包含的顶点属性信息。例如,上面的第一个顶点结构的灵活顶点格式可以按如下方式定义:
#define VERTEX_FVF D3DFVF_XYZ | D3DFVF_DIFFUSE
上面的顶点格式的宏定义中用到了D3DFVF_XYZ和D3DFVF_DIFFUSE,这两个都是D3D中已经定义好的一些宏。用来代表各种属性,比较几个常用的如下,其他的可以查看帮助文档。
D3DFVF_XYZ | 包含未经过坐标变换的顶点坐标值,不可以和D3DFVF_XYZRHW一起使用 |
D3DFVF_XYZRHW | 包含经过坐标变换的顶点坐标值,不可以和D3DFVF_XYZ以D3DFVF_NORMAL一起使用 |
D3DFVF_DIFFUSE | 包含漫反射的颜色值 |
D3DFVF_SPECULAR | 包含镜面反射的数值 |
D3DFVF_NORMAL | 包含法线向量的数值 |
D3DFVF_TEX1-TEX8 | 表示包含1~8个纹理坐标信息,是几重纹理后缀就用几,最多8层纹理 |
下面举一个关于顶点结构定义的完整例子:
struct Vertex { float x, y, z; D3DCOLOR color; }; #define VERTEX_FVF D3DFVF_XYZ | D3DFVF_DIFFUSE //或者也可以用静态变量将顶点格式定义在结构内部 struct Vertex { float x, y, z; D3DCOLOR color; const static DWORD VERTEX_FVF; }; const DWORD Vertex::VERTEX_FVF = D3DFVF_XYZ | D3DFVF_DIFFUSE;
(2)创建顶点缓存
定义好了顶点结构,就可以创建顶点缓存了。D3D中使用IDirect3DVertexBuffer9接口对象来代表顶点缓存。创建顶点缓存的基本步骤如下:
a.获取IDirect3DVertexBuffer9接口指针
D3D中通过CreateVertexBuffer函数来获取IDirect3DVertexBuffer9接口指针,对应函数的原型声明如下:
HRESULT CreateVertexBuffer( [in] UINT Length, [in] DWORD Usage, [in] DWORD FVF, [in] D3DPOOL Pool, [out, retval] IDirect3DVertexBuffer9 **ppVertexBuffer, [in] HANDLE *pSharedHandle );
参数说明:
Length | 表示顶点缓冲区的长度,单位为字节 |
Usage | 设置缓存的一些附加属性。可以设置为0,表示没有附加属性。具体取值参看帮助文档 |
FVF | 顶点的灵活顶点格式 |
Pool | 用于指定顶点缓存的存储的位置,具体参看帮助文档 |
ppVertexBuffer | 用于返回IDirectDVertexBuffer9对象指针 |
pSharedHandle | 保留参数,一般设为NULL或者0 |
例如一个IDirectDVertexBuffer9对象指针的例子:
IDirectDVertexBuffer9 *pVertexBuffer = nullptr; pDevice->CreateVertexBuffer(sizeof(Vertex) * 3, 0, VERTEX_FVF, D3DPOOL_DEFAULT, &pVertexBuffer, nullptr);
b.数据复制
IDirectDVertexBuffer9对象指针我们已经得到了,但是我们还并没有向缓存中写入数据,所以说我们的下一步就是向缓存中写入数据。
首先第一步我们需要锁定需要写入数据的缓存,通过调用IDirect3DVertexBuffer9::Lock()来执行该操作。函数的原型声明如下:
HRESULT Lock( [in] UINT OffsetToLock, [in] UINT SizeToLock, [out] VOID **ppbData, [in] DWORD Flags );
OffsetToLock | 表示加锁区域自存储空间的起始位置到开始锁定位置的偏移量,单位为字节 |
SizeToLock | 表示要锁定的字节数,也就是加锁区域的大小 |
ppbData | 指向被锁定的存储区的起始地址的指针 |
Flags | 表示锁定的方式,我们可以把它设为0,具体参看帮助文档D3DLOCK |
锁定时的具体示意图如下:
得到了被锁定缓存的首地址之后就可以直接向该地址下的存储区写入数据了。写入数据之后然后解锁缓存,通过调用IDirect3DVertexBuffer9::Unlock()来实现。函数原型声明如下:
HRESULT Unlock();
函数声明很简单,调用也很简单,直接简单调用一下就可以了。
pVertexBuffer->Unlock();
下面附上一段关于向缓存写入数据的完整代码:
Vertex triangle[] = { {400, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 500, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)} }; void *temp; pDevice->CreateVertexBuffer(sizeof(triangle), 0, Vertex::FVF, D3DPOOL_MANAGED, &g_pVertexBuffer, NULL); g_pVertexBuffer->Lock(0, sizeof(triangle), (void **)&temp, 0); memcpy(temp, triangle, sizeof(triangle)); g_pVertexBuffer->Unlock();
(3)使用顶点缓存进行图形渲染
现在已经创建好了顶点缓存,并且已经将其中填充好了顶点信息,下一步就可以进行渲染工作了,在正式进行渲染之前还有几个工作要完成。
a.设置数据流
首先需要设置数据流,通过调用IDirect3DDevice9::SetStreamSource来完成。IDirect3DDevice9::SetStreamSource用于把包含的几何体信息的顶点缓存和渲染流水线相关联,函数原型如下:
HRESULT SetStreamSource( [in] UINT StreamNumber, [in] IDirect3DVertexBuffer9 *pStreamData, [in] UINT OffsetInBytes, [in] UINT Stride );
StreamNumber | 用于指定与该顶点缓存建立连接的数据流,由于一般只用一个数据流,所以通常设为0 |
pStreamData | 顶点缓存的指针 |
OffsetInBytes | 表示在数据流中以字节为单位的偏移量,通常设为0 |
Stride | 表示在顶点缓存中存储的每个顶点结构的大小,单位为字节 |
一个调用的具体实例为:
pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(Vertex));
b.设置灵活顶点格式
在渲染之前还需要设置灵活顶点格式,通过函数IDirect3DDevice9::SetFVF来完成,函数声明如下:
HRESULT SetFVF( [in] DWORD FVF );
调用很简单,参数就是顶点结构的灵活顶点格式,如下所示:
pDevice->SetFVF(Vertex::FVF);
c.进行渲染
所有东西都已经设置好了(如果使用了纹理,材质,还需要进行相关的设置),现在就可以进行渲染了。通过调用函数IDirect3DDevice9::DrawPrimitive来完成,函数的具体声明如下:
HRESULT DrawPrimitive( [in] D3DPRIMITIVETYPE PrimitiveType, [in] UINT StartVertex, [in] UINT PrimitiveCount );
参数说明:
PrimitiveType | 绘制的图元类型,为D3DPRIMITIVETYPE枚举类型 |
StartVertex | 从顶点缓存中读取顶点数据的起始索引位置 |
PrimitiveCount | 需要绘制的图元的数量 |
说一下D3DPRIMITIVETYPE,D3DPRIMITIVETYPE是一个枚举类型,用来表示图元的类型,声明如下:
typedef enum D3DPRIMITIVETYPE { D3DPT_POINTLIST = 1, D3DPT_LINELIST = 2, D3DPT_LINESTRIP = 3, D3DPT_TRIANGLELIST = 4, D3DPT_TRIANGLESTRIP = 5, D3DPT_TRIANGLEFAN = 6, D3DPT_FORCE_DWORD = 0x7fffffff } D3DPRIMITIVETYPE, *LPD3DPRIMITIVETYPE;
D3DPT_POINTLIST | 用来绘制一系列的点 |
D3DPT_LINELIST | 绘制线列,例如:1 2顶点为第一条线 3 4顶点为第二条线 以此类推 |
D3DPT_LINESTRIP | 绘制线带,例如:1 2顶点为第一条线 2 3顶点为第二条线 以此类推 |
D3DPT_TRIANGLELIST | 绘制三角形序列,例如: 1 2 3为第一个三角形 4 5 6为第二个三角形 |
D3DPT_TRIANGLESTRIP | 绘制三角形带, 例如:1 2 3为第一个三角形 2 3 4则为第二个三角形 |
D3DPT_TRIANGLEFAN | 绘制三角形扇,例如:1 2 3为第一个三角形 1 3 4为第二个三角形 |
调用实例如下:
pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
(4)实例:渲染矩形
现在要画一个矩形,有了前面的准备,应该来非常简单了。一个矩形是由两个三角形组成,所以,填充的顶点数据如下:
Vertex rect[] = { {100, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {700, 500, 0, 1.0, D3DCOLOR_XRGB(255, 255, 255)} }; void *temp; IDirect3DDevice9 *pDevice = g_pDevice->GetDevice(); pDevice->CreateVertexBuffer(sizeof(rect), 0, Vertex::FVF, D3DPOOL_MANAGED, &g_pVertexBuffer, NULL); g_pVertexBuffer->Lock(0, sizeof(rect), (void **)&temp, 0); memcpy(temp, rect, sizeof(rect)); g_pVertexBuffer->Unlock();
最后再按照前面所说的步骤,设置数据流,设置灵活顶点格式,然后最后渲染矩形。与笔记一中所不同的是,这里是画的一个矩形,所以需要渲染两个三角形,IDirect3DDevice9::DrawPrimitive中的第三个参数要改为2,代码如下:
pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(Vertex)); pDevice->SetFVF(Vertex::FVF); pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
与笔记一中画三角形的代码,需要改变的就上面所说的两个地方。第一个改变写入缓存的数据,改变图元绘制时的数量,附上运行截图:
2.索引缓存
在前面,我们学习了顶点缓存的知识,并且渲染了一个矩形。但是我们会发现一个问题,在向顶点缓存中写入数据的时候,我们填充了6个顶点结构,但是一个矩形只有4个顶点。有两个顶点被写入缓存了两次,这样可能会造成一些内存的浪费,特别是需要渲染的模型很复杂的时候,那时重复的顶点会变得更多,所以在D3D中还引入了索引缓存。索引缓存((Index Buffers)),人如其名,它就是一个索引,用于记录顶点缓存中每一个顶点的索引位置。例如一个矩形:
如果在没有使用索引缓存的情况下,我们需要创建一个这样的顶点缓存:
vertex = {v1, v2, v3, v3, v2, v4};
需要六个顶点来渲染这个矩形,如果现在我们使用索引缓存的话,就可以这样实现:
vertex = {v1, v2, v3, v4}; index = {1, 2, 3, 3, 2, 4};
索引中表明了,绘制时使用的顶点在顶点缓存中的位置。D3D中用IDirect3DIndexBuffer9接口对象来表示索引缓存,它的创建和使用和顶点缓存类似。具体步骤如下:
(1)获取IDirect3DIndexBuffer9对象指针
D3D中通过CreateIndexBuffer函数来获取IDirect3DIndexBuffer9接口指针,对应函数的原型声明如下:
HRESULT CreateIndexBuffer( [in] UINT Length, [in] DWORD Usage, [in] D3DFORMAT Format, [in] D3DPOOL Pool, [out, retval] IDirect3DIndexBuffer9 **ppIndexBuffer, [in] HANDLE *pSharedHandle );
这个函数中的几个参数和CreateIndexBuffer的参数类似,除了第三个参数和第四个参数。第四个参数分别用于返回顶点缓存和索引缓存接口指针。而对于第三个参数,CreateIndexBuffer中传入灵活顶点格式,而CreateIndexBuffer则用于指定索引的格式,为一个D3DFORMAT类型的变量。它的取值一般是以下两个:
D3DFMT_INDEX16 |
表示为16位的索引 |
D3DFMT_INDEX32 |
表示为32位的索引 |
一个具体的实例如下:
IDirect3DIndexBuffer9 *g_pIndexBuffer = nullptr; pDevice->CreateIndexBuffer(sizeof(WORD) * 6, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &g_pIndexBuffer, nullptr);
(2)写入索引数据
和前面顶点缓存一样,我们在获取IDirect3DIndexBuffer9接口指针之后,还需要向缓存中写入索引数据。至于写入索引数据的方法和顶点缓存的方法一模一样,都是通过调用Lock和Unlock来完成,这里不多说。附上代码实例:
WORD index[] = {0, 1, 2, 2, 1, 3}; void *temp; pDevice->CreateIndexBuffer(sizeof(WORD) * 6, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &g_pIndexBuffer, nullptr); g_pIndexBuffer->Lock(0, sizeof(index), (void **)&temp, 0); memcpy(temp, index, sizeof(index)); g_pIndexBuffer->Unlock();
(3)结合顶点缓存进行渲染
在使用索引缓存和顶点缓存进行渲染之前,也需要完成一些预先的工作。
a.设置索引
顶点缓存在进行渲染之前,需要设置数据流将它和渲染流水线相关联。索引缓存也需要类似的工作,所以说我们首先必须要设置索引。通过IDirect3DDevice9::SetIndices函数来完成这个工作,函数的声明如下:
HRESULT SetIndices( [in] IDirect3DIndexBuffer9 *pIndexData );
参数很简单,就是我们前面已经填充好索引数据的IDirect3DIndexBuffer9对象的指针。调用实例如下:
pDevice->SetIndices(g_pIndexBuffer);
b.使用IDirect3DDevice9::DrawIndexedPrimitive进行渲染
设置好了索引然后就可以开始渲染了,通过调用IDirect3DDevice9::DrawIndexedPrimitive来完成,函数的声明如下:
HRESULT DrawIndexedPrimitive( [in] D3DPRIMITIVETYPE Type, [in] INT BaseVertexIndex, [in] UINT MinIndex, [in] UINT NumVertices, [in] UINT StartIndex, [in] UINT PrimitiveCount );
参数说明:
Type | 表示将要绘制的图元类型 |
BaseVertexIndex | 表示将要进行绘制的索引缓存的起始顶点的索引位置 |
MinIndex | 表示索引数组中最小的索引值,通常都设为0 |
NumVertices | 表示顶点的数量 |
StartIndex | 表示从索引中的第几个索引处开始绘制我们的图元 |
PrimitiveCount | 表示要绘制的图元数量 |
调用实例:
pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 2);
(4)实例:矩形渲染
现在进行一个矩形的渲染,只需要将顶点缓存中的顶点去掉两个,然后创建好索引缓存,设置好相关的东西就可以渲染了。修改部分的代码如下:
Vertex rect[] = { {100, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)}, {700, 500, 0, 1.0, D3DCOLOR_XRGB(255, 255, 255)} }; void *temp; IDirect3DDevice9 *pDevice = g_pDevice->GetDevice(); pDevice->CreateVertexBuffer(sizeof(rect), 0, Vertex::FVF, D3DPOOL_MANAGED, &g_pVertexBuffer, NULL); g_pVertexBuffer->Lock(0, sizeof(rect), (void **)&temp, 0); memcpy(temp, rect, sizeof(rect)); g_pVertexBuffer->Unlock(); WORD index[] = {0, 1, 2, 2, 1, 3}; pDevice->CreateIndexBuffer(sizeof(WORD) * 6, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &g_pIndexBuffer, nullptr); g_pIndexBuffer->Lock(0, sizeof(index), (void **)&temp, 0); memcpy(temp, index, sizeof(index)); g_pIndexBuffer->Unlock();
pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(Vertex)); pDevice->SetFVF(Vertex::FVF); pDevice->SetIndices(g_pIndexBuffer); pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 2);
二.背面消隐与着色模式
1.背面消隐
每个多边形都有两个面,一个正面,另一个是背面。通常情况下,多边形的背面是不可见的。在D3D中会将多边形的背面加以剔除,这个就是背面消隐。为了实现背面消隐,D3D必须区分哪些多边形是正面朝向的,哪些多边形是背面朝向的。在D3D中,默认顶点排列顺序为顺时针的三角形是正面朝向的,排列顺序为逆时针的是背面朝向的。可以看见,在前面无论是三角形还是矩形的渲染中,我们三角形的排列顺序都是顺时针的。那是因为我们不想D3D将其剔除,而是正确的显示出来。在D3D你也可以改变背面消隐的模式,通过SetRenderState函数来实现。函数的原型声明如下:
HRESULT SetRenderState( [in] D3DRENDERSTATETYPE State, [in] DWORD Value );
State是一个D3DRENDERSTATETYPE枚举类型的变量,表示要设置的状态类型。而第一个表示要将第一个的状态类型设置为什么,第二个参数的取值范围取决于第一个参数。这里我们要设置背面消隐的模式,第一个就必须设置为D3DRS_CULLMODE。第二个参数这个时候可以取以下几个值:
D3DCULL_NONE | 禁用背面消隐 |
D3DCULL_CW | 对顺时针方向的三角形进行消隐 |
D3DCULL_CCW | 默认值,对逆时针方向的三角形进行消隐 |
例如,我们要对顺时针方向的三角形进行消隐操作,可以进行如下调用:
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
2.着色模式
在上面渲染的三角形中,我们将第一个顶点的颜色设为红色,第二顶点设为了绿色,第三个顶点设为了蓝色,第四个顶点设为了灰色。但是最后运行出来的结果,有点不一样。只有在每个顶点附近的那一块区域和顶点的颜色相似度最高,离顶点越远和该顶点设置的颜色差别就越大,这是为什么呢?这就是因为D3D中着色模式的原因,D3D中默认的着色模式是Gouraud着色。在Gouraud着色模式之下,图元中各个像素的颜色值由各个顶点经过线性插值得到。例如:
上面的图中一端的颜色为红色,另一端的颜色为蓝色。他们之间中点的颜色和3/4处的颜色值就是通过如上图线性插值的方式得到的。所以说我们渲染出来的矩形,在离他们顶点处越近的地方,颜色值就和该顶点设置的颜色值越接近,因为在那个位置该顶点颜色在插值时的权重越大。Gouraud着色模式是D3D中默认的着色模式,D3D中还有另一种着色模式叫平面着色模式。在这种模式下,对于一个图元,它取图元顶点中的第一个顶点的颜色为图元中每个像素的颜色值。例如,下面的代码在平面着色模式下,三角形的颜色为红色,因为第一个顶点的颜色为红色。
Vertex rect[] = { {100, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)} };
着色模式也可以通过SetRenderState函数来改变,第一个参数为D3DRS_SHADEMODE,第二个参数为可以取:
D3DSHADE_FLAT | 平面着色模式 |
D3DSHADE_GOURAUD | Gouraud着色模式 |
例如下面的代码,可以将其设置为平面着色模式:
pDevice->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FLAT);
我们可以再上面的矩形渲染截图中,将着色模式设置为平面着色模式,然后看下效果。附上运行截图:
可以看到矩形变成了一半红色,一半蓝色。那是因为第一个三角形的第一个顶点颜色设置为红色,第二个三角形的第一个顶点颜色设置为蓝色。
(完)