1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

校对 03 模型 部分

This commit is contained in:
Geequlim
2015-07-31 16:15:57 +08:00
parent c98b32cabe
commit 8a150ce5f4
3 changed files with 163 additions and 115 deletions

View File

@@ -1,12 +1,13 @@
<style>
.centerHV{text-align:center}
</style>
# Assimp (Assimp开源模型导入库)
本文作者JoeyDeVries由Cocoonshu翻译自[http://learnopengl.com](http://learnopengl.com/#!Model-Loading/Assimp)
原文 | [Assimp](http://learnopengl.com/#!Model-Loading/Assimp)
---|---
作者 | JoeyDeVries
翻译 | Cocoonshu
校对 | [Geequlim](http://geequlim.com)
## Assimp (Assimp开源模型导入库)
到目前为止我们已经在所有的场景中大面积滥用了我们的容器盒小盆友但就是容器盒是我们的好朋友时间久了我们也会喜新厌旧。一些图形应用里经常会使用很多复杂且好玩儿的模型它们看起来比静态的容器盒可爱多了。但是我们无法像定义容器盒一样手动地去指定房子、货车或人形角色这些复杂模型的顶点、法线和纹理坐标。我们需要做的也是应该要做的是把这些模型导入到应用程序中而设计制作这些3D模型的工作应该交给像Blender、3DS Max或者Maya这样的工具软件。
到目前为止我们已经在所有的场景中大面积滥用了我们的容器盒小盆友但就是容器盒是我们的好朋友时间久了我们也会喜新厌旧。一些图形应用里经常会使用很多复杂且好玩儿的模型它们看起来比静态的容器盒可爱多了。但是我们无法像定义容器盒一样手动地去指定房子、货车或人形角色这些复杂模型的顶点、法线和纹理坐标。我们需要做的也是应该要做的是把这些模型导入到应用程序中而设计制作这些3D模型的工作应该交给像[Blender](http://www.blender.org/)、[3DS Max](http://www.autodesk.nl/products/3ds-max/overview)或者[Maya](http://www.autodesk.com/products/autodesk-maya/overview)这样的工具软件。
那些3D建模工具可以让美工们构建一些复杂的形状并将贴图应用到形状上去即纹理映射。然后在导出模型文件时建模工具会自己生成所有的顶点坐标、顶点法线和纹理坐标。这样美工们可以不用了解大量的图像技术细节就能有大量的工具集去随心地构建高品质的模型。所有的技术细节内容都隐藏在里导出的模型文件里。而我们这些图形开发者就必须得去关注这些技术细节了。
@@ -22,16 +23,19 @@
<img src="http://learnopengl.com/img/model_loading/assimp_structure.png"/>
</div>
- 所有的模型、场景数据都包含在scene对象中如所有的材质和模型点云。同样场景的根节点引用也包含在这个scene对象中
- 所有的模型、场景数据都包含在scene对象中如所有的材质和Mesh。同样场景的根节点引用也包含在这个scene对象中
- 场景的根节点可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际了Mesh对象而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用(译者注C/C++称为指针Java/C#称为引用)
- 一个Mesh(模型点云)对象本身包含渲染所需的所有相关数据,比如顶点位置、法线向量、纹理坐标、面片及物体的材质
- 一个Mesh(模型点云)会包含多个面片。一个Face面片表示渲染中的一个最基本的形状单位即图元基本图元有点、线、三角面片、矩形面片。一个面片记录了一个图元的顶点索引通过这个索引可以在mMeshes[]中寻找到对应的顶点位置数据。顶点数据和索引分开存放可以便于我们使用缓存VBO、NBO、TBO、IBO来高速渲染物体。详见[Hello Triangle](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
- 一个Mesh(模型点云)还会包含一个Material材质对象用于指定物体的一些材质属性。如颜色、纹理贴图漫反射贴图、高光贴图等
- 一个Mesh对象本身包含渲染所需的所有相关数据比如顶点位置、法线向量、纹理坐标、面片及物体的材质
- 一个Mesh会包含多个面片。一个Face面片表示渲染中的一个最基本的形状单位即图元基本图元有点、线、三角面片、矩形面片。一个面片记录了一个图元的顶点索引通过这个索引可以在mMeshes[]中寻找到对应的顶点位置数据。顶点数据和索引分开存放可以便于我们使用缓存VBO、NBO、TBO、IBO来高速渲染物体。详见[Hello Triangle](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
- 一个Mesh还会包含一个Material材质对象用于指定物体的一些材质属性。如颜色、纹理贴图漫反射贴图、高光贴图等
所以我们要做的第一件事就是加载一个模型文件为scene对象然后获取每个节点对应的Mesh对象我们需要递归搜索每个节点的子节点来获取所有的节点并处理每个Mesh对象对应的顶点数据、索引以及它的材质属性。最终我们得到一个只包含我们需要的数据的Mesh集合。
> ##Mesh模型点云
> 用建模工具构建物体时,美工通常不会直接使用单个形状来构建一个完整的模型。一般来说,一个模型会由几个子模型/形状组合拼接而成。而模型中的那些子模型/形状就是我们所说的一个Mesh。例如一个人形模型美工通常会把头、四肢、衣服、武器这些组件都分别构建出来然后在把所有的组件拼合在一起形成最终的完整模型。一个Mesh包含顶点、索引和材质属性是我们在OpenGL中绘制物体的最小单位。一个模型通常有多个Mesh组成。
!!! Important
**Mesh(网格,或被译为“模型点云”)**
用建模工具构建物体时,美工通常不会直接使用单个形状来构建一个完整的模型。一般来说,一个模型会由几个子模型/形状组合拼接而成。而模型中的那些子模型/形状就是我们所说的一个Mesh。例如一个人形模型美工通常会把头、四肢、衣服、武器这些组件都分别构建出来然后在把所有的组件拼合在一起形成最终的完整模型。一个Mesh包含顶点、索引和材质属性是我们在OpenGL中绘制物体的最小单位。一个模型通常有多个Mesh组成。
下一节教程中我们将用上述描述的数据结构来创建我们自己的Model类和Mesh类用于加载和保存那些导入的模型。如果我们想要绘制一个模型我们不会去渲染整个模型而是去渲染这个模型所包含的所有独立的Mesh。不管怎样我们开始导入模型之前我们需要先把Assimp导入到我们的工程中。
@@ -42,10 +46,14 @@
这里我们列出一些编译Assimp时可能遇到的问题以便大家参考和排除:
- CMake在读取配置列表时报出与DirectX库丢失相关的一些错误。报错如下
<pre> Could not locate DirecX
CMake Error at cmake-modules/FindPkgMacros.cmake:110 (message):
Required library DirectX not found! Install the library (including dev packages) and try again. If the library is already installed, set the missing variables manually in cmake. </pre>
这个问题的解决方案如果你之前没有安装过DirectX SDK那么请安装。下载地址[DirectX SDK](http://www.microsoft.com/en-us/download/details.aspx?id=6812)
```
Could not locate DirecX
CMake Error at cmake-modules/FindPkgMacros.cmake:110 (message):
Required library DirectX not found! Install the library (including dev packages) and try again. If the library is already installed, set the missing variables manually in cmake.
```
这个问题的解决方案如果你之前没有安装过DirectX SDK那么请安装。下载地址[DirectX SDK](http://www.microsoft.com/en-us/download/details.aspx?id=6812)
- 安装DirectX SDK时可以遇到一个错误码为<b>S1023</b>的错误。遇到这个问题请在安装DirectX SDK前先安装C++ Redistributable package(s)。
问题解释:[已知问题DirectX SDK (June 2010) 安装及S1023错误](Known Issue: DirectX SDK (June 2010) Setup and the S1023 error)
- 一旦配置完成你就可以生成解决方案文件了打开解决方案文件并编译Assimp库编译为Debug版本还是Release版本根据你的需要和心情来定吧
@@ -54,6 +62,9 @@
- 把编译好的LIB文件和DLL文件拷贝到工程的相应目录下并链接到你的解决方案中。同时还好记得把Assimp的头文件也拷贝到工程里去Assimp的头文件可以在include目录里找到
如果你还遇到了其他问题,可以在下面给出的链接里获取帮助。
>如果你想要让Assimp使用多线程支持来提高性能你可以使用<b>Boost</b>库来编译 Assimp。在[Boost安装页面](http://assimp.sourceforge.net/lib_html/install.html)你能找到关于Boost的完整安装介绍。
现在你应该已经能够编译Assimp库并链接Assimp到你的工程里去了。下一节内容[导入完美的3D物件](http://www.learnopengl.com/#!Model-Loading/Mesh)
!!! Important
如果你想要让Assimp使用多线程支持来提高性能你可以使用<b>Boost</b>库来编译 Assimp。在[Boost安装页面](http://assimp.sourceforge.net/lib_html/install.html)你能找到关于Boost的完整安装介绍。
现在你应该已经能够编译Assimp库并链接Assimp到你的工程里去了。下一节内容[导入完美的3D物件](http://learnopengl-cn.readthedocs.org/zh/latest/03%20Model%20Loading/02%20Mesh/)

View File

@@ -1,8 +1,12 @@
本文作者JoeyDeVries由[Django](http://bullteacher.com/20-mesh.html)翻译自[http://learnopengl.com](http://learnopengl.com/)
# Mesh
使用Assimp可以把多种不同格式的模型加载到程序里但是一旦载入它们就都被储存为Assimp自己的数据结构。我们最终的想法是把这些数据转变为OpenGL可读的数据用来渲染物体。我们从前面的教程了解到一个网格代表一个可绘制实体现在我们就定义一个自己的网格类。
原文 | [Mesh](http://learnopengl.com/#!Model-Loading/Mesh)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
使用Assimp可以把多种不同格式的模型加载到程序中但是一旦载入它们就都被储存为Assimp自己的数据结构。我们最终的目的是把这些数据转变为OpenGL可读的数据才能用OpenGL来渲染物体。我们从前面的教程了解到一个网格(Mesh)代表一个可绘制实体,现在我们就定义一个自己的网格类。
先来复习一点目前学到知识考虑一个网格最少需要哪些数据。一个网格应该至少需要一组顶点每个顶点包含一个位置向量一个法线向量一个纹理坐标向量。一个网格也应该包含一个索引绘制用的索引以纹理diffuse/specular map形式表现的材质数据。
@@ -17,7 +21,8 @@ struct Vertex
glm::vec2 TexCoords;
};
```
我们把每个需要的向量储存到一个叫做Vertex的结构体中它被用来索引每个顶点属性。另外除了Vertex结构体外我们也希望组织纹理数据所以我们定义一个Texture结构体
我们把每个需要的向量储存到一个叫做`Vertex`的结构体中,它被用来索引每个顶点属性。另外除了`Vertex`结构体外,我们也希望组织纹理数据,所以我们定义一个`Texture`结构体:
```c++
@@ -27,7 +32,8 @@ struct Texture
String type;
};
```
我们储存纹理的id和它的类型比如diffuse纹理或者specular纹理。
我们储存纹理的id和它的类型比如`diffuse`纹理或者`specular`纹理。
知道了顶点和纹理的实际表达,我们可以开始定义网格类的结构:
@@ -47,9 +53,10 @@ private:
void setupMesh();
}
```
如你所见这个类一点都不复杂构造方法里我们初始化网格所有必须数据。在setupMesh函数里初始化缓冲。最后通过Draw函数绘制网格。注意我们把shader传递给Draw函数。通过把shader传递给mesh在绘制之前我们设置几个uniform就像链接采样器到纹理单元
构造函数的内容非常直接。我们简单设置类的公有变量使用的是构造函数相应的参数。我们在构造函数中也调用setupMesh函数
如你所见这个类一点都不复杂,构造方法里我们初始化网格所有必须数据。在`setupMesh`函数里初始化缓冲。最后通过`Draw`函数绘制网格。注意,我们把`shader`传递给`Draw`函数。通过把`shader`传递给Mesh在绘制之前我们设置几个uniform就像链接采样器到纹理单元
构造函数的内容非常直接。我们简单设置类的公有变量,使用的是构造函数相应的参数。我们在构造函数中也调用`setupMesh`函数:
```c++
@@ -62,11 +69,11 @@ Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures)
this->setupMesh();
}
```
这里没什么特别的现在让我们研究一下setupMesh函数。
这里没什么特别的,现在让我们研究一下`setupMesh`函数。
##初始化
## 初始化
现在我们有一大列的网格数据可用于渲染这要感谢构造函数。我们确实需要设置合适的缓冲通过顶点属性指针vertex attribute pointers定义顶点着色器layout。现在你应该对这些概念很熟悉但是我们我们通过介绍了结构体中使用顶点数据所以稍微有点不一样
@@ -88,15 +95,15 @@ void setupMesh()
glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint),
&this->indices[0], GL_STATIC_DRAW);
// Vertex Positions
// 设置顶点坐标指针
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)0);
// Vertex Normals
// 设置法线指针
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, Normal));
// Vertex Texture Coords
// 设置顶点的纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, TexCoords));
@@ -104,9 +111,10 @@ void setupMesh()
glBindVertexArray(0);
}
```
如你所想代码没什么特别不同的地方在Vertex结构体的帮助下有了一些小把戏。
C++的结构体有一个重要的属性那就是在内存中它们是连续的。如果我们用结构体表示一列数据这个结构体只包含结构体的连续的变量它就会直接转变为一个float实际上是byte数组我们就能用于一个数组缓冲array buffer中了。比如如果我们填充一个Vertex结构体,它在内存中的排布等于:
如你所想代码没什么特别不同的地方,在`Vertex`结构体的帮助下有了一些小把戏。
C++的结构体有一个重要的属性,那就是在内存中它们是连续的。如果我们用结构体表示一列数据,这个结构体只包含结构体的连续的变量,它就会直接转变为一个`float`实际上是byte数组我们就能用于一个数组缓冲array buffer中了。比如如果我们填充一个`Vertex`结构体,它在内存中的排布等于:
```c++
@@ -116,33 +124,33 @@ vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
```
感谢这个有用的特性我们能直接把一个作为缓冲数据的一大列Vertex结构体的指针传递过去它们会翻译成glBufferData能用的参数
感谢这个有用的特性,我们能直接把一个作为缓冲数据的一大列`Vertex`结构体的指针传递过去,它们会翻译成`glBufferData`能用的参数:
```c++
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex),
&this->vertices[0], GL_STATIC_DRAW);
```
自然地sizeof函数也可以使用于结构体来计算字节类型的大小。它应该是32字节8float * 4
一个预处理指令叫做offsetof(s, m)把结构体作为它的第一个参数第二个参数是这个结构体名字的变量。这是结构体另外的一个重要用途。函数返回这个变量从结构体开始的字节偏移量offset。这对于定义glVertexAttribPointer函数偏移量参数效果很好
自然地,`sizeof`函数也可以使用于结构体来计算字节类型的大小。它应该是32字节8float * 4
一个预处理指令叫做`offsetof(s,m)`把结构体作为它的第一个参数第二个参数是这个结构体名字的变量。这是结构体另外的一个重要用途。函数返回这个变量从结构体开始的字节偏移量offset。这对于定义`glVertexAttribPointer`函数偏移量参数效果很好:
```c++
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, Normal));
```
偏移量现在使用offsetof函数定义了在这个例子里设置法线向量的字节偏移量等于法线向量在结构体的字节偏移量它是3float也就是12字节一个float占4字节。注意我们同样设置步长参数等于Vertex结构体的大小。
偏移量现在使用`offsetof`函数定义了,在这个例子里,设置法线向量的字节偏移量等于法线向量在结构体的字节偏移量,它是`3float`也就是12字节一个float占4字节。注意我们同样设置步长参数等于`Vertex`结构体的大小。
使用一个像这样的结构体,不仅能提供可读性更高的代码同时也是我们可以轻松的扩展结构体。如果我们想要增加另一个顶点属性,我们把它可以简单的添加到结构体中,由于它的可扩展性,渲染代码不会被破坏。
## 渲染
##渲染
我们需要为`Mesh`类定义的最后一个函数是它的Draw函数。在真正渲染前我们希望绑定合适的纹理然后调用`glDrawElements`。可因为我们从一开始不知道这个网格有多少纹理以及它们应该是什么类型的,所以这件事变得很困难。所以我们该怎样在着色器中设置纹理单元和采样器呢?
我们需要为Mesh类定义的最后一个函数是它的Draw函数。在真正渲染前我们希望绑定合适的纹理然后调用glDrawElements。可因为我们从一开始不知道这个网格有多少纹理以及它们应该是什么类型的所以这件事变得很困难。所以我们该怎样在着色器中设置纹理单元和采样器呢
解决这个问题我们需要假设一个特定的名称惯例每个diffuse纹理被命名为texture_diffuseN,每个specular纹理应该被命名为texture_specularN.N是一个从1到纹理才抢其允许使用的最大值之间的数。可以说在一个网格中我们有3个diffuse纹理和2个specular纹理它们的纹理采样器应该这样被调用
解决这个问题,我们需要假设一个特定的名称惯例:每个`diffuse`纹理被命名为`texture_diffuseN`,每个`specular`纹理应该被命名为`texture_specularN`。N是一个从1到纹理才抢其允许使用的最大值之间的数。可以说在一个网格中我们有3个`diffuse`纹理和2个`specular`纹理,它们的纹理采样器应该这样被调用:
```c++
@@ -152,6 +160,7 @@ uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
```
使用这样的惯例我们能定义我们在着色器中需要的纹理采样器的数量。如果一个网格真的有这么多纹理我们就知道它们的名字应该是什么。这个惯例也使我们能够处理一个网格上的任何数量的纹理通过定义合适的采样器开发者可以自由使用希望使用的数量虽然定义少的话就会有点浪费绑定和uniform调用了
像这样的问题有很多不同的解决方案,如果你不喜欢这个方案,你可以自己创造一个你自己的方案。
@@ -165,15 +174,15 @@ void Draw(Shader shader)
GLuint specularNr = 1;
for(GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // Activate proper texture unit before binding
// Retrieve texture number (the N in diffuse_textureN)
glActiveTexture(GL_TEXTURE0 + i); // 在绑定纹理前需要激活适当的纹理单元
// 检索纹理序列号 (N in diffuse_textureN)
stringstream ss;
string number;
string name = this->textures[i].type;
if(name == "texture_diffuse")
ss << diffuseNr++; // Transfer GLuint to stream
ss << diffuseNr++; // 将GLuin输入到string stream
else if(name == "texture_specular")
ss << specularNr++; // Transfer GLuint to stream
ss << specularNr++; // 将GLuin输入到string stream
number = ss.str();
glUniform1f(glGetUniformLocation(shader.Program, ("material." + name + number).c_str()), i);
@@ -181,15 +190,19 @@ void Draw(Shader shader)
}
glActiveTexture(GL_TEXTURE0);
// Draw mesh
// 绘制Mesh
glBindVertexArray(this->VAO);
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
```
这不是最漂亮的代码但是这主要归咎于C++转换类型时的丑陋比如int转string时。我们首先计算N-元素每个纹理类型把它链接到纹理类型字符串来获取合适的uniform名。然后查找合适的采样器位置给它位置值对应当前激活纹理单元绑定纹理。这也是我们需要在Draw方法是用shader的原因。我们添加”material.”到作为结果的uniform名因为我们通常把纹理储存进材质结构体对于每个实现也许会有不同
注意当我们把diffuse和specular传递到字符串流stringstream的时候计数器会增加在C++自增叫做:变量++它会先返回自身然后加1而++变量先加1再返回自身我们的例子里我们先传递原来的计数器值到字符串流然后再加1下一轮生效
你可以从这里得到Mesh类的源码。
这不是最漂亮的代码但是这主要归咎于C++转换类型时的丑陋,比如`int`转`string`时。我们首先计算N-元素每个纹理类型把它链接到纹理类型字符串来获取合适的uniform名。然后查找合适的采样器位置给它位置值对应当前激活纹理单元绑定纹理。这也是我们需要在`Draw`方法是用`shader`的原因。我们添加`material.`到作为结果的uniform名因为我们通常把纹理储存进材质结构体对于每个实现也许会有不同
!!! Important
注意,当我们把`diffuse`和`specular`传递到字符串流(`stringstream`的时候计数器会增加在C++自增叫做:变量++它会先返回自身然后加1而++变量先加1再返回自身我们的例子里我们先传递原来的计数器值到字符串流然后再加1下一轮生效。
你可以从这里得到[Mesh类的源码](http://learnopengl.com/code_viewer.php?code=mesh&type=header)。
Mesh类是对我们前面的教程里讨论的很多话题的的简洁的抽象在下面的教程里我们会创建一个模型它用作乘放多个网格物体的容器真正的实现Assimp的加载接口。

View File

@@ -1,36 +1,41 @@
本文作者JoeyDeVries由[Django](http://bullteacher.com/)翻译自[http://learnopengl.com](http://learnopengl.com/)
# 模型(Model)
现在是时候着手启用Assimp并开始创建实际的加载和转换代码了。本教程的目标是创建另一个类这个类可以表达模型的全部。更确切的说一个模型包含多个网格一个网格可能带有多个对象。一个别墅包含一个木制阳台一个尖顶或许也有一个游泳池它仍然被加载为一个单一模型。我们通过Assimp加载模型把它们转变为多个网格Mesh对象这些对象是是先前教程里创建的。
原文 | [Model](http://learnopengl.com/#!Model-Loading/Model)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com)
现在是时候着手启用Assimp并开始创建实际的加载和转换代码了。本教程的目标是创建另一个类这个类可以表达模型的全部。更确切的说一个模型包含多个网格(Mesh)一个网格可能带有多个对象。一个别墅包含一个木制阳台一个尖顶或许也有一个游泳池它仍然被加载为一个单一模型。我们通过Assimp加载模型把它们转变为多个网格Mesh对象这些对象是是先前教程里创建的。
闲话少说我把Model类的结构呈现给你
```c++
class Model
{
public:
/* Functions */
/* 成员函数 */
Model(GLchar* path)
{
this->loadModel(path);
}
void Draw(Shader shader);
private:
/* Model Data */
/* 模型数据 */
vector<Mesh> meshes;
string directory;
/* Functions */
/* 私有成员函数 */
void loadModel(string path);
void processNode(aiNode* node, const aiScene* scene);
Mesh processMesh(aiMesh* mesh, const aiScene* scene);
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName);
};
```
Model类包含一个Mesh对象的向量我们需要在构造函数中给出文件的位置。之后在构造其中它通过loadModel函数加载文件。私有方法都被设计为处理一部分的Assimp导入的常规动作我们会简单讲讲它们。同样我们储存文件路径的目录这样稍后加载纹理的时候会用到。
函数Draw没有什么特别之处基本上是循环每个网格调用各自的Draw函数
`Model`类包含一个`Mesh`对象的向量,我们需要在构造函数中给出文件的位置。之后,在构造其中,它通过`loadModel`函数加载文件。私有方法都被设计为处理一部分的Assimp导入的常规动作我们会简单讲讲它们。同样我们储存文件路径的目录这样稍后加载纹理的时候会用到
函数`Draw`没有什么特别之处基本上是循环每个网格调用各自的Draw函数。
```c++
@@ -41,7 +46,7 @@ void Draw(Shader shader)
}
```
##把一个3D模型导入到OpenGL
## 把一个3D模型导入到OpenGL
为了导入一个模型并把它转换为我们自己的数据结构第一件需要做的事是包含合适的Assimp头文件这样编译器就不会对我们抱怨了。
@@ -51,7 +56,8 @@ void Draw(Shader shader)
#include <assimp/scene.h>
#include <assimp/postprocess.h>
```
我们将要调用的第一个函数是loadModel,它被构造函数直接调用。在loadModel函数里面我们使用Assimp加载模型到Assimp中被称为scene对象的数据结构。你可能还记得模型加载系列的第一个教程中这是Assimp的数据结构的根对象。一旦我们有了场景对象我们就能从已加载模型中获取所有所需数据了。
我们将要调用的第一个函数是`loadModel`,它被构造函数直接调用。在`loadModel`函数里面我们使用Assimp加载模型到Assimp中被称为scene对象的数据结构。你可能还记得模型加载系列的第一个教程中这是Assimp的数据结构的根对象。一旦我们有了场景对象我们就能从已加载模型中获取所有所需数据了。
Assimp最大优点是它简约的抽象了所加载所有不同格式文件的技术细节用一行可以做到这一切
@@ -60,14 +66,16 @@ Assimp最大优点是它简约的抽象了所加载所有不同格式文件
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
```
我们先来声明一个Importer对象它的名字空间是Assimp然后调用它的ReadFile函数。这个函数需要一个文件路径第二个参数是后处理post-processing选项。除了可以简单加载文件外Assimp允许我们定义几个选项来强制Assimp去对导入数据做一些额外的计算或操作。通过设置aiProcess_Triangulate我们告诉Assimp如果模型不是全部由三角形组成应该转换所有的模型的原始几何形状为三角形。aiProcess_FlipUVs基于y轴翻转纹理坐标在处理的时候是必须的你可能记得我们在纹理教程中我们说过在OpenGL大多数图像会被沿着y轴反转所以这个小小的后处理选项会为我们修正这个。一少部分其他有用的选项如下
aiProcess_GenNormals:如果模型没有包含法线向量,就为每个顶点创建法线。
aiProcess_SplitLargeMeshes:把大的网格成几个小的的下级网格,当你渲染有一个最大数量顶点的限制时或者只能处理小块网格时很有用。
aiProcess_OptimizeMeshes:和上个选项相反,它把几个网格结合为一个更大的网格。以减少绘制函数调用的次数的方式来优化
我们先来声明一个`Importer`对象,它的名字空间是`Assimp`,然后调用它的`ReadFile`函数。这个函数需要一个文件路径第二个参数是后处理post-processing选项。除了可以简单加载文件外Assimp允许我们定义几个选项来强制Assimp去对导入数据做一些额外的计算或操作。通过设置`aiProcess_Triangulate`我们告诉Assimp如果模型不是全部由三角形组成应该转换所有的模型的原始几何形状为三角形。`aiProcess_FlipUVs`基于y轴翻转纹理坐标在处理的时候是必须的你可能记得我们在纹理教程中我们说过在OpenGL大多数图像会被沿着y轴反转所以这个小小的后处理选项会为我们修正这个。一少部分其他有用的选项如下
* `aiProcess_GenNormals` : 如果模型没有包含法线向量,就为每个顶点创建法线
* `aiProcess_SplitLargeMeshes` : 把大的网格成几个小的的下级网格,当你渲染有一个最大数量顶点的限制时或者只能处理小块网格时很有用。
* `aiProcess_OptimizeMeshes` : 和上个选项相反,它把几个网格结合为一个更大的网格。以减少绘制函数调用的次数的方式来优化。
Assimp提供了后处理说明你可以从这里找到所有内容。事实上通过Assimp加载一个模型超级简单。困难的是使用返回的场景对象把加载的数据变换到一个Mesh对象的数组。
完整的loadModel函数在这里列出
完整的`loadModel`函数在这里列出:
```c++
@@ -86,45 +94,48 @@ void loadModel(string path)
this->processNode(scene->mRootNode, scene);
}
```
在我们加载了模型之后我们检验是否场景和场景的根节点为空查看这些标记中的一个来看看返回的数据是否完整。如果发生了任何一个错误我们通过导入器impoter的GetErrorString函数返回错误报告。我们同样重新获取文件的目录路径。
如果没什么错误发生我们希望处理所有的场景节点所以我们传递第一个节点根节点到递归函数processNode。因为每个节点可能包含多个子节点我们希望先处理父节点再处理子节点以此类推。这符合递归结构所以我们定义一个递归函数。递归函数就是一个做一些什么处理之后用不同的参数调用它自身的函数此种循环不会停止直到一个特定条件发生。在我们的例子里特定条件是所有的节点都被处理
在我们加载了模型之后我们检验是否场景和场景的根节点为空查看这些标记中的一个来看看返回的数据是否完整。如果发生了任何一个错误我们通过导入器impoter的`GetErrorString`函数返回错误报告。我们同样重新获取文件的目录路径
也许你记得Assimp的结构每个节点包含一个网格集合的索引每个索引指向一个在场景对象中特定的网格位置。我们希望获取这些网格索引获取每个网格处理每个网格然后对其他的节点的子节点做同样的处理。processNode函数的内容如下
如果没什么错误发生,我们希望处理所有的场景节点,所以我们传递第一个节点(根节点)到递归函数`processNode`。因为每个节点(可能)包含多个子节点,我们希望先处理父节点再处理子节点,以此类推。这符合递归结构,所以我们定义一个递归函数。递归函数就是一个做一些什么处理之后,用不同的参数调用它自身的函数,此种循环不会停止,直到一个特定条件发生。在我们的例子里,特定条件是所有的节点都被处理。
也许你记得Assimp的结构每个节点包含一个网格集合的索引每个索引指向一个在场景对象中特定的网格位置。我们希望获取这些网格索引获取每个网格处理每个网格然后对其他的节点的子节点做同样的处理。`processNode`函数的内容如下:
```c++
void processNode(aiNode* node, const aiScene* scene)
{
// Process all the node's meshes (if any)
// 添加当前节点中的所有Mesh
for(GLuint i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
this->meshes.push_back(this->processMesh(mesh, scene));
}
// Then do the same for each of its children
// 递归处理该节点的子孙节点
for(GLuint i = 0; i < node->mNumChildren; i++)
{
this->processNode(node->mChildren[i], scene);
}
}
```
我们首先利用场景的mMeshes数组来检查每个节点的网格索引以获取相应的网格。被返回的网格被传递给processMesh函数它返回一个网格对象我们可以把它储存在meshes的list或vectorSTL里的两种实现链表的数据结构中。
我们首先利用场景的`mMeshes`数组来检查每个节点的网格索引以获取相应的网格。被返回的网格被传递给`processMesh`函数,它返回一个网格对象,我们可以把它储存在`meshes`的list或vectorSTL里的两种实现链表的数据结构中。
一旦所有的网格都被处理我们遍历所有子节点同样调用processNode函数。一旦一个节点不再拥有任何子节点函数就会停止执行。
认真的读者会注意到,我们可能基本忘记处理任何的节点,简单循环出场景所有的网格,而不是用索引做这件复杂的事。我们这么做的原因是,使用这种节点的原始的想法是,在网格之间定义一个父-子关系。通过递归遍历这些关系,我们可以真正定义特定的网格作为其他网格的父(节点)。
!!! Important
关于这个系统的一个有用的例子是,当你想要平移一个汽车网格需要确保把它的子(节点)比如,引擎网格,方向盘网格和轮胎网格都进行平移;使用父-子关系这样的系统很容易被创建出来
认真的读者会注意到,我们可能基本忘记处理任何的节点,简单循环出场景所有的网格,而不是用索引做这件复杂的事。我们这么做的原因是,使用这种节点的原始的想法是,在网格之间定义一个父-子关系。通过递归遍历这些关系,我们可以真正定义特定的网格作为其他网格的父(节点)
关于这个系统的一个有用的例子是,当你想要平移一个汽车网格需要确保把它的子(节点)比如,引擎网格,方向盘网格和轮胎网格都进行平移;使用父-子关系这样的系统很容易被创建出来。
现在我们没用这种系统,但是无论何时你想要对你的网格数据进行额外的控制,这通常是一种坚持被推荐的做法。这些模型毕竟是那些定义了这些节点风格的关系的艺术家所创建的。
现在我们没用这种系统,但是无论何时你想要对你的网格数据进行额外的控制,这通常是一种坚持被推荐的做法。这些模型毕竟是那些定义了这些节点风格的关系的艺术家所创建的
下一步是用上个教程创建的Mesh类开始真正处理Assimp的数据。
下一步是用上个教程创建的`Mesh`类开始真正处理Assimp的数据
## 从Assimp到网格
##从Assimp到网格
把一个aiMesh对象转换为一个我们自己定义的网格对象并不难。我们所要做的全部是获取每个网格相关的属性并把这些属性储存到我们自己的对象。通常processMesh函数的结构会是这样
把一个`aiMesh`对象转换为一个我们自己定义的网格对象并不难。我们所要做的全部是获取每个网格相关的属性并把这些属性储存到我们自己的对象。通常`processMesh`函数的结构会是这样:
```c++
@@ -137,13 +148,13 @@ Mesh processMesh(aiMesh* mesh, const aiScene* scene)
for(GLuint i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// Process vertex positions, normals and texture coordinates
// 处理顶点坐标、法线和纹理坐标
...
vertices.push_back(vertex);
}
// Process indices
// 处理顶点索引
...
// Process material
// 处理材质
if(mesh->mMaterialIndex >= 0)
{
...
@@ -152,9 +163,10 @@ Mesh processMesh(aiMesh* mesh, const aiScene* scene)
return Mesh(vertices, indices, textures);
}
```
处理一个网格基本由三部分组成获取所有顶点数据获取网格的索引获取相关材质数据。处理过的数据被储存在3个向量其中之一里面一个Mesh被以这些数据创建返回到函数的调用者。
获取顶点数据很简单我们定义一个Vertex结构体在每次遍历后我们把这个结构体添加到Vertices数组。我们为存在于网格中的众多顶点循环通过mesh->mNumVertices获取。在遍历的过程中我们希望用所有相关数据填充这个结构体。每个顶点位置会像这样被处理
获取顶点数据很简单:我们定义一个`Vertex`结构体,在每次遍历后我们把这个结构体添加到`Vertices`数组。我们为存在于网格中的众多顶点循环(通过`mesh->mNumVertices`获取)。在遍历的过程中,我们希望用所有相关数据填充这个结构体。每个顶点位置会像这样被处理:
```c++
@@ -164,9 +176,13 @@ vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
```
注意为了传输Assimp的数据我们定义一个vec3的宿主我们需要它是因为Assimp维持它自己的数据类型这些类型用于向量、材质、字符串等。这些数据类型转换到glm的数据类型时通常效果不佳。
Assimp调用他们的顶点位置数组mVertices真有点违反直觉
注意为了传输Assimp的数据我们定义一个`vec3`的宿主我们需要它是因为Assimp维持它自己的数据类型这些类型用于向量、材质、字符串等。这些数据类型转换到glm的数据类型时通常效果不佳
!!! Important
Assimp调用他们的顶点位置数组`mVertices`真有点违反直觉。
对应法线的步骤毫无疑问是这样的:
@@ -176,6 +192,7 @@ vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
```
纹理坐标也基本一样但是Assimp允许一个模型的每个顶点有8个不同的纹理坐标我们可能用不到所以我们只关系第一组纹理坐标。我们也希望检查网格是否真的包含纹理坐标可能并不总是如此:
@@ -190,13 +207,12 @@ if(mesh->mTextureCoords[0]) // Does the mesh contain texture coordinates?
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
```
Vertex结构体现在完全被所需的顶点属性填充了我们能把它推到vertices向量的尾部。要对每个网格的顶点做相同的处理。
`Vertex`结构体现在完全被所需的顶点属性填充了,我们能把它添加到`vertices`向量的尾部。要对每个网格的顶点做相同的处理。
##顶点
## 顶点
Assimp的接口定义每个网格有一个以面faces为单位的数组每个面代表一个单独的图元在我们的例子中由于aiProcess_Triangulate选项总是三角形一个面包含索引这些索引定义我们需要绘制的顶点以在那样的顺序提供给每个图元所以如果我们遍历所有面把所有面的索引储存到indices向量我们需要这么做
Assimp的接口定义每个网格有一个以面faces为单位的数组每个面代表一个单独的图元在我们的例子中由于`aiProcess_Triangulate`选项)总是三角形,一个面包含索引,这些索引定义我们需要绘制的顶点以在那样的顺序提供给每个图元,所以如果我们遍历所有面,把所有面的索引储存到`indices`向量,我们需要这么做:
```c++
@@ -207,14 +223,14 @@ for(GLuint i = 0; i < mesh->mNumFaces; i++)
indices.push_back(face.mIndices[j]);
}
```
所有外部循环结束后我们现在有了一个完整点的顶点和索引数据来绘制网格这要调用glDrawElements函数。可是为了结束这个讨论并向网格提供一些细节我们同样希望处理网格的材质。
所有外部循环结束后,我们现在有了一个完整点的顶点和索引数据来绘制网格,这要调用`glDrawElements`函数。可是,为了结束这个讨论,并向网格提供一些细节,我们同样希望处理网格的材质。
##材质
如同节点一个网格只有一个指向材质对象的索引获取网格实际的材质我们需要索引场景的mMaterials数组。网格的材质索引被设置在mMaterialIndex属性中通过这个属性我们同样能够检验一个网格是否包含一个材质
## 材质
如同节点,一个网格只有一个指向材质对象的索引,获取网格实际的材质,我们需要索引场景的`mMaterials`数组。网格的材质索引被设置在`mMaterialIndex`属性中,通过这个属性我们同样能够检验一个网格是否包含一个材质:
```c++
if(mesh->mMaterialIndex >= 0)
@@ -228,9 +244,10 @@ if(mesh->mMaterialIndex >= 0)
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
```
我么先从场景的mMaterials数组获取aimaterial对象然后我们希望加载网格的diffuse或/和specular纹理。一个材质储存了一个数组这个数组为每个纹理类型提供纹理位置。不同的纹理类型都以aiTextureType_为前缀。我们使用一个帮助函数loadMaterialTextures来从材质获取纹理。这个函数返回一个Texture结构体的向量我们在之后储存在模型的textures坐标的后面。
loadMaterialTextures函数遍历所有给定纹理类型纹理位置获取纹理的文件位置然后加载生成纹理把信息储存到Vertex结构体。看起来像这样
我么先从场景的`mMaterials`数组获取`aimaterial`对象然后我们希望加载网格的diffuse或/和specular纹理。一个材质储存了一个数组这个数组为每个纹理类型提供纹理位置。不同的纹理类型都以`aiTextureType_`为前缀。我们使用一个帮助函数:`loadMaterialTextures`来从材质获取纹理。这个函数返回一个`Texture`结构体的向量,我们在之后储存在模型的`textures`坐标的后面。
`loadMaterialTextures`函数遍历所有给定纹理类型的纹理位置,获取纹理的文件位置,然后加载生成纹理,把信息储存到`Vertex`结构体。看起来像这样:
```c++
@@ -250,16 +267,18 @@ vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string
return textures;
}
```
我们先通过GetTextureCount函数检验材质中储存的纹理以期得到我们希望得到的纹理类型。然后我们通过GetTexture函数获取每个纹理的文件位置这个位置以aiString类型储存。然后我们使用另一个帮助函数它被命名为TextureFromFile加载一个纹理使用SOIL返回纹理的ID。你可以查看列在最后的完整的代码如果你不知道这个函数应该怎样写出来的话。
注意我们假设纹理文件与模型是在相同的目录里。我们可以简单的链接纹理位置字符串和之前获取的目录字符串在loadModel函数中得到的来获得完整的纹理路径这就是为什么GetTexture函数同样需要目录字符串
我们先通过`GetTextureCount`函数检验材质中储存的纹理,以期得到我们希望得到的纹理类型。然后我们通过`GetTexture`函数获取每个纹理的文件位置,这个位置以`aiString`类型储存。然后我们使用另一个帮助函数,它被命名为:`TextureFromFile`加载一个纹理使用SOIL返回纹理的ID。你可以查看列在最后的完整的代码如果你不知道这个函数应该怎样写出来的话
有些在互联网上找到的模型使用绝对路径,它们的纹理位置就不会在每天机器上都有效了。例子里,你可能希望手工编辑这个文件来使用本地路径为纹理所使用(如果可能的话)。
这就是使用Assimp来导入一个模型的全部了。你可以在这里找到Model类的全部。
!!! Important
注意,我们假设纹理文件与模型是在相同的目录里。我们可以简单的链接纹理位置字符串和之前获取的目录字符串(在`loadModel`函数中得到的)来获得完整的纹理路径(这就是为什么`GetTexture`函数同样需要目录字符串)。
有些在互联网上找到的模型使用绝对路径,它们的纹理位置就不会在每天机器上都有效了。例子里,你可能希望手工编辑这个文件来使用本地路径为纹理所使用(如果可能的话)。
##重大优化
这就是使用Assimp来导入一个模型的全部了。你可以在这里找到[Model类的代码](http://learnopengl.com/code_viewer.php?code=model_loading/model_unoptimized)。
## 重大优化
我们现在还没做完。因为我们还想做一个重大的优化(但是不是必须的)。大多数场景重用若干纹理,把它们应用到网格;还是思考那个别墅,它有个花岗岩的纹理作为墙面。这个纹理也可能应用到地板、天花板,楼梯,或者一张桌子、一个附近的小物件。加载纹理需要不少操作,当前的实现中一个新的纹理被加载和生成,来为每个网格使用,即使同样的纹理之前已经被加载了好几次。这会很快转变为你的模型加载实现的瓶颈。
@@ -273,13 +292,15 @@ struct Texture {
aiString path; // We store the path of the texture to compare with other textures
};
```
然后我们把所有家在过的纹理储存到另一个向量中,它是作为一个私有变量声明在模型类的顶部:
```c++
vector<Texture> textures_loaded;
```
然后在loadMaterialTextures函数中我们希望把纹理路径和所有texture_loaded向量对比看看是否当前纹理路径和其中任何一个是否相同如果是我们跳过纹理加载/生成部分,简单的使用已加载纹理结构体作为网格纹理。这个函数如下所示:
然后,在`loadMaterialTextures`函数中,我们希望把纹理路径和所有`texture_loaded`向量对比,看看是否当前纹理路径和其中任何一个是否相同,如果是,我们跳过纹理加载/生成部分,简单的使用已加载纹理结构体作为网格纹理。这个函数如下所示:
```c++
@@ -301,35 +322,40 @@ vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string
}
}
if(!skip)
{ // If texture hasn't been loaded already, load it
{ // 如果纹理没有被加载过,加载之
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
this->textures_loaded.push_back(texture); // Add to loaded textures
this->textures_loaded.push_back(texture); // 添加到纹理列表 textures
}
}
return textures;
}
```
所以现在我们不仅有了一个通用模型加载系统,同时我们也得到了一个能使加载对象更快的优化版本。
有些版本的Assimp当使用调试版或/和使用你的IDE的调试模式时模型加载模型实在慢所以确保在当你加载得很慢的时候用发布版再测试。
你可以从这里获得优化的Model类的完整源代码。
!!! Attention
有些版本的Assimp当使用调试版或/和使用你的IDE的调试模式时模型加载模型实在慢所以确保在当你加载得很慢的时候用发布版再测试。
##和箱子模型告别!
你可以从这里获得优化的[Model类的完整源代码](http://learnopengl.com/code_viewer.php?code=model&type=header)。
## 和箱子模型告别!
现在给我们导入一个天才艺术家创建的模型看看效果不是我这个天才做的你不得不承认这个箱子也许是你见过的最漂亮的立体图形。因为我不想过于自夸所以我会时不时的给其他艺术家进入这个行列的机会这次我们会加载Crytek原版的孤岛危机游戏中的纳米铠甲。这个模型被输出为obj和mtl文件mtl包含模型的diffuse和specular以及法线贴图后面会讲。你可以下载这个模型注意所有的纹理和模型文件都应该放在同一个目录以便载入纹理。
你从这个站点下载的版本是修改过的版本,每个纹理文件路径已经修改改为本地相对目录,原来的资源是绝对目录。
!!! Important
你从这个站点下载的版本是修改过的版本,每个纹理文件路径已经修改改为本地相对目录,原来的资源是绝对目录。
现在在代码中声明一个Model对象把它模型的文件位置传递给它。模型应该自动加载如果没有错误的话在游戏循环中使用它的Draw函数绘制这个对象。没有更多的缓冲配置属性指针和渲染命令仅仅简单的一行。如果你创建几个简单的着色器像素着色器只输出对象的diffuse纹理颜色结果看上去会有点像这样
![](http://www.learnopengl.com/img/model_loading/model_diffuse.png)
你可以从这里找到带有顶点和像素着色器的完整的源码。
你可以从这里找到带有[顶点](http://learnopengl.com/code_viewer.php?code=model_loading/model&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=model_loading/model&type=fragment)着色器的[完整的源码](http://learnopengl.com/code_viewer.php?code=model_loading/model_diffuse)
我们也可以变得更加有创造力,引入两个点光源到我们之前从光照教程学过的渲染等式,结合高光贴图获得惊艳效果:
@@ -337,8 +363,6 @@ vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string
虽然我不得不承认这个相比之前用过的容器也太炫了。使用Assimp你可以载入无数在互联网上找到的模型。只有很少的资源网站提供多种格式的免费3D模型给你下载。一定注意有些模型仍然不能很好的载入纹理路径无效或者这种格式Assimp不能读。
## 练习
##练习
你可以使用两个点光源重建上个场景吗?方案,着色器。
你可以使用两个点光源重建上个场景吗?[方案](http://learnopengl.com/code_viewer.php?code=model_loading/model-exercise1)[着色器](http://learnopengl.com/code_viewer.php?code=model_loading/model-exercise1-shaders)。