1. Assimp

目前为止,我们已经可以绘制一个物体,并添加不同的光照效果了。但是我们的顶点数据太过简单,只能绘制简单的立方体。但是房子汽车这种不规则的形状我们的顶点数据就很难定制了。索性,这部分并不需要我们苦逼的开发人员去考虑。成熟的3D建模工具可以将设计师设计的模型导出模型文件,借助模型加载库就可以将他们转化为顶点数据。

 2. 模型加载库

一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。由于Assimp的数据结构保持不变,不论导入的是什么种类的文件格式,它都能够将我们从这些不同的文件格式中抽象出来,用同一种方式访问我们需要的数据。

当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:

这个结构图是一个模型在assimp中的基础结构,如果看不懂也没关系,到后面我们会频繁的使用它。

3. 网格

在上一节中,我们知道了Assimp中的基本单元式Mesh或者Model。这一节中我们就先定义一个自己的Mesh类。

4. Mesh

Mesh应该作为一个最基本的绘制单元,那么他应该自己维护VAO、VBO、EBO这些数据。并且他应该具备自动绑定数据及定义针对自身的Mesh自动设置数据格式等功能。

之前我们都是一个float数组来表示,这是因为GL在绑定VAO的时候需要的是一个连续的内存,我们通过指定数据其实地址和数据长度就可以告诉GL如何去绑定数据。但是数组看起来并不是一个直观的形式,我们希望能找到一个更加明了的形式来方便我们查看数据。庆幸的是,结构体中的内存地址是连续的。我们将数组中的数值替换成结构体,这样我们可以清楚地区分出不同顶点。

此外,数组我们也可以进一步简化一下:数组的长度是一个定长,需要在一开始就指定数组长度,而且数组的元素个数也需要计算才可以获得。c++中提供了一个很好的扩展就是向量vector。

这里我们直接放一下Mesh类的代码,注意现在我们还没有将他写成一个通用的Mesh类,而是针对当前箱子模型的片段着色器写的一个Mesh类。

  1 #ifndef Mesh_h
2 #define Mesh_h
3
4 ///system framework
5 #include <vector>
6
7 ///third party framework
8 #include <glm/glm.hpp>
9 #include <glm/gtc/matrix_transform.hpp>
10 #include <glm/gtc/type_ptr.hpp>
11
12 ///custom framework
13 #include "Shader.h"
14
15 using namespace std;
16
17 struct Mesh_Vertex {
18 glm::vec3 Position;
19 glm::vec3 Normal;
20 glm::vec2 TexCoords;
21 };
22
23 ///纹理结构体(标明已经加载的纹理的纹理ID及纹理对应类型)
24 struct Mesh_Texture {
25 unsigned int t_id;
26 string type;
27 };
28
29 class Mesh {
30 public:
31
32 vector<Mesh_Vertex> vertices;
33 vector<unsigned int> indices;
34 vector<Mesh_Texture> textures;
35 unsigned int VAO;
36
37 Mesh(vector<Mesh_Vertex> aVertices, vector<unsigned int> aIndices, vector<Mesh_Texture> aTextures) {
38 vertices = aVertices;
39 indices = aIndices;
40 textures = aTextures;
41 setupMesh();
42 }
43
44 Mesh(vector<Mesh_Vertex> aVertices, vector<unsigned int> aIndices) {
45 vertices = aVertices;
46 indices = aIndices;
47 setupMesh();
48 }
49
50 Mesh() {
51
52 }
53
54 void Draw(Shader shader) {
55 for (int i = 0; i < textures.size(); ++i) {
56 ///首先激活指定位置的纹理单元
57 glActiveTexture(GL_TEXTURE0 + i);
58 string name;
59 string type = textures[i].type;
60 if (type == "diffuse") {
61 name = "material.diffuse";
62 } else if (type == "specular") {
63 name = "material.specular";
64 }
65 shader.setInt(name, i);
66 glBindTexture(GL_TEXTURE_2D,textures[i].t_id);
67 }
68 DrawWithoutConfigImage();
69 ///结束顶点数组对象的绑定
70 glBindVertexArray(0);
71 glActiveTexture(GL_TEXTURE0);
72 }
73
74 void DrawWithoutConfigImage() {
75 glBindVertexArray(VAO);
76 glDrawElements(GL_TRIANGLES, (int)indices.size(), GL_UNSIGNED_INT, 0);
77 }
78
79 void ReleaseMesh() {
80 ///释放对象
81 glDeleteVertexArrays(1, &VAO);
82 glDeleteBuffers(1, &VBO);
83 glDeleteBuffers(1, &EBO);
84 }
85
86 private:
87 unsigned int VBO,EBO;
88
89 void setupMesh(){
90 glGenVertexArrays(1,&VAO);
91 glGenBuffers(1,&VBO);
92 glGenBuffers(1,&EBO);
93 glBindVertexArray(VAO);
94 glBindBuffer(GL_ARRAY_BUFFER,VBO);
95 glBufferData(GL_ARRAY_BUFFER,vertices.size() * sizeof(Mesh_Vertex),&vertices[0],GL_STATIC_DRAW);
96 glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Mesh_Vertex),(void *)(offsetof(Mesh_Vertex, Position)));
97 glEnableVertexAttribArray(0);
98 glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Mesh_Vertex),(void *)(offsetof(Mesh_Vertex, Normal)));
99 glEnableVertexAttribArray(1);
100 glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,sizeof(Mesh_Vertex),(void *)(offsetof(Mesh_Vertex, TexCoords)));
101 glEnableVertexAttribArray(2);
102 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
103 glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
104 glBindVertexArray(0);
105 glBindBuffer(GL_ARRAY_BUFFER, 0);
106 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
107 }
108 };
109
110 #endif

