DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率
前言
一个模型通常是由三个部分组成:网格、纹理、材质。在一开始的时候,我们是通过geometry类来生成简单几何体的网格。但现在我们需要寻找合适的方式去表述一个复杂的网格,而且包含网格的文件类型多种多样,对应的描述方式也存在着差异。这一章我们主要研究obj格式文件的读取。
directx11 with windows sdk完整目录
.obj格式
.obj格式是alias|wavefront公司推出的一种模型文件格式,通常以文本形式进行描述,因此你可以按记事本来打开查看里面的内容。通过市面上的一些常见的建模软件如3dsmax,maya等都可以导出.obj文件。一些游戏引擎如unity3d也支持导入.obj格式的模型。该文件可以直接描述多边形、法向量、纹理坐标等等信息。
.obj文件结构简述
.obj文件内部的每一行具体含义取决于开头以空格、制表符分隔的关键字是什么。这里只根据当前项目需要的部分来描述关键字
关键字 | 含义 |
---|---|
# | 这一行是一条注释 |
顶点数据:
关键字 | 含义 |
---|---|
v | 这是一个3d顶点坐标 |
vt | 这是一个纹理坐标 |
vn | 这是一个3d法向量 |
元素:
关键字 | 含义 |
---|---|
f | 这是一个面,这里我们只支持三角形构成的面 |
组合:
关键字 | 含义 |
---|---|
g | 这是一个组,后面接着的内容是组的名称 |
o | 这是一个对象,后面接着的内容是对象的名称 |
材质:
关键字 | 含义 |
---|---|
mtllib | 需要加载.mtl材质文件,后面接着的内容是文件名 |
usemtl | 使用加载的.mtl材质文件中的某一材质,后面接着的内容是材质名 |
.mtl文件结构简述
.mtl文件内部描述方式和.obj文件一样,但里面使用的关键字有所不同
关键字 | 含义 |
---|---|
# | 这一行是一条注释 |
newmtl | 这是一个新的材质,后面接着的内容是材质名称 |
材质描述:
关键字 | 含义 |
---|---|
ka | 环境光反射颜色 |
kd | 漫射光反射颜色 |
ks | 镜面反射光反射颜色 |
d | 不透明度,即alpha值 |
tr | 透明度,即1.0 - alpha值 |
map_ka | 环境光反射指定的纹理文件 |
map_kd | 漫射光反射指定的纹理文件 |
简单示例
现在要通过.obj文件来描述一个平面正方形草丛。ground.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对应的顶点
注意: 索引的初始值在.obj中为1,而不是0!
而诸如1/1/1
这样的三索引对应的含义为:顶点坐标索引/纹理坐标索引/法向量索引
若写成1//1
,则表明不使用纹理坐标,但现在在我们的项目中不允许缺少上面任何一种索引
这样在一个f
里面出现顶点坐标索引/纹理坐标索引/法向量索引
的次数说明了该面的顶点数目,目前我们也仅考虑支持三角形面
一个模型最少需要包含一个组或一个对象
注意:.obj纹理坐标是基于笛卡尔坐标系的,即(0.3, 0.7)对应的是实际的纹理坐标(0.3, 0.3),即需要做(x, 1.0 - y)的变换
而.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字节 // [漫射光材质文件名]520字节 // [材质]64字节 // [顶点数]4字节 // [索引数]4字节 // [顶点]32*顶点数 字节 // [索引]2(或4)*索引数 字节,取决于顶点数是否不超过65535 // ] // ...
这里将.obj中的一个组或一个对象定义为.mbo格式中的一个模型部分,然后顶点使用的是vertexposnormaltex
类型,大小为32字节。索引使用word
或dword
类型,若当前part不同的顶点数超过65535,则必须使用dword
类型来存储索引。
环境光/漫射光材质文件名使用的是wchar_t[max_path]
的数组,大小为2*260字节。
但要注意一开始从.obj导出的顶点数组是没有经过处理(包含重复顶点),需要通过一定的操作分离出顶点数组和索引数组才能传递给.mbo格式。
objreader--读取.obj/.mbo格式模型
objreader.h中包含了objreader
类和mtlreader
类:
#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 texstra; // 环境光纹理文件名,需为相对路径,且在mbo必须占260字节 std::wstring texstrd; // 漫射光纹理文件名,需为相对路径,在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, 0.2f, 1.0f); objparts.back().material.diffuse = xmfloat4(0.8f, 0.8f, 0.8f, 1.0f); objparts.back().material.specular = xmfloat4(1.0f, 1.0f, 1.0f, 1.0f); vertexcache.clear(); } else if (wstr == l"v") { // // 顶点位置 // xmfloat3 pos; wfin >> pos.x >> pos.y >> 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") { // // 顶点法向量 // float x, y, z; wfin >> x >> y >> z; normals.emplace_back(xmfloat3(x, y, 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().texstra = mtlreader.mapkastrs[mtlname]; objparts.back().texstrd = mtlreader.mapkdstrs[mtlname]; } else if (wstr == l"f") { // // 几何面 // vertexposnormaltex vertex; dword vpi, vni, vti; wchar_t ignore; // 确定 // 顶点位置索引/纹理坐标索引/法向量索引 wfin >> vpi >> ignore >> vti >> ignore >> vni; vertex.pos = positions[vpi - 1]; vertex.normal = normals[vni - 1]; vertex.tex = texcoords[vti - 1]; addvertex(vertex, vpi, vti, vni); wfin >> vpi >> ignore >> vti >> ignore >> vni; vertex.pos = positions[vpi - 1]; vertex.normal = normals[vni - 1]; vertex.tex = texcoords[vti - 1]; addvertex(vertex, vpi, vti, vni); wfin >> vpi >> ignore >> vti >> ignore >> vni; vertex.pos = positions[vpi - 1]; vertex.normal = normals[vni - 1]; vertex.tex = texcoords[vti - 1]; addvertex(vertex, vpi, vti, vni); 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字节 // [漫射光材质文件名]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].texstra = filepath; // [漫射光材质文件名]520字节 fin.read(reinterpret_cast<char*>(filepath), max_path * sizeof(wchar_t)); objparts[i].texstrd = 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].texstra.c_str()); // [环境光材质文件名]520字节 fout.write(reinterpret_cast<const char*>(filepath), max_path * sizeof(wchar_t)); wcscpy_s(filepath, objparts[i].texstrd.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), sizeof(uint)); // [顶点]32*顶点数 字节 fout.write(reinterpret_cast<const char*>(objparts[i].vertices.data()), 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 vpi, dword vti, 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 = objparts.back().vertices.size() - 1; vertexcache[idxstr] = pos; objparts.back().indices32.push_back(pos); } } bool mtlreader::readmtl(const wchar_t * mtlfilename) { materials.clear(); mapkastrs.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_ka" || wstr == l"map_kd") { // // map_ka为环境光反射使用的纹理,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; if (wstr == l"map_ka") mapkastrs[currmtl] = dir.erase(pos) + filename; else mapkdstrs[currmtl] = dir.erase(pos) + filename; } } return true; }
其中addvertex
方法用于去除重复的顶点,并构建索引数组。
在改为读取.mbo文件后,原本读取.obj需要耗时3s,现在可以降到2ms以内,大幅提升了读取效率。其关键点就在于要构造连续性的二进制数据以减少读取次数,并剔除掉原本读取.obj时的各种词法分析部分(在该部分也浪费了大量的时间)。
由于objreader
类对.obj格式的文件要求比较严格,如果出现不能正确加载的现象,请检查是否出现下面这些情况,否则需要自行修改.obj/.mtl文件,或者给objreader
实现更多的功能:
- 使用了/将下一行的内容连接在一起表示一行
- 存在索引为负数
- 使用了类似1//2这样的顶点(即不包含纹理坐标的顶点)
- 使用了绝对路径的文件引用
- 相对路径使用了.和..两种路径格式
- 若.mtl材质文件不存在,则内部会使用默认材质值
- 若.mtl内部没有指定纹理文件引用,需要另外自行加载纹理
- f的顶点数不为3(网格只能以三角形构造,即一个f的顶点数只能为3)
gameobject类的改进
因为下一章还会讲到硬件实例化,所以gameobject
类在后期还会有所改动,现在只放出声明部分:
class gameobject { public: // 使用模板别名(c++11)简化类型名 template <class t> using comptr = microsoft::wrl::comptr<t>; struct gameobjectpart { material material; comptr<id3d11shaderresourceview> texa; comptr<id3d11shaderresourceview> texd; comptr<id3d11buffer> vertexbuffer; comptr<id3d11buffer> indexbuffer; uint vertexcount; uint indexcount; dxgi_format indexformat; }; gameobject(); // 获取位置 directx::xmfloat3 getposition() const; // 获取子模型 const gameobjectpart& getpart(size_t pos) const; // 获取包围盒 void getboundingbox(directx::boundingbox& box) const; void getboundingbox(directx::boundingbox& box, directx::fxmmatrix worldmatrix) const; // // 设置模型 // void setmodel(comptr<id3d11device> device, const objreader& model); // // 设置网格 // void setmesh(comptr<id3d11device> device, const geometry::meshdata& meshdata); void setmesh(comptr<id3d11device> device, const std::vector<vertexposnormaltex>& vertices, const std::vector<word> indices); void setmesh(comptr<id3d11device> device, const std::vector<vertexposnormaltex>& vertices, const std::vector<dword> indices); // // 设置纹理 // void settexture(comptr<id3d11shaderresourceview> texture); void settexture(comptr<id3d11shaderresourceview> texa, comptr<id3d11shaderresourceview> texd); void settexture(size_t partindex, comptr<id3d11shaderresourceview> texture); void settexture(size_t partindex, comptr<id3d11shaderresourceview> texa, comptr<id3d11shaderresourceview> texd); // // 设置材质 // void setmaterial(const material& material); void setmaterial(size_t partindex, const material& material); // // 设置矩阵 // void setworldmatrix(const directx::xmfloat4x4& world); void setworldmatrix(directx::fxmmatrix world); void settextransformmatrix(const directx::xmfloat4x4& textransform); void settextransformmatrix(directx::fxmmatrix textransform); // 绘制对象 void draw(comptr<id3d11devicecontext> devicecontext); private: void setmesh(comptr<id3d11device> device, const vertexposnormaltex* vertices, uint vertexcount, const void * indices, uint indexcount, dxgi_format indexformat); private: directx::xmfloat4x4 mworldmatrix; // 世界矩阵 directx::xmfloat4x4 mtextransform; // 纹理变换矩阵 std::vector<gameobjectpart> mparts; // 模型的各个部分 directx::boundingbox mboundingbox; // 模型的aabb盒 };
剩余一些不是很重大的变动就不放出来了,比如basicobjectfx
类和对应的hlsl的变动,可以查看源码(文首文末都有)。
模型加载演示
这里我选用了之前合作项目时设计师完成的房屋模型,经过objreader
加载后实装到gameobject
以进行绘制。效果如下:
上一篇: 科学计算库Numpy——numpy.ndarray
下一篇: Go Web:HttpRouter路由