OpenGL学习:模型加载-obj模型和AssImp模型
2017-09-26 16:16
411 查看
前面介绍了光照基础内容,以及材质和lighting
maps,和光源类型,我们对使用光照增强场景真实感有了一定了解。但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很乏味。本节开始介绍模型加载,通过加载丰富的模型,能够丰富我们的场景,变得好玩。本节的示例代码均可以在我的github下载。
加载模型可以使用比较好的库,例如obj模型加载的库,Assimp加载库。本节作为入门篇,我们一开始不使用这些库加载很酷的模型,而是熟悉下模型以及模型加载的概念,然后我们封装一个简单的obj模型加载类,加载一个简单的立方体模型。 之后我们会使用Assimp库会加载一个酷炫的3d模型,但是首先还是注重多感受下模型加载的基础
通过本节可以了解到
Mesh的概念
Obj模型数据格式
Obj模型简单的加载类和加载实验
AssImp模型加载实验
在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模型内部以文本存储,例如从Model
loading处获取的一个立方体模型cube.obj的数据如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[/code]
对这个文本格式做一个简要说明:
usemtl和mtllib表示的材质相关数据,解析材质数据稍微繁琐,本节我们只是为了说明加载模型的原理,不做讨论。
o 引入一个新的object
v 表示顶点位置
vt 表示顶点纹理坐标
vn 表示顶点法向量
f 表示一个面,面使用1/2/8这样格式,表示顶点位置/纹理坐标/法向量的索引,这里索引的是前面用v,vt,vn定义的数据 注意这里Obj的索引是从1开始的,而不是0
模型一般通过3d建模软件,例如Blender, 3DS
Max 或者 Maya等工具建模,导出时的数据格式变化较大,:将一种模型数据文件表示的模型,转换为OpenGL可以利用的数据。例如上面的Obj文件中,我们需要解析顶点位置,纹理坐标等数据,构成OpenGL可以渲染的Mesh对象。
上面说明了Obj的数据格式,那么在OpenGL中我们怎么表达Mesh呢?首先定义顶点属性数据如下所示:
2
3
4
5
6
7
[/code]
Mesh中包含顶点属性,纹理对象等信息,本节我们定义Mesh数据结构如下所示:
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]
载入obj模型的过程,就是读取obj文件,并转换为上面Mesh对象的过程。这个过程的思路大致是这样的,读取文件的每一行,根据行首部的指示,确定数据类型,然后加载到mesh的vertData里面去,这个框架是这样:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[/code]
上面提供了一个读取obj文件格式的框架,例如解析纹理坐标数据如下:
2
3
4
5
6
7
8
9
[/code]
其余的也类似处理。读取到数据后,在Mesh对象里面需要向前面绘制物体时一样建立缓冲数据,如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]
建立缓冲区的同时,本节我们使用的立方体模型cube.dds纹理如下图所示:
这与以前使用的png纹理不一样,这里我用C++重新改编了Model
loading处的加载dds纹理的函数,加载纹理不是本节的重点,具体可以查看github代码。加载纹理后,可以渲染这个obj表达的立方体模型,整个过程如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]
这里我们可以看到,与以往在程序中通过数值指定立方体模型相比,我们的代码更简洁,后面介绍使用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加载库时将会改善这一点。
AssImp是一个模型加载库,它将不同格式的模型数据转换为统一的抽象的数据类型,因而支持较多的模型文件格式。下载和编译这个库的过程,你可以参考官方文档。在linux下可以直接apt-get安装: apt-get
install libassimp-dev。
加载模型的任务就是将抽象的模型数据转换为OpenGL可以处理的VBO,EBO,纹理数据。在程序内部我们定义了Mesh,Model结构来作为内部格式。Mesh表达是绘制的最小实体,它包含顶点属性数据、材质数据;Model则是包含1个或者多个Mesh的模型。定义Mesh结构如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[/code]
为了简化程序,这里我们只处理了材质中的纹理数据。Model则是一个包含多个Mesh的类,定义如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[/code]
需要注意的是模型文件所在文件路径我们通过modelFileDir保存起来,因为模型中纹理数据可能使用相对路径来表示纹理,通过modelFileDir加上这个相对路径才能找到纹理图片的正确路径。
加载模型时首先创建 Assimp::Importer的示例,然后通过它的l Assimp::Importer::ReadFile()方法加载模型,如下所示:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[/code]
ReadFile函数中第二个参数就是后处理选项,它是一个枚举类型aiPostProcessSteps,可以使用位或操作包含多个选项,例如选项aiProcess_MakeLeftHanded表示将默认的右手系坐标数据转换为左手系坐标数据,aiProcess_Triangulate选项将索引数据多余3个的多边形划分为多个三角形,方便我们使用三角形进行绘制。完整的后处理选项列表,可以参考官方文档。
通过上面的加载我们获取到了模型的根结构数据aiScene,接下来的工作就是:从aiScene获取OpenGL所需要的VBO,EBO,纹理数据。
AssImp中数据通过aiNode组织父子结点,包含了层次信息,我们可以忽略这些信息,直接读取所有我们需要的VBO,EBO,纹理数据,但是这种父子结构信息在后面制作骨骼动画时会再次用到,因此这里还是按照层次的方式来解析aiScene数据。
所谓结点就是包含一个多个Mesh的部位,例如一个人物角色,可能包含头部,颈部,手臂,胸部等多个结点,每个结点也可以包含更多的细化结点。解析aiScene这种父子结点的层次数据,直观的方法就是使用递归,递归就是一个函数直接调用自己,一层一层调用下去,当遇到一个合适条件时终止调用,函数一层层返回。从aiScene解析模型数据获取OpenGL所需数据的框架大概是这样的:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[/code]
上面的框架给出了从aiScene获取数据,建立内部格式Model和Mesh的思路,具体实现细节可以参考程序源码。
到这里,我们可以来欣赏酷炫的模型了,首先加载一个从learnopengl获取的纳米战斗服模型nanosuit,效果如下图左所示:
这里没有使用光照,上图右是实现了一个点光源的效果. 可以从机器人胸部的高光部分看到,实现光照时的区别。
1.加载模型后,需要适当设置模型变换矩阵,否则模型显示在奇怪的位置。这个模型变换矩阵,目前还没找到合适的方法从模型数据中获取。
2.下载的模型,有些路径是不正确的,本文统一采用绝对路径方式。路径不正确或者文件缺失时的错误提示
3.部分纹理图片的格式,模型的格式目前并未处理,不支持加载。
上面加载的模型,已经让人很兴奋了,但是还不够真实,高效。在实验过程中,思考还需要通过以下方面进行改进:
1.我们这里的材质只处理了纹理部分,实际上模型中如果没有通过纹理定义材质,还需要获取ambient等颜色表示的材质。而且纹理可能不止一个,本文目前只处理了一个纹理(主要原因是下载的素材里面没有找不到更多的纹理坐标)。可以通过定义下面的材质结构体,并处理这个材质数据来丰富场景:
2
3
4
5
6
7
8
9
[/code]
2.模型中要通过光源和相机加以改善。目前在模型中通过以下方式:
2
3
4
5
6
7
8
[/code]
获取光源数据时,大量从网络上下载的模型中并没有找到光源数据,比较可惜。
3.实际模型的材质中包含了map_Bump数据,但目前还未学习处理方法。
4.目前通过Model加载模型时耗时非常多,效率不高,需要进一步提高模型加载和渲染的速度
maps,和光源类型,我们对使用光照增强场景真实感有了一定了解。但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很乏味。本节开始介绍模型加载,通过加载丰富的模型,能够丰富我们的场景,变得好玩。本节的示例代码均可以在我的github下载。
加载模型可以使用比较好的库,例如obj模型加载的库,Assimp加载库。本节作为入门篇,我们一开始不使用这些库加载很酷的模型,而是熟悉下模型以及模型加载的概念,然后我们封装一个简单的obj模型加载类,加载一个简单的立方体模型。 之后我们会使用Assimp库会加载一个酷炫的3d模型,但是首先还是注重多感受下模型加载的基础
通过本节可以了解到
Mesh的概念
Obj模型数据格式
Obj模型简单的加载类和加载实验
AssImp模型加载实验
模型的表达
在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模型内部以文本存储,例如从Modelloading处获取的一个立方体模型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 ... vt 0.748573 0.750412 vt 0.749279 0.501284 vt 0.999110 0.501077 ... vn 0.000000 0.000000 -1.000000 vn -1.000000 -0.000000 -0.000000 vn -0.000000 -0.000000 1.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 ...1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[/code]
对这个文本格式做一个简要说明:
usemtl和mtllib表示的材质相关数据,解析材质数据稍微繁琐,本节我们只是为了说明加载模型的原理,不做讨论。
o 引入一个新的object
v 表示顶点位置
vt 表示顶点纹理坐标
vn 表示顶点法向量
f 表示一个面,面使用1/2/8这样格式,表示顶点位置/纹理坐标/法向量的索引,这里索引的是前面用v,vt,vn定义的数据 注意这里Obj的索引是从1开始的,而不是0
模型一般通过3d建模软件,例如Blender, 3DS
Max 或者 Maya等工具建模,导出时的数据格式变化较大,:将一种模型数据文件表示的模型,转换为OpenGL可以利用的数据。例如上面的Obj文件中,我们需要解析顶点位置,纹理坐标等数据,构成OpenGL可以渲染的Mesh对象。
从Obj到OpenGL可以理解的Mesh
上面说明了Obj的数据格式,那么在OpenGL中我们怎么表达Mesh呢?首先定义顶点属性数据如下所示:// 表示一个顶点属性 struct Vertex { glm::vec3 position; // 顶点位置 glm::vec2 texCoords; // 纹理坐标 glm::vec3 normal; // 法向量 };1
2
3
4
5
6
7
[/code]
Mesh中包含顶点属性,纹理对象等信息,本节我们定义Mesh数据结构如下所示:
// 表示一个OpenGL渲染的最小实体 class Mesh { public: void draw(Shader& shader) // 绘制Mesh Mesh(const std::vector<Vertex>& vertData, GLint textureId) // 构造一个Mesh private: std::vector<Vertex> vertData;// 顶点数据 GLuint VAOId, VBOId; // 缓存对象 GLint textureId; // 纹理对象id void setupMesh(); // 建立VAO,VBO等缓冲区 };1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]
载入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 { // 其余内容 暂时不处理 } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[/code]
上面提供了一个读取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); }1
2
3
4
5
6
7
8
9
[/code]
其余的也类似处理。读取到数据后,在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); }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]
建立缓冲区的同时,本节我们使用的立方体模型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"); // 在游戏主循环中渲染立方体1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]
这里我们可以看到,与以往在程序中通过数值指定立方体模型相比,我们的代码更简洁,后面介绍使用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加载库时将会改善这一点。
下载和安装AssImp
AssImp是一个模型加载库,它将不同格式的模型数据转换为统一的抽象的数据类型,因而支持较多的模型文件格式。下载和编译这个库的过程,你可以参考官方文档。在linux下可以直接apt-get安装: apt-getinstall libassimp-dev。
OpenGL需要的数据结构
加载模型的任务就是将抽象的模型数据转换为OpenGL可以处理的VBO,EBO,纹理数据。在程序内部我们定义了Mesh,Model结构来作为内部格式。Mesh表达是绘制的最小实体,它包含顶点属性数据、材质数据;Model则是包含1个或者多个Mesh的模型。定义Mesh结构如下:// 表示一个顶点属性 struct Vertex { glm::vec3 position; glm::vec2 texCoords; glm::vec3 normal; }; // 表示一个Texture struct Texture { GLuint id; aiTextureType type; std::string path; }; // 表示一个用于渲染的最小实体 class Mesh { public: void draw(const Shader& shader) const;// 绘制Mesh Mesh():VAOId(0), VBOId(0), EBOId(0){} Mesh(const std::vector<Vertex>& vertData, const std::vector<Texture> & textures, const std::vector<GLuint>& indices); // 构造一个Mesh void final() const; // 释放VBO等空间 private: std::vector<Vertex> vertData; // 顶点属性数据 std::vector<GLuint> indices; // 索引数据 std::vector<Texture> textures; // 纹理数据 GLuint VAOId, VBOId, EBOId; void setupMesh(); // 建立VAO,VBO等缓冲区 };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[/code]
为了简化程序,这里我们只处理了材质中的纹理数据。Model则是一个包含多个Mesh的类,定义如下:
// 代表一个模型 模型可以包含一个或多个Mesh class Model { public: void draw(const Shader& shader) const { for (mesh in meshes) { mesh->draw(shader); } } bool loadModel(const std::string& filePath); ~Model() { for (mesh in meshes) { mesh->final(); } } private: bool processNode(const aiNode* node, const aiScene* sceneObjPtr); bool processMesh(const aiMesh* meshPtr, const aiScene* sceneObjPtr, Mesh& meshObj); bool processMaterial(const aiMaterial* matPtr, const aiScene* sceneObjPtr, Material& material); private: std::vector<Mesh> meshes; // 保存Mesh std::string modelFileDir; // 保存模型文件的文件夹路径 typedef std::map<std::string, Texture> LoadedTextMapType; // key = texture file path LoadedTextMapType loadedTextureMap; // 保存已经加载的纹理 };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[/code]
需要注意的是模型文件所在文件路径我们通过modelFileDir保存起来,因为模型中纹理数据可能使用相对路径来表示纹理,通过modelFileDir加上这个相对路径才能找到纹理图片的正确路径。
AssImp加载模型
加载模型时首先创建 Assimp::Importer的示例,然后通过它的l Assimp::Importer::ReadFile()方法加载模型,如下所示:#include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> Assimp::Importer importer; const aiScene* sceneObjPtr = importer.ReadFile(filePath, aiProcess_Triangulate | aiProcess_FlipUVs); if (!sceneObjPtr || sceneObjPtr->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !sceneObjPtr->mRootNode) { std::cerr << "Error:Model::loadModel, description: " << importer.GetErrorString() << std::endl; return false; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[/code]
ReadFile函数中第二个参数就是后处理选项,它是一个枚举类型aiPostProcessSteps,可以使用位或操作包含多个选项,例如选项aiProcess_MakeLeftHanded表示将默认的右手系坐标数据转换为左手系坐标数据,aiProcess_Triangulate选项将索引数据多余3个的多边形划分为多个三角形,方便我们使用三角形进行绘制。完整的后处理选项列表,可以参考官方文档。
通过上面的加载我们获取到了模型的根结构数据aiScene,接下来的工作就是:从aiScene获取OpenGL所需要的VBO,EBO,纹理数据。
AssImp中数据通过aiNode组织父子结点,包含了层次信息,我们可以忽略这些信息,直接读取所有我们需要的VBO,EBO,纹理数据,但是这种父子结构信息在后面制作骨骼动画时会再次用到,因此这里还是按照层次的方式来解析aiScene数据。
所谓结点就是包含一个多个Mesh的部位,例如一个人物角色,可能包含头部,颈部,手臂,胸部等多个结点,每个结点也可以包含更多的细化结点。解析aiScene这种父子结点的层次数据,直观的方法就是使用递归,递归就是一个函数直接调用自己,一层一层调用下去,当遇到一个合适条件时终止调用,函数一层层返回。从aiScene解析模型数据获取OpenGL所需数据的框架大概是这样的:
bool loadModel(const std::string& filePath) { // 加载模型 得到aiScene const aiScene* sceneObjPtr = importer.ReadFile(filePath, aiProcess_Triangulate | aiProcess_FlipUVs); // 递归处理结点 return this->processNode(sceneObjPtr->mRootNode, sceneObjPtr); } bool processNode(const aiNode* node, const aiScene* sceneObjPtr) { for (size_t i = 0; i < node->mNumMeshes; ++i) // 先处理自身结点 { // 注意node中的mesh是对sceneObject中mesh的索引 const aiMesh* meshPtr = sceneObjPtr->mMeshes[node->mMeshes[i]]; this->processMesh(meshPtr, sceneObjPtr, meshObj); // 处理Mesh } for (size_t i = 0; i < node->mNumChildren; ++i) // 再处理孩子结点 { this->processNode(node->mChildren[i], sceneObjPtr); } return true; } bool processMesh(const aiMesh* meshPtr, const aiScene* sceneObjPtr, Mesh& meshObj) { // 从Mesh得到顶点数据、法向量、纹理数据 for (size_t i = 0; i < meshPtr->mNumVertices; ++i){...} // 获取索引数据 for (size_t i = 0; i < meshPtr->mNumFaces; ++i){...} // 获取纹理数据 if (meshPtr->mMaterialIndex >= 0) { this->processMaterial(materialPtr, sceneObjPtr, aiTextureType_DIFFUSE, diffuseTexture); this->processMaterial(materialPtr, sceneObjPtr, aiTextureType_SPECULAR, specularTexture); } return true; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[/code]
上面的框架给出了从aiScene获取数据,建立内部格式Model和Mesh的思路,具体实现细节可以参考程序源码。
加载纳米战斗服模型
到这里,我们可以来欣赏酷炫的模型了,首先加载一个从learnopengl获取的纳米战斗服模型nanosuit,效果如下图左所示:这里没有使用光照,上图右是实现了一个点光源的效果. 可以从机器人胸部的高光部分看到,实现光照时的区别。
加载模型需要注意的地方
1.加载模型后,需要适当设置模型变换矩阵,否则模型显示在奇怪的位置。这个模型变换矩阵,目前还没找到合适的方法从模型数据中获取。2.下载的模型,有些路径是不正确的,本文统一采用绝对路径方式。路径不正确或者文件缺失时的错误提示
3.部分纹理图片的格式,模型的格式目前并未处理,不支持加载。
还需要改进的地方
上面加载的模型,已经让人很兴奋了,但是还不够真实,高效。在实验过程中,思考还需要通过以下方面进行改进:1.我们这里的材质只处理了纹理部分,实际上模型中如果没有通过纹理定义材质,还需要获取ambient等颜色表示的材质。而且纹理可能不止一个,本文目前只处理了一个纹理(主要原因是下载的素材里面没有找不到更多的纹理坐标)。可以通过定义下面的材质结构体,并处理这个材质数据来丰富场景:
struct Material // 表示材质属性 { glm::vec3 ambient; glm::vec3 diffuse; glm::vec3 specular; float shininess; std::vector<Texture> textures; };1
2
3
4
5
6
7
8
9
[/code]
2.模型中要通过光源和相机加以改善。目前在模型中通过以下方式:
if (sceneObjPtr->HasLights() && !this->processLightSource(sceneObjPtr)) { std::cerr << "Error:Model::loadModel, process lights failed." << std::endl; return false; }1
2
3
4
5
6
7
8
[/code]
获取光源数据时,大量从网络上下载的模型中并没有找到光源数据,比较可惜。
3.实际模型的材质中包含了map_Bump数据,但目前还未学习处理方法。
4.目前通过Model加载模型时耗时非常多,效率不高,需要进一步提高模型加载和渲染的速度
相关文章推荐
- OpenGL学习--07--模型加载(obj)
- OpenGL学习脚印-AssImp模型加载
- Opengl学习之模型加载——Assimp
- OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)
- OpenGL -- OBJ 模型加载
- OpenGL OBJ模型加载.
- OPENGL 模型加载方法 ASSimp
- three.js学习笔记 obj模型加载问题
- Qt下学习OpenGL之OBJ模型
- OpenGL教程翻译 第二十二课 使用Assimp加载模型
- Qt下学习OpenGL之OBJ模型
- OpenGL深入探索——使用Assimp加载模型
- OpenGL ES 学习教程(六) 使用开源库 Assimp 将 Obj 模型 转换成自己的格式
- opengl加载obj模型
- Three.js学习笔记——3dsmax 加载obj模型
- 孙其功陪你学之——OpenGL加载OBJ模型文件并进行纹理修饰
- OpenGL---加载obj模型
- OpenGL 入门基础教程 —— 加载obj模型
- three.js学习笔记 obj模型加载问题 (转)
- OpenGL ES 学习教程(六) 使用开源库 Assimp 将 Obj 模型 转换成自己的格式