首先介绍一下Assimp
库,它是Opengl中常使用的模型加载库,全称 Open Asset Import Library。它支持多种格式的模型文件,如obj、3ds、c4e等。模型一般通过Blender、3DS Max 或者Maya这样的工具软件制作,然后可以导出模型文件。我们在使用Opengl时,就需要将这些文件中的数据内容解析出来,内容主要有顶点数据、法线、纹理坐标等,还有材质、光照等信息,只有解析出这些数据之后我们才能做后续的渲染工作。
当导入一个模型文件时,Assimp加载所有模型和场景数据到一个Scene类型的对象中,同时为场景节点、模型节点生成具有对应关系的数据结构。数据结构图如下:
从图片中看出结构结构对于初学者可能有些复杂,不过不要紧,只要先学会使用 Assimp库来把模型数据加载进来就可以了,后续可以自己尝试读取Obj等文件数据,对文件数据结构和解析过程进行细致的学习。
一般一个Model通常有多个网格构成,如一个人体模型可以由四肢、头部、上身等部位组成,每个部位都是一个Mesh。
那么怎么理解Mesh呢?如果你练习过绘制一个三角形的话,就知道一个三角形就是一个Mesh,包含顶点位置、法线、纹理坐标,其实这个三角形也是一个Face,而实际复杂的模型中的Mesh,一般包含很多Face,Face是绘制的基本图元,有三角形、点、线、多边形等,常用的主要是三角形。
一个Mesh是顶点、边和面的结合,也是绘制3D物体的最小单元,一个3D模型,拥有越多的Mesh,这个模型越接近真实,但同时带来了硬件设备渲染的负担,特别是对于移动设备,如在手机上使用Opengl ES渲染时就要衡量GPU性能以及Mesh数目 的设定。(来自:What is a mesh in Opengl?)
好了,对Mesh有了一个大致的概念之后,我们就开始对模型进行加载,模型可以从网站上下载,本例中选择的模型也是Learn Opengl网上推荐的纳米铠甲模型,下载位置在这里。
一个Mesh应该至少需要一组顶点,每个顶点包含一个位置向量,一个法线向量,一个纹理坐标向量。一个网格也应该包含纹理(diffuse/specular map)的类型和ID。
struct Vertex {
// Position
glm::vec3 Position;
// Normal
glm::vec3 Normal;
// TexCoords
glm::vec2 TexCoords;
};
struct Texture {
GLuint id;
string type;
aiString path;
};
定义顶点和纹理的数据结构,索引的类型为GLuint类型
class Mesh {
public:
/* Mesh Data */
vector vertices;
vector indices;
vector textures;
// Constructor
Mesh(vector vertices, vector indices, vector textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
this->setupMesh();
}
// Render the mesh
void Draw(Shader shader)
{
...
//绑定相应的纹理,即设定片断着色器中的uniform类型
// 绘制网格
glBindVertexArray(this->VAO);
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// 解绑定纹理
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()
{
...
//创建缓冲区、绑定、并分配顶点位置、法线、纹理数据
}
};
完整的代码可以在这里找到。
要想使用Assimp库,需要在工程中配置相应的库文件,即导入dll文件和Include文件夹,Cmake的代码在这里下载。
class Model
{
public:
/* 成员函数 */
Model(GLchar* path)
{
this->loadModel(path);
}
void Draw(Shader shader);
private:
/* 模型数据 */
vector meshes;
string directory;
/* 私有成员函数 */
void loadModel(string path);
void processNode(aiNode* node, const aiScene* scene);
Mesh processMesh(aiMesh* mesh, const aiScene* scene);
vector loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName);
};
以上就是Model类,其中构造函数中传入的是路径,通过loadModel()函数加载obj类型的文件,然后得到 aiScene类型的对象,进而解析出meshes
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
Mesh类中需要vertices, indices, textures三种结构的vector,解析的过程也就是得到这三种数据并不断构造出Mesh类,然后pushback到meshes中,然后Model类的draw过程实际上就是遍历meshes中所有的Mesh实例,通过Mesh实来调用Mesh中draw()方法实现渲染。Model类的完整源代码在这里可以找到。
效果如下,该例中加入了光照,由于光源位置的关系和材质并没有都进行光照设定,效果并不是很好,可以自行设置光照,并影响到全部的材质上,应该会有不错的效果。