加載模型-02.網(wǎng)格(Mesh)

使用Assimp可以把多種不同格式的模型加載到程序中,但是一旦載入,它們就都被儲存為Assimp自己的數(shù)據(jù)結(jié)構(gòu)。我們最終的目的是把這些數(shù)據(jù)轉(zhuǎn)變?yōu)镺penGL可讀的數(shù)據(jù),才能用OpenGL來渲染物體。我們從前面的教程了解到,一個網(wǎng)格(Mesh)代表一個可繪制實(shí)體,現(xiàn)在我們就定義一個自己的網(wǎng)格類。

先來復(fù)習(xí)一點(diǎn)目前學(xué)到知識,考慮一個網(wǎng)格最少需要哪些數(shù)據(jù)。一個網(wǎng)格應(yīng)該至少需要一組頂點(diǎn),每個頂點(diǎn)包含一個位置向量,一個法線向量,一個紋理坐標(biāo)向量。一個網(wǎng)格也應(yīng)該包含一個索引繪制用的索引,以紋理(diffuse/specular map)形式表現(xiàn)的材質(zhì)數(shù)據(jù)。

為了在OpenGL中定義一個頂點(diǎn),現(xiàn)在我們設(shè)置有最少需求的一個網(wǎng)格類:

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

我們把每個需要的向量儲存到一個叫做Vertex的結(jié)構(gòu)體中,它被用來索引每個頂點(diǎn)屬性。另外除了Vertex結(jié)構(gòu)體外,我們也希望組織紋理數(shù)據(jù),所以我們定義一個Texture結(jié)構(gòu)體:

struct Texture
{
    GLuint id;
    string type;
};

我們儲存紋理的id和它的類型,比如diffuse紋理或者specular紋理。

知道了頂點(diǎn)和紋理的實(shí)際表達(dá),我們可以開始定義網(wǎng)格類的結(jié)構(gòu):

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 ();
};

如你所見這個類一點(diǎn)都不復(fù)雜,構(gòu)造方法里我們初始化網(wǎng)格所有必須數(shù)據(jù)。在setupMesh函數(shù)里初始化緩沖。最后通過Draw函數(shù)繪制網(wǎng)格。注意,我們把shader傳遞給Draw函數(shù)。通過把shader傳遞給Mesh,在繪制之前我們設(shè)置幾個uniform(比如鏈接采樣器到紋理單元)。

構(gòu)造函數(shù)的內(nèi)容非常直接。我們簡單設(shè)置類的公有變量,使用的是構(gòu)造函數(shù)相應(yīng)的參數(shù)。我們在構(gòu)造函數(shù)中也調(diào)用setupMesh函數(shù):

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 ();
}

這里沒什么特別的,現(xiàn)在讓我們研究一下setupMesh函數(shù)。

初始化

現(xiàn)在我們有一大列的網(wǎng)格數(shù)據(jù)可用于渲染,這要感謝構(gòu)造函數(shù)。我們確實(shí)需要設(shè)置合適的緩沖,通過頂點(diǎn)屬性指針(vertex attribute pointers)定義頂點(diǎn)著色器layout?,F(xiàn)在你應(yīng)該對這些概念很熟悉,但是我們介紹了結(jié)構(gòu)體中頂點(diǎn)數(shù)據(jù),所以稍微有點(diǎn)不一樣:

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++的結(jié)構(gòu)體有一個重要的屬性,那就是在內(nèi)存中它們是連續(xù)的。如果我們用結(jié)構(gòu)體表示一列數(shù)據(jù),這個結(jié)構(gòu)體只包含結(jié)構(gòu)體的連續(xù)的變量,它就會直接轉(zhuǎn)變?yōu)橐粋€float(實(shí)際上是byte)數(shù)組,我們就能用于一個數(shù)組緩沖(array buffer)中了。比如,如果我們填充一個Vertex結(jié)構(gòu)體,它在內(nèi)存中的排布等于:

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];

感謝這個有用的特性,我們能直接把一個作為緩沖數(shù)據(jù)的一大列Vertex結(jié)構(gòu)體的指針傳遞過去,它們會翻譯成glBufferData能用的參數(shù):

glBufferData (GL_ARRAY_BUFFER, this->vertices.size () * sizeof (Vertex),
    &this->vertices[0], GL_STATIC_DRAW);

自然地,sizeof函數(shù)也可以使用于結(jié)構(gòu)體來計(jì)算字節(jié)類型的大小。它應(yīng)該是32字節(jié)(8float * 4)。

一個預(yù)處理指令叫做offsetof(s,m)把結(jié)構(gòu)體作為它的第一個參數(shù),第二個參數(shù)是這個結(jié)構(gòu)體內(nèi)的變量。這是結(jié)構(gòu)體另外的一個重要用途。函數(shù)返回這個變量從結(jié)構(gòu)體開始的字節(jié)偏移量(offset)。這對于定義glVertexAttribPointer函數(shù)偏移量參數(shù)效果很好:

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

