本章讲述使用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模型相关推荐

  1. threejs加载obj模型_Vulkan编程指南(章节31-载入模型)

    章节31 载入模型 介绍 本章节我们将会渲染一个带有纹理的三维模型. 库 我们使用tinyobjloader库来从OBJ文件加载顶点数据.tinyobjloader库是一个简单易用的单文件OBJ加载器 ...

  2. three.js加载obj模型和材质

    1.Vue中安装three.js和加载用的包 安装three.js使用npm install three --save 安装加载obj和mtl文件的包npm install three-obj-mtl ...

  3. three.js加载OBJ模型

    three.js加载OBJ模型 推荐一个免费下载3D模型的网址https://www.cgtrader.com,包含多种格式(obj, mtl等). three.js现在是es6语法,旧版本是es5的 ...

  4. OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)

    写在前面 前面介绍了光照基础内容,以及材质和lighting maps,和光源类型,我们对使用光照增强场景真实感有了一定了解.但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很 ...

  5. qt opengl 加载3d模型(obj格式)

    和一般c++程序加载3d模型一样,解读出数据内容,再用一个常规的着色程序就可以了. 我实现的效果如下,采用的免费模型 实现思路和前面的略有不同,就是把自己生成顶点.纹理.法线的过程变成从文件读取了. ...

  6. WPF加载obj模型-2

    安装微软Expression Blend: 新建一个WPF项目: 把obj文件添加到项目: 然后把obj文件拖到MainWindow:模型出来了: 运行一下如下: 右击添加模型以后的xaml文件,外部 ...

  7. Assimp库调用mtl加载obj模型

    网上查阅了很多资料,通过测试都未通过,后来在两位大神博客的帮助下最终完成了obj及mtl的加载. 参考博客链接: OpenGL学习: uniform blocks(UBO)在着色器中的使用_arag2 ...

  8. 首次使用three.js加载obj模型未成功

    接此,https://blog.csdn.net/bcbobo21cn/article/details/110676331 基本代码如下: <!DOCTYPE html> <html ...

  9. threejs加载obj模型_倾斜摄影三维模型几种常见的格式,你能说出哪些?

    本文首发于公众号Wish3D,原文链接:倾斜摄影三维模型几种常见的格式,你能说出哪些? 无人机航拍的影像经过建模软件处理产出之时,有很多成果的数据需要我们去选择输出,对于新手而言,如何选择数据格式呢? ...

  10. 网页中加载obj模型比较慢_R语言估计时变VAR模型时间序列的实证研究分析案例...

    原文 http://tecdat.cn/?p=3364​tecdat.cn 加载R包和数据集 上述症状数据集包含在R-package 中,并在加载时自动可用. 加载包后,我们将此数据集中包含的12个心 ...

最新文章

  1. MIT黑科技:“不开卷也有益”,计算机不翻书就能读完一本书
  2. 右边补0 润乾报表_制作按奖金分段统计的员工业绩报表
  3. Sql Server 2008 精简版(Express)和管理工具的安装以及必须重新启动计算机才能安装 SQLServer的问题和第一次使用sqlexpress的连接问题
  4. 九江学院计算机主任黄冬久,陈春生副校长到实验中间调研引导工作
  5. B - 数字三角形问题
  6. 查看docker内部路径_web应用在Docker容器中部署(Windows)
  7. android编辑框显示,为EditText输入框加上提示信息
  8. iOS 利用UIPresentationController自定义转场动画
  9. PHPCMS 使用图示和PHPCMS二次开发教程(转)
  10. 我的世界java 4k_我的世界:原版VS“4K光影”牺牲2块显卡,让你看看差距有多大!...
  11. matlab三轴定位程序,三边测量定位MATLAB源码
  12. Xcode8 官方下载地址
  13. arduino烧录_用Arduino UNO烧录Attiny85芯片
  14. ASP.NET 学习路线图
  15. Oracle11g64位安装教程
  16. Altium designer (AD)中如何设置区域规则和器件规则
  17. 基于CANoen协议实现DSP系统与上位机CAN的通讯
  18. Windows静默安装
  19. wince版千千静听出炉
  20. 有什么md5修改工具?快把这些工具收好

热门文章

  1. matlab偏最小二乘法及其检验
  2. c语言实现滑动窗口类,C语言模拟滑动窗口协议
  3. 华为OSN7500结构特点及产品定位相关知识
  4. Excel金额大小写转换公式
  5. Python之itchat
  6. 简单常用的互联网赚钱工具分享
  7. FFmpeg 以及帧率的解释
  8. 单元格里的字怎样居中_word表格中文字如何设置左右和上下居中(水平和垂直居中)...
  9. JanusGraph部署方案
  10. 中国大学MOOC 程序设计入门——C语言 翁凯 编程测试题汇总