C++解析obj模型文件方法介紹
一、前言
tinyobjloader地址:
而tinyobjloader庫只有一個頭文件,可以很方便的讀取obj文件。支持材質(zhì),不過不支持骨骼動畫,vulkan官方教程便是使用的它。不過沒有骨骼動畫還是有很大的局限性,這里只是分享一下怎么讀取材質(zhì)和拆分網(wǎng)格。
二、中間文件
我抽象了一個ModelObject類表示模型數(shù)據(jù),而一個ModelObject包含多個Sub模型,每個Sub模型使用同一材質(zhì)(有的人稱為圖元Primitive或DrawCall)。最后我將其保存為文件,這樣我的引擎便可直接解析ModelObject文件,而不是再去讀obj、fbx等其他文件了。
這一節(jié)可以跳過,下一節(jié)是真正使用tinyobjloader庫。
//一個文件會有多個ModelObject,一個ModelObject根據(jù)材質(zhì)分為多個ModelSub //注意ModelSub為一個材質(zhì),需要讀取時合并網(wǎng)格 class ModelObject { friend class VK; public: //從源文件加載模型 static vector<ModelObject*> Create(string_view path_name); void Load(string_view path_name); //保存到文件 void SaveToFile(string_view path_name); private: vector<ModelObjectSub> _allSub; //下標(biāo)減1 為材質(zhì),0為沒有材質(zhì) vector<Vertex> _allVertex;//頂點(diǎn)緩存 vector<uint32_t> _allIndex;//索引緩存 vector<ModelObjectMaterial> _allMaterial;//所有材質(zhì) //------------------不同格式加載實(shí)現(xiàn)-------------------------------- //obj static vector<ModelObject*> _load_obj(string_view path_name); static vector<ModelObject*> _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表示模型材質(zhì):
//! 材質(zhì) struct Material { glm::vec4 _diffuseAlbedo;//漫反射率 glm::vec3 _fresnelR0; //菲涅耳系數(shù) float _roughness; //粗糙度 }; //模型對象 材質(zhì) struct ModelObjectMaterial { //最后轉(zhuǎn)為Model時,變?yōu)榭梢杂玫闹髻Y源 Material _material; string _materialName; //路徑為空,則表示沒有(VK加載時會返回0) string _pathTexDiffuse; string _pathTexNormal; };
三、使用
首先引入頭文件:
#define TINYOBJLOADER_IMPLEMENTATION #include <tiny_obj_loader.h>
接口原型,將obj文件變?yōu)槎鄠€ModelObject:
vector<ModelObject*> ModelObject::_load_obj_2(string_view path_name);
取得文件名,和文件所在路徑(會自動加載路徑下的同名mtl文件,里面包含了材質(zhì)):
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();
基本數(shù)據(jù):
debug(format("開始加載obj文件:{},{}", filename, basepath)); bool triangulate = true;//三角化 tinyobj::attrib_t attrib; // 所有的數(shù)據(jù)放在這里 std::vector<tinyobj::shape_t> shapes;//子模型 std::vector<tinyobj::material_t> materials;//材質(zhì) 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("頂點(diǎn)數(shù):{}", attrib.vertices.size() / 3)); debug(format("法線數(shù):{}", attrib.normals.size() / 3)); debug(format("UV數(shù):{}", attrib.texcoords.size() / 2)); debug(format("子模型數(shù):{}", shapes.size())); debug(format("材質(zhì)數(shù):{}", materials.size()));
這將打印以下數(shù)據(jù):
由于obj文件只產(chǎn)生一個ModelObject,我們?nèi)缦绿砑右粋€,并返回頂點(diǎn)、索引、材質(zhì)等引用,用于后面填充:
//obj只有一個ModelObject vector<ModelObject*> ret; ModelObject* model_object = new ModelObject; std::vector<Vertex>& mo_vertices = model_object->_allVertex; std::vector<uint32_t>& mo_indices = model_object->_allIndex; vector<ModelObjectMaterial>& mo_material = model_object->_allMaterial; ret.push_back(model_object);
首先記錄材質(zhì)信息:
//------------------獲取材質(zhì)------------------- mo_material.resize(materials.size()); for (size_t i = 0; i < materials.size(); ++i) { tinyobj::material_t m = materials[i]; debug(format("材質(zhì):{},{}", 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; }
這將產(chǎn)生以下輸出:
然后遍歷shape,按材質(zhì)記錄頂點(diǎn)。這里需要注意的是,一個obj文件有多個shape,每個shape由n個三角面組成。而每個三角形擁有獨(dú)立的材質(zhì)編號,所以這里按材質(zhì)分別記錄,而不是一般的合并為整體:
//------------------獲取模型------------------- //按 材質(zhì) 放入面的頂點(diǎn) vector<vector<tinyobj::index_t>> all_sub; all_sub.resize(1 + materials.size());//0為默認(rèn) 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("索引數(shù):{};面數(shù):{}", num_index, num_face)); //當(dāng)前mesh下標(biāo)(每個面遞增3) size_t index_offset = 0; //每一個面 for (size_t j = 0; j < num_face; ++j) { int index_mat = shape.mesh.material_ids[j];//每個面的材質(zhì) vector<tinyobj::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++]); } }
按材質(zhì)記錄頂點(diǎn)的索引(tinyobj::index_t)后,接下來就是讀取頂點(diǎn)的實(shí)際數(shù)據(jù),并防止重復(fù)讀?。?/p>
//生成子模型,并填入頂點(diǎn) std::unordered_map<tinyobj::index_t, size_t, hash_idx, equal_idx> uniqueVertices;//避免重復(fù)插入頂點(diǎn) size_t i = 0; for (vector<tinyobj::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;
上面用到的哈希函數(shù):
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); } };
最后打印出來的數(shù)據(jù)如下:
對于材質(zhì)的處理,漫反射貼圖即是基本貼圖,而法線(凹凸)貼圖、漫反射率、菲涅耳系數(shù)、光滑度等需要渲染管線支持并與光照計(jì)算產(chǎn)生效果。
四、完整代碼
可以此處獲取最新的源碼(我會改用Assimp,并添加骨骼動畫、Blinn-Phong光照模型),也可以用后面的:傳送門
如果有用,歡迎點(diǎn)贊、收藏、關(guān)注,我將更新更多C++相關(guān)的文章。
#define TINYOBJLOADER_IMPLEMENTATION #include <tiny_obj_loader.h> 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*> 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; // 所有的數(shù)據(jù)放在這里 std::vector<tinyobj::shape_t> shapes;//子模型 std::vector<tinyobj::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("頂點(diǎn)數(shù):{}", attrib.vertices.size() / 3)); debug(format("法線數(shù):{}", attrib.normals.size() / 3)); debug(format("UV數(shù):{}", attrib.texcoords.size() / 2)); debug(format("子模型數(shù):{}", shapes.size())); debug(format("材質(zhì)數(shù):{}", materials.size())); //obj只有一個ModelObject vector<ModelObject*> ret; ModelObject* model_object = new ModelObject; std::vector<Vertex>& mo_vertices = model_object->_allVertex; std::vector<uint32_t>& mo_indices = model_object->_allIndex; vector<ModelObjectMaterial>& mo_material = model_object->_allMaterial; ret.push_back(model_object); //------------------獲取材質(zhì)------------------- mo_material.resize(materials.size()); for (size_t i = 0; i < materials.size(); ++i) { tinyobj::material_t m = materials[i]; debug(format("材質(zhì):{},{}", 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; } //------------------獲取模型------------------- //按 材質(zhì) 放入面的頂點(diǎn) vector<vector<tinyobj::index_t>> all_sub; all_sub.resize(1 + materials.size());//0為默認(rèn) 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("索引數(shù):{};面數(shù):{}", num_index, num_face)); //當(dāng)前mesh下標(biāo)(每個面遞增3) size_t index_offset = 0; //每一個面 for (size_t j = 0; j < num_face; ++j) { int index_mat = shape.mesh.material_ids[j];//每個面的材質(zhì) vector<tinyobj::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++]); } } //生成子模型,并填入頂點(diǎn) std::unordered_map<tinyobj::index_t, size_t, hash_idx, equal_idx> uniqueVertices;//避免重復(fù)插入頂點(diǎn) size_t i = 0; for (vector<tinyobj::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; }
到此這篇關(guān)于C++解析obj模型文件方法介紹的文章就介紹到這了,更多相關(guān)C++解析obj內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++驗(yàn)證LeetCode包圍區(qū)域的DFS方法
這篇文章主要介紹了C++驗(yàn)證LeetCode包圍區(qū)域的DFS方法,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07C/C++使用C語言實(shí)現(xiàn)多態(tài)
這篇文章主要介紹了C/C++多態(tài)的實(shí)現(xiàn)機(jī)制理解的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下,希望能給你帶來幫助2021-08-08Qt編寫地圖實(shí)現(xiàn)實(shí)時動態(tài)軌跡效果
實(shí)時動態(tài)軌跡主要是需要在地圖上動態(tài)顯示GPS的運(yùn)動軌跡,也是編寫地圖時一個重要的功能。本文將利用Qt實(shí)現(xiàn)這一功能,需要的可以參考一下2022-02-02C++共享智能指針shared_ptr的實(shí)現(xiàn)
在C++中沒有垃圾回收機(jī)制,必須自己釋放分配的內(nèi)存,否則就會造成內(nèi)存泄露,解決這個問題最有效的方法是使用智能指針,本文主要介紹了C++共享智能指針shared_ptr的實(shí)現(xiàn),感興趣的可以了解一下2023-12-12