使用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的加載接口。