加载模型-02.网格(Mesh)

使用Assimp可以把多种不同格式的模型加载到程序中,但是一旦载入,它们就都被储存为Assimp自己的数据结构。我们最终的目的是把这些数据转变为OpenGL可读的数据,才能用OpenGL来渲染物体。我们从前面的教程了解到,一个网格(Mesh)代表一个可绘制实体,现在我们就定义一个自己的网格类。

先来复习一点目前学到知识,考虑一个网格最少需要哪些数据。一个网格应该至少需要一组顶点,每个顶点包含一个位置向量,一个法线向量,一个纹理坐标向量。一个网格也应该包含一个索引绘制用的索引,以纹理(diffuse/specular map)形式表现的材质数据。

为了在OpenGL中定义一个顶点,现在我们设置有最少需求的一个网格类:

struct Vertex
{
    glm::vec3 Position;
    glm::vec3 Normal;
    glm::vec2 TexCoords;
};

我们把每个需要的向量储存到一个叫做Vertex的结构体中,它被用来索引每个顶点属性。另外除了Vertex结构体外,我们也希望组织纹理数据,所以我们定义一个Texture结构体:

struct Texture
{
    GLuint id;
    string type;
};

我们储存纹理的id和它的类型,比如diffuse纹理或者specular纹理。

知道了顶点和纹理的实际表达,我们可以开始定义网格类的结构:

class Mesh {
public:
    /*  Mesh Data  */
    vector<Vertex> vertices;
    vector<GLuint> indices;
    vector<Texture> textures;
    /*  Functions  */
    Mesh (vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures);
    void Draw (Shader shader);
private:
    /*  Render data  */
    GLuint VAO, VBO, EBO;
    /*  Functions    */
    void setupMesh ();
};

如你所见这个类一点都不复杂,构造方法里我们初始化网格所有必须数据。在setupMesh函数里初始化缓冲。最后通过Draw函数绘制网格。注意,我们把shader传递给Draw函数。通过把shader传递给Mesh,在绘制之前我们设置几个uniform(比如链接采样器到纹理单元)。

构造函数的内容非常直接。我们简单设置类的公有变量,使用的是构造函数相应的参数。我们在构造函数中也调用setupMesh函数:

Mesh (vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures)
{
    this->vertices = vertices;
    this->indices = indices;
    this->textures = textures;

    // Now that we have all the required data, set the vertex buffers and its attribute pointers.
    this->setupMesh ();
}

这里没什么特别的,现在让我们研究一下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);

    // 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));
    // Vertex Texture Coords
    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);

自然地,sizeof函数也可以使用于结构体来计算字节类型的大小。它应该是32字节(8float * 4)。

一个预处理指令叫做offsetof(s,m)把结构体作为它的第一个参数,第二个参数是这个结构体内的变量。这是结构体另外的一个重要用途。函数返回这个变量从结构体开始的字节偏移量(offset)。这对于定义glVertexAttribPointer函数偏移量参数效果很好:

glVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex),
    (GLvoid*) offsetof (Vertex, Normal));

偏移量现在使用offsetof函数定义了,在这个例子里,设置法线向量的字节偏移量等于法线向量在结构体的字节偏移量,它是3float
,也就是12字节(一个float占4字节)。注意,我们同样设置步长参数等于Vertex结构体的大小。

使用一个像这样的结构体,不仅能提供可读性更高的代码同时也是我们可以轻松的扩展结构体。如果我们想要增加另一个顶点属性,我们把它可以简单的添加到结构体中,由于它的可扩展性,渲染代码不会被破坏。

渲染

我们需要为Mesh类定义的最后一个函数,是它的Draw函数。在真正渲染前我们希望绑定合适的纹理,然后调用glDrawElements。可因为我们从一开始不知道这个网格有多少纹理以及它们应该是什么类型的,所以这件事变得很困难。所以我们该怎样在着色器中设置纹理单元和采样器呢?

解决这个问题,我们需要假设一个特定的名称惯例:每个diffuse纹理被命名为texture_diffuseN,每个specular纹理应该被命名为texture_specularN。N是一个从1到纹理允许使用的最大值之间的数。可以说,在一个网格中我们有3个diffuse纹理和2个specular纹理,它们的纹理采样器应该这样被调用:

uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;

使用这样的惯例,我们能定义我们在着色器中需要的纹理采样器的数量。如果一个网格真的有(这么多)纹理,我们就知道它们的名字应该是什么。这个惯例也使我们能够处理一个网格上的任何数量的纹理,通过定义合适的采样器开发者可以自由使用希望使用的数量(虽然定义少的话就会有点浪费绑定和uniform调用了)。

像这样的问题有很多不同的解决方案,如果你不喜欢这个方案,你可以自己创造一个你自己的方案。

最后的绘制代码:

void Draw (Shader shader)
{
    GLuint diffuseNr = 1;
    GLuint specularNr = 1;
    for (GLuint i = 0; i < this->textures.size (); i++)
    {
        glActiveTexture (GL_TEXTURE0 + i); // Activate proper texture unit before binding
                            // Retrieve texture number (the N in diffuse_textureN)
        stringstream ss;
        string number;
        string name = this->textures[i].type;
        if (name == "texture_diffuse")
            ss << diffuseNr++; // Transfer GLuint to stream
        else if (name == "texture_specular")
            ss << specularNr++; // Transfer GLuint to 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);

    // Draw mesh
    glBindVertexArray (this->VAO);
    glDrawElements (GL_TRIANGLES, this->indices.size (), GL_UNSIGNED_INT, 0);
    glBindVertexArray (0);
}

这不是最漂亮的代码,但是这主要归咎于C++转换类型时的丑陋,比如int转string时。我们首先计算N-元素每个纹理类型,把它链接到纹理类型字符串来获取合适的uniform名。然后查找合适的采样器位置,给它位置值对应当前激活纹理单元,绑定纹理。这也是我们需要在Draw方法是用shader的原因。我们添加material.到作为结果的uniform名,因为我们通常把纹理储存进材质结构体(对于每个实现也许会有不同)

注意,当我们把diffuse和specular传递到字符串流(stringstream
)的时候,计数器会增加,在C++自增叫做:变量++,它会先返回自身然后加1,而++变量,先加1再返回自身,我们的例子里,我们先传递原来的计数器值到字符串流,然后再加1,下一轮生效。

你可以从这里得到Mesh类的源码

Mesh类是对我们前面的教程里讨论的很多话题的的简洁的抽象。在下面的教程里,我们会创建一个模型,它用作乘放多个网格物体的容器,真正的实现Assimp的加载接口。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容

  • 现在是时候着手启用Assimp,并开始创建实际的加载和转换代码了。本教程的目标是创建另一个类,这个类可以表达模型的...
    IceMJ阅读 2,742评论 0 3
  • <转>我也忘了转自哪里,抱歉,感谢原作者 什么是Shader Shader(着色器)是一段能够针对3D对象进行操作...
    星易乾川阅读 5,561评论 1 16
  • 更新:【面试题含答案】http://bbs.9ria.com/thread-288394-1-1.html 高频问...
    好怕怕阅读 4,712评论 3 52
  • 优酷腾讯爱奇艺等左右网站的独播网剧 VIP用券才能看的电影 在更新或者已经完结的美剧日剧韩剧tvb 各种院线电影...
    一别两宽丶阅读 128评论 0 0