写在前面
前面介绍了光照基础内容,以及材质和lighting maps,和光源类型,我们对使用光照增强场景真实感有了一定了解。但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很乏味。本节开始介绍模型加载,通过加载丰富的模型,能够丰富我们的场景,变得好玩。本节的示例代码均可以在我的github下载。

加载模型可以使用比较好的库,例如obj模型加载的库,Assimp加载库。本节作为入门篇,我们一开始不使用这些库加载很酷的模型,而是熟悉下模型以及模型加载的概念,然后我们封装一个简单的obj模型加载类,加载一个简单的立方体模型。

不要太急于看到漂亮的3D模型,下一节我们会使用Assimp库会加载一个酷炫的3d模型,但是本节还是注重多感受下模型加载的基础,否则下一节学习起来会吃力。

通过本节可以了解到

  • Mesh的概念
  • Obj模型数据格式
  • Obj模型简单的加载类和加载实验

模型的表达

在3d图形处理中,一个模型(model)通常由一个或者多个Mesh(网格)组成,一个Mesh是可绘制的独立实体。例如复杂的人物模型,可以分别划分为头部,四肢,服饰,武器等各个部分来建模,这些Mesh组合在一起最终形成人物模型。

Mesh由顶点、边、面Faces组成的,它包含绘制所需的数据,例如顶点位置、纹理坐标、法向量,材质属性等内容,它是OpenGL用来绘制的最小实体。Mesh的概念示意如下图所示(来自:What is a mesh in OpenGL?):

Mesh可以包含多个Face,一个Face是Mesh中一个可绘制的基本图元,例如三角形,多边形,点。要想模型更加逼真,一般需要增加更多图元使Mesh更加精细,当然这也会受到硬件处理能力的限制,例如PC游戏的处理能力要强于移动设备。由于多边形都可以划分为三角形,而三角形是图形处理器中都支持的基本图元,因此使用得较多的就是三角形网格来建模。例如下面的图(来自:What is a mesh in OpenGL?)表达了使用越来越复杂的Mesh建模一只兔子的过程:

随着增加三角形个数,兔子模型变得越来越真实。

目前模型存储的格式很丰富,比较常用的,例如Wavefront .obj file,COLLADA等,要了解各个格式的特点,可以参考wiki 3D graphics file formats。在众多的格式中以obj格式比较通用,它内部是以文本形式表达的,接下来我们通过熟悉下obj格式,了解模型是如何定义的,以及如何加载到OpenGL中来渲染模型。

Obj模型数据格式

obj模型内部以文本存储,例如从Model loading处获取的一个立方体模型cube.obj的数据如下:

# Blender3D v249 OBJ File: untitled.blend
# www.blender3d.org
mtllib cube.mtl
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 0.748573 0.750412
vt 0.749279 0.501284
vt 0.999110 0.501077
vt 0.999455 0.750380
vt 0.250471 0.500702
vt 0.249682 0.749677
vt 0.001085 0.750380
vt 0.001517 0.499994
vt 0.499422 0.500239
vt 0.500149 0.750166
vt 0.748355 0.998230
vt 0.500193 0.998728
vt 0.498993 0.250415
vt 0.748953 0.250920
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
vn -0.000001 0.000000 1.000000
vn 1.000000 -0.000000 0.000000
vn 1.000000 0.000000 0.000001
vn 0.000000 1.000000 -0.000000
vn -0.000000 -1.000000 0.000000
usemtl Material_ray.png
s off
f 5/1/1 1/2/1 4/3/1
f 5/1/1 4/3/1 8/4/1
f 3/5/2 7/6/2 8/7/2
f 3/5/2 8/7/2 4/8/2
f 2/9/3 6/10/3 3/5/3
f 6/10/4 7/6/4 3/5/4
f 1/2/5 5/1/5 2/9/5
f 5/1/6 6/10/6 2/9/6
f 5/1/7 8/11/7 6/10/7
f 8/11/7 7/12/7 6/10/7
f 1/2/8 2/9/8 3/13/8
f 1/2/8 3/13/8 4/14/8

