DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用
原文:
DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率
前言一个模型通常是由三个部分组成:网格、纹理、材质。在一开始的时候,我们是通过Geometry类来生成简单几何体的网格。但现在我们需要寻找合适的方式去表述一个复杂的网格,而且包含网格的文件类型多种多样,对应的描述方式也存在着差异。这一章我们主要研究obj格式文件的读取。 因为精力问题无法对obj做完整支持,如果需要读取obj格式的模型文件,推荐各位使用ASSIMP库 纹理映射回顾 DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 .obj格式.obj格式是Alias|Wavefront公司推出的一种模型文件格式,通常以文本形式进行描述,因此你可以按记事本来打开查看里面的内容。通过市面上的一些常见的建模软件如3dsMax,Maya等都可以导出.obj文件。一些游戏引擎如Unity3d也支持导入.obj格式的模型。该文件可以直接描述多边形、法向量、纹理坐标等等信息。 .obj文件结构简述.obj文件内部的每一行具体含义取决于开头以空格、制表符分隔的关键字是什么。这里只根据当前项目需要的部分来描述关键字
顶点数据:
元素:
组合:
材质:
.mtl文件结构简述.mtl文件内部描述方式和.obj文件一样,但里面使用的关键字有所不同
材质描述:
简单示例现在要通过.obj文件来描述一个平面正方形草丛。 mtllib ground.mtl v -10.0 -1.0 -10.0 v -10.0 -1.0 10.0 v 10.0 -1.0 10.0 v 10.0 -1.0 -10.0 vn 0.0 0.0 -1.0 vt 0.0 0.0 vt 0.0 5.0 vt 5.0 5.0 vt 5.0 0.0 g Square usemtl TestMat f 1/1/1 2/2/1 3/3/1 f 3/3/1 4/4/1 1/1/1 # 2 faces 其中根据v的先后出现顺序,对应的索引为1到4。若索引值为3,则对应第3行v对应的顶点
而诸如 若写成 这样在一个 一个模型最少需要包含一个组或一个对象
而.mtl文件的描述如下 newmtl TestMat d 1.0000 Ns 10.0000 Ka 0.8000 0.8000 0.8000 Kd 0.3000 0.3000 0.3000 Ks 0.0000 0.0000 0.0000 map_Ka grass.dds map_Kd grass.dds 漫反射和环境光反射都将使用同一种纹理。 使用自定义二进制数据格式提升读取效率使用文本类型的.obj格式文件进行读取的话必然要面临一个比较严重的问题:模型网格的面数较多会导致文本量极大,直接读取.obj的效率会非常低下。通常推荐在第一次读取.obj文件导入到程序后,再将读取好的顶点等信息以二进制文件的形式合理安排内容布局并保存,然后下次运行的时候读取该二进制文件来获取模型信息,可以大幅度加快读取速度,并且节省了一定的内存空间。 现在来说明下当前项目下自定义二进制格式.mbo的字节布局: // [Part数目] 4字节 // [AABB盒顶点vMax] 12字节 // [AABB盒顶点vMin] 12字节 // [Part // [漫射光材质文件名]520字节 // [材质]64字节 // [顶点数]4字节 // [索引数]4字节 // [顶点]32*顶点数 字节 // [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535 // ] // ... 这里将.obj中的一个组或一个对象定义为.mbo格式中的一个模型部分,然后顶点使用的是 环境光/漫射光材质文件名使用的是 但要注意一开始从.obj导出的顶点数组是没有经过处理(包含重复顶点),需要通过一定的操作分离出顶点数组和索引数组才能传递给.mbo格式。 ObjReader--读取.obj/.mbo格式模型ObjReader.h中包含了 #ifndef OBJREADER_H #define OBJREADER_H #include <iostream> #include <vector> #include <string> #include <fstream> #include <unordered_map> #include <map> #include <algorithm> #include <locale> #include <filesystem> #include "Vertex.h" #include "LightHelper.h" class MtlReader; class ObjReader { public: struct ObjPart { Material material; // 材质 std::vector<VertexPosNormalTex> vertices; // 顶点集合 std::vector<WORD> indices16; // 顶点数不超过65535时使用 std::vector<DWORD> indices32; // 顶点数超过65535时使用 std::wstring texStrDiffuse; // 漫射光纹理文件名,需为相对路径,在mbo必须占260字节 }; // 指定.mbo文件的情况下,若.mbo文件存在,优先读取该文件 // 否则会读取.obj文件 // 若.obj文件被读取,且提供了.mbo文件的路径,则会根据已经读取的数据创建.mbo文件 bool Read(const wchar_t* mboFileName,const wchar_t* objFileName); bool ReadObj(const wchar_t* objFileName); bool ReadMbo(const wchar_t* mboFileName); bool WriteMbo(const wchar_t* mboFileName); public: std::vector<ObjPart> objParts; DirectX::XMFLOAT3 vMin,vMax; // AABB盒双顶点 private: void AddVertex(const VertexPosNormalTex& vertex,DWORD vpi,DWORD vti,DWORD vni); // 缓存有v/vt/vn字符串信息 std::unordered_map<std::wstring,DWORD> vertexCache; }; class MtlReader { public: bool ReadMtl(const wchar_t* mtlFileName); public: std::map<std::wstring,Material> materials; std::map<std::wstring,std::wstring> mapKaStrs; std::map<std::wstring,std::wstring> mapKdStrs; }; #endif ObjReader.cpp定义如下: #include "ObjReader.h" using namespace DirectX; using namespace std::experimental; bool ObjReader::Read(const wchar_t * mboFileName,const wchar_t * objFileName) { if (mboFileName && filesystem::exists(mboFileName)) { return ReadMbo(mboFileName); } else if (objFileName && filesystem::exists(objFileName)) { bool status = ReadObj(objFileName); if (status && mboFileName) return WriteMbo(mboFileName); return status; } return false; } bool ObjReader::ReadObj(const wchar_t * objFileName) { objParts.clear(); vertexCache.clear(); MtlReader mtlReader; std::vector<XMFLOAT3> positions; std::vector<XMFLOAT3> normals; std::vector<XMFLOAT2> texCoords; XMVECTOR vecMin = g_XMInfinity,vecMax = g_XMNegInfinity; std::wifstream wfin(objFileName); // 切换中文 std::locale china("chs"); wfin.imbue(china); for (;;) { std::wstring wstr; if (!(wfin >> wstr)) break; if (wstr[0] == '#') { // // 忽略注释所在行 // while (!wfin.eof() && wfin.get() != 'n') continue; } else if (wstr == L"o" || wstr == L"g") { // // 对象名(组名) // objParts.emplace_back(ObjPart()); // 提供默认材质 objParts.back().material.Ambient = XMFLOAT4(0.2f,0.2f,1.0f); objParts.back().material.Diffuse = XMFLOAT4(0.8f,0.8f,1.0f); objParts.back().material.Specular = XMFLOAT4(1.0f,1.0f,1.0f); vertexCache.clear(); } else if (wstr == L"v") { // // 顶点位置 // // 注意obj使用的是右手坐标系,而不是左手坐标系 // 需要将z值反转 XMFLOAT3 pos; wfin >> pos.x >> pos.y >> pos.z; pos.z = -pos.z; positions.push_back(pos); XMVECTOR vecPos = XMLoadFloat3(&pos); vecMax = XMVectorMax(vecMax,vecPos); vecMin = XMVectorMin(vecMin,vecPos); } else if (wstr == L"vt") { // // 顶点纹理坐标 // // 注意obj使用的是笛卡尔坐标系,而不是纹理坐标系 float u,v; wfin >> u >> v; v = 1.0f - v; texCoords.emplace_back(XMFLOAT2(u,v)); } else if (wstr == L"vn") { // // 顶点法向量 // // 注意obj使用的是右手坐标系,而不是左手坐标系 // 需要将z值反转 float x,y,z; wfin >> x >> y >> z; z = -z; normals.emplace_back(XMFLOAT3(x,z)); } else if (wstr == L"mtllib") { // // 指定某一文件的材质 // std::wstring mtlFile; wfin >> mtlFile; // 去掉前后空格 size_t beg = 0,ed = mtlFile.size(); while (iswspace(mtlFile[beg])) beg++; while (ed > beg && iswspace(mtlFile[ed - 1])) ed--; mtlFile = mtlFile.substr(beg,ed - beg); // 获取路径 std::wstring dir = objFileName; size_t pos; if ((pos = dir.find_last_of('/')) == std::wstring::npos && (pos = dir.find_last_of('')) == std::wstring::npos) { pos = 0; } else { pos += 1; } mtlReader.ReadMtl((dir.erase(pos) + mtlFile).c_str()); } else if (wstr == L"usemtl") { // // 使用之前指定文件内部的某一材质 // std::wstring mtlName; std::getline(wfin,mtlName); // 去掉前后空格 size_t beg = 0,ed = mtlName.size(); while (iswspace(mtlName[beg])) beg++; while (ed > beg && iswspace(mtlName[ed - 1])) ed--; mtlName = mtlName.substr(beg,ed - beg); objParts.back().material = mtlReader.materials[mtlName]; objParts.back().texStrDiffuse = mtlReader.mapKdStrs[mtlName]; } else if (wstr == L"f") { // // 几何面 // VertexPosNormalTex vertex; DWORD vpi[3],vni[3],vti[3]; wchar_t ignore; // 顶点位置索引/纹理坐标索引/法向量索引 // 原来右手坐标系下顶点顺序是逆时针排布 // 现在需要转变为左手坐标系就需要将三角形顶点反过来输入 for (int i = 2; i >= 0; --i) { wfin >> vpi[i] >> ignore >> vti[i] >> ignore >> vni[i]; } for (int i = 0; i < 3; ++i) { vertex.pos = positions[vpi[i] - 1]; vertex.normal = normals[vni[i] - 1]; vertex.tex = texCoords[vti[i] - 1]; AddVertex(vertex,vpi[i],vti[i],vni[i]); } while (iswblank(wfin.peek())) wfin.get(); // 几何面顶点数可能超过了3,不支持该格式 if (wfin.peek() != 'n') return false; } } // 顶点数不超过WORD的最大值的话就使用16位WORD存储 for (auto& part : objParts) { if (part.vertices.size() < 65535) { for (auto& i : part.indices32) { part.indices16.push_back((WORD)i); } part.indices32.clear(); } } XMStoreFloat3(&vMax,vecMax); XMStoreFloat3(&vMin,vecMin); return true; } bool ObjReader::ReadMbo(const wchar_t * mboFileName) { // [Part数目] 4字节 // [AABB盒顶点vMax] 12字节 // [AABB盒顶点vMin] 12字节 // [Part // [漫射光材质文件名]520字节 // [材质]64字节 // [顶点数]4字节 // [索引数]4字节 // [顶点]32*顶点数 字节 // [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535 // ] // ... std::ifstream fin(mboFileName,std::ios::in | std::ios::binary); if (!fin.is_open()) return false; UINT parts = (UINT)objParts.size(); // [Part数目] 4字节 fin.read(reinterpret_cast<char*>(&parts),sizeof(UINT)); objParts.resize(parts); // [AABB盒顶点vMax] 12字节 fin.read(reinterpret_cast<char*>(&vMax),sizeof(XMFLOAT3)); // [AABB盒顶点vMin] 12字节 fin.read(reinterpret_cast<char*>(&vMin),sizeof(XMFLOAT3)); for (UINT i = 0; i < parts; ++i) { wchar_t filePath[MAX_PATH]; // [漫射光材质文件名]520字节 fin.read(reinterpret_cast<char*>(filePath),MAX_PATH * sizeof(wchar_t)); objParts[i].texStrDiffuse = filePath; // [材质]64字节 fin.read(reinterpret_cast<char*>(&objParts[i].material),sizeof(Material)); UINT vertexCount,indexCount; // [顶点数]4字节 fin.read(reinterpret_cast<char*>(&vertexCount),sizeof(UINT)); // [索引数]4字节 fin.read(reinterpret_cast<char*>(&indexCount),sizeof(UINT)); // [顶点]32*顶点数 字节 objParts[i].vertices.resize(vertexCount); fin.read(reinterpret_cast<char*>(objParts[i].vertices.data()),vertexCount * sizeof(VertexPosNormalTex)); if (vertexCount > 65535) { // [索引]4*索引数 字节 objParts[i].indices32.resize(indexCount); fin.read(reinterpret_cast<char*>(objParts[i].indices32.data()),indexCount * sizeof(DWORD)); } else { // [索引]2*索引数 字节 objParts[i].indices16.resize(indexCount); fin.read(reinterpret_cast<char*>(objParts[i].indices16.data()),indexCount * sizeof(WORD)); } } fin.close(); return true; } bool ObjReader::WriteMbo(const wchar_t * mboFileName) { // [Part数目] 4字节 // [AABB盒顶点vMax] 12字节 // [AABB盒顶点vMin] 12字节 // [Part // [环境光材质文件名]520字节 // [漫射光材质文件名]520字节 // [材质]64字节 // [顶点数]4字节 // [索引数]4字节 // [顶点]32*顶点数 字节 // [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535 // ] // ... std::ofstream fout(mboFileName,std::ios::out | std::ios::binary); UINT parts = (UINT)objParts.size(); // [Part数目] 4字节 fout.write(reinterpret_cast<const char*>(&parts),sizeof(UINT)); // [AABB盒顶点vMax] 12字节 fout.write(reinterpret_cast<const char*>(&vMax),sizeof(XMFLOAT3)); // [AABB盒顶点vMin] 12字节 fout.write(reinterpret_cast<const char*>(&vMin),sizeof(XMFLOAT3)); // [Part for (UINT i = 0; i < parts; ++i) { wchar_t filePath[MAX_PATH]; wcscpy_s(filePath,objParts[i].texStrDiffuse.c_str()); // [漫射光材质文件名]520字节 fout.write(reinterpret_cast<const char*>(filePath),MAX_PATH * sizeof(wchar_t)); // [材质]64字节 fout.write(reinterpret_cast<const char*>(&objParts[i].material),sizeof(Material)); UINT vertexCount = (UINT)objParts[i].vertices.size(); // [顶点数]4字节 fout.write(reinterpret_cast<const char*>(&vertexCount),sizeof(UINT)); UINT indexCount; if (vertexCount > 65535) { indexCount = (UINT)objParts[i].indices32.size(); // [索引数]4字节 fout.write(reinterpret_cast<const char*>(&indexCount),sizeof(UINT)); // [顶点]32*顶点数 字节 fout.write(reinterpret_cast<const char*>(objParts[i].vertices.data()),vertexCount * sizeof(VertexPosNormalTex)); // [索引]4*索引数 字节 fout.write(reinterpret_cast<const char*>(objParts[i].indices32.data()),indexCount * sizeof(DWORD)); } else { indexCount = (UINT)objParts[i].indices16.size(); // [索引数]4字节 fout.write(reinterpret_cast<const char*>(&indexCount),vertexCount * sizeof(VertexPosNormalTex)); // [索引]2*索引数 字节 fout.write(reinterpret_cast<const char*>(objParts[i].indices16.data()),indexCount * sizeof(WORD)); } } // ] fout.close(); return true; } void ObjReader::AddVertex(const VertexPosNormalTex& vertex,DWORD vni) { std::wstring idxStr = std::to_wstring(vpi) + L"/" + std::to_wstring(vti) + L"/" + std::to_wstring(vni); // 寻找是否有重复顶点 auto it = vertexCache.find(idxStr); if (it != vertexCache.end()) { objParts.back().indices32.push_back(it->second); } else { objParts.back().vertices.push_back(vertex); DWORD pos = (DWORD)objParts.back().vertices.size() - 1; vertexCache[idxStr] = pos; objParts.back().indices32.push_back(pos); } } bool MtlReader::ReadMtl(const wchar_t * mtlFileName) { materials.clear(); mapKdStrs.clear(); std::wifstream wfin(mtlFileName); std::locale china("chs"); wfin.imbue(china); if (!wfin.is_open()) return false; std::wstring wstr; std::wstring currMtl; for (;;) { if (!(wfin >> wstr)) break; if (wstr[0] == '#') { // // 忽略注释所在行 // while (wfin.get() != 'n') continue; } else if (wstr == L"newmtl") { // // 新材质 // std::getline(wfin,currMtl); // 去掉前后空格 size_t beg = 0,ed = currMtl.size(); while (iswspace(currMtl[beg])) beg++; while (ed > beg && iswspace(currMtl[ed - 1])) ed--; currMtl = currMtl.substr(beg,ed - beg); } else if (wstr == L"Ka") { // // 环境光反射颜色 // XMFLOAT4& ambient = materials[currMtl].Ambient; wfin >> ambient.x >> ambient.y >> ambient.z; if (ambient.w == 0.0f) ambient.w = 1.0f; } else if (wstr == L"Kd") { // // 漫射光反射颜色 // XMFLOAT4& diffuse = materials[currMtl].Diffuse; wfin >> diffuse.x >> diffuse.y >> diffuse.z; if (diffuse.w == 0.0f) diffuse.w = 1.0f; } else if (wstr == L"Ks") { // // 镜面光反射颜色 // XMFLOAT4& specular = materials[currMtl].Specular; wfin >> specular.x >> specular.y >> specular.z; } else if (wstr == L"Ns") { // // 镜面系数 // wfin >> materials[currMtl].Specular.w; } else if (wstr == L"d" || wstr == L"Tr") { // // d为不透明度 Tr为透明度 // float alpha; wfin >> alpha; if (wstr == L"Tr") alpha = 1.0f - alpha; materials[currMtl].Ambient.w = alpha; materials[currMtl].Diffuse.w = alpha; } else if (wstr == L"map_Kd") { // // map_Kd为漫反射使用的纹理 // std::wstring fileName; std::getline(wfin,fileName); // 去掉前后空格 size_t beg = 0,ed = fileName.size(); while (iswspace(fileName[beg])) beg++; while (ed > beg && iswspace(fileName[ed - 1])) ed--; fileName = fileName.substr(beg,ed - beg); // 追加路径 std::wstring dir = mtlFileName; size_t pos; if ((pos = dir.find_last_of('/')) == std::wstring::npos && (pos = dir.find_last_of('')) == std::wstring::npos) pos = 0; else pos += 1; mapKdStrs[currMtl] = dir.erase(pos) + fileName; } } return true; } 其中 在改为读取.mbo文件后,原本读取.obj需要耗时3s,现在可以降到2ms以内,大幅提升了读取效率。其关键点就在于要构造连续性的二进制数据以减少读取次数,并剔除掉原本读取.obj时的各种词法分析部分(在该部分也浪费了大量的时间)。 由于
Model类现在使用一个模型类来管理从 struct ModelPart { // 使用模板别名(C++11)简化类型名 template <class T> using ComPtr = Microsoft::WRL::ComPtr<T>; ModelPart() : material(),texDiffuse(),vertexBuffer(),indexBuffer(),vertexCount(),indexCount(),indexFormat() {} ModelPart(const ModelPart&) = default; ModelPart& operator=(const ModelPart&) = default; ModelPart(ModelPart&&) = default; ModelPart& operator=(ModelPart&&) = default; Material material; ComPtr<ID3D11ShaderResourceView> texDiffuse; ComPtr<ID3D11Buffer> vertexBuffer; ComPtr<ID3D11Buffer> indexBuffer; UINT vertexCount; UINT indexCount; DXGI_FORMAT indexFormat; }; struct Model { // 使用模板别名(C++11)简化类型名 template <class T> using ComPtr = Microsoft::WRL::ComPtr<T>; Model(); Model(ID3D11Device * device,const ObjReader& model); // 设置缓冲区 template<class VertexType,class IndexType> Model(ID3D11Device * device,const Geometry::MeshData<VertexType,IndexType>& meshData); template<class VertexType,const std::vector<VertexType> & vertices,const std::vector<IndexType>& indices); Model(ID3D11Device * device,const void* vertices,UINT vertexSize,UINT vertexCount,const void * indices,UINT indexCount,DXGI_FORMAT indexFormat); // // 设置模型 // void SetModel(ID3D11Device * device,const ObjReader& model); // // 设置网格 // template<class VertexType,class IndexType> void SetMesh(ID3D11Device * device,IndexType>& meshData); template<class VertexType,const std::vector<IndexType>& indices); void SetMesh(ID3D11Device * device,DXGI_FORMAT indexFormat); // // 调试 // // 设置调试对象名 // 若模型被重新设置,调试对象名也需要被重新设置 void SetDebugObjectName(const std::string& name); std::vector<ModelPart> modelParts; DirectX::BoundingBox boundingBox; UINT vertexStride; }; GameObject类因为下一章还会讲到硬件实例化,所以 class GameObject { public: // 使用模板别名(C++11)简化类型名 template <class T> using ComPtr = Microsoft::WRL::ComPtr<T>; GameObject(); // 获取位置 DirectX::XMFLOAT3 GetPosition() const; // // 获取包围盒 // DirectX::BoundingBox GetLocalBoundingBox() const; DirectX::BoundingBox GetBoundingBox() const; DirectX::BoundingOrientedBox GetBoundingOrientedBox() const; // // 设置模型 // void SetModel(Model&& model); void SetModel(const Model& model); // // 设置矩阵 // void SetWorldMatrix(const DirectX::XMFLOAT4X4& world); void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX world); // // 绘制 // // 绘制对象 void Draw(ID3D11DeviceContext * deviceContext,BasicEffect& effect); // // 调试 // // 设置调试对象名 // 若模型被重新设置,调试对象名也需要被重新设置 void SetDebugObjectName(const std::string& name); private: Model m_Model; // 模型 DirectX::XMFLOAT4X4 m_WorldMatrix; // 世界矩阵 }; GameObject::Draw方法该方法根据已有的模型数据绘制出来: void GameObject::Draw(ID3D11DeviceContext * deviceContext,BasicEffect & effect) { UINT strides = m_Model.vertexStride; UINT offsets = 0; for (auto& part : m_Model.modelParts) { // 设置顶点/索引缓冲区 deviceContext->IASetVertexBuffers(0,1,part.vertexBuffer.GetAddressOf(),&strides,&offsets); deviceContext->IASetIndexBuffer(part.indexBuffer.Get(),part.indexFormat,0); // 更新数据并应用 effect.SetWorldMatrix(XMLoadFloat4x4(&m_WorldMatrix)); effect.SetTextureDiffuse(part.texDiffuse.Get()); effect.SetMaterial(part.material); effect.Apply(deviceContext); deviceContext->DrawIndexed(part.indexCount,0); } } 剩余一些不是很重大的变动就不放出来了,比如 模型加载演示这里我选用了之前合作项目时设计师完成的房屋模型,经过 Obj文件完整说明今天在修改的时候查到一份十分详细的obj和mtl文件说明,有兴趣的读者可以点击下面的链接: Object Files (.obj) Material Files (.mtl) DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- Windows API一日一练 24 DrawText函数
- Windows桌面应用程序和Windows应用商店应用程序有什么区别
- windows – 使用Lazarus为Mac编写代码
- windows – 将注册表值添加到域上的所有用户
- 在Windows上搭建Nuget服务
- 在Windows Phone 8 C#app中不调用静态字段初始值设定项
- 在Windows中,使用命令行,如何检查远程端口是否打开?
- windows-server-2003 – 更改Windows域用户名
- windows – 此任务要求指定的用户帐户具有作为批处理作业权
- 来自IsolatedStorage的Windows Phone 7 Silverlight绑定映像