Assimp
首先介绍一下Assimp
库,它是Opengl中常使用的模型加载库,全称 Open Asset Import Library。它支持多种格式的模型文件,如obj、3ds、c4e等。模型一般通过Blender、3DS Max 或者Maya这样的工具软件制作,然后可以导出模型文件。我们在使用Opengl时,就需要将这些文件中的数据内容解析出来,内容主要有顶点数据、法线、纹理坐标等,还有材质、光照等信息,只有解析出这些数据之后我们才能做后续的渲染工作。
当导入一个模型文件时,Assimp加载所有模型和场景数据到一个Scene类型的对象中,同时为场景节点、模型节点生成具有对应关系的数据结构。数据结构图如下:
- 所有的模型、场景数据都包含在scene对象中,如Material质和Mesh。同样,场景的根节点引用也包含在这个scene对象中。
- 场景的根节点可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里保存了实际了Mesh对象,而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用。
- 一个Mesh对象本身包含渲染所需的所有相关数据,比如顶点位置、法线向量、纹理坐标、面片及物体的材质。
- 一个Mesh会包含多个面。一个Face(面)表示渲染中的一个最基本的形状单位,即图元(基本图元有点、线、三角面片、矩形面片)。一个面片记录了一个图元的顶点索引,通过这个索引,可以在mMeshes[]中寻找到对应的顶点位置数据。
- 一个Mesh还会包含一个Material(材质)对象用于指定物体的一些材质属性。如颜色、纹理贴图(漫反射贴图、高光贴图等)。
从图片中看出结构结构对于初学者可能有些复杂,不过不要紧,只要先学会使用 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类
一个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<Vertex> vertices;
vector<GLuint> indices;
vector<Texture> textures;
// Constructor
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> 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()
{
...
//创建缓冲区、绑定、并分配顶点位置、法线、纹理数据
}
};
完整的代码可以在这里找到。
- Model类
要想使用Assimp库,需要在工程中配置相应的库文件,即导入dll文件和Include文件夹,Cmake的代码在这里下载。
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);
};
以上就是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类的完整源代码在这里可以找到。
效果如下,该例中加入了光照,由于光源位置的关系和材质并没有都进行光照设定,效果并不是很好,可以自行设置光照,并影响到全部的材质上,应该会有不错的效果。
参考资料
- What is a mesh in OpenGL?
- Learn Opengl学习网站——一个初学者不错的网站