我们看到我们只是将原来在main.mm中的数据绑定过程移到了Mesh类中,其他的地方基本没有什么变化。

5. 通用Mesh类

观察我们上面的代码,我们唯一不通用的地方就是纹理绑定的时候。如果想通用,就要求我们的片段着色器中的纹理命名应该是可以用一个通式表达出来的形式。如:

那么如果是这样的,我们的绑定部分就可以改造成这个样子:

这里还是简单的解释一下我们的Mesh类工作的流程。

  • 1.初始化时传入顶点数据、索引数据、纹理数据(这里我们确定了绘制什么、如何绘制的问题)。
  • 2.自动绑定VAO、EBO。获取到可复用的模型对象。
  • 3.绘制时每次都重新绑定GL当前激活的纹理单元,并按照索引绘制模型。

几个可以重点解释的地方:

  • 1.传入Mesh类的实际为已经提交给GL的纹理的ID。在外界的时候我们加载图像后,GL中即已存在该纹理的一份拷贝,我们可以通过GL返回给我们的ID找到对应的数据。在想要使用的时候只要将指定位置的纹理单元激活后将对应的ID绑定在该纹理单元上即可让激活的纹理单元上的数据指向指定纹理数据,而后再将片段着色器中纹理绑定为指定纹理单元即可。
  • 2.GL中可用的纹理单元是有限的,故而我们要反复使用纹理单元,所以在每次使用前应重新绑定纹理纹理数据。当然这是相对的,如果你使用的纹理单元足够少而不用复用的话,你也可以只绑定一次。具体还是要视情况而定。
  • 3.在每一次Mesh绘制完毕后,我们要记得恢复当前激活的纹理位置为GL_TEXTURE0。这样是为了保持其与系统默认行为一致,不至于引起额外变量。

