opengl 加载obj模型
本章讲述使用opengl加载obj模型,如果你想自己实现, 你需要一个opengl的开发环境,以及相关的知识 ,最好是看完LearnOpenGL CN 的入门教程
使用opengl开发的同学都需要加载模型,github上有很多的现成库,比较常用的是 Assimp 。这篇博客主要记录自己手动去加载模型的一个过程,首先建模软件有很多,模型的格式也不一样,像 fbx,obj,.blend等,这里加载的模型是obj格式的,可以从这里下载,不知道为什么csdn上传资源下载一定要积分.... ,有需要的可以私我发给你。
模型文件格式
一个使用Blender、3DS Max或者Maya等建模软件制作出来的模型 一般由一个或者多个网格(Mesh)组成,在游戏中,我们的主角模型可以由头部 ,身体,四肢,武器等多个网格组成。我们在opengl去渲染一个模型 可以分别渲染模型的多个mesh ,那么渲染一个mesh通常都需要哪些属性呢? 首先顶点是必不可少的 ,可能还有纹理和 纹理坐标 ,法线,切线等,一个mesh就是由这些属性组成的 。之所以选择加载obj格式的模型 ,是因为我们可以直接看到obj模型的对应属性 ,模型文件可以在这里下载 ,model文件夹下是模型 script文件下是具体代码。下载解压完资源后我们可以直接将文件夹内 spongebob_bind.obj 的后缀改为txt ,使用文本工具打开 可以看到obj模型的数据组织格式 ,如下图所示 :
可以看到obj的数据格式是按照行来组织的 ,最明显的以 v 开头行,代表记录着一个顶点 ,将文本往下拉,可以看到还有以 vt 、vn 、f 、usemtl等字符开头的 ,以下常见的开头字符所代表的数据:
v x y z v :表示本行指定一个顶点,此关键字后跟着3个单精度浮点数,代表着该顶点坐标值
vt u v vt:表示本行指定一个纹理坐标,此关键字后跟着2个单精度浮点数,代表着该纹理坐标UV值
vn x y z vn:表示本行指定一个法线向量,此关键字后跟着3个单精度浮点数,代表着该法向量坐标值
f v/vt/vn v/vt/vn v/vt/vn f:面 代表一个三角形(三个顶点),v/vt/vn 描述了一个顶点的基本数据,顶点位置、纹理坐标和法线 。要注意的是这里存储的只是索引
g 3D_Object_4 g:表示一个名称为3D_Object_4的组,指定从此行开始到下一个g开头的所有对象为一组
o 3D_Object_4 o:指定一个名称为3D_Object_4的对象(也就是一个mesh)
mtllib spongebob_bind.mtl mtllib:此关键字后为文件名称,指定了obj所使用的材质库文件名称
usemtl spongebob usemtl:此关键字后为材质名称,指定了从此行之后到下一个以usemtl开头的行之间的所有表面所使用的材质名称,该材质可以在obj文件所附属的mtl文件中找到具体信息
我们需要看一下obj的数据组织格式,首先按数据类型(顶点,纹理坐标,法线各自放在一起)分类,放在文件的前面,然后以关键字 f 指定三角面的每个顶点的顶点坐标(v)、纹理坐标(vt)、法线向量(vn)的索引,这样就已经可以绘制一个mesh的形状了 。 另外需要说明的就是mtlib ,该关键字指定了一个关联的材质库文件,该文件后缀为mtl,一般来说与obj文件在同一目录,里面包含了obj模型使用到的材质列表,材质中存储着贴图、光照参数等属性 ,跟obj文件一样,我们可以通过将后缀改为txt,来查看mtl文件的内容
数据格式也与obj一样按行组织,使用关键字符进行标识 ,下面是关键字符对应的具体含义
newmtl spongebob newmtl: 定义一个名称为XX的材质
Ns 0 Ns: 材质的光亮度
d 1 d: 材质的Alpha透明度
illum 2 illum: 材质的照明度
Kd 0.8 0.8 0.8 Kd: 漫反射参数
Ks 0.0 0.0 0.0 Ks: 镜面参数
Ka 0.2 0.2 0.2 Ka: 环境参数
map_Ka spongebob.png map_Ka:环境贴图
这里我们只列出了上面给到的obj文件中出现的关键字符,还有很多其他的关键字符,如果不知道代表着什么含义,可以参考这里。在了解obj和mtl文件格式后,我们需要加载对应的文件,其实这就是一个文本解析的过程 ,定义合适的数据结构,解析每一行的关键字,读取对应的数据到数据结构中就可以了。
数据结构
顶点MVertex中需要包含顶点的位置,法线,以及贴图坐标:
struct MVertex
{vec3 pos;vec3 normal;vec2 texcoord;
};
材质MMaterial根据mtl文件出现的数据定义结构如下图,与mtl文件的关键字一一对应
struct MMaterial
{string name;// Specular Exponentfloat Ns;// Dissolvefloat d;// Illuminationfloat illlum;vec3 Ka; // ambient Colorvec3 Kd; // diffuse Color vec3 Ks; // specular Colorstring map_Ka; // Ambient Texture Mapint ka_id; // Ambient Texture id
};
我们使用一个MMesh类来存储mesh的所有数据 ,包含当前mesh的顶点和索引,以及所使用的材质。
class MMesh
{
public:string name;vector<MVertex> vertexs;vector<unsigned int> indecies;MMaterial material;MMesh() {}MMesh(vector<MVertex> Vertexs, vector<unsigned int> Indecies){vertexs = Vertexs;indecies = Indecies;}
};
再使用一个MModel 来存储Model的所有数据,主要包含所有的mesh 以及 材质
class MModel
{
public:vector<MMaterial> loadedMaterials;vector<MMesh> loadedMeshes;
};
接下来只要写一个解析函数,将obj和mtl文件数据加载到对应数据结构中就可以了。我直接将这个解析过程定义在了MModel的构造函数中,下面是该构造函数的实现
MModel::MModel(string path)
{string str(absolutePath);path = str + path;if (path.substr(path.size() - 4, 4) != ".obj"){cout << "Only Load Obj Model" << path << endl;return;}ifstream file(path);if (!file.is_open()){cout << "file is not exist " << path << endl;return;}bool isFirst = true;string meshName;MMesh tmpMesh;vector<vec3> allPos;vector<vec3> allNormals;vector<vec2> allTexcoords;vector<MVertex> allVertexs;vector<unsigned int> allIndices;vector<string> meshMatNames;string curline;while (getline(file, curline)){// Tools::FirstToken 取当前行的关键字 string firstToken = Tools::FirstToken(curline); if (firstToken == "o" || firstToken == "g"){if (isFirst){isFirst = false;// Tools::TailToken 取当前行关键字后面的内容meshName = Tools::TailToken(curline);}else{if (!allIndices.empty() && !allVertexs.empty()){tmpMesh = MMesh(allVertexs, allIndices);tmpMesh.name = meshName;loadedMeshes.push_back(tmpMesh);allIndices.clear();allVertexs.clear();meshName.clear();meshName = Tools::TailToken(curline);}else{meshName = Tools::TailToken(curline);}}}else if(firstToken == "v"){vector<string> strVec;vec3 pos;Tools::split(Tools::TailToken(curline), strVec, " ");pos.x = stof(strVec[0]);pos.y = stof(strVec[1]);pos.z = stof(strVec[2]);allPos.push_back(pos);}else if (firstToken == "vn"){vector<string> strVec;vec3 normal;Tools::split(Tools::TailToken(curline), strVec, " ");normal.x = stof(strVec[0]);normal.y = stof(strVec[1]);normal.z = stof(strVec[2]);allNormals.push_back(normal);}else if (firstToken == "vt"){vector<string> strVec;vec2 texcoord;Tools::split(Tools::TailToken(curline), strVec, " ");texcoord.x = stof(strVec[0]);texcoord.y = stof(strVec[1]);allTexcoords.push_back(texcoord);}else if (firstToken == "f"){vector<MVertex> vVerts;GenVerticesObj(vVerts, allPos, allNormals, allTexcoords, curline);for (int i = 0;i < vVerts.size();i++){allVertexs.push_back(vVerts[i]);}for (int i = 0; i < 3; i++){unsigned int indnum = (unsigned int)((allVertexs.size()) - vVerts.size()) + i;allIndices.push_back(indnum);}}else if (firstToken == "mtllib"){vector<string> pathArr;string mtlPath;Tools::split(path, pathArr, "/");for (int i = 0; i < pathArr.size() - 1; i++){mtlPath += pathArr[i] + "/";}mtlPath += Tools::TailToken(curline);LoadMaterial(mtlPath);}else if (firstToken == "usemtl"){string matName = Tools::TailToken(curline);meshMatNames.push_back(matName);}}if (!allIndices.empty() && !allVertexs.empty()){tmpMesh = MMesh(allVertexs, allIndices);tmpMesh.name = meshName;loadedMeshes.push_back(tmpMesh);}file.close();for (int i = 0;i < meshMatNames.size();i++){string meshMatName = meshMatNames[i];for (int j = 0;j < loadedMaterials.size();j++){if (loadedMaterials[j].name == meshMatName){loadedMeshes[i].material = loadedMaterials[j];break;}}}
}
上面这个解析函数相对比较简单,就是逐行读取关键字,根据不同的关键字将后面的内容赋值到对应的数据结构中,其中用到了三个工具函数 ,Tools::FirstToken 和 Tools::TailToken分别是读取当前行的关键字和关键字后面的内容 ,Tools::split是根据字符分割字符串。
在解析关键字 f开头的行时用到了 函数 GenVerticesObj(vVerts, allPos, allNormals, allTexcoords, curline) ,该函数根据关键字 f 开头的行,构造一个面的三个顶点数据结构,因为 f 后面的数据是三角面的每个顶点的顶点坐标(v)、纹理坐标(vt)、法线向量(vn)的索引号 ,所以需要将前面加载得到的所有顶点,纹理坐标,法线数据都传进去,才能根据索引号取到具体数据。 另外还需要注意一点,在代码中取纹理坐标时,取得值是 tmpVertex.texcoord.y = 1 - tmpVertex.texcoord.y,这是因为openGl里纹理空间的原点位于左下角,而文件中的纹理坐标是基于纹理空间原点位于左上角的 。
void MModel::GenVerticesObj(vector<MVertex> &outVerts, vector<vec3>& poses, vector<vec3>& normals, vector<vec2>& texcoords, std::string curline)
{std::vector<std::string> faces, verts;Tools::split(Tools::TailToken(curline), faces, " ");for (int i = 0;i < faces.size();i++){MVertex tmpVertex;verts.clear();Tools::split(faces[i], verts, "/");int types = verts.size();tmpVertex.pos = Tools::getElement(poses, verts[0]);tmpVertex.normal = Tools::getElement(normals, verts[2]);tmpVertex.texcoord = Tools::getElement(texcoords, verts[1]);tmpVertex.texcoord.y = 1 - tmpVertex.texcoord.y;outVerts.push_back(tmpVertex);}
}
另外在解析关键字mtllib时 使用了LoadMaterial(mtlPath)去加载解析一个mtl文件,方式与加载obj文件一样 ,代码如下
bool MModel::LoadMaterial(string path)
{if (path.substr(path.size() - 4, path.size()) != ".mtl")return false;ifstream file;file.open(path);MMaterial material;string curline;bool isFirst = true;while (std::getline(file, curline)){string first = Tools::FirstToken(curline);if (first == "newmtl"){if (isFirst){if (curline.size() > 7) material.name = Tools::TailToken(curline);}else{loadedMaterials.push_back(material);material = MMaterial();if (curline.size() > 7) material.name = Tools::TailToken(curline);}}else if (first == "Ns"){material.Ns = stof(Tools::TailToken(curline));}else if (first == "d"){material.d = stof(Tools::TailToken(curline));}else if (first == "illum"){material.illlum = stof(Tools::TailToken(curline));}else if (first == "Ka"){vector<string> kaArr;Tools::split(Tools::TailToken(curline), kaArr, " ");material.Ka = glm::vec3(stof(kaArr[0]), stof(kaArr[1]), stof(kaArr[2]));}else if (first == "Kd"){vector<string> kdArr;Tools::split(Tools::TailToken(curline), kdArr, " ");material.Kd = glm::vec3(stof(kdArr[0]), stof(kdArr[1]), stof(kdArr[2]));}else if (first == "Ks"){vector<string> ksArr;Tools::split(Tools::TailToken(curline), ksArr, " ");material.Ks = glm::vec3(stof(ksArr[0]), stof(ksArr[1]), stof(ksArr[2]));}else if (first == "map_Ka"){material.map_Ka = Tools::TailToken(curline);vector<string> pathArr;string texturePath;Tools::split(path, pathArr, "/");for (int i = 0;i < pathArr.size() - 1;i++){texturePath += pathArr[i] + "/";}texturePath += material.map_Ka;material.ka_id = TextureFromFile(texturePath.c_str());}}loadedMaterials.push_back(material);if (loadedMaterials.empty()) return false;return true;
}
到这里我们已经将模型的数据全部加载完了,接下来就是使用这些数据进行绘制 ,绘制是以mesh为单位进行的 ,所以我们在MMesh类中添加 函数SetupMesh 、 Draw,来分别设置mesh的渲染状态和 绘制mesh。下面是MMesh的代码
class MMesh
{
public:string name;vector<MVertex> vertexs;vector<unsigned int> indecies;MMaterial material;MMesh() {}MMesh(vector<MVertex> Vertexs, vector<unsigned int> Indecies){vertexs = Vertexs;indecies = Indecies;setupMesh();}void Draw(ShaderC shader);private:unsigned int VAO, VBO, EBO;void setupMesh();};void MMesh::setupMesh()
{glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(MVertex) * vertexs.size(), &vertexs[0], GL_STATIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(MVertex), (void*)0);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(MVertex), (void*)offsetof(MVertex, normal));glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(MVertex), (void*)offsetof(MVertex, texcoord));glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, indecies.size() * sizeof(unsigned int), &indecies[0], GL_STATIC_DRAW);glBindVertexArray(0);
}void MMesh::Draw(ShaderC shader)
{shader.setInt("diffTex", 0);glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, material.ka_id);glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, indecies.size(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);glActiveTexture(GL_TEXTURE0);
}
此处在Draw函数中我们只用到了材质数据中的环境贴图map_Ka ,并将其赋值到shader中定义的uniform变量中,以便片段着色器进行纹理采样 ,其他的数据也可以使用,关键在于你的shader怎么写,需要什么数据。另外我们将mesh的顶点数据放在mesh的构造函数中设置。到这里,其实一个obj已经能够加载渲染出来了,无非是根据一个路径创建一个MModel类,在构造函数中加载所有数据,在每一帧使用自定义的shader去调用所有mesh的Draw函数。下面的文章开始提供的oibj模型的渲染结果(很可爱的一个海绵宝宝):
博客所涉及的所有代码都在资源的scrpit文件中,但它并不是一个可以运行的工程,因为我认为像这种知识,一定要自己去实现一遍才行,看看博客,运行一下别人的工程,始终是过眼云烟,结合上LearnOpenGL CN的入门教程是很容易实现的
opengl 加载obj模型相关推荐
- threejs加载obj模型_Vulkan编程指南(章节31-载入模型)
章节31 载入模型 介绍 本章节我们将会渲染一个带有纹理的三维模型. 库 我们使用tinyobjloader库来从OBJ文件加载顶点数据.tinyobjloader库是一个简单易用的单文件OBJ加载器 ...
- three.js加载obj模型和材质
1.Vue中安装three.js和加载用的包 安装three.js使用npm install three --save 安装加载obj和mtl文件的包npm install three-obj-mtl ...
- three.js加载OBJ模型
three.js加载OBJ模型 推荐一个免费下载3D模型的网址https://www.cgtrader.com,包含多种格式(obj, mtl等). three.js现在是es6语法,旧版本是es5的 ...
- OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)
写在前面 前面介绍了光照基础内容,以及材质和lighting maps,和光源类型,我们对使用光照增强场景真实感有了一定了解.但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很 ...
- qt opengl 加载3d模型(obj格式)
和一般c++程序加载3d模型一样,解读出数据内容,再用一个常规的着色程序就可以了. 我实现的效果如下,采用的免费模型 实现思路和前面的略有不同,就是把自己生成顶点.纹理.法线的过程变成从文件读取了. ...
- WPF加载obj模型-2
安装微软Expression Blend: 新建一个WPF项目: 把obj文件添加到项目: 然后把obj文件拖到MainWindow:模型出来了: 运行一下如下: 右击添加模型以后的xaml文件,外部 ...
- Assimp库调用mtl加载obj模型
网上查阅了很多资料,通过测试都未通过,后来在两位大神博客的帮助下最终完成了obj及mtl的加载. 参考博客链接: OpenGL学习: uniform blocks(UBO)在着色器中的使用_arag2 ...
- 首次使用three.js加载obj模型未成功
接此,https://blog.csdn.net/bcbobo21cn/article/details/110676331 基本代码如下: <!DOCTYPE html> <html ...
- threejs加载obj模型_倾斜摄影三维模型几种常见的格式,你能说出哪些?
本文首发于公众号Wish3D,原文链接:倾斜摄影三维模型几种常见的格式,你能说出哪些? 无人机航拍的影像经过建模软件处理产出之时,有很多成果的数据需要我们去选择输出,对于新手而言,如何选择数据格式呢? ...
- 网页中加载obj模型比较慢_R语言估计时变VAR模型时间序列的实证研究分析案例...
原文 http://tecdat.cn/?p=3364tecdat.cn 加载R包和数据集 上述症状数据集包含在R-package 中,并在加载时自动可用. 加载包后,我们将此数据集中包含的12个心 ...
最新文章
- MIT黑科技:“不开卷也有益”,计算机不翻书就能读完一本书
- 右边补0 润乾报表_制作按奖金分段统计的员工业绩报表
- Sql Server 2008 精简版(Express)和管理工具的安装以及必须重新启动计算机才能安装 SQLServer的问题和第一次使用sqlexpress的连接问题
- 九江学院计算机主任黄冬久,陈春生副校长到实验中间调研引导工作
- B - 数字三角形问题
- 查看docker内部路径_web应用在Docker容器中部署(Windows)
- android编辑框显示,为EditText输入框加上提示信息
- iOS 利用UIPresentationController自定义转场动画
- PHPCMS 使用图示和PHPCMS二次开发教程(转)
- 我的世界java 4k_我的世界:原版VS“4K光影”牺牲2块显卡,让你看看差距有多大!...
- matlab三轴定位程序,三边测量定位MATLAB源码
- Xcode8 官方下载地址
- arduino烧录_用Arduino UNO烧录Attiny85芯片
- ASP.NET 学习路线图
- Oracle11g64位安装教程
- Altium designer (AD)中如何设置区域规则和器件规则
- 基于CANoen协议实现DSP系统与上位机CAN的通讯
- Windows静默安装
- wince版千千静听出炉
- 有什么md5修改工具?快把这些工具收好