11. Mesh Part Two
本文译自《Introduction to 3D Game Programming with DirectX 9.0》第十一章“Mesh Part Two”,敬请斧正。
本章介绍D3DX库提供的与Mesh有关的接口、结构、函数。通过本章的学习,将能够加载复杂的3D模型,能够控制Mesh对象的精细程度。本章要达到的目标:
l 学习加载.x文件
l 理解使用渐进Mesh(Progressive Mesh)的好处和学习如何使用渐进Mesh接口ID3DXPMesh。将原文中的Progressive Mesh翻译为渐进网格,不知是否恰当
l 学习边界范围(Bounding Volume),以及如何使用D3DX函数创建边界范围
11.1. 关于ID3DXBuffer
这个接口贯穿整个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 );
11.2. X文件
使用D3DXCreate*函数,可以创建一些简单的几何体,如球、圆柱、立方体等。如果想通过手动设定顶点的方式创建较复杂的3D对象,你会发现这太麻烦了,简直无法做到!现在,可以使用很多种3D建模工具软件来完成这项枯燥工作,如3DS MAX,LightWave 3D,Maya等。使用这样的建模工具,可以在可视化的、交互的环境中设计复杂、逼真的模型,而且还有丰富的工具可用,使整个建模过程相当简单。这里的简单是相对于在“程序中手动设定顶点的方式建模”,实际上,这些建模工具还是相当复杂的,想得心应手的使用,可不是一朝一夕之功。
这些建模工具可以将所建立的模型的数据(几何信息,材质,动画等)保存到文件。我们需要从文件中分析提取需要的数据,然后应用到自己的3D程序中。有一种常用的文件格式,XFile,其扩展名为.x,较为简单,是Direct3D定义的文件格式,D3DX库提供了完整的支持,可满足一般的需要。
11.2.1. 加载一个.x文件
使用下面的函数加载存储在.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对象
11.2.2. XFile材质
函数D3DXLoadMeshFromX的第七个参数返回Mesh对象的材质数量,第五个参数是D3DXMATERIAL的数组,包含Mesh的材质数据。D3DXMATERIAL结构的定义如下:
typedef struct D3DXMATERIAL {
D3DMATERIAL9 MatD3D;
LPSTR pTextureFilename;
} D3DXMATERIAL;
这个结构很简单,包含一个D3DMATERIAL9结构和一个以0字符结束的字符串的指针,表示相关联的纹理文件。. x文件并不包含纹理数据,只包含纹理文件的文件名。使用该函数加载.x文件后,还需要根据纹理文件的文件名手动加载纹理。
函数D3DXLoadMeshFromX返回的D3DXMATERIAL数组正好与Mesh对象的子集相对应。也就是说,第I个子集的材质纹理信息就存储在ppMaterials[I]中。
11.2.3. X文件的应用实例
这个例子相当的简单,它加载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);
11.2.4. 创建顶点的法向量
有时.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
}
11.3. 渐进模型(Progressive Mesh)
渐进Mesh是ID3DXPMesh接口的对象,可以简化边缩减转换(Edge Collapse Transformations (ECT))。每次ECT都回减少一个顶点和一两个面。由于ECT过程是可逆的(他的逆过程叫顶点分裂),所以,可以通过逆过程将Mesh恢复到原始状态。当然,我们也无法得到比原始状态更精细的Mesh对象,最多只能将其恢复到原始状态。
Progressive Mesh和纹理中的mipmap十分相似。在较小的和远距离的对象上使用高分辨率的纹理纯粹是浪费,因为纹理的细节根本就表现不出来。对于Mesh对象也是一样,较小的距离较远的Mesh不需要太多的三角形,多了纯粹是浪费。所以,在渲染时,实在没有必要在这些根本表现不出来的地方浪费时间。
一种方法是,根据Mesh对象距离视点的距离调整其精细水准(LOD,Level Of Detail)。当距离增加时,可降低LOD;反之,则增加LOD。
这里只讨论ID3DXPMesh接口的用法,不讨论其实现细节。如果你感兴趣,可参考其它资料。
11.3.1. 生成一个渐进Mesh
使用下面的函数创建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
11.3.2. 顶点的属性权
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};
一般情况下,推荐使用默认的权值,除非你认为非常有必要使用不同的权值。
11.3.3. ID3DXPMesh的方法
接口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
); --该方法与上面的方法相似。
11.3.4. 应用举例:Progressive Mesh
这个例子与前面的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);
11.4. 对象的边界范围
有时,需要计算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
);
前三个参数与计算边界球的函数相同;后两个参数返回边界盒的最小和最大点。
11.4.1. 边界检测类型
为了使边界检测易于使用,我们实现几个辅助的数据结构:
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;
}
11.4.2. 边界范围应用举例
该例子演示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假定顶点坐标在顶点结构的开头位置,一般都是如此。
11.5. 总结
l 现在,我们可以用3D建模软件导出的.x文件构建复杂的Mesh对象。使用D3DXLoadMeshFromX函数取得ID3DXMesh对象,就可以在自己的应用程序中自由使用了。
l 使用ID3DXPMesh接口表示的渐进Mesh,可以控制其精细程度。可以根据对象在场景中的突出程度调整PMesh的精细程度。
l 我们可以使用D3DXComputeBoundingSphere和D3DXComputeBoundingBox函数计算Mesh对象的边界。边界范围很有用,其接近对象真实的边界,可加速碰撞检测等的计算。