本章介绍D3DX库提供的与Mesh有关的接口、结构、函数。通过本章的学习,将能够加载复杂的3D模型,能够控制Mesh对象的精细程度。本章要达到的目标:
l 学习加载.x文件
l 理解使用渐进Mesh(Progressive Mesh)的好处和学习如何使用渐进Mesh接口ID3DXPMesh。将原文中的Progressive Mesh翻译为渐进网格,不知是否恰当
l 学习边界范围(Bounding Volume),以及如何使用D3DX函数创建边界范围
这个接口贯穿整个D3DX库,需要对该接口有大体上的认识。ID3DXBuffer是D3DX用来管理连续内存块的结构,他只有两个方法:
l LPVOID GetBufferPointer(); --返回数据块的首地址
l DWORD GetBufferSize(); --返回缓冲区的大小,以字节为单位
例如,D3DXLoadMeshFromX函数就使用ID3DXBuffer返回Mesh对象的邻接信息。因邻接信息是DWORD数组,所以需要进行类型转换。如:
DWORD* info =(DWORD*)adjacencyInfo->GetBufferPointer(); D3DXMATERIAL* mtrls = (D3DXMATERIAL*)mtrlBuffer->GetBufferPointer(); |
又因为ID3DXBuffer是一个COM对象,所以,用完后,需要进行释放。
adjacencyInfo->Release(); mtrlBuffer->Release(); |
也可以使用如下函数创建一个空的ID3DXBuffer对象:
HRESULT WINAPI D3DXCreateBuffer( DWORD NumBytes, LPD3DXBUFFER *ppBuffer ); |
其中参数的含义显而易见。例如,创建一个包含四个整型数的缓冲区:
ID3DXBuffer* buffer = 0; D3DXCreateBuffer( 4 * sizeof(int), &buffer ); |
使用D3DXCreate*函数,可以创建一些简单的几何体,如球、圆柱、立方体等。如果想通过手动设定顶点的方式创建较复杂的3D对象,你会发现这太麻烦了,简直无法做到!现在,可以使用很多种3D建模工具软件来完成这项枯燥工作,如3DS MAX,LightWave 3D,Maya等。使用这样的建模工具,可以在可视化的、交互的环境中设计复杂、逼真的模型,而且还有丰富的工具可用,使整个建模过程相当简单。这里的简单是相对于在“程序中手动设定顶点的方式建模”,实际上,这些建模工具还是相当复杂的,想得心应手的使用,可不是一朝一夕之功。
这些建模工具可以将所建立的模型的数据(几何信息,材质,动画等)保存到文件。我们需要从文件中分析提取需要的数据,然后应用到自己的3D程序中。有一种常用的文件格式,XFile,其扩展名为.x,较为简单,是Direct3D定义的文件格式,D3DX库提供了完整的支持,可满足一般的需要。
使用下面的函数加载存储在.x文件中的Mesh数据。它创建一个ID3DXMesh对象,然后从.x文件中读取Mesh的几何信息。
HRESULT WINAPI D3DXLoadMeshFromX( LPCTSTR pFilename, DWORD Options, LPDIRECT3DDEVICE9 pD3DDevice, LPD3DXBUFFER *ppAdjacency, LPD3DXBUFFER *ppMaterials, LPD3DXBUFFER *ppEffectInstances, DWORD *pNumMaterials, LPD3DXMESH *ppMesh ); |
l pFileName –.x文件的文件名
l Options –创建Mesh的标志。详情可参考SDK文档中的D3DXMESH枚举类型。常用的几个标志如下:
n D3DXMESH_32BIT –使用32位的顶点索引,默认为16位
n D3DXMESH_MANAGED –使用受控的内存缓冲池
n D3DXMESH_WRITEONLY –缓冲区只可执行写操作
n D3DXMESH_DYNAMIC –使用动态内存缓冲池
l pD3DDevice –D3D设备指针
l ppAdjacency –使用ID3DXBuffer返回Mesh的邻接信息,这是一个DWORD数组
l ppMaterials –使用ID3DXBuffer返回Mesh的材质数据,这是一个D3DXMATERIAL类型数组
l ppEffectInstances –使用ID3DXBuffer返回一个D3DXEFFECTINSTANCE结构数组
l pNumMaterials –返回Mesh对象的材质数量,也就是通过ppMaterials返回的D3DXMATERIAL数组的元素数
l ppMesh –返回ID3DXMesh对象
函数D3DXLoadMeshFromX的第七个参数返回Mesh对象的材质数量,第五个参数是D3DXMATERIAL的数组,包含Mesh的材质数据。D3DXMATERIAL结构的定义如下:
typedef struct D3DXMATERIAL { D3DMATERIAL9 MatD3D; LPSTR pTextureFilename; } D3DXMATERIAL; |
这个结构很简单,包含一个D3DMATERIAL9结构和一个以0字符结束的字符串的指针,表示相关联的纹理文件。. x文件并不包含纹理数据,只包含纹理文件的文件名。使用该函数加载.x文件后,还需要根据纹理文件的文件名手动加载纹理。
函数D3DXLoadMeshFromX返回的D3DXMATERIAL数组正好与Mesh对象的子集相对应。也就是说,第I个子集的材质纹理信息就存储在ppMaterials[I]中。
这个例子相当的简单,它加载bigship1.x文件,这是DirectX SDK中的一个文件。这里只列出代码的主要框架。
ID3DXMesh* Mesh = 0; vector<D3DMATERIAL9> Mtrls(0); vector<IDirect3DTexture9*> Textures(0);
bool Setup() { HRESULT hr = 0; // // Load the XFile data. // ID3DXBuffer* adjBuffer = 0; ID3DXBuffer* mtrlBuffer = 0; DWORD numMtrls = 0; hr = D3DXLoadMeshFromX( "bigship1.x", D3DXMESH_MANAGED, Device, &adjBuffer, &mtrlBuffer, 0, &numMtrls, &Mesh); if(FAILED(hr)) { ::MessageBox(0, "D3DXLoadMeshFromX() - FAILED", 0, 0); return false; } // // Extract the materials, load textures. // if( mtrlBuffer != 0 && numMtrls != 0 ) { D3DXMATERIAL* mtrls=(D3DXMATERIAL*)mtrlBuffer->GetBufferPointer(); for(int i = 0; i < numMtrls; i++) { // the MatD3D property doesn't have an ambient value // set when it’s loaded, so set it now: mtrls[i].MatD3D.Ambient = mtrls[i].MatD3D.Diffuse; // save the ith material Mtrls.push_back( mtrls[i].MatD3D ); // check if the ith material has an associative // texture if( mtrls[i].pTextureFilename != 0 ) { // yes, load the texture for the ith subset IDirect3DTexture9* tex = 0; D3DXCreateTextureFromFile( Device, mtrls[i].pTextureFilename, &tex); // save the loaded texture Textures.push_back( tex ); } else { // no texture for the ith subset Textures.push_back( 0 ); } } } Release<ID3DXBuffer*>(mtrlBuffer); // done w/ buffer . . // Snipped irrelevant code to this chapter (e.g., setting up lights, . // view and projection matrices, etc.) . return true; } |
最后,渲染Mesh对象:
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,0xffffffff, 1.0f, 0); Device->BeginScene(); for(int i = 0; i < Mtrls.size(); i++) { Device->SetMaterial( &Mtrls[i] ); Device->SetTexture(0, Textures[i]); Mesh->DrawSubset(i); } Device->EndScene(); Device->Present(0, 0, 0, 0); |
有时.x文件不包含顶点的法向量,这时,如果使用光照,则需要手动计算顶点的法向量。对于接口ID3DXMesh和其父接口ID3DXBaseMesh,可以使用如下函数计算顶点的法向量:
HRESULT WINAPI D3DXComputeNormals( LPD3DXBASEMESH pMesh, const DWORD *pAdjacency ); |
该函数将使用法向量的平均值作为顶点的法向量。如果提供了Mesh对象的邻接信息,则重复的顶点会被忽略;如果没有邻接信息,重复的顶点也会被重复计算。另外一点更加重要,需要计算法向量的Mesh对象的顶点格式必须包含D3DFVF_NORMAL标志。
如果.x文件中没有法向量数据,通过D3DXLoadMeshFromX函数创建的ID3DXMesh对象的顶点格式就不包含D3DFVF_NORMAL标志。因此,在计算法向量之前,必须使用D3DFVF_NORMAL标志复制Mesh对象。
// does the mesh have a D3DFVF_NORMAL in its vertex format? if ( !(pMesh->GetFVF() & D3DFVF_NORMAL) ) { // no, so clone a new mesh and add D3DFVF_NORMAL to its format: ID3DXMesh* pTempMesh = 0; pMesh->CloneMeshFVF( D3DXMESH_MANAGED, pMesh->GetFVF() | D3DFVF_NORMAL, // add it here Device, &pTempMesh ); // compute the normals: D3DXComputeNormals( pTempMesh, 0 ); pMesh->Release(); // get rid of the old mesh pMesh = pTempMesh; // save the new mesh with normals } |
渐进Mesh是ID3DXPMesh接口的对象,可以简化边缩减转换(Edge Collapse Transformations (ECT))。每次ECT都回减少一个顶点和一两个面。由于ECT过程是可逆的(他的逆过程叫顶点分裂),所以,可以通过逆过程将Mesh恢复到原始状态。当然,我们也无法得到比原始状态更精细的Mesh对象,最多只能将其恢复到原始状态。
Progressive Mesh和纹理中的mipmap十分相似。在较小的和远距离的对象上使用高分辨率的纹理纯粹是浪费,因为纹理的细节根本就表现不出来。对于Mesh对象也是一样,较小的距离较远的Mesh不需要太多的三角形,多了纯粹是浪费。所以,在渲染时,实在没有必要在这些根本表现不出来的地方浪费时间。
一种方法是,根据Mesh对象距离视点的距离调整其精细水准(LOD,Level Of Detail)。当距离增加时,可降低LOD;反之,则增加LOD。
这里只讨论ID3DXPMesh接口的用法,不讨论其实现细节。如果你感兴趣,可参考其它资料。
使用下面的函数创建ID3DXPMesh对象:
HRESULT WINAPI D3DXGeneratePMesh( LPD3DXMESH pMesh, const DWORD *pAdjacency, const D3DXATTRIBUTEWEIGHTS *pVertexAttributeWeights, const FLOAT *pVertexWeights, DWORD MinValue, DWORD Options, LPD3DXPMESH *ppPMesh ); |
l pMesh –输入的普通的Mesh对象
l pAdjacency –Mesh对象的邻接信息,这是一个DWORD数组
l pVertexAttributeWeights –结构D3DXATTRIBUTEWEIGHTS的数组,元素个数为pMesh->GetNumVertices(),表示顶点的属性的权。在简化Mesh对象时,权值决定一个顶点被删除的可能性大小。该参数可以设为NULL,这时顶点使用默认的权值。
l pVertexWeights –顶点的权,是float数组,元素个数是pMesh->GetNumVertices(),用于决定顶点在简化时被删除的可能性的大小。该参数也可设为NULL,这时,顶点默认的权值为1.0f。
l MinValue –在简化Mesh时,顶点或者三角形数的最小个数。该参数是必要的,而且与顶点权值和顶点属性权值有关系,最终也许达不到该数值。
l Options –只能取D3DXMESHSIMP枚举类型中的一个值:
n D3DXMESHSIMP_VERTEX –上一个参数MinValue指顶点数
n D3DXMESHSIMP_FACE –上一个参数MinValue指三角形数
l ppPMesh –返回生成的渐进Mesh
typedef struct _D3DXATTRIBUTEWEIGHTS { FLOAT Position; FLOAT Boundary; FLOAT Normal; FLOAT Diffuse; FLOAT Specular; FLOAT Texcoord[8]; FLOAT Tangent; FLOAT Binormal; } D3DXATTRIBUTEWEIGHTS, *LPD3DXATTRIBUTEWEIGHTS; |
通过这个结构,可以为顶点的每个属性指定一个权值,0.0表示属性没有权。权值越高,在简化时,越不易被删除。默认的权值如下:
D3DXATTRIBUTEWEIGHTS AttributeWeights; AttributeWeights.Position = 1.0; AttributeWeights.Boundary = 1.0; AttributeWeights.Normal = 1.0; AttributeWeights.Diffuse = 0.0; AttributeWeights.Specular = 0.0; AttributeWeights.Tex[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; |
一般情况下,推荐使用默认的权值,除非你认为非常有必要使用不同的权值。
接口ID3DXPMesh继承自ID3DXBaseMesh,下面介绍一些常用的方法。
l DWORD GetMaxFaces(VOID); --返回Mesh的最大三角形数
l DWORD GetMaxVertices(VOID); --返回Mesh的最大顶点数
l DWORD GetMinFaces(VOID); --返回Mesh的最少三角形数
l DWORD GetMinVertices(VOID); --返回Mesh的最少顶点数
l HRESULT SetNumFaces(DWORD Faces); --设置Mesh的三角形数。例如,假定Mesh现在有50个三角形,而想将其简化为30个三角形,则调用pmesh->SetNumFaces(30)。调整后的三角形数可能并不是我们设定的个数,因为PMesh的三角形数还有最大和最少的限制。
l HRESULT SetNumVertices(DWORD Vertices); --设置PMesh的顶点个数。例如,假设现在PMesh有20个顶点,而为了增加其精细程度将顶点数增为40个,则只需调用pmesh->SetNumVertices(40)。与三角形数一样,最终的结果可能不是我们指定的数值,同样有最大最少个数的限制。
l HRESULT TrimByFaces(
DWORD NewFacesMin,
DWORD NewFacesMax,
DWORD *rgiFaceRemap,
DWORD *rgiVertRemap
); --该方法设定PMesh三角形数的最大最小值。新的最大最小值必须在当前的最大最小值之间,即必须在[GetMinFaces(),GetMaxFaces()]内。同时,该方法还将返回三角形和顶点的重影射信息。
l HRESULT TrimByVertices(
DWORD NewVerticesMin,
DWORD NewVerticessMax,
DWORD *rgiFaceRemap,
DWORD *rgiVertRemap
); --该方法与上面的方法相似。
这个例子与前面的XFile例子相似,只是其中使用ID3DXPMesh接口。
与前例相似,我们使用如下的全局变量:
ID3DXMesh* SourceMesh = 0; ID3DXPMesh* PMesh = 0; // progressive mesh vector<D3DMATERIAL9> Mtrls(0); vector<IDirect3DTexture9*> Textures(0); |
在创建Progressive Mesh之前,需要使用ID3DXMesh接口加载.x文件:
HRESULT hr = 0; // ...Load XFile data into SourceMesh snipped. // // ...Extracting materials and textures snipped.
// // Generate the progressive mesh. // hr = D3DXGeneratePMesh( SourceMesh, (DWORD*)adjBuffer->GetBufferPointer(), // adjacency 0, // default vertex attribute weights 0, // default vertex weights 1, // simplify as low as possible D3DXMESHSIMP_FACE, // simplify by face count &PMesh); Release<ID3DXMesh*>(SourceMesh); // done w/ source mesh Release<ID3DXBuffer*>(adjBuffer); // done w/ buffer if(FAILED(hr)) { ::MessageBox(0, "D3DXGeneratePMesh() - FAILED", 0, 0); return false; } |
通常,因为顶点和顶点属性权值的缘故,很难将Mesh简化到只有一个三角形的程度,但是,如果指定将Mesh简化到一个三角形的程度,则可以将Mesh简化到解析度最低的程度。
现在,渐进Mesh已经生成了,但是,如果直接渲染,则Mesh的解析度此时最低。如果想渲染全解析度的PMesh,首先需要设置其三角形数:
// set to original (full) detail DWORD maxFaces = PMesh->GetMaxFaces(); PMesh->SetNumFaces(maxFaces); |
在渲染PMesh时,我们使用键盘输入控制其解析度:A键将增加解析度,S键减小解析度。
// Get the current number of faces the pmesh has. int numFaces = PMesh->GetNumFaces(); // Add a face, note the SetNumFaces() will automatically // clamp the specified value if it goes out of bounds. if( ::GetAsyncKeyState('A') & 0x8000f ) { // Sometimes we must add more than one face to invert // an edge collapse transformation because of the internal // implementation details of the ID3DXPMesh interface. In // other words, adding one face may possibly result in a // mesh with the same number of faces as before. Thus to // increase the face count we may sometimes have to add // two faces at once. PMesh->SetNumFaces(numFaces + 1); if(PMesh->GetNumFaces() == numFaces) PMesh->SetNumFaces(numFaces + 2); } // Remove a face, note the SetNumFaces() will automatically // clamp the specified value if it goes out of bounds. if(::GetAsyncKeyState('S') & 0x8000f) PMesh->SetNumFaces(numFaces - 1); |
上面的方法直截了当,只是增加三角形数时,有时需要增加两个来满足ECT的需要。
最后,使用和渲染ID3DXMesh同样的方法渲染ID3DXPMesh。另外,为了更加直观的观察PMesh的三角形数的变化情况,使用黄色材质在线框模式(Wireframe Mode)下渲染Mesh的三角形。
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,0xffffffff, 1.0f, 0); Device->BeginScene(); for(int i = 0; i < Mtrls.size(); i++) { Device->SetMaterial( &Mtrls[i] ); Device->SetTexture(0, Textures[i]); PMesh->DrawSubset(i); // draw wireframe outline Device->SetMaterial(&d3d::YELLOW_MTRL); Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); PMesh->DrawSubset(i); Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); } Device->EndScene(); Device->Present(0, 0, 0, 0); |
有时,需要计算Mesh对象的边界范围,常用的有两种类型:立方体和球。也有使用其它方法的,如圆柱体、椭球体、菱形体、太空舱形等。这里,我们只讨论立方体和球体两种边界形式。
边界盒或边界球常用来加速多物体间的可视范围测试、碰撞检测等。如果一个Mesh的边界盒/球不可见,就可认为Mesh也不可见。检测边界盒/球是否可见比检测Mesh中所有的三角形是否可见要方便得多。在碰撞检测中,如果一枚导弹点火起飞,我们需要检测他是否撞到了同一个场景中的目标。由于这些对象全是大量的三角形构成,我们可以依次检测每个对象的每个三角形,检测导弹(可以使用数学模型中的射线)是否撞到了这些三角形。这个方法需要进行多次的射线/三角形交点的运算。较好的方法是使用边界盒或边界球,计算射线与场景中的每个对象的边界盒/边界球的交点。如果射线与对象的边界范围相交,可以认为该对象被击中了。这是一个公平的近似方法,如果需要更高的精度,可以用边界范围法先去除那些明显不会相撞的对象,然后用更精确地方法检测很可能相撞的对象。如果边界范围检测发现相撞,则该对象就很有可能相撞。
D3DX库提供了计算Mesh对象边界盒/球的函数。这些函数使用顶点数组作为输入计算边界盒/球,可以使用各种顶点格式:
HRESULT WINAPI D3DXComputeBoundingSphere( const D3DXVECTOR3 *pFirstPosition, DWORD NumVertices, DWORD dwStride, D3DXVECTOR3 *pCenter, FLOAT *pRadius ); |
l pFirstPosition –顶点数组的地址,顶点的第一个向量需要是顶点的位置坐标
l NumVertices –顶点的数目
l dwStride –顶点大小,以字节为单位。因顶点中有很多附加数据,如法向量、纹理坐标等,计算边界范围不需要这些数据,所以,需要知道跳过多少数据才能找到下一个顶点的坐标。
l pCenter –返回边界范围的中心
l pRadius –返回边界球的半径
HRESULT WINAPI D3DXComputeBoundingBox( const D3DXVECTOR3 *pFirstPosition, DWORD NumVertices, DWORD dwStride, D3DXVECTOR3 *pMin, D3DXVECTOR3 *pMax ); |
前三个参数与计算边界球的函数相同;后两个参数返回边界盒的最小和最大点。
为了使边界检测易于使用,我们实现几个辅助的数据结构:
struct BoundingBox { BoundingBox(); bool isPointInside(D3DXVECTOR3& p); D3DXVECTOR3 _min; D3DXVECTOR3 _max; }; struct BoundingSphere { BoundingSphere(); D3DXVECTOR3 _center; float _radius; };
BoundingBox::BoundingBox() { // infinite small bounding box _min.x = FLT_MAX; _min.y = FLT_MAX; _min.z = FLT_MAX; _max.x = -FLT_MAX; _max.y = -FLT_MAX; _max.z = -FLT_MAX; } bool BoundingBox::isPointInside(D3DXVECTOR3& p) { // is the point inside the bounding box? if (p.x >= _min.x && p.y >= _min.y && p.z >= _min.z && p.x <= _max.x && p.y <= _max.y && p.z <= _max.z) { return true; } else { return false; } } BoundingSphere::BoundingSphere() { _radius = 0.0f; } |
该例子演示D3DXComputeBoundingSphere和D3DXComputeBoundingBox函数的用法。程序首先加载一个.x文件,然后计算Mesh的边界盒/球。代码中创建两个ID3DXMesh对象,分别使用边界盒和边界球。最后,分别渲染他们。
这个例子很简单,这里只给出有关边界范围的代码:
bool ComputeBoundingSphere( ID3DXMesh* mesh, // mesh to compute bounding sphere for BoundingSphere* sphere) // return bounding sphere { HRESULT hr = 0; BYTE* v = 0; mesh->LockVertexBuffer(0, (void**)&v); hr = D3DXComputeBoundingSphere( (D3DXVECTOR3*)v, mesh->GetNumVertices(), D3DXGetFVFVertexSize(mesh->GetFVF()), &sphere->_center, &sphere->_radius); mesh->UnlockVertexBuffer(); if( FAILED(hr) ) return false; return true; } bool ComputeBoundingBox( ID3DXMesh* mesh, // mesh to compute bounding box for BoundingBox* box) // return bounding box { HRESULT hr = 0; BYTE* v = 0; mesh->LockVertexBuffer(0, (void**)&v); hr = D3DXComputeBoundingBox( (D3DXVECTOR3*)v, mesh->GetNumVertices(), D3DXGetFVFVertexSize(mesh->GetFVF()), &box->_min, &box->_max); mesh->UnlockVertexBuffer(); if( FAILED(hr) ) return false; return true; } |
类型转换(D3DXVECTOR3*)v假定顶点坐标在顶点结构的开头位置,一般都是如此。
l 现在,我们可以用3D建模软件导出的.x文件构建复杂的Mesh对象。使用D3DXLoadMeshFromX函数取得ID3DXMesh对象,就可以在自己的应用程序中自由使用了。
l 使用ID3DXPMesh接口表示的渐进Mesh,可以控制其精细程度。可以根据对象在场景中的突出程度调整PMesh的精细程度。
l 我们可以使用D3DXComputeBoundingSphere和D3DXComputeBoundingBox函数计算Mesh对象的边界。边界范围很有用,其接近对象真实的边界,可加速碰撞检测等的计算。