10. Mesh Part One
本文译自《Introduction to 3D Game Programming with DirectX 9.0》第十章“Mesh Part One”,敬请斧正。
在D3DX中,有很多函数都使用了ID3DXMesh接口,如D3DXCreate*之类。ID3DXMesh接口的主要功能继承自ID3DXBaseMesh父类接口,还有其他的Mesh接口也是从ID3DXBaseMesh接口继承的,如ID3DXPMesh接口,这个接口用于Progressive Mesh,把它翻译成“渐进Mesh”,不知是否合适。
本节要达到的目标:
l 学习ID3DXMesh对象的内部数据组织
l 学习创建一个ID3DXMesh对象
l 学习优化ID3DXMesh
l 学习渲染ID3DXMesh
10.1. 几何结构信息
接口ID3DXBaseMesh具有顶点缓冲和顶点索引缓冲,分别用于存储Mesh的顶点数据和顶点的索引数据,二者结合在一起才能够渲染出组成Mesh的三角形。使用下面的两个方法可得到指向两个缓冲区的指针:
HRESULT ID3DXMesh::GetVertexBuffer(LPDIRECT3DVERTEXBUFFER9* ppVB);
HRESULT ID3DXMesh::GetIndexBuffer(LPDIRECT3DINDEXBUFFER9* ppIB);
下面是一个关于上述两个方法用法的例子:
IDirect3DVertexBuffer9* pVB=NULL;
Mesh->GetVertexBuffer(&pVB);
IDirect3DIndexBuffer9* pIB=NULL;
Mesh->GetIndexBuffer(&pIB);
另外,如果想修改顶点缓冲和顶点索引缓冲,需要先使用下面的两个方法加锁:
HRESULT ID3DXMesh::LockVertexBuffer(DWORD Flags,LPVOID* ppData);
HRESULT ID3DXMesh::LockIndexBuffer(DWORD Flags,LPVOID* ppData);
参数Flags用于说明加锁的方式,参数ppData返回被锁定的内存的地址。记住,如果加锁成功还需要调用与之配对的解锁函数:
HRESULT ID3DXMesh::UnlockVertexBuffer();
HRESULT ID3DXMesh::UnlockIndexBuffer();
下面是另外一些与Mesh的几何结构有关的ID3DXMesh接口的方法:
l DWORD GetFVF(); --返回顶点的格式
l DWORD GetNumVertices(); --返回顶点缓冲中的顶点数
l DWORD GetNumBytesPerVertex(); --返回一个顶点所占的字节数
l DWORD GetNumFaces(); --返回Mesh的面数,也就是三角形数
10.2. 子集和属性缓冲
一个Mesh由数个子集组成。子集是Mesh中的一组使用相同属性渲染的三角形。这里的属性指的是材质、纹理、渲染状态。每一个子集用一个唯一的非负整数表示其ID,如0,1,2,3等。
Mesh中的每一个三角形都与一个属性ID相关联,表示该三角形属于该子集。例如,在一个表示房子的Mesh中,组成地板的三角形具有属性ID 0,这就表示这些三角形属于子集0;同样的,组成墙的三角形的属性ID为1,他们属于子集1。
三角形的属性ID存储在Mesh的属性缓冲中,这是一个DWORD数组。因为每个面对应属性缓冲中的一项,所以属性缓冲中的项目数等于Mesh中的面的个数。属性缓冲中的项目和索引缓冲定义的三角形一一对应;也就是说,属性缓冲的第I项和索引缓冲中定义的第I个三角形相对应。三角形I有下面三个索引缓冲中的索引项定义:
A=I*3
B=I*3 + 1
C=I*3 + 2
可以使用下面的方法访问属性缓冲:
DWORD* buffer=NULL;
Mesh->LockAttributeBuffer(lockingFlags,&buffer);
// do something...
Mesh->UnlockAttributeBuffer();
10.3. 渲染
接口ID3DXMesh提供了DrawSubset(DWORD AttribID)方法渲染参数AttribID指示的子集中的各个三角形。例如,如果渲染子集0中的所有三角形,可以使用如下方法:
Mesh->DrawSubset(0);
如果要渲染整个Mesh,需要分别渲染Mesh的各个子集。因为子集序列与Mesh使用的材质、纹理的序列相对应,即子集I和材质、纹理数组的第I项对应,所以可以使用一个简单的循环渲染Mesh:
for (int i=0;i<numSubsets;i++)
{
Device->SetMaterial(mtrls[i]);
Device->SetTexture(0,textures[i]);
Mesh->DrawSubset(i);
}
10.4. 优化
为了更加有效的渲染Mesh,可以重新组织其中的顶点和索引,也就是优化Mesh。可以使用如下方法进行优化:
HRESULT ID3DXMesh::OptimizeInplace(
DWORD Flags,
CONST DWORD *pAdjacencyIn,
DWORD *pAdjacencyOut,
DWORD *pFaceRemap,
LPD3DXBUFFER *ppVertexRemap
);
l Flags –优化选项,告诉该方法执行什么类型的优化。可以区下面的一个或几个值:
n D3DXMESHOPT_COMPACT –删除没有用的顶点和索引项
n D3DXMESHOPT_ATTRSORT –根据属性给三角形排序并调整属性表,这将使DrawSubset方法更有效的执行
n D3DXMESHOPT_VERTEXCACHE –增加顶点缓冲的命中率
n D3DXMESHOPT_STRIPREORDER –重组顶点索引使三角形条带(Triangle Strip)尽量长
n D3DXMESHOPT_IGNOREVERTS –只优化索引,忽略顶点
l pAdijacencyIn –没有优化的Mesh的邻接数组
l pAdjacencyOut –输出优化的Mesh的邻接信息的数组。这个DWORD数组必须有ID3DXMesh::GetNumFaces() * 3个元素。如果不需要该信息,可以传递NULL。
l pFaceRemap –一个DWORD数组,用于接收面重影射信息。这个数组应不小于ID3DXMesh::GetNumFaces()。当Mesh被优化时,由索引缓冲定义的面可能被移动,也就是说,如果pFaceRemap的第I项表示第I个原始面被移到的面索引值。如果不需要该信息,可以使用NULL。
l ppVertexRemap –指向ID3DXBuffer的指针的地址,返回顶点重影射信息。该缓冲区应包含ID3DXMesh::GetNumVertices()个顶点。当Mesh被优化时,顶点可能被移动,该重影射信息用于说明原来的顶点被移动到新位置,也就是说,ppVertexRemap的第I项指示原来的第I个顶点的新位置。如果不需要该信息,可以使用NULL。
// Get the adjacency info of the non-optimized mesh.
DWORD adjacencyInfo[Mesh->GetNumFaces() * 3];
Mesh->GenerateAdjacency(0.0f, adjacencyInfo);
// Array to hold optimized adjacency info.
DWORD optimizedAdjacencyInfo[Mesh->GetNumFaces() * 3];
Mesh->OptimizeInplace(
D3DXMESHOPT_ATTRSORT |
D3DXMESHOPT_COMPACT |
D3DXMESHOPT_VERTEXCACHE,
adjacencyInfo,
optimizedAdjacencyInfo,
0,
0);
另一个相似的方法是Optimize(),它输出一个优化的Mesh,而不是在原来Mesh的基础上进行优化:
HRESULT ID3DXMesh::Optimize(
DWORD Flags,
CONST DWORD *pAdjacencyIn,
DWORD *pAdjacencyOut,
DWORD *pFaceRemap,
LPD3DXBUFFER *ppVertexRemap,
LPD3DXMESH *ppOptMesh
);
10.5. 属性表
如果一个Mesh使用D3DXMESHOPT_ATTRSORT标志进行优化,Mesh的结构信息将按属性进行排序,这样各个子集的顶点/顶点索引将组成连续的块。
除了进行几何信息的排序外,D3DXMESHOPT_ATTRSORT优化选项还将创建一个属性表。该表是D3DXATTRIBUTERANGE结构的一个数组,其中的每一项对应Mesh的一个子集并指示顶点/顶点索引的一个连续块,这个子集的几何信息就包含在这个块里。结构D3DXATTRIBUTERANGE的定义如下:
typedef struct _D3DXATTRIBUTERANGE
{
DWORD AttribId;
DWORD FaceStart;
DWORD FaceCount;
DWORD VertexStart;
DWORD VertexCount;
} D3DXATTRIBUTERANGE;
l AttribId –子集的ID
l FaceStart –该子集的面的起始值,FaceStart*3就是起始三角形在索引缓冲的索引偏移
l FaceCount –子集中的面数,也就是三角形数
l VertexStart –该子集的起始顶点在顶点缓冲中的偏移
l VertexCount –该子集包含的定点数
很容易看出该结构与上面图中表示的信息之间的联系。上图中Mesh的属性表中的每一项对应一个子集。
建立属性表后,渲染一个子集就很容易了,仅仅查一下属性表找出该自己的几何信息。如果没有属性表,每渲染一个子集就需要对属性缓冲区进行一次线性搜索来找出该子集包含的几何信息。
可以使用如下方法访问Mesh的属性表:
HRESULT ID3DXMesh::GetAttributeTable(
D3DXATTRIBUTERANGE *pAttribTable,
DWORD *pAttribTableSize
);
该方法可以完成两个功能:可以返回属性表的属性数,也可以将返回完整的属性表。
要得到属性表的元素个数,可以给第一个参数传NULL,如:
DWORD numSubsets = 0;
Mesh->GetAttributeTable(0, &numSubsets);
然后,就可以取得属性表了:
D3DXATTRIBUTERANGE table = new D3DXATTRIBUTERANGE [numSubsets];
Mesh->GetAttributeTable( table, &numSubsets );
还可以使用ID3DXMesh::SetAttributeTable方法直接修改属性表。
D3DXATTRIBUTERANGE attributeTable[12];
// ...fill attributeTable array with data
Mesh->SetAttributeTable( attributeTable, 12);
10.6. 邻接信息
对于Mesh的某些操作,如优化,需要知道三角形间的邻接信息,而Mesh的邻接数组就存储这样的信息。
邻接数组是DWORD类型数组,其中的每一项对应Mesh中的一个三角形。例如,邻接数组的第I项对应的三角形有以下三个索引值定义:
A=I*3
B=I*3 + 1
C=I*3 +2
这里,使用ULONG_MAX=4294967295表示该边没有邻接三角形。其实,这个数就是-1。
由于每个三角形有三条边,所以,他有三个邻接三角形。
因此,每个三角形可能有三个邻接三角形,邻接数组必须有(ID3DXMesh::GetNumFaces() * 3)个元素。
在D3DX中,有很多函数可以输出Mesh的邻接信息,如:
HRESULT ID3DXMesh::GenerateAdjacency(
FLOAT fEpsilon,
DWORD* pAdjacency
);
l fEpsilon –指示当两点距离有多近时,可以认为是一个点。当两点间的距离小于epsilon时,可认为他们是同一个点
l pAdjacency –用于存储邻接信息的邻接数组
例如:
DWORD adjacencyInfo[Mesh->GetNumFaces() * 3];
Mesh->GenerateAdjacency(0.001f, adjacencyInfo);
10.7. 克隆
有时,需要将Mesh的数据另外复制一份,可以使用ID3DXMesh::CloneMeshFVF方法:
HRESULT ID3DXMesh::CloneMeshFVF(
DWORD Options,
DWORD FVF,
LPDIRECT3DDEVICE9 pDevice,
LPD3DXMESH *ppCloneMesh
);
l Options –创建Mesh的标志。完整信息可参考SDK文档。下面的几个很常用:
n D3DXMESH_32BIT –使用32位顶点索引
n D3DXMESH_MANAGED –Mesh数据将被放在受控的内存缓冲池中
n D3DXMESH_WRITEONLY –Mesh数据只能执行写操作,不能执行读操作
n D3DXMESH_DYNAMIC –Mesh缓冲将是动态的
l FVF –创建新Mesh的灵活定点格式
l pDevice –与克隆Mesh相关联的D3D设备
l ppCloneMesh –输出新Mesh
这个方法允许指定与原Mesh不同的Options和FVF。例如,现在有一个定点格式为D3DXFVF_XYZ的Mesh,我们想复制一个顶点格式为D3DXFVF_XYZ|D3DXFVF_NORMAL的Mesh,可以这样做:
// assume _mesh and device are valid
ID3DXMesh* clone = 0;
Mesh->CloneMeshFVF(
Mesh->GetOptions(), // use same options as source mesh
D3DFVF_XYZ | D3DFVF_NORMAL,// specify clones FVF
Device,
&clone);
10.8. 创建Mesh(D3DXCreateMeshFVF)
我们可以使用D3DXCreateMeshFVF函数创建一个空的Mesh对象。所谓空,是指我们已经指定了顶点数和面数(也就是三角形数),函数D3DXCreateMeshFVF也分配了适当大小的内存给顶点、定点索引、属性缓冲区。有了这些缓冲区后,就可以手动填写上下文数据了(需要分别向定点缓冲区、索引缓冲区、属性缓冲区提供定点、索引、属性数据)。
HRESULT WINAPI D3DXCreateMeshFVF(
DWORD NumFaces,
DWORD NumVertices,
DWORD Options,
DWORD FVF,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXMESH *ppMesh
);
l NumFaces –Mesh中的三角形数,必须指定一个大于0的数
l NumVertices –定点数,也必须是一个大于0的数
l Options –创建Mesh的选项标志,常用的标志如下:
n D3DXMESH_32BIT –Mesh使用32位顶点索引
n D3DXMESH_MANAGED –使用受控内存池
n D3DXMESH_WRITEONLY –Mesh的数据只能被写,不能被读
n D3DXMESH_DYNAMIC –使用动态缓冲
l FVF –Mesh的顶点格式
l pD3DDevice –与Mesh相关联的D3D设备
l ppMesh –输出的Mesh指针
在下一节,将举例说明该函数的用法,到时将手动填充Mesh对象的数据。
另外,还可以使用D3DXCreateMesh函数创建空的Mesh对象。其原形如下:
HRESULT WINAPI D3DXCreateMesh(
DWORD NumFaces,
DWORD NumVertices,
DWORD Options,
const LPD3DVERTEXELEMENT9 *pDeclaration,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXMESH *ppMesh
);
其中的参数的含义就不需要再解释了,他们与D3DXCreateMeshFVF的参数相似。但是,第四个参数是D3DVERTEXELEMENT9的数组,而不是先前的FVF。
10.9. 应用举例:创建并渲染Mesh
我们手动创建一个立方体,它将使用本章讨论的大部分知识点:
l 创建一个空的Mesh
l 向Mesh中填写立方体的几何信息
l 划分子集
l 生成Mesh的邻接信息
l 优化
l 渲染
这里,给出代码的框架,这个例子使用D3DXCreateMeshFVF函数创建Mesh对象。
在代码中,定义如下的全局变量:
ID3DXMesh* Mesh = 0;
const DWORD NumSubsets = 3;
IDirect3DTexture9* Textures[3] = {0, 0, 0};// texture for each subset
第一个是Mesh对象的指针;Mesh对象中有三个子集。每个子集用不同的纹理渲染,数组Textures中存储每个子集的纹理。
主要的代码在Setup函数中,他首先创建一个空的Mesh对象:
bool Setup()
{
HRESULT hr = 0;
hr = D3DXCreateMeshFVF(
12,
24,
D3DXMESH_MANAGED,
Vertex::FVF,
Device,
&Mesh);
这里,创建的Mesh具有12个面(三角形),24个顶点,使用他们来描述立方体。
现在,Mesh对象还是空的,需要为其填充适当的顶点/索引数据。
// Fill in vertices of a box
Vertex* v = 0;
Mesh->LockVertexBuffer(0, (void**)&v);
// fill in the front face vertex data
v[0] = Vertex(-1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
v[1] = Vertex(-1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f);
.
.
.
v[22] = Vertex( 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
v[23] = Vertex( 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f);
Mesh->UnlockVertexBuffer();
// Define the triangles of the box
WORD* i = 0;
Mesh->LockIndexBuffer(0, (void**)&i);
// fill in the front face index data
i[0] = 0; i[1] = 1; i[2] = 2;
i[3] = 0; i[4] = 2; i[5] = 3;
.
.
.
// fill in the right face index data
i[30] = 20; i[31] = 21; i[32] = 22;
i[33] = 20; i[34] = 22; i[35] = 23;
Mesh->UnlockIndexBuffer();
立方体的几何信息输入完毕后,还需要为各个三角形划分子集。可以通过修改Mesh对象的属性缓冲区,指定每个三角形所属的子集。在这个例子中,由索引缓冲定义的12个三角形中,前四个属于子集0,再下面四个属于子集1,最后四个属于子集2。
DWORD* attributeBuffer = 0;
Mesh->LockAttributeBuffer(0, &attributeBuffer);
for(int a = 0; a < 4; a++) // triangles 1-4
attributeBuffer[a] = 0; // subset 0
for(int b = 4; b < 8; b++) // triangles 5-8
attributeBuffer[b] = 1; // subset 1
for(int c = 8; c < 12; c++) // triangles 9-12
attributeBuffer[c] = 2; // subset 2
Mesh->UnlockAttributeBuffer();
到这里,这个Mesh对象已经创建完毕了,可以渲染了。但是,它还没有经过优化。当然,对于这个微不足道的立方体,优化得不到任何好处,但是,我们还是用它演示ID3DXMesh接口方法的用法。在优化之前,需要计算Mesh对象的邻接信息:
std::vector<DWORD> adjacencyBuffer(Mesh->GetNumFaces() * 3);
Mesh->GenerateAdjacency(0.0f, &adjacencyBuffer[0]);
然后,执行优化:
hr = Mesh->OptimizeInplace(
D3DXMESHOPT_ATTRSORT|D3DXMESHOPT_COMPACT|D3DXMESHOPT_VERTEXCACHE,
&adjacencyBuffer[0],
0, 0, 0);
到这里,创建Mesh对象的工作已经完成了。最后,如果一切顺利,函数Setup应该返回true:
return true;
} // end Setup()
上面说过,经过优化,Mesh对象将建立属性表。使用下面的代码验证一下。
// number of entries in the attribute table
DWORD numEntries = 0;
mesh->GetAttributeTable(0, &numEntries);
vector<D3DXATTRIBUTERANGE> table(numEntries);
mesh->GetAttributeTable(&table[0], &numEntries);
for(int i = 0; i < numEntries; i++)
{
cout << "Entry " << i << endl;
cout << "------" << endl;
cout << "Subset ID: " << table[i].AttribId << endl;
cout << "Face Start: " << table[i].FaceStart << endl;
cout << "Face Count: " << table[i].FaceCount << endl;
cout << "Vertex Start: " << table[i].VertexStart << endl;
cout << "Vertex Count: " << table[i].VertexCount << endl;
cout <<endl;
}
下面是部分输出信息:
Entry 0
------------
Subset ID: 0
Face Start: 0
Face Count: 4
Vertex Start: 0
Vertex Count: 8
Entry 1
------------
Subset ID: 1
Face Start: 4
Face Count: 4
Vertex Start: 8
Vertex Count: 8
Entry 2
------------
Subset ID: 2
Face Start: 8
Face Count: 4
Vertex Start: 16
Vertex Count: 8
与前面的设计相符。最后,渲染Mesh对象:
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,0x00000000, 1.0f, 0);
Device->BeginScene();
for(int i = 0; i < NumSubsets; i++)
{
Device->SetTexture( 0, Textures[i] );
Mesh->DrawSubset( i );
}
Device->EndScene();
Device->Present(0, 0, 0, 0);
10.10. 总结
l Mesh对象包含顶点、索引、属性缓冲区。顶点和索引缓冲区包含Mesh对象的几何信息。属性缓冲区的每一项对应一个Mesh对象的一个三角形,指示该三角形所属的子集。
l Mesh对象可以使用OptimizeInplace和Optimize方法优化。为了更加有效的渲染,优化将重组Mesh的几何信息。使用D3DXMESHOPT_ATTRSORT标志进行优化,会产生属性表,据此,访问一次属性表,就可以完整的渲染一个子集。
l Mesh对象的邻接信息是一个DWORD数组,每个三角形有三个邻接项,代表与该三角形相邻接的三角形。
l 可以使用D3DXCreateMeshFVF函数创建空的Mesh对象。然后使用相应的Lock*方法填写适当的数据组成Mesh的几何信息和属性信息。