偏移量現(xiàn)在使用offsetof函數(shù)定義了,在這個例子里,設(shè)置法線向量的字節(jié)偏移量等于法線向量在結(jié)構(gòu)體的字節(jié)偏移量,它是3float
,也就是12字節(jié)(一個float占4字節(jié))。注意,我們同樣設(shè)置步長參數(shù)等于Vertex結(jié)構(gòu)體的大小。

使用一個像這樣的結(jié)構(gòu)體,不僅能提供可讀性更高的代碼同時(shí)也是我們可以輕松的擴(kuò)展結(jié)構(gòu)體。如果我們想要增加另一個頂點(diǎn)屬性,我們把它可以簡單的添加到結(jié)構(gòu)體中,由于它的可擴(kuò)展性,渲染代碼不會被破壞。

渲染

我們需要為Mesh類定義的最后一個函數(shù),是它的Draw函數(shù)。在真正渲染前我們希望綁定合適的紋理,然后調(diào)用glDrawElements。可因?yàn)槲覀儚囊婚_始不知道這個網(wǎng)格有多少紋理以及它們應(yīng)該是什么類型的,所以這件事變得很困難。所以我們該怎樣在著色器中設(shè)置紋理單元和采樣器呢?

解決這個問題,我們需要假設(shè)一個特定的名稱慣例:每個diffuse紋理被命名為texture_diffuseN,每個specular紋理應(yīng)該被命名為texture_specularN。N是一個從1到紋理允許使用的最大值之間的數(shù)??梢哉f,在一個網(wǎng)格中我們有3個diffuse紋理和2個specular紋理,它們的紋理采樣器應(yīng)該這樣被調(diào)用:

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

使用這樣的慣例,我們能定義我們在著色器中需要的紋理采樣器的數(shù)量。如果一個網(wǎng)格真的有(這么多)紋理,我們就知道它們的名字應(yīng)該是什么。這個慣例也使我們能夠處理一個網(wǎng)格上的任何數(shù)量的紋理,通過定義合適的采樣器開發(fā)者可以自由使用希望使用的數(shù)量(雖然定義少的話就會有點(diǎn)浪費(fèi)綁定和uniform調(diào)用了)。

像這樣的問題有很多不同的解決方案,如果你不喜歡這個方案,你可以自己創(chuàng)造一個你自己的方案。

最后的繪制代碼:

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);
}

這不是最漂亮的代碼,但是這主要?dú)w咎于C++轉(zhuǎn)換類型時(shí)的丑陋,比如int轉(zhuǎn)string時(shí)。我們首先計(jì)算N-元素每個紋理類型,把它鏈接到紋理類型字符串來獲取合適的uniform名。然后查找合適的采樣器位置,給它位置值對應(yīng)當(dāng)前激活紋理單元,綁定紋理。這也是我們需要在Draw方法是用shader的原因。我們添加material.到作為結(jié)果的uniform名,因?yàn)槲覀兺ǔ0鸭y理儲存進(jìn)材質(zhì)結(jié)構(gòu)體(對于每個實(shí)現(xiàn)也許會有不同)

注意,當(dāng)我們把diffuse和specular傳遞到字符串流(stringstream
)的時(shí)候,計(jì)數(shù)器會增加,在C++自增叫做:變量++,它會先返回自身然后加1,而++變量,先加1再返回自身,我們的例子里,我們先傳遞原來的計(jì)數(shù)器值到字符串流,然后再加1,下一輪生效。

你可以從這里得到Mesh類的源碼

Mesh類是對我們前面的教程里討論的很多話題的的簡潔的抽象。在下面的教程里,我們會創(chuàng)建一個模型,它用作乘放多個網(wǎng)格物體的容器,真正的實(shí)現(xiàn)Assimp的加載接口。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 現(xiàn)在是時(shí)候著手啟用Assimp,并開始創(chuàng)建實(shí)際的加載和轉(zhuǎn)換代碼了。本教程的目標(biāo)是創(chuàng)建另一個類,這個類可以表達(dá)模型的...
    IceMJ閱讀 2,883評論 0 3
  • <轉(zhuǎn)>我也忘了轉(zhuǎn)自哪里,抱歉,感謝原作者 什么是Shader Shader(著色器)是一段能夠針對3D對象進(jìn)行操作...
    星易乾川閱讀 5,861評論 1 16
  • 更新:【面試題含答案】http://bbs.9ria.com/thread-288394-1-1.html 高頻問...
    好怕怕閱讀 5,095評論 3 53
  • 優(yōu)酷騰訊愛奇藝等左右網(wǎng)站的獨(dú)播網(wǎng)劇 VIP用券才能看的電影 在更新或者已經(jīng)完結(jié)的美劇日劇韓劇tvb 各種院線電影...
    一別兩寬丶閱讀 165評論 0 0

友情鏈接更多精彩內(nèi)容