Assimp里的一些知识
Posted zhlabcd
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Assimp里的一些知识相关的知识,希望对你有一定的参考价值。
OpenGL 学习到模型加载的时候,介绍了一个模型导入库(Open Asset Import Library,Assimp)的用法。初学的时候觉得稍微有些复杂,故借由这篇blog来简单地捋一下其中的细节。
首先,当我们使用Assimp导入模型的时候,它通常会将整个模型加载到一个场景(Scene)对象,这个对象包含了导入模型的所有数据。具体结构如下图所示(这个图结构十分重要,需要充分理解):
- Scene 场景。Scene场景有3个成员,分别是mRootNode(场景根节点),mMeshes(场景中所有网格Mesh的集合),mMaterials(网格的材质属性)。
- Node节点。场景的根节点(Root Node, mRootNode指向)包含了子节点,同时每个节点中有一系列指向场景对象中具体mMeshes成员的索引(Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引)。此外,这个子结构让我们可以使用递归的方式来处理节点(稍后讲到)。
- Mesh网格,一个Mesh 网格是可以看做渲染的一个单位,它包含了顶点数据,法线,纹理坐标(只是坐标数据,不是纹理对象),面等数组,以及材质索引(指向Scene中的mMaterials)
- Face面,面数组由面组成(废话),概念上,一个面表示的是物体的渲染图元(primitive)(三角形,方形,点)。在这里,我们则让它包含了组成图元的顶点索引(也就是指向Mesh里面的顶点数据啦),从而我们可以使用EBO来绘制图形。
- Material材质,一个Mesh网格还包括了一个材质索引(非数组),索引指向Scene中的具体材质。
这里基本上就把Assimp的结构讲解完毕了,在这里补充一下关于材质,贴图,纹理等的相关知识(防止混淆):贴图,纹理,材质的区别是什么?
顺便补充一下什么叫Mesh,以及它的图元如何组成Mesh:What is a mesh in OpenGL?
好了,Assimp导入模型到场景之后的数据结构我们已经清楚了,现在我们需要一个类来专门处理这些导入的数据。其实也很简单,数据处理过程与之前绘制箱子的过程基本一致,只是由于未知导入模型的数据量,我们采用了vector模板类来存储相关数据。
首先,一个网格需要的最少数据量就是顶点数据,法线,纹理坐标。用于面绘制的索引。以及纹理形式的材质数据,,因此我们在这里可以定义一些结构体(没定义到的后面Mesh类也会出现的啦):
struct Vertex { glm::vec3 Position; glm::vec3 Normal; glm::vec2 TexCoords; }; struct Texture { unsigned int id; string type; //such as diffuseMap or specularMap };
定义好结构体之后,就是Mesh类的具体定义了:
class Mesh { public: vector<Vertex> vertices; //顶点数据 vector<unsigned int> indices //顶点绘制索引,也就是Face里面的indices vector<Texture> texture; //材质(纹理)数据。严格的说是物体的材质贴图。详见上面关于材质纹理贴图的知乎链接啦。 Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> texture); void Draw(Shader shader); //绘制网格的函数 private: unsigned int VAO, VBO, EBO; void setupMesh(); //处理网格数据的函数 }
不废话了,直接给setupMesh的源码(其实是懒)
void setupMesh() { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); // 顶点位置 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); // 顶点法线 glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); // 顶点纹理坐标 glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords)); glBindVertexArray(0); }
这里有一些需要注意的东西。
第一个是传入数据至VBO以及EBO的时候,由于我们使用的是vector模板类,不可以直接用sizeof()计算大小(为什么?),所以通过用.size(),以及结构体的大小来计算数据量。还有我们用的&vertices[0],而不是像之前那样的vertices,是因为之前用数组实现,数组名即为指针,用vector实现,需要手动添加首元素的地址。
实际上因为结构体的存储方式是连续的(sequential),我们得以直接传入结构体的指针(&vertices[0])作为缓冲的数据(的起始点)
其二是OffsetOf函数,顾名思义用来计算偏移量。
然后就是Draw函数了:
void Draw(Shader shader) { unsigned int diffuseNr = 1; unsigned int specularNr = 1; for(unsigned int i = 0; i < textures.size(); i++) { glActiveTexture(GL_TEXTURE0 + i); // 在绑定之前激活相应的纹理单元 // 获取纹理序号(diffuse_textureN 中的 N) string number; string name = textures[i].type; if(name == "texture_diffuse") number = std::to_string(diffuseNr++); else if(name == "texture_specular") number = std::to_string(specularNr++); shader.setFloat(("material." + name + number).c_str(), i); glBindTexture(GL_TEXTURE_2D, textures[i].id); } glActiveTexture(GL_TEXTURE0); // 绘制网格 glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); }
这里我们稍微回顾一下纹理的相关知识:如何把纹理传入fragment shader,首先激活一个对应的纹理单元,然后我们将要绑定的纹理绑定起来,接着将在着色器里面纹理采样器的值设置为对应的纹理单元值(0-15)。上面Draw函数干的就是这事儿,并且最后根据索引(EBO),当前激活的着色器等绘制图元,
Model类下一节讲。
以上是关于Assimp里的一些知识的主要内容,如果未能解决你的问题,请参考以下文章
我的OpenGL学习进阶之旅Assimp库支持哪些3D模型格式?