網站首頁 編程語言 正文
一、前言
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<ModelObject*> Create(string_view path_name);
void Load(string_view path_name);
//保存到文件
void SaveToFile(string_view path_name);
private:
vector<ModelObjectSub> _allSub; //下標減1 為材質,0為沒有材質
vector<Vertex> _allVertex;//頂點緩存
vector<uint32_t> _allIndex;//索引緩存
vector<ModelObjectMaterial> _allMaterial;//所有材質
//------------------不同格式加載實現--------------------------------
//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表示模型材質:
//! 材質
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 <tiny_obj_loader.h>
接口原型,將obj文件變為多個ModelObject:
vector<ModelObject*> 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<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("頂點數:{}", 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<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);
首先記錄材質信息:
//------------------獲取材質-------------------
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<vector<tinyobj::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<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++]);
}
}
按材質記錄頂點的索引(tinyobj::index_t)后,接下來就是讀取頂點的實際數據,并防止重復讀取:
//生成子模型,并填入頂點
std::unordered_map<tinyobj::index_t, size_t, hash_idx, equal_idx>
uniqueVertices;//避免重復插入頂點
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;
上面用到的哈希函數:
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光照模型),也可以用后面的:傳送門
如果有用,歡迎點贊、收藏、關注,我將更新更多C++相關的文章。
#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; // 所有的數據放在這里
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("頂點數:{}", 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<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);
//------------------獲取材質-------------------
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<vector<tinyobj::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<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++]);
}
}
//生成子模型,并填入頂點
std::unordered_map<tinyobj::index_t, size_t, hash_idx, equal_idx>
uniqueVertices;//避免重復插入頂點
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;
}
原文鏈接:https://blog.csdn.net/u014629601/article/details/126663528
相關推薦
- 2022-05-15 Web?API身份認證解決方案之Basic基礎認證_實用技巧
- 2022-12-24 Docker網絡及容器通信原理詳解_docker
- 2022-07-19 statfs函數使用
- 2022-05-13 Android水波紋效果
- 2022-07-04 Python異步處理返回進度——使用Flask實現進度條_python
- 2023-03-22 React使用Context與router實現權限路由詳細介紹_React
- 2022-04-25 .Net?Core?Aop之IResourceFilter的具體使用_實用技巧
- 2023-01-02 Python創建相同值數組/列表的兩種方法_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支