网格(一)闲话ID3DXMesh接口
关于网格部分,因为非常重要,故分为几个部分来学习。网格(一)主要讲解一下网格之中使用最多的接口ID3DXMesh。在这部分关于网格的学习完成之后,最后会详细学习综合的示例。
在Direct3D渲染绘制过程中,网格是一个非常重要的概念。游戏中,我们需要将现实生活中各种物体,如一个足球,一个台灯在电脑上显示出其3D效果。通常我们的做法如下:
(1)先把该物体放在一个虚拟的三维坐标系中,该坐标称为局部坐标系(Local Space),一般以物体的中心作为坐标原点,采用左手坐标系。
(2) 然后,对坐标系中的物体进行点采样(Point Sample), 这些采样点按一定顺序连接成为一系列的小平面(三角形或共面的四边形,五边形等),这些小平面称为图元(Primitive),3D引擎会处理每一个图元,称为一个独立的渲染单位。这样取样后的物体看起来像是由许许多多的三角形,四边形或五边形组成的,就像网一样,我们称为一个网格(Mesh).
这个采样过程又可称为物体的3D建模。当然现在都有功能非常强大的3D建模工具,例如,3D Max, 3D Cool等建模工具,省去了我们这方面的许多工作。
(3) 我们纪录这些顶点数据和连线情况到一个文件中,3D引擎读取这些数据,依次渲染每一个图元,就能在显示屏幕上再现物体。当然了,取样的点越多,再现的物体也会越逼真,要处理的数据量也越大。
在D3D中,纪录这些顶点数据和连线情况的文件称为X文件(X File)。它是以X作为文件名后缀的。这有点类似于2D中的位图文件。
在具体的实现代码中,我们通常用一个叫做ID3DXMesh的接口(我们可以把它当做一个类来使用)来实现。但是,这里要说明的是,并不只有这个进口可以操作网格,Microsoft开发了很多操作网格的接口,但是ID3DXMesh用的最多。在网格(一)中,我们主要了解下ID3DXMsh接口的成员数据及方法概述。
1. 几何信息
在ID3DXMesh接口中,必然有用来描述网格数据的顶点缓存和索引缓存。相应地,有GetVertexBuffer和GetIndexBuffer方法用来获取指向这些缓存的指针。如下:
HRESULT ID3DMesh::GetVertexBuffer(LPDIRECT3DVERTEXBUFFER9* ppVB);
HRESULT ID3DMesh::GetIndexBuffer(LPDIRECT3DVERTEXBUFFER9* ppIB);
其中,参数ppVB和ppIB是输出的指向缓存的指针的地址。ID3DXMesh还有其他一些方法用于获取相关的几何信息,如下:
DWORD GetFVF(); // 返回描述顶点格式的DWORD类型的值
DWORD GetNumVertices(); // 返回顶点缓存中顶点的个数
DWORD GetNumBytesPerVertex(); // 返回每个顶点所占的字节数
DWORD GetNumFaces(); // 返回网格中面片的个数
这里需要声明下,ID3DXMesh接口只支持三角形图元。不过,可以放心的是,现在绝大部分都用的是三角形图元。
2. 子集和属性缓存
在游戏绘制过程中,我们很可能有很多对象所用到一些属性(材质、颜色、纹理等)是一样的。Direct3D为了节省资源,弄出了一个子集的概念。子集(subset)就是网格中一组可用相同属性进行绘制的三角形单元(注意:ID3DXMesh接口只支持三角形单元)。如,你想在屏幕上画出一个房子,房子的地板、窗户、墙、屋顶等就可以分别作为一个子集,用相同属性来绘制。
当然,有了子集,就必须有子集ID用以区分它的兄弟们,一般用DWORD类型的非负整数标记。
同样地,一个网格中可以有成千上万的三角形图元,每个图元用到的属性可能不一样(也就是他们所归属的子集不一样),这就需要为每个三角形图元分配一个属性ID,用以指定他们绘制时所用到的属性。这些属性ID存储在一个唤作属性缓存(attribute buffer)中。由于每个面片(face,即三角形单元)在属性缓存都有对应项,因此,属性缓存的元素个数即为三角形图元的个数,即为面片的个数。
说明一下,属性缓存中的项与索引缓存(描述绘制对象时三角形图元的顺序)中定义的三角形图元是一一对应的,即属性缓存中第i个元素对应于索引缓存中第i元素(即该第i个三角形图元用属性缓存中第i个元素对应属性来绘制)。
3. 绘制
铺垫了这么多,就是为了在屏幕上画出我们想要绘制的对象(当然,要3D效果~)。ID3DXMesh接口提供了方法DrawSubset(DWORD Attribld)用于绘制由参数Attribld指定的子集(具有相同绘制属性的三角形图元)中的三角形图元。如下:
//
// Mesh是已经定义了的ID3DXMesh接口对象
//
Mesh->DrawSubset(0); // 绘制子集0中的所有三角形单元
//
// 绘制整个网格,利用for循环
for(int i = 0; i < numSubsets; i++)
{
Device->SetMaterial(mtrls[i]);
Device->SetTexture(0,textures[i]);
Mesh->DrawSubset(i);
}
4. 邻接信息和网格优化
我们都已经知道,顶点缓存中存储的是顶点数组,索引缓存定义存储的是用以绘制的三角形图元并制定了绘制顺序。但是我们所想的绘制顺序对于计算机可能并不是最优的,这里就需要一个对顶点缓存和索引缓存进行一个重组以使得其更加优化,这个过程称为网格优化(Optimizing)。但是,计算机不是神,需要我们给出各个三角形图元的邻接信息(Adjacency Information)才能够使得三角形图元的绘制得到优化。
邻接信息,通俗地讲,就是对于任意一个三角形单元,描述了哪些面片与之相邻,这些相邻的信息都存储在一个DWORD类型的叫做邻接数组(adjacency array)的结构中。很显然,每一个三角形图元最多只会有三个图元与之相邻,所以邻接数组的维数必须等于GetNumFaces()*3。另外不要注意的是,如果邻接数组中某一项等于ULONG_MAX=4294 967 295,则表明网格中某一特定边没有邻接面片,我们可以讲该项赋值为-1表示此种情形。可以用下面的方法获取邻接信息,如下:
HRESULT ID3DXMesh::GenerateAdjacency(
FLOAT fEpsilon, // 如果两点间距离小于epsilon值,则认为该两点是同一个点
DWORD* pAdjacency // 【out】指向一个DWORD类型的数组的指针,存储了邻接信息
);
了解完邻接信息之后,我们来看下如何进行网格优化。很简单,只需要调用如下方法即可:
HRESULT ID3DXMesh::OptimizeInPlace(
DWORD Flags, // 优化选项标记
CONST DWORD* pAdjacencyIn, // 指向未经优化的网格的邻接数组的指针
DWORD* pAdjacencyOut, // 指向一个DWORD类型数组的指针,该数组填充了经优化后的网格的邻接信息
DWORD* pFaceRemap, // 指向一个DWORD类型数组的指针,填充了网格面片的重绘信息
LPD3DXBUFFER* ppVertexRemap // 指向ID3DXMesh对象指针的地址,保存了顶点的重绘信息
);
5. 创建网格
在前面讲过使用D3DXCreate*系列函数创建网格(如创建茶壶、球等),这些茶壶、球等的信息均是Direct3D内部自动提供的。但是我们想要创建一个属于自己的对象的网格的时候(如一个奥特曼~),就需要我们自己提供关于对象的一些信息。当然这些信息就是在顶点缓存、索引缓存和属性缓存中描述的。
Direct3D提供了方法D3DXCreateMeshFVF用以创建一个空的网格对象。当然,这就需要我们在相应的缓存中填入我们想要填入的信息。该函数原型如下:
HRESULT WINAPI D3DXCreateMeshFVF(
DWORD NumFaces, // 网格所具有的面片个数
DWORD NumVertices, // 网格所具有的顶点个数
DWORD Options, // 创建网格时所使用的创建标记
DWORD FVF, // 顶点的顶点灵活格式
LPDIRECT3DDEVICE9 pDevice, // 与该网格相关的设备指针
LPD3DXMESH *ppMesh // 所创建的网格对象的指针
);
另外,我们还可以用函数D3DXCreateMesh函数来创建空网格。
本章完。
————Josh 2012年11月30日