到目前为止,在本书中,我们已经使用程序生成的网格(例如圆柱体,网格,球体)以及从文件加载的一些简单网格数据(例如,头骨网格)。在这个简短的章节中,我们扩展了我们用来支持材料和其他信息的.m3d格式。在第25章中,我们将再次扩展这种格式以支持角色动画。
.m3d格式不是标准的网格格式;它是我们为本书开发的一种格式,可以使网格加载到我们的演示中变得相对简单。工作室通常有他们自己的自定义模型格式,它们的应用程序和渲染引擎是特定的。大多数3D建模程序提供了编写插件的工具,以便您可以将建模程序中的数据导出为您自己的自定义格式。另一种方法是将标准模型格式的转换器写入您的自定义模型格式。例如,“MeshFromOBJ10”SDK示例演示如何加载要由Direct3D呈现的.obj网格。作为第二个例子,XNA Game Studio内容管道支持.x和.fbx文件格式。内容管道将数据加载到准备好由XNA框架呈现的游戏就绪XNA模型类中。避免写你自己的。 x或.fbx转换器,可以使用XNA将文件加载到XNA Model类中,提取所需数据,然后将其保存为自定义格式。读者可能对Open Asset Import Library(http://assimp.sourceforge.net/index.html)感兴趣,它支持加载各种标准模型格式。加载数据后,您可以遍历库的数据结构,提取所需的数据,并将其保存为自定义格式。无论您是编写插件还是转换器,它都可能是一项重大的努力,并且很难实现强大的功能。因此,使用XNA Game Studio内容管道等现有解决方案或Open Asset Import Library将节省大量时间。
目标:
1.要学习如何从.m3d格式加载网格和材质。
2.了解网格如何分解成不同的子集。
23.1 .M3D格式
.m3d基于文本,便于手动读取和编辑。 实际上,使用二进制格式会更有效率。
23.1.1 标题
格式以一个标题开头,该标题指定了一些全局信息,这些信息基本上告诉我们的加载器需要读取多少数据:
***************m3d-File-Header***************
#Materials 2
#Vertices 2258
#Triangles 1674
#Bones 0
#AnimationClips 0
1.#Materials:网格使用的不同材质的数量。
2.#Vertices:网格的顶点数。
3.#Triangles:网格中的三角形数量。
4.#Bones:网格中骨骼的数量 - 这将在第25章中使用; 现在,我们的网格都没有骨骼,所以这些值将为0。
5.#AnimationClips:网格中动画片段的数量 - 这将在第25章中使用; 目前,我们的网格都没有动画数据,所以这些值将为0。
我们加载标题数据如下:
bool M3DLoader::LoadM3d(const std::string& filename,
std::vector<Vertex::PosNormalTexTan>& vertices,
std::vector<USHORT>& indices,
std::vector<MeshGeometry::Subset>& subsets,
std::vector<M3dMaterial>& mats)
{
std::ifstream fin(filename);
UINT numMaterials = 0;
UINT numVertices = 0;
UINT numTriangles = 0;
UINT numBones = 0;
UINT numAnimationClips = 0;
std::string ignore;
if(fin)
{
fin >> ignore; // file header text
fin >> ignore >> numMaterials;
fin >> ignore >> numVertices;
fin >> ignore >> numTriangles;
fin >> ignore >> numBones;
fin >> ignore >> numAnimationClips;
23.1.2材料
.m3d格式的下一个“块”是一个材料列表。 在这个特定的例子中,网格有两种材料:
***************Materials*********************
Ambient: 0.4 0.4 0.4
Diffuse: 0.9 0.9 0.9
Specular: 0.4 0.4 0.4
SpecPower: 16
Reflectivity: 0 0 0
AlphaClip: 1
Effect: Normal
DiffuseMap: tree01-leaves_diffuse.dds
NormalMap: tree01-leaves_normal.dds
Ambient: 0.4 0.4 0.4
Diffuse: 0.9 0.9 0.9
Specular: 0.4 0.4 0.4
SpecPower: 16
Reflectivity: 0 0 0
AlphaClip: 0
Effect: Normal
DiffuseMap: tree01-bark_diffuse.dds
NormalMap: tree01-bark_normal.dds
该文件包含我们熟悉的材质数据(环境,漫反射,镜面反射等),但还包含其他信息,如要应用的纹理,是否需要应用Alpha剪裁以及效果名称。 我们在本书中没有使用效果名称,但它可以用来指定几何体需要渲染时的特定效果; 例如,也许某些几何图形需要置换映射,另一种常规映射和另一种环境映射。
我们定义一个匹配的材料结构来存储我们想要的信息,并加载材料数据如下:
struct M3dMaterial
{
Material Mat;
bool AlphaClip;
std::string EffectTypeName;
std::wstring DiffuseMapName;
std::wstring NormalMapName;
};
void M3DLoader::ReadMaterials(std::ifstream& fin, UINT
numMaterials, std::vector<M3dMaterial>& mats)
{
std::string ignore;
mats.resize(numMaterials);
std::string diffuseMapName;
std::string normalMapName;
fin >> ignore; // materials header text
for(UINT i = 0; i < numMaterials; ++i)
{
fin >> ignore >> mats[i].Mat.Ambient.x >>
mats[i].Mat.Ambient.y >> mats[i].Mat.Ambient.z;
fin >> ignore >> mats[i].Mat.Diffuse.x >>
mats[i].Mat.Diffuse.y >> mats[i].Mat.Diffuse.z;
fin >> ignore >> mats[i].Mat.Specular.x >>
mats[i].Mat.Specular.y >> mats[i].Mat.Specular.z;
fin >> ignore >> mats[i].Mat.Specular.w;
fin >> ignore >> mats[i].Mat.Reflect.x >>
mats[i].Mat.Reflect.y >> mats[i].Mat.Reflect.z;
fin >> ignore >> mats[i].AlphaClip;
fin >> ignore >> mats[i].EffectTypeName;
fin >> ignore >> diffuseMapName;
fin >> ignore >> normalMapName;
mats[i].DiffuseMapName.resize(diffuseMapName.size(), ' ');
mats[i].NormalMapName.resize(normalMapName.size(), ' ');
// convert to wstring
std::copy(diffuseMapName.begin(),
diffuseMapName.end(),
mats[i].DiffuseMapName.begin());
std::copy(normalMapName.begin(),
normalMapName.end(),
mats[i].NormalMapName.begin());
}
}
23.1.3子集
网格由一个或多个子集组成。 子集是网格中的一组三角形,可以使用相同的材质进行渲染。通过相同的材质,我们指的是相同的效果,纹理和渲染状态。 图23.1说明了代表汽车的网格如何分成几个子集。
图23.1 按子集划分的一辆汽车。 这里只有每个子集的材质不同,但我们也可以想象纹理被添加和不同。另外,渲染状态可能不同; 例如,为了透明度,可以使用阿尔法混合来渲染玻璃窗。
有一个子集对应于每种材料,第i个子集对应于第i种材料。 第i个子集定义了一个连续的几何图形块,应该使用第i个材质进行渲染。
***************SubsetTable*******************
SubsetID: 0 VertexStart: 0 VertexCount: 1600 FaceStart: 0 FaceCount: 800
SubsetID: 1 VertexStart: 1600 VertexCount: 658 FaceStart: 800 FaceCount: 874
In the previous example, the first 800 triangles of the mesh (which reference vertices 0–1599) should be rendered
with material 0, and the next 874 triangles of the mesh (which reference vertices 1600–2257) should be rendered with
material 1.
struct Subset
{
Subset() :
Id(-1),
VertexStart(0), VertexCount(0),
FaceStart(0), FaceCount(0)
{}
UINT Id;
UINT VertexStart;
UINT VertexCount;
UINT FaceStart;
UINT FaceCount;
};
void M3DLoader::ReadSubsetTable(std::ifstream& fin, UINT numSubsets,
std::vector<MeshGeometry::Subset>& subsets)
{
std::string ignore;
subsets.resize(numSubsets);
fin >> ignore; // subset header text
for(UINT i = 0; i < numSubsets; ++i)
{
fin >> ignore >> subsets[i].Id;
fin >> ignore >> subsets[i].VertexStart;
fin >> ignore >> subsets[i].VertexCount;
fin >> ignore >> subsets[i].FaceStart;
fin >> ignore >> subsets[i].FaceCount;
}
}
23.1.4顶点和三角形索引
最后两个数据块只是顶点和索引列表(每个三角形有3个索引):
***************Vertices**********************
Position: 0.9207547 10.77502 -1.320696
Tangent: -0.2406725 0.05866166 -0.968832 1
Normal: 0.020704 0.9982551 0.05530001
Tex-Coords: 1 0
Position: -0.08739337 10.78203 -1.069832
Tangent: -0.2406725 0.05866166 -0.968832 1
Normal: 0.020704 0.9982551 0.05530001
Tex-Coords: 1 1
…
***************Triangles*********************
0 1 2
3 0 2
4 5 6
…
我们的.m3d格式的局限性是我们强加所有的顶点具有相同的格式(位置,法线,切线,纹理坐标)。 应用程序可能有使用不同顶点格式的模型。 然而,为格式增加这种灵活性会使事情变得复杂,而我们格式的目标是为了演示的目的而保持简单。
加载顶点和索引的代码如下:
void M3DLoader::ReadVertices(std::ifstream& fin, UINT numVertices,
std::vector<Vertex::PosNormalTexTan>& vertices)
{
std::string ignore;
vertices.resize(numVertices);
fin >> ignore; // vertices header text
for(UINT i = 0; i < numVertices; ++i)
{
fin>>ignore >> vertices[i].Pos.x >>
vertices[i].Pos.y >>
vertices[i].Pos.z;
fin>>ignore >> vertices[i].TangentU.x >>
vertices[i].TangentU.y >>
vertices[i].TangentU.z >>
vertices[i].TangentU.w;
fin>>ignore >> vertices[i].Normal.x >>
vertices[i].Normal.y >>
vertices[i].Normal.z;
fin>>ignore >> vertices[i].Tex.x >> vertices[i].Tex.y;
}
}
void M3DLoader::ReadTriangles(std::ifstream& fin, UINT numTriangles,
std::vector<USHORT>& indices)
{
std::string ignore;
indices.resize(numTriangles*3);
fin >> ignore; // triangles header text
for(UINT i = 0; i < numTriangles; ++i)
{
fin >> indices[i*3+0] >> indices[i*3+1] >> indices[i*3+2];
}
}
23.2网格几何
为了组织网格数据,我们定义了一个称为MeshGeometry的低级类,它封装了顶点和索引缓冲区以及子集。
class MeshGeometry
{
public:
struct Subset { … };
public: MeshGeometry();
~MeshGeometry();
template <typename VertexType>
void SetVertices(ID3D11Device* device,
const VertexType* vertices, UINT count);
void SetIndices(ID3D11Device* device,
const USHORT* indices, UINT count);
void SetSubsetTable(std::vector<Subset>& subsetTable);
void Draw(ID3D11DeviceContext* dc, UINT subsetId);
private:
MeshGeometry(const MeshGeometry& rhs);
MeshGeometry& operator=(const MeshGeometry& rhs);
private:
ID3D11Buffer* mVB;
ID3D11Buffer* mIB;
DXGI_FORMAT mIndexBufferFormat; // Always 16-bit
// Cache sizeof(VertexType) to pass to IASetVertexBuffers
UINT mVertexStride;
std::vector<Subset> mSubsetTable;
};
成员函数的实现很简单,所以我们将省略除Draw调用之外的代码。该函数使用子集表仅绘制属于指定子集的三角形:
void MeshGeometry::Draw(ID3D11DeviceContext* dc, UINT subsetId)
{
UINT offset = 0;
dc->IASetVertexBuffers(0, 1, &mVB, &mVertexStride, &offset);
dc->IASetIndexBuffer(mIB, mIndexBufferFormat, 0);
dc->DrawIndexed(
mSubsetTable[subsetId].FaceCount*3,
mSubsetTable[subsetId].FaceStart*3,
0);
}
23.3基本模型
在更高层次上,我们定义了一个BasicModel类; 这包含一个MeshGeometry实例,用于指定模型的几何图形以及绘制网格所需的材质。 此外,它还保留网格的系统内存副本。 当我们需要读取网格数据时需要系统内存副本,例如计算边界卷,拾取或碰撞检测。
class BasicModel
{
public: BasicModel(ID3D11Device* device,
TextureMgr& texMgr,
const std::string& modelFilename,
const std::wstring& texturePath);
~BasicModel();
UINT SubsetCount;
std::vector<Material> Mat;
std::vector<ID3D11ShaderResourceView*> DiffuseMapSRV;
std::vector<ID3D11ShaderResourceView*> NormalMapSRV;
// Keep CPU copies of the mesh data to read from.
std::vector<Vertex::PosNormalTexTan> Vertices;
std::vector<USHORT> Indices;
std::vector<MeshGeometry::Subset> Subsets;
MeshGeometry ModelMesh;
};
BasicModel::BasicModel(ID3D11Device* device,
TextureMgr& texMgr,
const std::string& modelFilename,
const std::wstring& texturePath)
{
std::vector<M3dMaterial> mats;
M3DLoader m3dLoader;
m3dLoader.LoadM3d(modelFilename, Vertices, Indices, Subsets, mats);
ModelMesh.SetVertices(device, &Vertices[0], Vertices.size());
ModelMesh.SetIndices(device, &Indices[0], Indices.size());
ModelMesh.SetSubsetTable(Subsets);
SubsetCount = mats.size();
for(UINT i = 0; i < SubsetCount; ++i)
{
Mat.push_back(mats[i].Mat);
ID3D11ShaderResourceView* diffuseMapSRV = texMgr.CreateTexture(
texturePath + mats[i].DiffuseMapName);
DiffuseMapSRV.push_back(diffuseMapSRV);
ID3D11ShaderResourceView* normalMapSRV = texMgr.CreateTexture(
texturePath + mats[i].NormalMapName);
NormalMapSRV.push_back(normalMapSRV);
}
}
现在,绘制一个BasicModel等于循环其子集,为第i个子集设置材质,然后将第i个子集几何图形提交给渲染管线:
for(UINT subset = 0; subset < mModelInstances[modelIndex].Model->SubsetCount; ++subset)
{
Effects::NormalMapFX->SetMaterial(Model->Mat[subset]);
Effects::NormalMapFX->SetDiffuseMap(Model->DiffuseMapSRV[subset]);
Effects::NormalMapFX->SetNormalMap(Model->NormalMapSRV[subset]);
tech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
Model->ModelMesh.Draw(md3dImmediateContext, subset);
}
由于某些网格物体会共享相同的纹理,因此如果不注意防止重复,则可以多次加载这些纹理。 为了防止重复,我们引入了TextureMgr类来创建纹理。 该类使用std :: map数据结构防止加载和创建两次纹理。 当用户尝试创建纹理时,管理员首先检查纹理是否已经存在于地图中。 如果是这样,管理器只是返回一个指向纹理的指针而不会再次加载它。 否则,管理器加载纹理并返回指向它的指针。 请注意,TextureMgr类也处理破坏纹理资源,所以主应用程序代码不需要这样做。 TextureMgr类在TextureMgr.h / .cpp中定义,并位于Common目录中。
我们可能希望通过多次绘制它来实例化一个BasicModel,但在不同的位置。 我们使用以下简单的结构来实现这一点:
struct BasicModelInstance
{
BasicModel* Model;
XMFLOAT4X4 World;
};
23.4网格查看器演示
图23.2显示了本章演示的屏幕截图。 它使用本章中讨论的加载代码和数据结构加载和绘制多个网格。
图23.2 “Mesh Viewer”演示的屏幕截图
23.5总结
1.网格由一个或多个子集组成。 子集是网格中的一组三角形,它们都可以使用相同的效果,纹理和渲染状态进行渲染。
2.为了组织网格数据,我们定义了一个称为MeshGeometry的低级类,它封装了顶点和索引缓冲区,并定义了网格的子集。
3. BasicModel类将MeshGeometry实例封装为绘制网格所需的材质。 此外,它还保留网格的系统内存副本。 当我们需要读取网格数据时需要系统内存副本,例如计算边界卷,拾取或碰撞检测。