OpenGL模型加载——Assimp
1、Assimp模型加载库
Assimp全称为Open Asset Import Library,可以导入几十种不同格式的模型文件(同样也可以导出部分模型格式)。只要Assimp加载完了模型文件,我们就可以从Assimp上获取所有我们需要的模型数据。Assimp把不同的模型文件都转换为一个统一的数据结构,所有无论我们导入何种格式的模型文件,都可以用同一个方式去访问我们需要的模型数据。
当导入一个模型文件时,即Assimp加载一整个包含所有模型和场景数据的模型文件到一个scene对象时,Assimp会为这个模型文件中的所有场景节点、模型节点都生成一个具有对应关系的数据结构,且将这些场景中的各种元素与模型数据对应起来。下图展示了一个简化的Assimp生成的模型文件数据结构:
a.所有的模型、场景数据都包含在scene对象中,如所有的Material(材质)和Mesh(网格)。同样,场景的RootNode(根节点)引用也包含在这个scene对象中
b.RootNode(根节点)也会包含很多ChildNode(子节点)和一个指向保存该模型所有Mesh(网格)的数组mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际的Mesh(网格)对象,而每个子节点上的mMeshes[]都只是指向根节点中的mMeshes[]的一个引用(C/C++称为指针,Java/C#称为引用),总体上而言可以这么理解:所有真实的Mesh(网格)对象的信息只有一份,且保存在RootNode(根节点)的mMeshes[]数组当中,而ChildNode(子节点)的mMeshes[]数组只记录该ChildNode(子节点)所需要的Mesh(网格)的指针,该指针指向RootNode(根节点)的mMeshes[]中的一个Mesh(网格)。
c.一个Mesh(网格)对象本身包含渲染所需的所有相关数据,比如顶点位置、法线向量、纹理坐标、面片及物体的材质。
d.一个Mesh(网格)会包含多个面片。一个Face(面片)表示渲染中的一个最基本的形状单位,即图元(基本图元有点、线、三角面片、矩形面片)。一个面片记录了一个图元的顶点索引,通过这个索引,可以在mMeshes[]中寻找到对应的顶点位置数据。顶点数据和索引分开存放,可以便于我们使用缓存(VBO、NBO、TBO、IBO)来高速渲染物体。
e.一个Mesh还会包含一个Material(材质)对象用于指定物体的一些材质属性。如颜色、纹理贴图(漫反射贴图、高光贴图等)。
所以我们要做的第一件事,就是加载一个模型文件为scene对象,然后获取每个节点对应的Mesh对象(我们需要递归搜索每个节点的子节点来获取所有的节点),并处理每个Mesh对象对应的顶点数据、索引以及它的材质属性。最终我们得到一个只包含我们需要的数据的Mesh集合。
2、网格
使用Assimp可以把多种不同格式的模型加载到程序中,但是一旦载入,它们就都被储存为Assimp自己的数据结构。我们最终的目的是把这些数据转变为OpenGL可读的数据,才能用OpenGL来渲染物体。我们已经知道一个网格(Mesh)代表一个可绘制实体,现在我们就定义一个自己的网格类。
先来复习一点目前学到知识,考虑一个网格最少需要哪些数据。一个网格应该至少需要一组顶点,每个顶点包含一个位置向量,一个法线向量,一个纹理坐标向量。一个网格也应该包含一个绘制用的索引,以纹理(diffuse/specular map)形式表现的材质数据。
struct Vertex
{
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
struct Texture
{
GLuint id;
String type;
};
知道了顶点和纹理的实际表达,我们可以开始定义网格类的结构:
class Mesh
{
Public:
vector<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> texture);
Void Draw(Shader shader);
private:
GLuint VAO, VBO, EBO;
void setupMesh();
}
构造函数:
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
this->setupMesh();
}
现在我们有一堆网格数据可用于渲染,这要感谢构造函数。我们确实需要设置合适的缓冲,通过顶点属性指针(vertex attribute pointers)定义顶点着色器layout。
void setupMesh()
{
glGenVertexArrays(1, &this->VAO);
glGenBuffers(1, &this->VBO);
glGenBuffers(1, &this->EBO);
glBindVertexArray(this->VAO);
glBindBuffer(GL_ARRAY_BUFFER, this->VBO);
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex),
&this->vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint),
&this->indices[0], GL_STATIC_DRAW);
// 设置顶点坐标指针
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)0);
// 设置法线指针
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, Normal));
// 设置顶点的纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
C++的结构体有一个重要的属性,那就是在内存中它的数据是连续的。如果我们用结构体表示一列数据,这个结构体只包含结构体的连续的变量,它就会直接转变为一个float(实际上是byte)数组,我们就能用于一个数组缓冲(array buffer)中了。比如,如果我们填充一个Vertex结构体,它在内存中的排布等于:
Vertex vertex;
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f);
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能用的参数:
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex),
&this->vertices[0], GL_STATIC_DRAW);
一个预处理指令叫做offsetof(s,m)把结构体作为它的第一个参数,第二个参数是这个结构体名字的变量。这是结构体另外的一个重要用途。函数返回这个变量从结构体开始的字节偏移量(offset)。这对于定义glVertexAttribPointer函数偏移量参数效果很好:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, Normal));
我们需要为Mesh类定义的最后一个函数,是它的Draw函数。在真正渲染前我们希望绑定合适的纹理,然后调用glDrawElements。可因为我们从一开始不知道这个网格有多少纹理以及它们应该是什么类型的,所以这件事变得很困难。所以我们该怎样在着色器中设置纹理单元和采样器呢?
解决这个问题,我们需要假设一个特定的名称惯例:每个漫反射贴图被命名为texture_diffuseN,每个镜面贴图应该被命名为texture_specularN。N是一个从1到纹理采样器允许使用的最大值之间的数。比如说,在一个网格中我们有3个漫反射贴图和2个镜面贴图,它们的纹理采样器应该在这之后被调用:
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
最后的DRAW函数代码:
void Draw(Shader shader)
{
GLuint diffuseNr = 1;
GLuint specularNr = 1;
for(GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // 在绑定纹理前需要激活适当的纹理单元
// 检索纹理序列号 (N in diffuse_textureN)
stringstream ss;
string number;
string name = this->textures[i].type;
if(name == "texture_diffuse")
ss << diffuseNr++; // 将GLuin输入到string stream
else if(name == "texture_specular")
ss << specularNr++; // 将GLuin输入到string stream
number = ss.str();
glUniform1f(glGetUniformLocation(shader.Program, ("material." + name + number).c_str()), i);
glBindTexture(GL_TEXTURE_2D, this->textures[i].id);
}
glActiveTexture(GL_TEXTURE0);
// 绘制Mesh
glBindVertexArray(this->VAO);
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
完整的Mesh类代码:
#pragma once
// Std. Includes
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>
using namespace std;
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include<GL\Shader.h>
//声明顶点结构体
struct Vertex
{
// 顶点坐标
glm::vec3 Position;
// 顶点法向量
glm::vec3 Normal;
// 顶点纹理坐标
glm::vec2 TexCoords;
};
//声明纹理结构体
struct Texture
{
GLuint id;
string type;
aiString path;
};
class Mesh
{
public:
//一个网格会有多个顶点,所以就会有多组索引,也可以同时有多个纹理
vector<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
// 构造函数,把通过Assimp读取的数据保存下来
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
// 得到数据后,设置顶点缓冲和属性指针
this->setupMesh();
}
void Draw(Shader shader)
{
// 绑定合适的纹理
GLuint diffuseNr = 1;
GLuint specularNr = 1;
for (GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // 再绑定之前激活相应的纹理单元
stringstream ss;
string number;
string name = this->textures[i].type;
if (name == "texture_diffuse")
ss << diffuseNr++;
else if (name == "texture_specular")
ss << specularNr++;
number = ss.str();//.str(),从ss中读取字符串数据
// 设置采样器对应正确的纹理单元
glUniform1i(glGetUniformLocation(shader.Program, (name + number).c_str()), i);
//.c_str()返回一个指向该字符串开头的指针
// 最后绑定纹理
glBindTexture(GL_TEXTURE_2D, this->textures[i].id);
}
// 不要忘记设置材质的发光值
glUniform1f(glGetUniformLocation(shader.Program, "material.shininess"), 16.0f);
// Draw mesh
glBindVertexArray(this->VAO);
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// 一个好习惯:解绑,使OoenGL的工作状态恢复至初状态
for (GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, 0);
}
}
private:
GLuint VAO, VBO, EBO;
void setupMesh()
{
glGenVertexArrays(1, &this->VAO);
glGenBuffers(1, &this->VBO);
glGenBuffers(1, &this->EBO);
glBindVertexArray(this->VAO);
glBindBuffer(GL_ARRAY_BUFFER, this->VBO);
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex), &this->vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO);
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));
//offsetof函数,第一个参数是结构体,第二个参数是结构体中的某一属性,返回的是该结构体中该属性所占空间大小
// Vertex Texture Coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
};
3、模型
现在是时候着手启用Assimp,并开始创建实际的加载和转换代码了。现在的目标是创建另一个类,这个类可以表达模型(Model)的全部。更确切的说,一个模型包含多个网格(Mesh),一个网格可能带有多个对象。一个别墅,包含一个木制阳台,一个尖顶或许也有一个游泳池,它仍然被加载为一个单一模型。
Model类包含一个Mesh对象的向量,我们需要在构造函数中给出文件的位置。之后,在构造其中,它通过loadModel函数加载文件。私有方法都被设计为处理一部分的Assimp导入的常规动作,我们会简单讲讲它们。同样,我们储存文件路径的目录,这样稍后加载纹理的时候会用到。
class Model
{
public:
/* 成员函数 */
Model(GLchar* path)
{
this->loadModel(path);
}
void Draw(Shader shader);
private:
/* 模型数据 */
vector<Mesh> meshes;
string directory;
/* 私有成员函数 */
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);
};
函数Draw没有什么特别之处,基本上是循环每个网格,调用各自的Draw函数。
void Draw(Shader shader)
{
for(GLuint i = 0; i < this->meshes.size(); i++)
this->meshes[i].Draw(shader);
}
为了导入一个模型,并把它转换为我们自己的数据结构,第一件需要做的事是包含合适的Assimp头文件。
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
我们将要调用的第一个函数是loadModel,它被构造函数直接调用。在loadModel函数里面,我们使用Assimp加载模型到Assimp中被称为scene对象的数据结构。你可能还记得模型加载系列的第一个教程中,这是Assimp的数据结构的根对象。一旦我们有了场景对象,我们就能从已加载模型中获取所有所需数据了。
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 : 和上个选项相反,它把几个网格结合为一个更大的网格。以减少绘制函数调用的次数的方式来优化。
在我们加载了模型之后,我们检验是否场景和场景的根节点为空,查看这些标记中的一个来看看返回的数据是否完整。如果发生了任何一个错误,我们通过导入器(impoter)的GetErrorString函数返回错误报告。我们同样重新获取文件的目录路径。
如果没什么错误发生,我们希望处理所有的场景节点,所以我们传递第一个节点(根节点)到递归函数processNode。因为每个节点(可能)包含多个子节点,我们希望先处理父节点再处理子节点,以此类推。这符合递归结构,所以我们定义一个递归函数。
void loadModel(string path)
{
Assimp::Importer import;
const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
if(!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
this->directory = path.substr(0, path.find_last_of('/'));
this->processNode(scene->mRootNode, scene);//递归处理所有节点
}
Assimp的结构,每个节点包含一个网格集合的索引,每个索引指向一个在scene场景对象中特定的网格位置。我们希望获取这些网格索引,从而能够获取每个网格,处理每个网格,然后对其他的节点的子节点做同样的处理。
我们首先利用场景的mMeshes数组来检查每个节点的网格索引以获取相应的网格。被返回的网格被传递给processMesh函数,它返回一个网格对象,我们可以把它储存在meshes的list或vector(STL里的两种实现链表的数据结构)中。
一旦所有的网格都被处理,我们遍历所有子节点,同样调用processNode函数。一旦一个节点不再拥有任何子节点,函数就会停止执行。
void processNode(aiNode* node, const aiScene* scene)
{
// 添加当前节点中的所有Mesh
for(GLuint i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
this->meshes.push_back(this->processMesh(mesh, scene));
}
// 递归处理该节点的子孙节点
for(GLuint i = 0; i < node->mNumChildren; i++)
{
this->processNode(node->mChildren[i], scene);
}
}
把一个aiMesh对象转换为一个我们自己定义的网格对象并不难。我们所要做的全部是获取每个网格相关的属性并把这些属性储存到我们自己的对象。通常processMesh函数的结构会是这样:
获取所有顶点数据,获取网格的索引,获取相关材质数据。处理过的数据被储存在3个向量其中之一里面,一个Mesh被以这些数据创建,返回到函数的调用者。
Mesh processMesh(aiMesh* mesh, const aiScene* scene)
{
vector<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
for(GLuint i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// 处理顶点坐标、法线和纹理坐标
...
vertices.push_back(vertex);
}
// 处理顶点索引
...
// 处理材质
if(mesh->mMaterialIndex >= 0)
{
...
}
return Mesh(vertices, indices, textures);
}
获取顶点数据很简单:我们定义一个Vertex结构体,在每次遍历后我们把这个结构体添加到Vertices数组。我们为存在于网格中的众多顶点循环(通过mesh->mNumVertices获取)。在遍历的过程中,我们希望用所有相关数据填充这个结构体。每个顶点位置会像这样被处理:
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;//Assimp将它的顶点位置数组称为mVertices
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
同样处理法线:
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
纹理坐标也基本一样,但是Assimp允许一个模型的每个顶点有8个不同的纹理坐标,我们可能用不到,所以我们只关系第一组纹理坐标。
if(mesh->mTextureCoords[0]) // Does the mesh contain texture coordinates?
{
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
Assimp的接口定义每个网格有一个以面(faces)为单位的数组,每个面代表一个单独的图元,在我们的例子中(由于aiProcess_Triangulate选项)总是三角形,一个面(faces)包含索引,这些索引定义我们需要绘制的顶点以怎样的顺序提供给每个图元,所以如果我们遍历所有面(faces),把所有面(faces)的索引储存到indices向量,我们需要这么做:
for(GLuint i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for(GLuint j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
如同节点,一个网格只有一个指向材质对象的索引,获取网格实际的材质,我们需要索引scene的mMaterials数组。网格的材质索引被设置在mMaterialIndex属性中,通过这个属性我们同样能够检验一个网格是否包含一个材质。
if(mesh->mMaterialIndex >= 0)
{
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
vector<Texture> diffuseMaps = this->loadMaterialTextures(material,
aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
vector<Texture> specularMaps = this->loadMaterialTextures(material,
aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
我么先从scene场景的mMaterials数组获取aimaterial对象,然后,我们希望加载网格的漫反射贴图和(或者)镜面贴图。一个材质储存了一个数组,这个数组为每个纹理类型提供纹理位置。不同的纹理类型都以aiTextureType_为前缀。我们使用一个帮助函数:loadMaterialTextures来从材质获取纹理。这个函数返回一个Texture结构体的向量,我们之后将其储存在模型的textures坐标的后面。
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(GLuint i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
}
return textures;
}
我们先通过GetTextureCount函数检验材质中储存的纹理,以期得到我们希望得到的纹理类型。然后我们通过GetTexture函数获取每个纹理的文件位置,这个位置以aiString类型储存。然后我们使用另一个帮助函数,它被命名为:TextureFromFile加载一个纹理(使用SOIL),返回纹理的ID。
Model类完整代码:
#pragma once
// Std. Includes
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
// GL Includes
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <GL\SOIL.h>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <gl\Mesh.h>
GLint TextureFromFile(const char* path, string directory);
class Model
{
public:
// Constructor, expects a filepath to a 3D model.
Model(GLchar* path)
{
this->loadModel(path);
}
// Draws the model, and thus all its meshes
void Draw(Shader shader)
{
for (GLuint i = 0; i < this->meshes.size(); i++)
this->meshes[i].Draw(shader);
}
private:
/* Model Data */
vector<Mesh> meshes;
string directory;
vector<Texture> textures_loaded; //将读取过的纹理保存下来,防止重复读取同一个纹理
void loadModel(string path)
{
// 通过ASSIMP读取文件
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
//aiProcess_Triangulate将模型转换成由三角形组成的模型
//aiProcess_GenNormals : 如果模型没有包含法线向量,就为每个顶点创建法线。
//aiProcess_SplitLargeMeshes : 把大的网格分成几个小的的下级网格,当你渲染有一个最大数量顶点的限制时或者只能处理小块网格时很有用。
//aiProcess_OptimizeMeshes : 和上个选项相反,它把几个网格结合为一个更大的网格。以减少绘制函数调用的次数的方式来优化。
// 检查是否出错
if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
// 保存文件目录
this->directory = path.substr(0, path.find_last_of('/'));
// 递归加工ASSIMP读取的节点数据
this->processNode(scene->mRootNode, scene);
}
// 以递归方式处理节点。处理位于节点上的每个单独的网格,并在其子节点上重复这个过程(如果有的话)。
void processNode(aiNode* node, const aiScene* scene)
{
// 处理位于当前节点的每个网格
for (GLuint i = 0; i < node->mNumMeshes; i++)
{
// 每个节点包含一个网格集合的索引,每个索引指向一个在场景对象中特定的网格位置
// 场景中包含的所有数据,节点仅仅是保持数据有序整齐(如节点间关系)
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
this->meshes.push_back(this->processMesh(mesh, scene));
}
// 递归处理子节点
for (GLuint i = 0; i < node->mNumChildren; i++)
{
this->processNode(node->mChildren[i], scene);
}
}
Mesh processMesh(aiMesh* mesh, const aiScene* scene)
{
vector<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
// 遍历网格中的每个顶点
for (GLuint i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
glm::vec3 vector;
// Positions
// Assimp将它的顶点位置数组称为mVertices
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// Normals
// Assimp将它的顶点法线数组称为mVertices
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
// Texture Coordinates
if (mesh->mTextureCoords[0]) // 先要判但是否含有纹理坐标
{
glm::vec2 vec;
// 一个顶点可以含有8组不同的纹理坐标,我们用不了那么多,所以通常只用一组就行
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
vertices.push_back(vertex);
}
// 现在将通过每一个网格的“面”(面是一个网格中的一个三角形)来检索相应的顶点索引。
for (GLuint i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// 检索所有“面”即三角形的索引属性,保存下来用于绘制图形
for (GLuint j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
if (mesh->mMaterialIndex >= 0)
{
//一个材质储存了一个数组,这个数组为每个纹理类型提供纹理位置。不同的纹理类型都以aiTextureType_为前缀。
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
// 我们假设在着色器中的采样器的命名按照如下方式:
// 采用例如 'texture_diffuseN' 这种形式,其中N是标号
// 漫反射纹理: texture_diffuseN
// 镜面反射纹理: texture_specularN
// 纹理法线: texture_normalN
// 1. Diffuse maps
vector<Texture> diffuseMaps = this->loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. Specular maps
vector<Texture> specularMaps = this->loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
// Return a mesh object created from the extracted mesh data
return Mesh(vertices, indices, textures);
}
// 检查给定类型的所有材质纹理,如果没有被加载过则加载纹理。
// 最后返回的是一个纹理结构体
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName)
{
/*
我们先通过GetTextureCount函数检验材质中储存的纹理,以期得到我们希望得到的纹理类型。
然后我们通过GetTexture函数获取每个纹理的文件位置,这个位置以aiString类型储存。
然后我们使用另一个帮助函数,它被命名为:TextureFromFile加载一个纹理(使用SOIL),返回纹理的ID。
*/
vector<Texture> textures;
for (GLuint i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);//得到type类型中第i个纹理的路径信息并传递给str
// 检查该纹理之前是否加载过
GLboolean skip = false;
for (GLuint j = 0; j < textures_loaded.size(); j++)
{
if (textures_loaded[j].path == str)
{
textures.push_back(textures_loaded[j]);
skip = true;
break;
}
}
if (!skip)
{
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); // 将加载过的纹理记录下来
}
}
return textures;
}
};
//加载纹理函数
GLint TextureFromFile(const char* path, string directory)
{
//Generate texture ID and load texture data
string filename = string(path);
filename = directory + '/' + filename;
cout << filename << endl;
GLuint textureID;
glGenTextures(1, &textureID);
int width, height;
unsigned char* image = SOIL_load_image(filename.c_str(), &width, &height, 0, SOIL_LOAD_RGB);
// Assign texture to ID
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
// Parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
SOIL_free_image_data(image);
return textureID;
}