前言:在这里,我们将继续研究 D3DX 库提供的与网格相关的接口,结构和函数,以 网格(一) 为基础,我们现在将研究一些更有趣的技术,例如从 X文件 中加载一个复杂的 3D 模型并将该模型绘制出来,或者利用渐进网格接口来控制网格的细节层次。
学习目标:
- 了解如何加载 X 文件到 ID3DXMesh 中
- 理解使用渐进网格的优点以及如何使用渐进网格接口 ID3DXPMesh
- 了解使用外接体的作用以及如何用 D3DX 函数创建外接体
ID3DXBuffer
ID3DXBuffer 接口是一种泛型数据结构,该接口为 D3DX 库所使用,并可将数据存储在一个连续的内存块中,该接口只有两个方法
- LPVOID GetBufferPointer(); 返回指向缓存中数据起始位置的指针
- DWORD GetBufferSize(); 返回缓存的大小,单位为字节
为了保持该接口的通用性,该接口使用了 void 类型的指针,这就意味着必须由我们来实现存储的数据类型,例如 D3DXLoadMeshFromX 函数用 ID3DXBuffer 类型的指针返回一个网格对象的邻接信息,由于邻接信息被存储在一个 DWORD 类型的数组中,所以当我们想要使用该接口的邻接信息时,我们必须对该缓存进行强制类型的转换。
例:
DWORD* info = (DWORD*)adjacencyInfo->GetBufferPointer();
D3DXMATERIAL* mtrls = (D3DXMATRIAL*)mtrlBuffer->GetBufferPointer();
由于 ID3DXBuffer 是一个 COM 对象,该接口使用完毕之后必须将其释放,以防内存泄漏
adjacencyInfo->Release();
mtrlBuffer->Release();
我们可用如下函数创建一个空的 ID3DXBuffer 对象:
HRESULT D3DXCreateBuffer(
DWORD NumBytes, //缓冲区大小
LPD3DXBUFFER* ppBuffer //返回创建的缓冲区
);
下面的例子创建一个能容纳 4 个整数的缓存:
ID3DXBuffer* buffer = 0;
D3DXCreateBuffer(4 * sizeof(int), &buffer);
XFile
用于将外部模型导入的方法
加载 XFlie 文件:
我们可以使用如下函数加载网格数据并将其存储在 XFile 文件中,该方法创建了一个 ID3DXMesh 对象并将 XFlie 中的几何数据加载到该对象中
HRESULT WINAPI
D3DXLoadMeshFromX(
LPCSTR pFilename, //所要加载的 XFile 文件名
DWORD Options, //网格创建标记
LPDIRECT3DDEVICE9 pD3DDevice, //设备对象
LPD3DXBUFFER *ppAdjacency, //返回的 ID3DXBuffer 对象,包含了该网格的邻接数组信息
LPD3DXBUFFER *ppMaterials, //返回的 ID3DXBuffer 对象,包含了该网格的材质数组信息
LPD3DXBUFFER *ppEffectInstances, //返回的 ID3DXBuffer 对象,包含了一个 D3DXEFFECTIN.STANCE 结构
DWORD *pNumMaterials, //返回网格中的材质数目,即 ppMaterials 输出的材质个数
LPD3DXMESH *ppMesh //返回所创建的并填充了 XFile 几何数据的 ID3DXMesh 对象
);
-
pFilename 指向指定文件名的字符串的指针。如果编译器设置需要Unicode,则数据类型LPCTSTR将解析为LPCWSTR否则,字符串数据类型将解析为LPCSTR见备注。
-
Options D3DXMESH枚举中一个或多个标志的组合,该枚举指定网格的创建选项,一些常用的标记如下
- D3DXMESH_32BIT 网格将使用32位索引
- D3DXMESH_MANAGED 网格数据将被存储于托管内存池中
- D3DXMESH_WRITEONLY 指定网格数据为只读
- D3DXMESH_DYNAMIC 网格缓存将使用动态内存
-
pD3DDevice 设备对象
-
ppAdjacency 指向包含相邻数据的缓冲区的指针。邻接数据包含一个数组,每个面包含三个DWORD,该数组为网格中的每个面指定三个邻居。有关访问缓冲区的详细信息,请参阅id3dxbuffer。
-
ppMaterials 指向包含材料数据的缓冲区的指针缓冲区包含一个d3dxmaterial结构数组,其中包含来自directx文件的信息。有关访问缓冲区的详细信息,请参阅id3dxbuffer。
-
ppEffectInstances 指向包含效果实例数组的缓冲区的指针,返回网格中每个属性组一个。效果实例是用于初始化效果的状态信息的特定实例。请参见D3DXEFFECTINSTANCE有关访问缓冲区的详细信息,请参阅id3dxbuffer。
-
pNumMaterials 当方法返回时,指向ppmaterials数组中的d3dxmaterial结构数的指针。
-
ppMesh 指向ID3DXMesh接口的指针的地址,表示加载的网格。
XFile 材质
D3DXLoadMeshFromX 函数中的第7个参数返回了网格对象所含的材质数目,第5个参数返回了一个存储了材质数据的 D3DXMATRIAL 类型的结构数组,D3DXMATERIAL 结构的定义如下:
typedef struct _D3DXMATERIAL
{
D3DMATERIAL9 MatD3D;
LPSTR pTextureFilename;
} D3DXMATERIAL;
该结构较为简单,它包含了 D3DMATERIAL9 结构和一个指向以 NULL 结尾 的字符串的指针,该字符串指定了与网格相关的纹理文件名,XFile 文件中并未存储纹理数据,它只包含了纹理图像文件名,该文件名是对包含了实际纹理数据的纹理图像的引用,这样,当用 D3DXLoadMeshFromX 函数加载了一个 XFile 文件后,我们必须根据指定的纹理文件名加载纹理数据。
值得一提的是 D3DXLoadMeshFromX 函数载入 XFile 数据后,返回的 D3DXMATERIAL 结构数组中的第 i 项就与第 i 个子集对应,所以我们将各子集按照0,1,2,…,n-1 的顺序进行标记,其中 n 是子集和材质的总数,这样就可用一个简单的循环对全部子集进行遍历和绘制,从而完成整个网格的绘制。
生成顶点法线
XFile文件中有可能没有存放顶点的法向量,如果出现这种情况,为了使用光照,我们可能需要手工计算每个顶点的法向量,在 光照 中简要的讨论了顶点法向量的计算,我们也可用如下方法来产生任意网格的顶点法向量:
HRESULT D3DXComputNormals(
LPD3DXBASEMESH pMesh, //要计算的法线的网格
CONST DWORD* pAdjacency //输入邻接信息
);
该函数通过法向量平均的方法生成顶点的法向量,如果提供了邻接信息,重叠的顶点就会被剔除,如果没有提供邻接信息,则重叠顶点的法向量由该顶点所依附的各面在该点的局部法向量取平均而得到,很重要的一点是,我们传入的参数 pMesh 的顶点格式中必须包含标记 D3DFVF_NORMAL
注意,如果一个 XFile 文件不含顶点法线数据,则由函数 D3DXLoadMeshFromX 所创建的 ID3DXMesh 网格对象在其顶点格式中将不含 D3DFVF_NORMAL 标记,所以,在使用函数 D3DXComputeNormals 之前,我们必须克隆该网格,并为其指定一个包含了 D3DFVF_NORMAL 标记的顶点格式,下面的例子演示了上诉的过程:
//网格是否具有顶点格式的D3DFVF_法线?
if(!(pMesh->GetFVF() & D3DFVF_NORMAL))
{
//没有,因此克隆一个新网格并将D3DFVF\u NORMAL添加到其格式:
ID3DXMesh* pTempMesh = 0;
pMesh->CloneMeshFVF(
D3DXMESH_MANAGED,
pMesh->GetFVF() | D3DFVF_NORMAL, //在此处添加
Device,
&pTempMesh
);
//计算法线:
D3DXComputeNormals(pTempMesh, 0);
pMesh->Release(); //把旧网去掉
pMesh = pTempMesh; //用法线保存新网格
}
渐进网格
渐进网格用 ID3DXPMesh 接口来表示,它允许我们运用一系列的边折叠变换对网格进行简化,每次 ECT 都移除一个顶点以及一个或两个面,由于每次 ECT 都是可逆的,我们可对简化过程进行逆转,从而将网格精确恢复到初始状态,当然这也意味着我们无法获得比原始网格更丰富的细节(当模型较远时可以不用太细致),我们只能对网格进行简化及其逆运算
渐进网格的思路与纹理中的多级渐进纹理类似,进行纹理映射时,我们会注意到如果对一个小而远的图元应用高分辨率的纹理实在是一种浪费,因为观察者根本不可能注意到这些细节,对于网格也是同样的道理,一个小而远的网格完全不必像大而近的网格一样使用大量的面片,所以在满足要求的条件下,我们总是用尽量少的面片来表达一个网格,以节省宝贵的绘制时间
注意,这里我们暂且不讨论如何实现渐进网格,我们所要介绍的是如何使用 ID3DXPMesh 接口
生成渐进网格:
我们可用如下函数创建 ID3DXPMesh 对象
HRESULT WINAPI
D3DXGeneratePMesh(
LPD3DXMESH pMesh,
CONST DWORD* pAdjacency,
CONST D3DXATTRIBUTEWEIGHTS *pVertexAttributeWeights,
CONST FLOAT *pVertexWeights,
DWORD MinValue,
DWORD Options,
LPD3DXPMESH* ppPMesh);
-
pMesh 该输入变量包含了网格数据,渐进网格将根据此网格产生
-
pAdjacency 指向包含了 pMesh 的邻接信息的 DWORD 类型的数组指针
-
pVertexAttributeWeights 指向 D3DXATTRIBUTEWEIGHTS 类型的结构数组的指针,该数组的维数为 pMesh->GetNumVertices(), 该数组的第 i 项对应于 pMesh 中的第 i 个顶点,并指定了相应顶点的属性权值,顶点属性权值用于决定在简化过程中顶点被移除的概率,可将该参数传入 NULL,则每个顶点将被赋予默认的属性权值
-
pVertexWeights 指向一个 float 类型数组的指针,该数组的维数为 pMesh->GetNumVertices(), 该数组的第 i 项对应于 pMesh 中的第 i 个顶点,并指定了相应顶点的权值,一个顶点的权值越高,在简化过程中被移除的概率就越小,可将该参数传入 NULL, 这样每个顶点就被赋予默认的顶点权值 1.0
-
MinValue 网格中的顶点数或面片数(由下一个参数Options决定)可被简化到的下限,请注意,该值只是一个期望值,实际还要依赖于顶点权值或属性权值,所以简化结果可能与该值不一致
-
Options 该参数实际上是 D3DXMESHSIMP 枚举类型的一个成员:
- D3DXMESHSIMP_VERTEX 指定了前面的参数 MinValue 是指顶点数
- D3DXMESHSIMP_FACE 指定了前面的参数 MinValue 是指面片数
-
ppPMesh 返回所生成的渐进网格
顶点属性权值:
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 接口方法:
ID3DXPMesh 接口继承自 ID3DXBaseMesh 接口,所以前者具有前面所提到 ID3DXMesh 接口的全部功能,此外 ID3DXPMesh 接口该具有以下方法
-
DWORD GetMaxFaces(VOID); 返回渐进网格面片数可被指定的上限
-
DWORD GetMaxVertices(VOID); 返回渐进网格顶点数可被指定的上限
-
DWORD GetMinFaces(VOID); 返回渐进网格面片数可被指定的下限
-
DWORD GetMinVertices(VOID); 返回渐进网格顶点数可被指定的下限
-
HRESULT SetNumFaces(DWORD Faces); 改方法允许我们设置网格的面片数可被简化或细化到的个数,例如假定网格目前有 50 个面片,我们将其简化到 30 个,可以这样做:
-
pMesh->SetNumFaces(30);
注意,调整后的网格面片数可能与期望的面片数不一致,如果 Faces 小于 GetMinFaces(),它将取为 GetMinFaces(),类似的,如果 Faces 大于 GetMaxFaces(),它将取为 GetMaxFaces(),
-
-
HRESULT SetNumVertices(DWORD Vertices); 改方法允许我们设置网格的顶点数可被简化或细化到的个数,例如,假定网格目前有 20 个顶点,我们想将其增加细节,将顶点数增加到 40 个, 可以这样做:
-
pMesh->SetNumVertices(30);
注意,调整后的网格顶点数可能与期望的顶点数不一致,如果 Vertices 小于 GetMinVertices(),它将被截取为 GetMinVertices(),类似的,如果Vertices 大于 GetMaxVertices(), 它将被截取为 GetMaxVertices()
-
-
TrimByFaces()
HRESULT TrimByFaces(
DWORD NewVerticesMin,
DWORD NewVerticessMax,
DWORD * rgiFaceRemap,
DWORD * rgiVertRemap
)
该方法允许我们重新设定面片数的最小值(NewFacesMin)和最大值(NewFacesMax),注意,NewFacesMin 和 NewFacesMax 必须位于区间 [GetMinFaces(), GetMaxFaces()]内
该函数同时也返回了面片和顶点的重绘信息
- TrimByVertices()
HRESULT TrimByVertices(
DWORD NewVerticesMin,
DWORD NewVerticessMax,
DWORD * rgiFaceRemap,
DWORD * rgiVertRemap
);
该方法允许我们重新设定顶点数的最小值(NewVerticesMin)和最大值(NewVerticessMax),注意,NewVerticesMin 和 NewVerticessMax 必须位于区间 [GetMinVertices(), GetMaxVertices()]内,该函数同时也返回了面片和顶点的重绘信息
注意: SetNumFaces 和 SetNumVertices 方法需要重视,因为这些方法允许我们调整一个网格的 LOD
外接体
外接体常用于加速可见性检测和碰撞检测,例如,一个网格的外接体不可见,我们就可认为该网格不可见,检测外接体的可见性要比检测网格中的每个片面的可见性的代价低很多,举一个例子,假定场景中有一个发射物,我们想要确定该发射物是否会几种场景中的某一物体,由于物体时由三角形面片构成的,所以我们需要遍历每个物体的每个面片,并检测发射物是否会几种某一面片,该方法需要进行大量的射线/三角形相交测试,场景中每个物体的每个三角面片都需要进行一次,一种更高效的途径是计算出每个网格的外接体,然后在对每个物体进行射线/外接体相交测试,如果射线与某一物体的外接体相交,我们就认为该物体被击中,这是一种很好的近似,如果希望提高检测精度,我们可借助外接体快速排出那些显然不可能被击中的物体,然后再对那些极有可能被击中的物体使用更精确的方法来检测,所谓极有可能被击中的物体就是其外接体被击中的那些物体
外接体分:外接体,外接球,圆柱体,椭球体,菱形体以及胶囊状容器,常见的外接体是外接球和外接体
D3DX 库提供了一些函数用来计算一个网格的外接球和外接体,这些函数接收一个顶点数组,然后依据这些顶点计算出外接球或外接体,这些函数具有很大的灵活性,可以适应多种顶点格式
//计算网格的边界球体
HRESULT WINAPI
D3DXComputeBoundingSphere(
CONST D3DXVECTOR3 *pFirstPosition, // 指向第一个位置的指针
DWORD NumVertices, // 顶点数
DWORD dwStride, // 计数到后续位置向量的字节数
D3DXVECTOR3 *pCenter, // D3DXVECTOR3结构,定义返回的边界球体的坐标中心
FLOAT *pRadius); // 返回的边界球体的半径
//计算网格的外接体
HRESULT WINAPI
D3DXComputeBoundingBox(
CONST D3DXVECTOR3 *pFirstPosition, // 指向第一个位置的指针。
DWORD NumVertices, // 顶点数
DWORD dwStride, // 顶点之间的字节数
D3DXVECTOR3 *pMin, // 指向D3DXVECTOR3结构的指针,描述返回的边界框左下角。见备注
D3DXVECTOR3 *pMax); // 指向D3DXVECTOR3结构的指针,描述返回的边界框右上角。见备注