Assimp里的一些知识(1)

OpenGL 学习到模型加载的时候,介绍了一个模型导入库(Open Asset Import Library,Assimp)的用法。初学的时候觉得稍微有些复杂,故借由这篇blog来简单地捋一下其中的细节。


首先,当我们使用Assimp导入模型的时候,它通常会将整个模型加载到一个场景(Scene)对象,这个对象包含了导入模型的所有数据。具体结构如下图所示(这个图结构十分重要,需要充分理解):

  1.  Scene 场景。Scene场景有3个成员,分别是mRootNode(场景根节点),mMeshes(场景中所有网格Mesh的集合),mMaterials(网格的材质属性)。
  2. Node节点。场景的根节点(Root Node, mRootNode指向)包含了子节点,同时每个节点中有一系列指向场景对象中具体mMeshes成员的索引(Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引)。此外,这个子结构让我们可以使用递归的方式来处理节点(稍后讲到)。
  3. Mesh网格,一个Mesh 网格是可以看做渲染的一个单位,它包含了顶点数据,法线,纹理坐标(只是坐标数据,不是纹理对象),面等数组,以及材质索引(指向Scene中的mMaterials
  4. Face面,面数组由面组成(废话),概念上,一个面表示的是物体的渲染图元(primitive)(三角形,方形,点)。在这里,我们则让它包含了组成图元的顶点索引(也就是指向Mesh里面的顶点数据啦),从而我们可以使用EBO来绘制图形。
  5. 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类下一节讲。

猜你喜欢

转载自www.cnblogs.com/zhlabcd/p/11726955.html
今日推荐