由于本人打算使用Assimp来加载模型,这里记录一下tinyobjloader库的使用。之前也研究过fbxsdk,除了骨骼动画暂未读取外,代码自认为还算可靠。
tinyobjloader地址:
https://github.com/tinyobjloader/tinyobjloader
而tinyobjloader库只有一个头文件,可以很方便的读取obj文件。支持材质,不过不支持骨骼动画,vulkan官方教程便是使用的它。不过没有骨骼动画还是有很大的局限性,这里只是分享一下怎么读取材质和拆分网格。
我抽象了一个ModelObject类表示模型数据,而一个ModelObject包含多个Sub模型,每个Sub模型使用同一材质(有的人称为图元Primitive或DrawCall)。最后我将其保存为文件,这样我的引擎便可直接解析ModelObject文件,而不是再去读obj、fbx等其他文件了。
这一节可以跳过,下一节是真正使用tinyobjloader库。
- //一个文件会有多个ModelObject,一个ModelObject根据材质分为多个ModelSub
- //注意ModelSub为一个材质,需要读取时合并网格
- class ModelObject
- {
- friend class VK;
- public:
- //从源文件加载模型
- static vector
Create(string_view path_name) ; -
- void Load(string_view path_name);
-
- //保存到文件
- void SaveToFile(string_view path_name);
-
- private:
- vector
_allSub; //下标减1 为材质,0为没有材质 -
- vector
_allVertex;//顶点缓存 - vector<uint32_t> _allIndex;//索引缓存
-
- vector
_allMaterial;//所有材质 -
- //------------------不同格式加载实现--------------------------------
- //obj
- static vector
_load_obj(string_view path_name); - static vector
_load_obj_2(string_view path_name); - };
ModelObjectSub只是表示在索引缓存的一段范围:
- //模型三角形范围
- struct ModelTriangleRange
- {
- ModelTriangleRange() :
- _countTriangle{ 0 },
- _offsetIndex{ 0 }
- {}
-
- size_t _countTriangle;
- size_t _offsetIndex;
- };
-
- //子模型对象 范围
- struct ModelObjectSub
- {
- ModelTriangleRange _range;
- };
而ModelObjectMaterial表示模型材质:
- //! 材质
- struct Material
- {
- glm::vec4 _diffuseAlbedo;//漫反射率
- glm::vec3 _fresnelR0; //菲涅耳系数
- float _roughness; //粗糙度
- };
-
- //模型对象 材质
- struct ModelObjectMaterial
- {
- //最后转为Model时,变为可以用的着色器资源
- Material _material;
- string _materialName;
- //路径为空,则表示没有(VK加载时会返回0)
- string _pathTexDiffuse;
- string _pathTexNormal;
- };
首先引入头文件:
- #define TINYOBJLOADER_IMPLEMENTATION
- #include
接口原型,将obj文件变为多个ModelObject:
vector ModelObject::_load_obj_2(string_view path_name);
取得文件名,和文件所在路径(会自动加载路径下的同名mtl文件,里面包含了材质):
- string str_path = string{ path_name };
- string str_base = String::EraseFilename(path_name);
-
- const char* filename = str_path.c_str();
- const char* basepath = str_base.c_str();
基本数据:
- debug(format("开始加载obj文件:{},{}", filename, basepath));
-
- bool triangulate = true;//三角化
-
- tinyobj::attrib_t attrib; // 所有的数据放在这里
- std::vector
shape_t> shapes;//子模型 - std::vector
material_t> materials;//材质 -
- std::string warn;
- std::string err;
加载并打印一些信息:
- bool b_read = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, filename,
- basepath, triangulate);
-
- //打印错误
- if (!warn.empty())
- debug_warn(warn);
-
- if (!err.empty())
- debug_err(err);
-
- if (!b_read)
- {
- debug_err(format("读取obj文件失败:{}", path_name));
- return {};
- }
- debug(format("顶点数:{}", attrib.vertices.size() / 3));
- debug(format("法线数:{}", attrib.normals.size() / 3));
- debug(format("UV数:{}", attrib.texcoords.size() / 2));
- debug(format("子模型数:{}", shapes.size()));
- debug(format("材质数:{}", materials.size()));
这将打印以下数据:
由于obj文件只产生一个ModelObject,我们如下添加一个,并返回顶点、索引、材质等引用,用于后面填充:
- //obj只有一个ModelObject
- vector
ret; -
- ModelObject* model_object = new ModelObject;
- std::vector
& mo_vertices = model_object->_allVertex; - std::vector<uint32_t>& mo_indices = model_object->_allIndex;
- vector
& mo_material = model_object->_allMaterial; -
- ret.push_back(model_object);
首先记录材质信息:
- //------------------获取材质-------------------
- mo_material.resize(materials.size());
- for (size_t i = 0; i < materials.size(); ++i)
- {
- tinyobj::material_t m = materials[i];
- debug(format("材质:{},{}", i, m.name));
-
- ModelObjectMaterial& material = model_object->_allMaterial[i];
- material._materialName = m.name;
-
- material._material._diffuseAlbedo = { m.diffuse[0], m.diffuse[1], m.diffuse[2], 1.0f };
- material._material._fresnelR0 = { m.specular[0], m.specular[1], m.specular[2] };
- material._material._roughness = ShininessToRoughness(m.shininess);
-
- if(!m.diffuse_texname.empty())
- material._pathTexDiffuse = str_base + m.diffuse_texname;
- if (!m.normal_texname.empty())
- material._pathTexNormal = str_base + m.normal_texname;
- }
这将产生以下输出:
然后遍历shape,按材质记录顶点。这里需要注意的是,一个obj文件有多个shape,每个shape由n个三角面组成。而每个三角形拥有独立的材质编号,所以这里按材质分别记录,而不是一般的合并为整体:
- //------------------获取模型-------------------
-
- //按 材质 放入面的顶点
- vector
index_t>> all_sub; - all_sub.resize(1 + materials.size());//0为默认
-
- for (size_t i = 0; i < shapes.size(); i++)
- {//每一个子shape
- tinyobj::shape_t& shape = shapes[i];
- size_t num_index = shape.mesh.indices.size();
- size_t num_face = shape.mesh.num_face_vertices.size();
- debug(format("读取子模型:{},{}", i, shape.name));
- debug(format("索引数:{};面数:{}", num_index, num_face));
-
- //当前mesh下标(每个面递增3)
- size_t index_offset = 0;
- //每一个面
- for (size_t j = 0; j < num_face; ++j)
- {
- int index_mat = shape.mesh.material_ids[j];//每个面的材质
- vector
index_t>& sub_idx = all_sub[1 + index_mat]; - sub_idx.push_back(shape.mesh.indices[index_offset++]);
- sub_idx.push_back(shape.mesh.indices[index_offset++]);
- sub_idx.push_back(shape.mesh.indices[index_offset++]);
- }
- }
按材质记录顶点的索引(tinyobj::index_t)后,接下来就是读取顶点的实际数据,并防止重复读取:
- //生成子模型,并填入顶点
- std::unordered_map
index_t, size_t, hash_idx, equal_idx> - uniqueVertices;//避免重复插入顶点
- size_t i = 0;
- for (vector
index_t>& sub_idx : all_sub) - {
- ModelObjectSub sub;
- sub._range._offsetIndex = i;
- sub._range._countTriangle = sub_idx.size() / 3;
- model_object->_allSub.push_back(sub);
-
- for (tinyobj::index_t& idx : sub_idx)
- {
- auto iter = uniqueVertices.find(idx);
- if (iter == uniqueVertices.end())
- {
- Vertex v;
- //v
- v._pos[0] = attrib.vertices[idx.vertex_index * 3 + 0];
- v._pos[1] = attrib.vertices[idx.vertex_index * 3 + 1];
- v._pos[2] = attrib.vertices[idx.vertex_index * 3 + 2];
-
- // vt
- v._texCoord[0] = attrib.texcoords[idx.texcoord_index * 2 + 0];
- v._texCoord[1] = attrib.texcoords[idx.texcoord_index * 2 + 1];
- v._texCoord[1] = 1.0f - v._texCoord[1];
-
- uniqueVertices[idx] = mo_vertices.size();
- mo_indices.push_back((uint32_t)mo_vertices.size());
- mo_vertices.push_back(v);
- }
- else
- {
- mo_indices.push_back((uint32_t)iter->second);
- }
- ++i;
- }
- }
- debug(format("解析obj模型完成:v{},i{}", mo_vertices.size(), mo_indices.size()));
- return ret;
上面用到的哈希函数:
- struct equal_idx
- {
- bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const
- {
- return a.vertex_index == b.vertex_index
- && a.texcoord_index == b.texcoord_index
- && a.normal_index == b.normal_index;
- }
- };
-
- struct hash_idx
- {
- size_t operator()(const tinyobj::index_t& a) const
- {
- return ((a.vertex_index
- ^ a.texcoord_index << 1) >> 1)
- ^ (a.normal_index << 1);
- }
- };
最后打印出来的数据如下:
对于材质的处理,漫反射贴图即是基本贴图,而法线(凹凸)贴图、漫反射率、菲涅耳系数、光滑度等需要渲染管线支持并与光照计算产生效果。
可以此处获取最新的源码(我会改用Assimp,并添加骨骼动画、Blinn-Phong光照模型),也可以用后面的:DND/src/DND.ModelObject.cpp · 略游/DND - Gitee.com
如果有用,欢迎点赞、收藏、关注,我将更新更多C++相关的文章。
- #define TINYOBJLOADER_IMPLEMENTATION
- #include
-
- struct equal_idx
- {
- bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const
- {
- return a.vertex_index == b.vertex_index
- && a.texcoord_index == b.texcoord_index
- && a.normal_index == b.normal_index;
- }
- };
-
- struct hash_idx
- {
- size_t operator()(const tinyobj::index_t& a) const
- {
- return ((a.vertex_index
- ^ a.texcoord_index << 1) >> 1)
- ^ (a.normal_index << 1);
- }
- };
-
- float ShininessToRoughness(float Ypoint)
- {
- float a = -1;
- float b = 2;
-
- float c;
- c = (Ypoint / 100) - 1;
-
- float D;
- D = b * b - (4 * a * c);
-
- float x1;
- x1 = (-b + sqrt(D)) / (2 * a);
-
- return x1;
- }
-
- vector
ModelObject::_load_obj_2(string_view path_name) - {
- string str_path = string{ path_name };
- string str_base = String::EraseFilename(path_name);
-
- const char* filename = str_path.c_str();
- const char* basepath = str_base.c_str();
- bool triangulate = true;
-
- debug(format("开始加载obj文件:{},{}", filename, basepath));
-
- tinyobj::attrib_t attrib; // 所有的数据放在这里
- std::vector
shape_t> shapes;//子模型 - std::vector
material_t> materials; -
- std::string warn;
- std::string err;
-
- bool b_read = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, filename,
- basepath, triangulate);
-
- //打印错误
- if (!warn.empty())
- debug_warn(warn);
-
- if (!err.empty())
- debug_err(err);
-
- if (!b_read)
- {
- debug_err(format("读取obj文件失败:{}", path_name));
- return {};
- }
- debug(format("顶点数:{}", attrib.vertices.size() / 3));
- debug(format("法线数:{}", attrib.normals.size() / 3));
- debug(format("UV数:{}", attrib.texcoords.size() / 2));
- debug(format("子模型数:{}", shapes.size()));
- debug(format("材质数:{}", materials.size()));
-
- //obj只有一个ModelObject
- vector
ret; - ModelObject* model_object = new ModelObject;
- std::vector
& mo_vertices = model_object->_allVertex; - std::vector<uint32_t>& mo_indices = model_object->_allIndex;
- vector
& mo_material = model_object->_allMaterial; - ret.push_back(model_object);
-
- //------------------获取材质-------------------
- mo_material.resize(materials.size());
- for (size_t i = 0; i < materials.size(); ++i)
- {
- tinyobj::material_t m = materials[i];
- debug(format("材质:{},{}", i, m.name));
-
- ModelObjectMaterial& material = model_object->_allMaterial[i];
- material._materialName = m.name;
-
- material._material._diffuseAlbedo = { m.diffuse[0], m.diffuse[1], m.diffuse[2], 1.0f };
- material._material._fresnelR0 = { m.specular[0], m.specular[1], m.specular[2] };
- material._material._roughness = ShininessToRoughness(m.shininess);
-
- if(!m.diffuse_texname.empty())
- material._pathTexDiffuse = str_base + m.diffuse_texname;
- if (!m.normal_texname.empty())//注意这里凹凸贴图(bump_texname)更常见
- material._pathTexNormal = str_base + m.normal_texname;
- }
-
- //------------------获取模型-------------------
-
- //按 材质 放入面的顶点
- vector
index_t>> all_sub; - all_sub.resize(1 + materials.size());//0为默认
-
- for (size_t i = 0; i < shapes.size(); i++)
- {//每一个子shape
- tinyobj::shape_t& shape = shapes[i];
- size_t num_index = shape.mesh.indices.size();
- size_t num_face = shape.mesh.num_face_vertices.size();
- debug(format("读取子模型:{},{}", i, shape.name));
- debug(format("索引数:{};面数:{}", num_index, num_face));
-
- //当前mesh下标(每个面递增3)
- size_t index_offset = 0;
- //每一个面
- for (size_t j = 0; j < num_face; ++j)
- {
- int index_mat = shape.mesh.material_ids[j];//每个面的材质
- vector
index_t>& sub_idx = all_sub[1 + index_mat]; - sub_idx.push_back(shape.mesh.indices[index_offset++]);
- sub_idx.push_back(shape.mesh.indices[index_offset++]);
- sub_idx.push_back(shape.mesh.indices[index_offset++]);
- }
- }
- //生成子模型,并填入顶点
- std::unordered_map
index_t, size_t, hash_idx, equal_idx> - uniqueVertices;//避免重复插入顶点
- size_t i = 0;
- for (vector
index_t>& sub_idx : all_sub) - {
- ModelObjectSub sub;
- sub._range._offsetIndex = i;
- sub._range._countTriangle = sub_idx.size() / 3;
- model_object->_allSub.push_back(sub);
-
- for (tinyobj::index_t& idx : sub_idx)
- {
- auto iter = uniqueVertices.find(idx);
- if (iter == uniqueVertices.end())
- {
- Vertex v;
- //v
- v._pos[0] = attrib.vertices[idx.vertex_index * 3 + 0];
- v._pos[1] = attrib.vertices[idx.vertex_index * 3 + 1];
- v._pos[2] = attrib.vertices[idx.vertex_index * 3 + 2];
-
- // vt
- v._texCoord[0] = attrib.texcoords[idx.texcoord_index * 2 + 0];
- v._texCoord[1] = attrib.texcoords[idx.texcoord_index * 2 + 1];
- v._texCoord[1] = 1.0f - v._texCoord[1];
-
- uniqueVertices[idx] = mo_vertices.size();
- mo_indices.push_back((uint32_t)mo_vertices.size());
- mo_vertices.push_back(v);
- }
- else
- {
- mo_indices.push_back((uint32_t)iter->second);
- }
- ++i;
- }
- }
- debug(format("解析obj模型完成:v{},i{}", mo_vertices.size(), mo_indices.size()));
- return ret;
- }