• 【C++】obj模型文件解析(tiny_obj_loader)


    obj模型文件解析

    一、前言

            由于本人打算使用Assimp来加载模型,这里记录一下tinyobjloader库的使用。之前也研究过fbxsdk,除了骨骼动画暂未读取外,代码自认为还算可靠。

            tinyobjloader地址:

    https://github.com/tinyobjloader/tinyobjloader

            而tinyobjloader库只有一个头文件,可以很方便的读取obj文件。支持材质,不过不支持骨骼动画,vulkan官方教程便是使用的它。不过没有骨骼动画还是有很大的局限性,这里只是分享一下怎么读取材质和拆分网格。

     

    二、中间文件

            我抽象了一个ModelObject类表示模型数据,而一个ModelObject包含多个Sub模型,每个Sub模型使用同一材质(有的人称为图元PrimitiveDrawCall)。最后我将其保存为文件,这样我的引擎便可直接解析ModelObject文件,而不是再去读obj、fbx等其他文件了。

            这一节可以跳过,下一节是真正使用tinyobjloader库。

    1. //一个文件会有多个ModelObject,一个ModelObject根据材质分为多个ModelSub
    2. //注意ModelSub为一个材质,需要读取时合并网格
    3. class ModelObject
    4. {
    5. friend class VK;
    6. public:
    7. //从源文件加载模型
    8. static vector Create(string_view path_name);
    9. void Load(string_view path_name);
    10. //保存到文件
    11. void SaveToFile(string_view path_name);
    12. private:
    13. vector _allSub; //下标减1 为材质,0为没有材质
    14. vector _allVertex;//顶点缓存
    15. vector<uint32_t> _allIndex;//索引缓存
    16. vector _allMaterial;//所有材质
    17. //------------------不同格式加载实现--------------------------------
    18. //obj
    19. static vector _load_obj(string_view path_name);
    20. static vector _load_obj_2(string_view path_name);
    21. };

             ModelObjectSub只是表示在索引缓存的一段范围:

    1. //模型三角形范围
    2. struct ModelTriangleRange
    3. {
    4. ModelTriangleRange() :
    5. _countTriangle{ 0 },
    6. _offsetIndex{ 0 }
    7. {}
    8. size_t _countTriangle;
    9. size_t _offsetIndex;
    10. };
    11. //子模型对象 范围
    12. struct ModelObjectSub
    13. {
    14. ModelTriangleRange _range;
    15. };

             而ModelObjectMaterial表示模型材质:

    1. //! 材质
    2. struct Material
    3. {
    4. glm::vec4 _diffuseAlbedo;//漫反射率
    5. glm::vec3 _fresnelR0; //菲涅耳系数
    6. float _roughness; //粗糙度
    7. };
    8. //模型对象 材质
    9. struct ModelObjectMaterial
    10. {
    11. //最后转为Model时,变为可以用的着色器资源
    12. Material _material;
    13. string _materialName;
    14. //路径为空,则表示没有(VK加载时会返回0)
    15. string _pathTexDiffuse;
    16. string _pathTexNormal;
    17. };

    三、使用

            首先引入头文件:

    1. #define TINYOBJLOADER_IMPLEMENTATION
    2. #include

            接口原型,将obj文件变为多个ModelObject:

    vector ModelObject::_load_obj_2(string_view path_name);

             取得文件名,和文件所在路径(会自动加载路径下的同名mtl文件,里面包含了材质):

    1. string str_path = string{ path_name };
    2. string str_base = String::EraseFilename(path_name);
    3. const char* filename = str_path.c_str();
    4. const char* basepath = str_base.c_str();

             基本数据:

    1. debug(format("开始加载obj文件:{},{}", filename, basepath));
    2. bool triangulate = true;//三角化
    3. tinyobj::attrib_t attrib; // 所有的数据放在这里
    4. std::vectorshape_t> shapes;//子模型
    5. std::vectormaterial_t> materials;//材质
    6. std::string warn;
    7. std::string err;

            加载并打印一些信息:

    1. bool b_read = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, filename,
    2. basepath, triangulate);
    3. //打印错误
    4. if (!warn.empty())
    5. debug_warn(warn);
    6. if (!err.empty())
    7. debug_err(err);
    8. if (!b_read)
    9. {
    10. debug_err(format("读取obj文件失败:{}", path_name));
    11. return {};
    12. }
    13. debug(format("顶点数:{}", attrib.vertices.size() / 3));
    14. debug(format("法线数:{}", attrib.normals.size() / 3));
    15. debug(format("UV数:{}", attrib.texcoords.size() / 2));
    16. debug(format("子模型数:{}", shapes.size()));
    17. debug(format("材质数:{}", materials.size()));

             这将打印以下数据:

             由于obj文件只产生一个ModelObject,我们如下添加一个,并返回顶点、索引、材质等引用,用于后面填充:

    1. //obj只有一个ModelObject
    2. vector ret;
    3. ModelObject* model_object = new ModelObject;
    4. std::vector& mo_vertices = model_object->_allVertex;
    5. std::vector<uint32_t>& mo_indices = model_object->_allIndex;
    6. vector& mo_material = model_object->_allMaterial;
    7. ret.push_back(model_object);

             首先记录材质信息:

    1. //------------------获取材质-------------------
    2. mo_material.resize(materials.size());
    3. for (size_t i = 0; i < materials.size(); ++i)
    4. {
    5. tinyobj::material_t m = materials[i];
    6. debug(format("材质:{},{}", i, m.name));
    7. ModelObjectMaterial& material = model_object->_allMaterial[i];
    8. material._materialName = m.name;
    9. material._material._diffuseAlbedo = { m.diffuse[0], m.diffuse[1], m.diffuse[2], 1.0f };
    10. material._material._fresnelR0 = { m.specular[0], m.specular[1], m.specular[2] };
    11. material._material._roughness = ShininessToRoughness(m.shininess);
    12. if(!m.diffuse_texname.empty())
    13. material._pathTexDiffuse = str_base + m.diffuse_texname;
    14. if (!m.normal_texname.empty())
    15. material._pathTexNormal = str_base + m.normal_texname;
    16. }

             这将产生以下输出:

             然后遍历shape,按材质记录顶点。这里需要注意的是,一个obj文件有多个shape,每个shape由n个三角面组成。而每个三角形拥有独立的材质编号,所以这里按材质分别记录,而不是一般的合并为整体:

    1. //------------------获取模型-------------------
    2. //按 材质 放入面的顶点
    3. vectorindex_t>> all_sub;
    4. all_sub.resize(1 + materials.size());//0为默认
    5. for (size_t i = 0; i < shapes.size(); i++)
    6. {//每一个子shape
    7. tinyobj::shape_t& shape = shapes[i];
    8. size_t num_index = shape.mesh.indices.size();
    9. size_t num_face = shape.mesh.num_face_vertices.size();
    10. debug(format("读取子模型:{},{}", i, shape.name));
    11. debug(format("索引数:{};面数:{}", num_index, num_face));
    12. //当前mesh下标(每个面递增3)
    13. size_t index_offset = 0;
    14. //每一个面
    15. for (size_t j = 0; j < num_face; ++j)
    16. {
    17. int index_mat = shape.mesh.material_ids[j];//每个面的材质
    18. vectorindex_t>& sub_idx = all_sub[1 + index_mat];
    19. sub_idx.push_back(shape.mesh.indices[index_offset++]);
    20. sub_idx.push_back(shape.mesh.indices[index_offset++]);
    21. sub_idx.push_back(shape.mesh.indices[index_offset++]);
    22. }
    23. }

            按材质记录顶点的索引(tinyobj::index_t)后,接下来就是读取顶点的实际数据,并防止重复读取:

    1. //生成子模型,并填入顶点
    2. std::unordered_mapindex_t, size_t, hash_idx, equal_idx>
    3. uniqueVertices;//避免重复插入顶点
    4. size_t i = 0;
    5. for (vectorindex_t>& sub_idx : all_sub)
    6. {
    7. ModelObjectSub sub;
    8. sub._range._offsetIndex = i;
    9. sub._range._countTriangle = sub_idx.size() / 3;
    10. model_object->_allSub.push_back(sub);
    11. for (tinyobj::index_t& idx : sub_idx)
    12. {
    13. auto iter = uniqueVertices.find(idx);
    14. if (iter == uniqueVertices.end())
    15. {
    16. Vertex v;
    17. //v
    18. v._pos[0] = attrib.vertices[idx.vertex_index * 3 + 0];
    19. v._pos[1] = attrib.vertices[idx.vertex_index * 3 + 1];
    20. v._pos[2] = attrib.vertices[idx.vertex_index * 3 + 2];
    21. // vt
    22. v._texCoord[0] = attrib.texcoords[idx.texcoord_index * 2 + 0];
    23. v._texCoord[1] = attrib.texcoords[idx.texcoord_index * 2 + 1];
    24. v._texCoord[1] = 1.0f - v._texCoord[1];
    25. uniqueVertices[idx] = mo_vertices.size();
    26. mo_indices.push_back((uint32_t)mo_vertices.size());
    27. mo_vertices.push_back(v);
    28. }
    29. else
    30. {
    31. mo_indices.push_back((uint32_t)iter->second);
    32. }
    33. ++i;
    34. }
    35. }
    36. debug(format("解析obj模型完成:v{},i{}", mo_vertices.size(), mo_indices.size()));
    37. return ret;

             上面用到的哈希函数:

    1. struct equal_idx
    2. {
    3. bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const
    4. {
    5. return a.vertex_index == b.vertex_index
    6. && a.texcoord_index == b.texcoord_index
    7. && a.normal_index == b.normal_index;
    8. }
    9. };
    10. struct hash_idx
    11. {
    12. size_t operator()(const tinyobj::index_t& a) const
    13. {
    14. return ((a.vertex_index
    15. ^ a.texcoord_index << 1) >> 1)
    16. ^ (a.normal_index << 1);
    17. }
    18. };

            最后打印出来的数据如下:

            对于材质的处理,漫反射贴图即是基本贴图,而法线(凹凸)贴图、漫反射率、菲涅耳系数、光滑度等需要渲染管线支持并与光照计算产生效果。

    四、完整代码

            可以此处获取最新的源码(我会改用Assimp,并添加骨骼动画、Blinn-Phong光照模型),也可以用后面的:DND/src/DND.ModelObject.cpp · 略游/DND - Gitee.com

            如果有用,欢迎点赞、收藏、关注,我将更新更多C++相关的文章。

    1. #define TINYOBJLOADER_IMPLEMENTATION
    2. #include
    3. struct equal_idx
    4. {
    5. bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const
    6. {
    7. return a.vertex_index == b.vertex_index
    8. && a.texcoord_index == b.texcoord_index
    9. && a.normal_index == b.normal_index;
    10. }
    11. };
    12. struct hash_idx
    13. {
    14. size_t operator()(const tinyobj::index_t& a) const
    15. {
    16. return ((a.vertex_index
    17. ^ a.texcoord_index << 1) >> 1)
    18. ^ (a.normal_index << 1);
    19. }
    20. };
    21. float ShininessToRoughness(float Ypoint)
    22. {
    23. float a = -1;
    24. float b = 2;
    25. float c;
    26. c = (Ypoint / 100) - 1;
    27. float D;
    28. D = b * b - (4 * a * c);
    29. float x1;
    30. x1 = (-b + sqrt(D)) / (2 * a);
    31. return x1;
    32. }
    33. vector ModelObject::_load_obj_2(string_view path_name)
    34. {
    35. string str_path = string{ path_name };
    36. string str_base = String::EraseFilename(path_name);
    37. const char* filename = str_path.c_str();
    38. const char* basepath = str_base.c_str();
    39. bool triangulate = true;
    40. debug(format("开始加载obj文件:{},{}", filename, basepath));
    41. tinyobj::attrib_t attrib; // 所有的数据放在这里
    42. std::vectorshape_t> shapes;//子模型
    43. std::vectormaterial_t> materials;
    44. std::string warn;
    45. std::string err;
    46. bool b_read = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, filename,
    47. basepath, triangulate);
    48. //打印错误
    49. if (!warn.empty())
    50. debug_warn(warn);
    51. if (!err.empty())
    52. debug_err(err);
    53. if (!b_read)
    54. {
    55. debug_err(format("读取obj文件失败:{}", path_name));
    56. return {};
    57. }
    58. debug(format("顶点数:{}", attrib.vertices.size() / 3));
    59. debug(format("法线数:{}", attrib.normals.size() / 3));
    60. debug(format("UV数:{}", attrib.texcoords.size() / 2));
    61. debug(format("子模型数:{}", shapes.size()));
    62. debug(format("材质数:{}", materials.size()));
    63. //obj只有一个ModelObject
    64. vector ret;
    65. ModelObject* model_object = new ModelObject;
    66. std::vector& mo_vertices = model_object->_allVertex;
    67. std::vector<uint32_t>& mo_indices = model_object->_allIndex;
    68. vector& mo_material = model_object->_allMaterial;
    69. ret.push_back(model_object);
    70. //------------------获取材质-------------------
    71. mo_material.resize(materials.size());
    72. for (size_t i = 0; i < materials.size(); ++i)
    73. {
    74. tinyobj::material_t m = materials[i];
    75. debug(format("材质:{},{}", i, m.name));
    76. ModelObjectMaterial& material = model_object->_allMaterial[i];
    77. material._materialName = m.name;
    78. material._material._diffuseAlbedo = { m.diffuse[0], m.diffuse[1], m.diffuse[2], 1.0f };
    79. material._material._fresnelR0 = { m.specular[0], m.specular[1], m.specular[2] };
    80. material._material._roughness = ShininessToRoughness(m.shininess);
    81. if(!m.diffuse_texname.empty())
    82. material._pathTexDiffuse = str_base + m.diffuse_texname;
    83. if (!m.normal_texname.empty())//注意这里凹凸贴图(bump_texname)更常见
    84. material._pathTexNormal = str_base + m.normal_texname;
    85. }
    86. //------------------获取模型-------------------
    87. //按 材质 放入面的顶点
    88. vectorindex_t>> all_sub;
    89. all_sub.resize(1 + materials.size());//0为默认
    90. for (size_t i = 0; i < shapes.size(); i++)
    91. {//每一个子shape
    92. tinyobj::shape_t& shape = shapes[i];
    93. size_t num_index = shape.mesh.indices.size();
    94. size_t num_face = shape.mesh.num_face_vertices.size();
    95. debug(format("读取子模型:{},{}", i, shape.name));
    96. debug(format("索引数:{};面数:{}", num_index, num_face));
    97. //当前mesh下标(每个面递增3)
    98. size_t index_offset = 0;
    99. //每一个面
    100. for (size_t j = 0; j < num_face; ++j)
    101. {
    102. int index_mat = shape.mesh.material_ids[j];//每个面的材质
    103. vectorindex_t>& sub_idx = all_sub[1 + index_mat];
    104. sub_idx.push_back(shape.mesh.indices[index_offset++]);
    105. sub_idx.push_back(shape.mesh.indices[index_offset++]);
    106. sub_idx.push_back(shape.mesh.indices[index_offset++]);
    107. }
    108. }
    109. //生成子模型,并填入顶点
    110. std::unordered_mapindex_t, size_t, hash_idx, equal_idx>
    111. uniqueVertices;//避免重复插入顶点
    112. size_t i = 0;
    113. for (vectorindex_t>& sub_idx : all_sub)
    114. {
    115. ModelObjectSub sub;
    116. sub._range._offsetIndex = i;
    117. sub._range._countTriangle = sub_idx.size() / 3;
    118. model_object->_allSub.push_back(sub);
    119. for (tinyobj::index_t& idx : sub_idx)
    120. {
    121. auto iter = uniqueVertices.find(idx);
    122. if (iter == uniqueVertices.end())
    123. {
    124. Vertex v;
    125. //v
    126. v._pos[0] = attrib.vertices[idx.vertex_index * 3 + 0];
    127. v._pos[1] = attrib.vertices[idx.vertex_index * 3 + 1];
    128. v._pos[2] = attrib.vertices[idx.vertex_index * 3 + 2];
    129. // vt
    130. v._texCoord[0] = attrib.texcoords[idx.texcoord_index * 2 + 0];
    131. v._texCoord[1] = attrib.texcoords[idx.texcoord_index * 2 + 1];
    132. v._texCoord[1] = 1.0f - v._texCoord[1];
    133. uniqueVertices[idx] = mo_vertices.size();
    134. mo_indices.push_back((uint32_t)mo_vertices.size());
    135. mo_vertices.push_back(v);
    136. }
    137. else
    138. {
    139. mo_indices.push_back((uint32_t)iter->second);
    140. }
    141. ++i;
    142. }
    143. }
    144. debug(format("解析obj模型完成:v{},i{}", mo_vertices.size(), mo_indices.size()));
    145. return ret;
    146. }

  • 相关阅读:
    3.1.OpenCV技能树--二值图像处理--阈值
    【愚公系列】2022年11月 .NET CORE工具案例-.NET 7中的WebTransport通信
    HTB-Irked
    7-3 LVS+Keepalived集群叙述与部署
    Jmeter性能实战之分布式压测
    5 Dijkstra算法的设计--来源王英S同学
    Oracle数据库:使用 bash脚本 + 定时任务 自动备份数据
    Git常用命令汇总
    java计算机毕业设计微服务的高校二手交易平台源程序+mysql+系统+lw文档+远程调试
    精简scrapy日志冗余占较大内存
  • 原文地址:https://blog.csdn.net/u014629601/article/details/126663528