对这个文本格式做一个简要说明:

  • 以#开始的行为注释行
  • usemtl和mtllib表示的材质相关数据,解析材质数据稍微繁琐,本节我们只是为了说明加载模型的原理,不做讨论。
  • o 引入一个新的object
  • v 表示顶点位置
  • vt 表示顶点纹理坐标
  • vn 表示顶点法向量
  • f 表示一个面,面使用1/2/8这样格式,表示顶点位置/纹理坐标/法向量的索引,这里索引的是前面用v,vt,vn定义的数据 注意这里Obj的索引是从1开始的,而不是0

模型一般通过3d建模软件,例如Blender, 3DS Max 或者 Maya等工具建模,导出时的数据格式变化较大,我们导入模型到OpenGL的任务就是:将一种模型数据文件表示的模型,转换为OpenGL可以利用的数据。例如上面的Obj文件中,我们需要解析顶点位置,纹理坐标等数据,构成OpenGL可以渲染的Mesh对象。

从Obj到OpenGL可以理解的Mesh

上面说明了Obj的数据格式,那么在OpenGL中我们怎么表达Mesh呢?首先定义顶点属性数据如下所示:

 // 表示一个顶点属性
struct Vertex
{glm::vec3 position;  // 顶点位置glm::vec2 texCoords; // 纹理坐标glm::vec3 normal;  // 法向量
};

Mesh中包含顶点属性,纹理对象等信息,本节我们定义Mesh数据结构如下所示:


// 表示一个OpenGL渲染的最小实体
class Mesh
{
public:void draw(Shader& shader) // 绘制MeshMesh(const std::vector<Vertex>& vertData, GLint textureId) // 构造一个Mesh
private:std::vector<Vertex> vertData;// 顶点数据GLuint VAOId, VBOId; // 缓存对象GLint textureId; // 纹理对象idvoid setupMesh();  // 建立VAO,VBO等缓冲区
};

载入obj模型的过程,就是读取obj文件,并转换为上面Mesh对象的过程。这个过程的思路大致是这样的,读取文件的每一行,根据行首部的指示,确定数据类型,然后加载到mesh的vertData里面去,这个框架是这样:

std::ifstream file(objFilePath);
while (getline(file, line))
{if (line.substr(0, 2) == "vt") // 顶点纹理坐标数据{// 解析顶点纹理数据}else if (line.substr(0, 2) == "vn") // 顶点法向量数据{// 解析法向量数据}else if (line.substr(0, 1) == "v") // 顶点位置数据{// 解析顶点位置数据}else if (line.substr(0, 1) == "f") // 面数据{// 解析面数据}else if (line[0] == '#') // 注释忽略{ }else  {// 其余内容 暂时不处理}
}

上面提供了一个读取obj文件格式的框架,例如解析纹理坐标数据如下:

if (line.substr(0, 2) == "vt") // 顶点纹理坐标数据
{std::istringstream s(line.substr(2));glm::vec2 v;s >> v.x; s >> v.y;v.y = -v.y;  // 注意这里加载的dds纹理 要对y进行反转temp_textCoords.push_back(v);
}

其余的也类似处理。读取到数据后,在Mesh对象里面需要向前面绘制物体时一样建立缓冲数据,如下:

void setupMesh()  // 建立VAO,VBO等缓冲区
{glGenVertexArrays(1, &this->VAOId);glGenBuffers(1, &this->VBOId);glBindVertexArray(this->VAOId);glBindBuffer(GL_ARRAY_BUFFER, this->VBOId);glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)* this->vertData.size(),&this->vertData[0], GL_STATIC_DRAW);// 顶点位置属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*)0);glEnableVertexAttribArray(0);// 顶点纹理坐标glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*)(3 * sizeof(GL_FLOAT)));glEnableVertexAttribArray(1);// 顶点法向量属性glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*)(5 * sizeof(GL_FLOAT)));glEnableVertexAttribArray(2);glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);
}