OpenGL 模型加载详解的更多相关文章

  1. Javascript 异步加载详解

    Javascript 异步加载详解 本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy ...

  2. Javascript 异步加载详解(转)

    本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy execution),async 属 ...

  3. javascript异步加载详解(转)

    本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy execution),async 属 ...

  4. 转:web前端面试题合集 (Javascript相关)(js异步加载详解)

    1. HTTP协议的状态消息都有哪些? 1**:请求收到,继续处理2**:操作成功收到,分析.接受3**:完成此请求必须进一步处理4**:请求包含一个错误语法或不能完成5**:服务器执行一个完全有效请 ...

  5. [转载]Javascript 同步异步加载详解

    http://handyxuefeng.blog.163.com/blog/static/4545217220131125022640/ 本文总结一下浏览器在 javascript 的加载方式. 关键 ...

  6. Javascript图片预加载详解

    预加载图片是提高用户体验的一个很好方法.图片预先加载到浏览器中,访问者便可顺利地在你的网站上冲浪,并享受到极快的加载速度.这对图片画廊及图片占据很大比例的网站来说十分有利,它保证了图片快速.无缝地发布 ...

  7. WEB容器启动——web.xml加载详解

    最近在看spring的源码,关于web.xml文件在容器(Tomcat.JBOSS等)启动时加载顺序问题很混乱,通过搜集资料,得出以下的结论: 1.加载顺序与它们在 web.xml 文件中的先后顺序无 ...

  8. iOS-上拉刷新,下拉加载-----------详解

    一.使用的第三方库 1.AFNetworking       ----> 网络请求 2. MJRefresh           ----> 刷新 3. MBProgressHUD  -- ...

  9. 单个SWF文件loading加载详解(转)

    通过带宽查看器,可以看到SWF中每帧所占带宽状况.另外,我们还可以在Flash发布设置中,选择生成体积报告. 勾选这一项之后,发布flash时,会自动在fla目录中生成一个名为”文件名 Report. ...

  10. js模块加载详解

    看着java中各种import加载,在回过头来看看javascript还在自己造轮子,写各种XX的模块加载框架,ECMASCRIPT6不知什么时候能够普及.不过DT归DT,该学的还是要学. 一 同步加 ...

随机推荐

  1. 文心一言 VS 讯飞星火 VS chatgpt (65)-- 算法导论6.5 4题

    四.在 MAX-HEAP-INSERT 的第 2行,为什么我们要先把关键字设为 -∞ ,然后又将其增加到所需的值呢? 文心一言: 在 MAX-HEAP-INSERT 算法的第二行将关键字设为 -∞,是 ...

  2. XCTF-CGfsb

    考察知识点 PWN.格式化字符串漏洞 题目链接 https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=0&am ...

  3. Android Studio Giraffe安装与gradle配置

    本机环境:win10专业版,64位,16G内存. 原先用的AS2.2,是很早之前在看<第一行代码Android(第2版)>的时候,按书里的链接下载安装的,也不用怎么配置.(PS:第一行代码 ...

  4. 如何通过API接口获取淘宝的商品评论

    在淘宝网上购买产品时,人们通常会查看其他客户留下的评价和评论.这些评价和评论对于购买决策非常有帮助,因为它们提供了其他客户的观点和建议.通过使用淘宝API接口,可以轻松地获取淘宝上任何商品的评论. 以 ...

  5. QA|Pycharm中的git分支提交冲突问题和解决|GIT

    前天,Pycharm中的git分支提交冲突了,原因是我PC上改了文件没有提交,笔记本又本地改代码,笔记本提交时就出现报错:提交拒绝,但pull也被拒绝,网上试了rebase等方法,均没得到解决,最终自 ...

  6. Asp-Net-Core开发笔记:FrameworkDependent搭配docker部署

    前言 之前我写过一篇使用 docker 部署 AspNetCore 应用的文章,这种方式搭配 CICD 非常方便, build 之后 push 到私有的 dockerhub ,在生产服务器上 pull ...

  7. 15.3K Star,超好用的开源协作式数字白板:tldraw

    大家好,我是TJ 今天给大家推荐一个开源协作式数字白板:tldraw. tldraw的编辑器.用户界面和其他底层库都是开源的,你可以在它的开源仓库中找到它们.它们也在NPM上分发,提供开发者使用.您可 ...

  8. .NET周刊【9月第2期 2023-09-10】

    国内文章 使用 OpenTelemetry 构建 .NET 应用可观测性(2):OpenTelemetry 项目简介 https://www.cnblogs.com/eventhorizon/p/17 ...

  9. SpringBoot拦截器和动态代理有什么区别?

    在 Spring Boot 中,拦截器和动态代理都是用来实现功能增强的,所以在很多时候,有人会认为拦截器的底层是通过动态代理实现的,所以本文就来盘点一下他们两的区别,以及拦截器的底层实现. 1.拦截器 ...

  10. 【目标检测】Fast R-CNN算法实现

    一.前言 2014年,Ross Girshick提出RCNN,成为目标检测领域的开山之作.一年后,借鉴空间金字塔池化思想,Ross Girshick推出设计更为巧妙的Fast RCNN(https:/ ...