第八集 Mesh
构造简单的球的3D模型已经复杂化了, 如果用代码构造比球更复杂的3D模型就更难了, 还好有专业级的3D模型设计软件, 这些软件构造的3D模型在DirectX Graphics中称为Mesh, DirectX Graphics中对应的接口为,
ID3DXBaseMesh
| - ID3DXMesh
| - ID3DXPMesh
ID3DXSPMesh
ID3DXPatchMesh
可以在DirectX9c SDK中的Mesh Support in D3DX主题中找到有关Mesh的描述.
8.1 ID3DXMesh中的信息
8.1.1 顶点和顶点索引
Mesh中包含的物体模型实际还是由顶点及顶点索引组成的, Mesh只是起把顶点, 顶点索引, 纹理属性, 材质属性包装在一起的作用, 简单资源的统一管理.
我们可以类似创建顶点一样来创建Mesh, DirectX Graphics提供的函数为,
HRESULT D3DXCreateMesh(DWORD NumFaces,
DWORD NumVertices,
DWORD Options,
CONST LPD3DVERTEXELEMENT9 * pDeclaration,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXMESH * ppMesh);
HRESULT D3DXCreateMeshFVF(DWORD NumFaces,
DWORD NumVertices,
DWORD Options,
DWORD FVF,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXMESH * ppMesh);
其中D3DXCreateMeshFVF函数比较实用, 创建一个长方体的代码为,
// #define D3DFVF_MYVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)
// LPDIRECT3DDEVICE9 m_pD3DDev;
// LPD3DXMESH m_pMesh;
D3DXCreateMeshFVF(12, 8, D3DXMESH_MANAGED,
D3DFVF_MYVERTEX, m_pD3DDev, &m_pMesh)
Directx Graphics 只是根据参数申请相应的内存, 长方体的顶点值和顶点索引还是要由我们自己去填写, 在Mesh中包含以下函数,
HRESULT LockVertexBuffer(DWORD Flags, LPVOID * ppData);
HRESULT UnlockVertexBuffer();
HRESULT LockIndexBuffer(DWORD Flags, LPVOID * ppData);
HRESULT UnlockIndexBuffer();
我们填写长方体的顶点值和顶点索引,
INT nSize = sizeof(MYVERTEX) * 8;
MYVERTEX aVertex[ ] =
{
{-1.0f, -1.0f, -1.0f, D3DCOLOR_ARGB(0, 255, 0, 0 ) },
{-1.0f, 1.0f, -1.0f, D3DCOLOR_ARGB(0, 0, 0, 255) },
{ 1.0f, 1.0f, -1.0f, D3DCOLOR_ARGB(0, 0, 255, 0 ) },
{ 1.0f, -1.0f, -1.0f, D3DCOLOR_ARGB(0, 0, 0, 255) },
{-1.0f, -1.0f, 1.0f, D3DCOLOR_ARGB(0, 0, 255, 0 ) },
{-1.0f, 1.0f, 1.0f, D3DCOLOR_ARGB(0, 0, 0, 255) },
{ 1.0f, 1.0f, 1.0f, D3DCOLOR_ARGB(0, 0, 255, 0 ) },
{ 1.0f, -1.0f, 1.0f, D3DCOLOR_ARGB(0, 0, 0, 255) }
};
LPVOID pV = NULL;
if (FAILED(m_pMesh->LockVertexBuffer(0, &pV)))
{
return E_FAIL;
}
MoveMemory(pV, aVertex, nSize);
m_pMesh->UnlockVertexBuffer();
nSize = sizeof(WORD) * 36;
WORD aIndex[] =
{
0, 1, 2,
2, 3, 0,
4, 7, 6,
6, 5, 4,
0, 3, 7,
7, 4, 0,
3, 2, 6,
6, 7, 3,
2, 1, 5,
5, 6, 2,
1, 0, 4,
4, 5, 1
};
if (FAILED(m_pMesh->LockIndexBuffer(0, &pV)))
{
return E_FAIL;
}
MoveMemory(pV, aIndex, nSize);
m_pMesh->UnlockIndexBuffer();
DirectX Graphics中提供了实用的基本模型创建函数,
HRESULT D3DXCreateBox(LPDIRECT3DDEVICE9 pDevice,
FLOAT Width,
FLOAT Height,
FLOAT Depth,
LPD3DXMESH * ppMesh,
LPD3DXBUFFER * ppAdjacency);
HRESULT D3DXCreateCylinder(LPDIRECT3DDEVICE9 pDevice,
FLOAT Radius1,
FLOAT Radius2,
FLOAT Length,
UINT Slices,
UINT Stacks,
LPD3DXMESH * ppMesh,
LPD3DXBUFFER * ppAdjacency);
HRESULT D3DXCreateSphere(LPDIRECT3DDEVICE9 pDevice,
FLOAT Radius,
UINT Slices,
UINT Stacks,
LPD3DXMESH * ppMesh,
LPD3DXBUFFER * ppAdjacency);
HRESULT D3DXCreateTeapot(LPDIRECT3DDEVICE9 pDevice,
LPD3DXMESH * ppMesh,
LPD3DXBUFFER * ppAdjacency);
HRESULT D3DXCreateTorus(LPDIRECT3DDEVICE9 pDevice,
FLOAT InnerRadius,
FLOAT OuterRadius,
UINT Sides,
UINT Rings,
LPD3DXMESH * ppMesh,
LPD3DXBUFFER * ppAdjacency);
注意这些函数创建的模型的中心轴是Z轴.
// 宽3.0, 高2.0, 厚1.0的长方体
D3DXCreateBox(m_pD3DDev, 3.0, 2.0, 1.0, &m_pMeshBox, NULL)
// 被分成16块的圆台, 上半径1.0, 底半径2.0, 高4.0, 圆台被分为2层
// 这个函数可创建棱台, 棱锥, 圆台, 圆锥...
D3DXCreateCylinder(m_pD3DDev, 1.0, 2.0, 4.0, 16, 2, &m_pMeshCylinder, NULL)
// 球模型, 上集有描述
D3DXCreateSphere(m_pD3DDev, 2.0, 8, 8, &m_pMeshSphere, NULL)
// 茶壶, 想不通DX和OpenGL都喜欢这东西?
D3DXCreateTeapot(m_pD3DDev, &m_pMeshTeapot, NULL)
// 甜甜圈
D3DXCreateTorus(m_pD3DDev, 1.0, 2.0, 8, 8, &m_pMeshTorus, NULL)
渲染模型时, 只需调用Mesh的DrawSubset函数就可以了,
m_pMeshBox->DrawSubset(0);
m_pMeshCylinder->DrawSubset(0);
m_pMeshSphere->DrawSubset(0);
m_pMeshTeapot->DrawSubset(0);
m_pMeshTorus->DrawSubset(0);
m_pMesh->DrawSubset(0);
8.1.2 属性表及Mesh的优化
Mesh可以包含模型的表面材质和纹理属性, 模型的不同部分的表面材质, 纹理属性或渲染状态可能是不同的, 于是Mesh的内部根据模型的材质和纹理属性将组成模型的三角形分成不同的子集(subset), 在同一子集中的三角形的表面材质, 纹理或渲染状态是相同的, 每个子集都有自己的属性描述, 将这些属性描述合在一起就是属性表了,属性表中包含的元素的数量对应的就是Mesh中子集的数量.
Attribute Buffer Index Buffer Texture & Material Buffer
[ 0 ]---------+--[ 0 ]--+-----------[ 0 ]---+
[ 1 ]----+ | [ 1 ] | +------[ 1 ] |
[ 2 ]-+ | | [ ... ] | | [ ... ] |
[ ... ] | | +--[ 7 ]--+ | |
| +-------[ 8 ]-------+ |
| | [ ... ] | |
| +-------[ 15 ]-------+ |
+----------[ 16 ]------------------------------+
| { ... ] |
+----------[ 23 ]------------------------------+
[ ... ]
图8.1
在图8.1中, Mesh中共有3个subset,其中subset0 和 subset2 使用相同的表面材质和纹理, 那么是否可以通过重新定义顶点索引把这两个subset合并呢? --- Mesh的优化.
Mesh优化的主要目的是为了渲染时更有效率, 可以根据模型的特点选择优化的方式,
HRESULT Optimize(DWORD Flags,
CONST DWORD * pAdjacencyIn,
DWORD * pAdjacencyOut,
DWORD * pFaceRemap,
LPD3DXBUFFER * ppVertexRemap,
LPD3DXMESH * ppOptMesh);
HRESULT OptimizeInplace(DWORD Flags,
CONST DWORD * pAdjacencyIn,
DWORD * pAdjacencyOut,
DWORD * pFaceRemap,
LPD3DXBUFFER * ppVertexRemap);
参考DirectX9c SDK中对两个函数的详细说明. 如果把图8.1中的subset0 和 subset2合并, Flags参数中需要包含D3DXMESHOPT_ATTRSORT.
现在我们把前面创建的长方体分成2个subset, 在渲染时使用不同的明暗处理,
DWORD dwSize = 0L;
LPD3DXATTRIBUTERANGE pRange = new D3DXATTRIBUTERANGE[2];
if (pRange != NULL)
{
m_pMesh->GetAttributeTable(pRange, &dwSize);
// 看看修改前的Mesh的属性表内容...
pRange[0].AttribId = 0;
pRange[0].FaceStart = 0;
pRange[0].FaceCount = 6;
pRange[0].VertexStart = 0;
pRange[0].VertexCount = 8;
pRange[1].AttribId = 1;
pRange[1].FaceStart = 6;
pRange[1].FaceCount = 6;
pRange[1].VertexStart = 0;
pRange[1].VertexCount = 8;
m_pMesh->SetAttributeTable(pRange, 2);
delete[] pRange;
}
渲染模型时,调用Mesh的DrawSubset函数, DrawSubset函数的参数就是Mesh中子集的序号
m_pD3DDev->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FLAT);
m_pMesh->DrawSubset(0);
m_pD3DDev->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD);
m_pMesh->DrawSubset(1);
8.2 ID3DXMesh的存储及读取
8.2.1 Mesh的存储
3D建模软件在存储模型时使用的存储格式是不相同的, DirectX Graphics只支持自己的存储文件格式X file, 可以参考DirectX9c SDK中X File Format Reference主题.现在把前面创建的所有模型都存成文件, 需要的函数是,
HRESULT D3DXSaveMeshToX(LPCTSTR pFilename,
LPD3DXMESH pMesh,
CONST DWORD * pAdjacency,
CONST D3DXMATERIAL * pMaterials,
CONST D3DXEFFECTINSTANCE * pEffectInstances,
DWORD NumMaterials,
DWORD Format);
D3DXSaveMeshToX(_T(".\\1.x"), m_pMeshBox, NULL, NULL, NULL, 0, D3DXF_FILEFORMAT_TEXT);
D3DXSaveMeshToX(_T(".\\2.x"), m_pMeshCylinder, NULL, NULL, NULL, 0, D3DXF_FILEFORMAT_TEXT);
D3DXSaveMeshToX(_T(".\\3.x"), m_pMeshSphere, NULL, NULL, NULL, 0, D3DXF_FILEFORMAT_TEXT);
D3DXSaveMeshToX(_T(".\\4.x"), m_pMeshTeapot, NULL, NULL, NULL, 0, D3DXF_FILEFORMAT_TEXT);
D3DXSaveMeshToX(_T(".\\5.x"), m_pMeshTorus, NULL, NULL, NULL, 0, D3DXF_FILEFORMAT_TEXT);
D3DXSaveMeshToX(_T(".\\6.x"), m_pMesh, NULL, NULL, NULL, 0, D3DXF_FILEFORMAT_TEXT);
8.2.2 Mesh的读取
从X文件读取数据, 创建相应的Mesh是最常见的方式, 也可以从可执行文件中的资源中读取X文件数据, 另外象Blizzard的游戏总把N多资源放在同一文件中, 运行时只需load这个文件, 这时我们是从内存中读取X文件数据的.
HRESULT D3DXLoadMeshFromX(LPCTSTR pFilename,
DWORD Options,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXBUFFER * ppAdjacency,
LPD3DXBUFFER * ppMaterials,
LPD3DXBUFFER * ppEffectInstances,
DWORD * pNumMaterials,
LPD3DXMESH * ppMesh);
HRESULT D3DXLoadMeshFromXResource(HMODULE Module,
LPCSTR Name,
LPCSTR Type,
DWORD Options,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXBUFFER * ppAdjacency,
LPD3DXBUFFER * ppMaterials,
LPD3DXBUFFER * ppEffectInstances,
DWORD * pNumMaterials,
LPD3DXMESH * ppMesh);
HRESULT D3DXLoadMeshFromXInMemory(LPCVOID Memory,
DWORD SizeOfMemory,
DWORD Options,
LPDIRECT3DDEVICE9 pD3DDevice,
LPD3DXBUFFER * ppAdjacency,
LPD3DXBUFFER * ppMaterials,
LPD3DXBUFFER * ppEffectInstances,
DWORD * pNumMaterials,
LPD3DXMESH * ppMesh);
其中Mesh模型使用的材质或纹理在ppMaterials指向的缓冲内存中, 数据以D3DXMATERIAL结构的方式组织; pNumMaterials指向的是Mesh的子集个数, 同时也是ppMaterials指向的缓冲内存中D3DXMATERIAL的个数,
LPD3DXBUFFER pBuffer = NULL;
LPD3DXMESH pMesh = NULL;
if (FAILED(D3DXLoadMeshFromX(pXFile, D3DXMESH_SYSTEMMEM, m_pD3DDev, NULL,
&pBuffer, NULL, &m_dwCount, &pMesh)))
{
return E_FAIL;
}
m_ppD3DTexture = new LPDIRECT3DTEXTURE9[m_dwCount];
m_pD3DMaterial = new D3DMATERIAL9[m_dwCount];
if ((m_ppD3DTexture == NULL) || (m_pD3DMaterial == NULL))
{
pBuffer->Release();
pMesh->Release();
return E_FAIL;
}
LPD3DXMATERIAL pMaterial = LPD3DXMATERIAL(pBuffer->GetBufferPointer());
WCHAR szTemp[512] = { 0 };
for (DWORD i = 0L; i < m_dwCount; i++)
{
m_pD3DMaterial[i] = pMaterial[i].MatD3D;
// 使用Unicode Character Set时, 需要转换
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pMaterial[i].pTextureFilename,
-1, szTemp, 512);
if (FAILED(D3DXCreateTextureFromFile(m_pD3DDev, szTemp, &(m_ppD3DTexture[i]))))
{
m_ppD3DTexture[i] = NULL;
}
}
pBuffer->Release();
// 使用CloneMeshFVF可以创建我们需要的FVF类型的Mesh
if (FAILED(pMesh->CloneMeshFVF(D3DXMESH_MANAGED, D3DFVF_MYVERTEXTEX, m_pD3DDev, &m_pMesh)))
{
pMesh->Release();
return E_FAIL;
}
pMesh->Release();
// 防止原来的Mesh中没有包含顶点法向量的情况, Clone的Mesh再计算一次顶点法向量
D3DXComputeNormals(m_pMesh, NULL);
在渲染这个Mesh时, 不同的subset要提前设置不同的材质和纹理,
for (DWORD i = 0L; i < m_dwCount; i++)
{
m_pD3DDev->SetMaterial(&(m_pD3DMaterial[i]));
m_pD3DDev->SetTexture(0, m_ppD3DTexture[i]);
m_pMesh->DrawSubset(i);
}
8.3 ID3DXPMesh
ID3DXPMesh(ID3DXSPMesh)的特点和mip-map类似, 可以根据Mesh离视点的远近距离设置不同精细层次(levels of detail, LOD). 例如一个球离视点很近时是 64 * 64[块数 * 层数], 中等距离 16 * 16, 较远时 4 * 4, ... ID3DXPMesh是在ID3DXMesh的基础上创建的,
HRESULT D3DXGeneratePMesh(LPD3DXMESH pMesh,
CONST DWORD * pAdjacency,
CONST D3DXATTRIBUTEWEIGHTS * pAttr,
CONST FLOAT * pVertexWeights,
DWORD MinValue,
DWORD Options,
LPD3DXPMESH * ppPMesh);
pAttr是指向一个描述顶点权重的数据结构, 权重越大顶点被移除的概率越小, 默认的权重数据结构为,
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后, 调用下面的函数来来设置面数或顶点数, 具体调用哪个函数要和D3DXGeneratePMesh函数中的参数Options一致.
HRESULT SetNumFaces(DWORD Faces);
HRESULT SetNumVertices(DWORD Vertices);
DWORD GetMaxFaces();
DWORD GetMinFaces();
DWORD GetMaxVertices();
DWORD GetMinVertices();
能设置的Faces或Verteices的取值区间分别为[GetMinFaces(), GetMaxFaces()]和[GetMinVertices(), GetMaxVertices()].
8.4 Mesh的例子
8.4.1 game8 project代码更新
game8 中只是演示了DirectX Graphics提供的基本模型构造函数的用法, ID3DXPMesh模型根据面数的增加或减少呈现的不同.
---------------------------------------------------------------
LPD3DXBUFFER pAdj = NULL;
LPD3DXBUFFER pBuffer = NULL;
LPD3DXMESH pMesh = NULL;
if (FAILED(D3DXLoadMeshFromX(pXFile, D3DXMESH_SYSTEMMEM, m_pD3DDev, &pAdj, &pBuffer, NULL, &m_dwCount, &pMesh)))
{
return E_FAIL;
}
m_ppD3DTexture = new LPDIRECT3DTEXTURE9[m_dwCount];
m_pD3DMaterial = new D3DMATERIAL9[m_dwCount];
if ((m_ppD3DTexture == NULL) || (m_pD3DMaterial == NULL))
{
pBuffer->Release();
pMesh->Release();
return E_FAIL;
}
LPD3DXMATERIAL pMaterial = LPD3DXMATERIAL(pBuffer->GetBufferPointer());
WCHAR szTemp[512] = { 0 };
for (DWORD i = 0L; i < m_dwCount; i++)
{
m_pD3DMaterial[i] = pMaterial[i].MatD3D;
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pMaterial[i].pTextureFilename, -1, szTemp, 512);
if (FAILED(D3DXCreateTextureFromFile(m_pD3DDev, szTemp, &(m_ppD3DTexture[i]))))
{
m_ppD3DTexture[i] = NULL;
}
}
pBuffer->Release();
if (FAILED(D3DXGeneratePMesh(pMesh, LPDWORD(pAdj->GetBufferPointer()), 0, 0, 1, D3DXMESHSIMP_FACE, &m_pMesh)))
{
pMesh->Release();
pAdj->Release();
return E_FAIL;
}
pMesh->Release();
pAdj->Release();
D3DXComputeNormals(m_pMesh, NULL);
m_lMinFace = DWORD(m_pMesh->GetMinFaces());
m_lMaxFace = DWORD(m_pMesh->GetMaxFaces());
m_pMesh->SetNumFaces(DWORD(m_lMaxFace));
m_lCurFace = m_lMaxFace;
---------------------------------------------------------------
8.4.2 game8 project说明
例子中没有光照, 我们只是简单的演示, 上下方向键控制视点在Y轴的移动, 左右方向键控制视点在X轴的移动, Page Up 增加ID3DXPMesh模型面数, Page Down减少ID3DXPMesh模型根据面数, 空格切换自动旋转, ENTER保存模型.
第八集 小结
这一集我们学习了要进行DirectX Graphics 3D编程中的Mesh的基本使用.