建立缓冲区的同时,本节我们使用的立方体模型cube.dds纹理如下图所示:

这与以前使用的png纹理不一样,这里我用C++重新改编了Model loading处的加载dds纹理的函数,加载纹理不是本节的重点,具体可以查看github代码。加载纹理后,可以渲染这个obj表达的立方体模型,整个过程如下:

//Section1 从obj文件加载数据
std::vector<Vertex> vertData;
ObjLoader::loadFromFile("cube.obj", vertData)// Section2 准备纹理
GLint textureId = TextureHelper::loadDDS("cube.dds");// Section3 建立Mesh对象
Mesh mesh(vertData, textureId);// Section4 准备着色器程序
Shader shader("cube.vertex", "cube.frag");// 在游戏主循环中渲染立方体

这里我们可以看到,与以往在程序中通过数值指定立方体模型相比,我们的代码更简洁,后面介绍使用Assimp加载库后,可以加载更多丰富的模型,当然要比这个立方体好看。但是本节还是看一下最终立方体的效果吧,如下:

最后的说明

在使用dds纹理的时候,要注意纹理的y轴相对于OpenGL是进行反转的,因此需要使用( coord.u, 1.0-coord.v) 来访问,这可以在加载obj时做,也可以在着色器里面做。没有使用反转的v坐标将导致,无法正常渲染,这也是困住我的一个地方。后来使用数据比对格式发现了这个错误,如下图,左边是反转了的数据,右边是未反转的数据:

在使用blender软件导出模型时,即使勾选了includ UVs,输出时仍然没有纹理坐标,这是因为除了勾选这些选项外,还需要一个uv map操作,关于这一点也是容易产生错误的,详细可以参考Add UV Mapped texture coordinates to OBJ file?。uv mappring这个操作的过程比较繁琐,就不再这里介绍了,感兴趣地可以参考UV Mapping a Mesh

最后本节的加载obj程序只是一个示例,并没有解析材质mtl部分。当没有使用纹理数据绘制经典的Suzanne 模型如下图所示:

这里缺少了纹理和光照,所以模型看起来不真实,下一节介绍使用Assimp加载库时将会改善这一点。

参考资料

  1. https://www.quora.com/What-is-a-mesh-in-OpenGL
  2. http://gamedev.stackexchange.com/questions/38412/whats-the-difference-between-mesh-and-a-model
  3. http://www.opengl-tutorial.org/beginners-tutorials/tutorial-7-model-loading/
  4. https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Load_OBJ
  5. http://www.opengl-tutorial.org/beginners-tutorials/tutorial-5-a-textured-cube/

OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)相关推荐

  1. OPENGL学习脚印

    OPENGL学习脚印 声明:本文转载自wangdingqiao的博客专栏–OPENGL学习脚印,仅仅对OPENGL学习脚印专栏的文章进行了整理,方便初学者按照顺序学习.同时也在这里感谢原创博主提供的学 ...

  2. OpenGL学习脚印: 二维纹理映射(2D textures)

    写在前面  前面两节介绍了向量和矩阵,以及坐标和转换相关的数学,再继续讨论模型变换等其他包含数学内容的部分之前,本节介绍二维纹理映射,为后面学习做一个准备.纹理映射本身也是比较大的主题,本节只限于讨论 ...

  3. OpenGL学习脚印:光源类型和使用多个光源(Light source and multiple lights)

    写在前面 上一节光照中使用材质和lighting maps介绍了使用材质属性和lighting maps使物体的光照效果能反映物体的材料特性,看起来更逼真.在前面的章节中使用的实际上都是一个点光源,本 ...

  4. OpenGL学习脚印:立方体纹理和天空包围盒(Cubemaps And Skybox)

    写在前面 之前学习了2D纹理映射,实际上还有其他类型的纹理有待我们进一步学习,本节将要学习的立方体纹理(cubemaps),是一种将多个纹理图片复合到一个立方体表面的技术.在游戏中应用得较多的天空包围 ...

  5. OpenGL学习脚印: 帧缓冲对象(Frame Buffer Object)

    写在前面 一直以来,我们在使用OpenGL渲染时,最终的目的地是默认的帧缓冲区,实际上OpenGL也允许我们创建自定义的帧缓冲区.使用自定义的帧缓冲区,可以实现镜面,离屏渲染,以及很酷的后处理效果.本 ...

  6. OpenGL学习脚印: 绘制一个三角形

    写在前面 接着上一节内容,开发环境搭建好后,我们当然想立即编写3D应用程序了.不过我们还需要些耐心,因为OpenGL是一套底层的API,因而我们要掌握的基本知识稍微多一点,在开始绘制3D图形之前,本节 ...

  7. OpenGL学习脚印: 投影矩阵和视口变换矩阵

    OpenGL中的视图可以利用照相机来进行比拟.产生目标的场景的变过过程类似于相机拍照.此步骤大概分为三个: 1)把相机固定在三角架上,并让它对准场景(视图变换) 2)对场景安排,使得各个物体在招片中的 ...

  8. OpenGL学习脚印:伽马校正(Gamma Correction)

    写在前面 由于CRT,LED等显示设备显示颜色时并非按照线性方式工作,因此我们在程序中输出的颜色,最终输出到显示器上时会产生亮度减弱的现象,这种现象在计算光照和实时渲染时对图形质量有一定影响,需要我们 ...

  9. OpenGL学习脚印:缓冲对象相关函数的使用(buffer object function)

    OpenGL中还包含除了我们前面介绍的VAO,VBO,EBO等其他类型的缓冲对象.本文将通过简洁.可靠的例子说明一些重要方法的使用,以辅助学习这些方法.本文的目的不是写成详细而厚重的手册,对于文中未详 ...

最新文章

  1. CMSGC造成内存碎片的解决方法
  2. JQuery-- 获取元素的宽高、获取浏览器的宽高和垂直滚动距离
  3. .net 导出html 到excel 合并单元格,C#导出Excel,并且设置Excel单元格格式,合并单元格....
  4. 在Rancher 1.6上部署Traefik负载均衡器
  5. MAP Protocol 协议(2)介绍二
  6. win10无法装载iso文件_win 10如何装载和弹出ISO映像文件
  7. 数据库-优化-慢查日志分析工具-pt-query-digest介绍及作用
  8. linux桌面版排行2019_新兴的桌面发行版 Septor Linux 发布 2019 版
  9. 【渝粤题库】国家开放大学2021春3700汽车电工电子基础题目
  10. Discuz!X集群部署的系统方案和改造方式讨论
  11. 网络爬虫--5.urllib库的基本使用(1)
  12. 今天你的静态变量和静态代码块执行了吗?
  13. 信息文档分工会在运动会象棋比赛中夺得佳绩
  14. 在优矿(uqer.io)上打开量化投资的黑箱-技术分析篇
  15. 化工过程开发与工程思维
  16. 服务器安全(防止被攻击)
  17. 新消费下的国货崛起新模式!
  18. MAC 清除 DNS 缓存
  19. 《精英日课》第三季_2019年四月新书《九个工作谎言》_1工作是具体的,公司是虚拟的
  20. W10的AndroidStudio4.0.1的下载安装与配置

热门文章

  1. Redis持久化的两种方式
  2. python快捷键设置_Pycharm快捷键配置
  3. 写一段js代码 生成心形动画,动画路径内部填充渐变色
  4. 雅虎邮箱账号防自动注册验证码系统被攻破
  5. vwap 公式_【IB交易者词汇】交易量加权平均价格(VWAP )
  6. 苹果电脑如何录屏?3个方法,轻松学会
  7. 蝙蝠算法的matlab程序,经典蝙蝠算法MATLAB实现
  8. 微软修复工具_GitHub 月榜第 3,微软的老照片修复工具火了
  9. unity AR开发中遇到的一些错误总结
  10. Java-反射机制详解