使用Assimp可以把多種不同格式的模型加載到程序中谷扣,但是一旦載入,它們就都被儲(chǔ)存為Assimp自己的數(shù)據(jù)結(jié)構(gòu)值桩。我們最終的目的是把這些數(shù)據(jù)轉(zhuǎn)變?yōu)镺penGL可讀的數(shù)據(jù)丽旅,才能用OpenGL來渲染物體。我們從前面的教程了解到舟误,一個(gè)網(wǎng)格(Mesh)代表一個(gè)可繪制實(shí)體葡秒,現(xiàn)在我們就定義一個(gè)自己的網(wǎng)格類。
先來復(fù)習(xí)一點(diǎn)目前學(xué)到知識(shí),考慮一個(gè)網(wǎng)格最少需要哪些數(shù)據(jù)眯牧。一個(gè)網(wǎng)格應(yīng)該至少需要一組頂點(diǎn)蹋岩,每個(gè)頂點(diǎn)包含一個(gè)位置向量,一個(gè)法線向量学少,一個(gè)紋理坐標(biāo)向量剪个。一個(gè)網(wǎng)格也應(yīng)該包含一個(gè)索引繪制用的索引,以紋理(diffuse/specular map)形式表現(xiàn)的材質(zhì)數(shù)據(jù)版确。
為了在OpenGL中定義一個(gè)頂點(diǎn)扣囊,現(xiàn)在我們?cè)O(shè)置有最少需求的一個(gè)網(wǎng)格類:
struct Vertex
{
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
我們把每個(gè)需要的向量?jī)?chǔ)存到一個(gè)叫做Vertex的結(jié)構(gòu)體中,它被用來索引每個(gè)頂點(diǎn)屬性绒疗。另外除了Vertex結(jié)構(gòu)體外侵歇,我們也希望組織紋理數(shù)據(jù),所以我們定義一個(gè)Texture結(jié)構(gòu)體:
struct Texture
{
GLuint id;
string type;
};
我們儲(chǔ)存紋理的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 ();
};
如你所見這個(gè)類一點(diǎn)都不復(fù)雜磨镶,構(gòu)造方法里我們初始化網(wǎng)格所有必須數(shù)據(jù)溃蔫。在setupMesh函數(shù)里初始化緩沖。最后通過Draw函數(shù)繪制網(wǎng)格琳猫。注意伟叛,我們把shader傳遞給Draw函數(shù)。通過把shader傳遞給Mesh脐嫂,在繪制之前我們?cè)O(shè)置幾個(gè)uniform(比如鏈接采樣器到紋理單元)统刮。
構(gòu)造函數(shù)的內(nèi)容非常直接。我們簡(jiǎn)單設(shè)置類的公有變量账千,使用的是構(gòu)造函數(shù)相應(yīng)的參數(shù)网沾。我們?cè)跇?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〈椎現(xiàn)在你應(yīng)該對(duì)這些概念很熟悉,但是我們介紹了結(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)體有一個(gè)重要的屬性饲齐,那就是在內(nèi)存中它們是連續(xù)的。如果我們用結(jié)構(gòu)體表示一列數(shù)據(jù)咧最,這個(gè)結(jié)構(gòu)體只包含結(jié)構(gòu)體的連續(xù)的變量捂人,它就會(huì)直接轉(zhuǎn)變?yōu)橐粋€(gè)float(實(shí)際上是byte)數(shù)組御雕,我們就能用于一個(gè)數(shù)組緩沖(array buffer)中了。比如滥搭,如果我們填充一個(gè)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];
感謝這個(gè)有用的特性,我們能直接把一個(gè)作為緩沖數(shù)據(jù)的一大列Vertex結(jié)構(gòu)體的指針傳遞過去瑟匆,它們會(huì)翻譯成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)愁溜。
一個(gè)預(yù)處理指令叫做offsetof(s,m)把結(jié)構(gòu)體作為它的第一個(gè)參數(shù)疾嗅,第二個(gè)參數(shù)是這個(gè)結(jié)構(gòu)體內(nèi)的變量。這是結(jié)構(gòu)體另外的一個(gè)重要用途冕象。函數(shù)返回這個(gè)變量從結(jié)構(gòu)體開始的字節(jié)偏移量(offset)代承。這對(duì)于定義glVertexAttribPointer函數(shù)偏移量參數(shù)效果很好:
glVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex),
(GLvoid*) offsetof (Vertex, Normal));
偏移量現(xiàn)在使用offsetof函數(shù)定義了,在這個(gè)例子里渐扮,設(shè)置法線向量的字節(jié)偏移量等于法線向量在結(jié)構(gòu)體的字節(jié)偏移量次泽,它是3float
,也就是12字節(jié)(一個(gè)float占4字節(jié))席爽。注意,我們同樣設(shè)置步長(zhǎng)參數(shù)等于Vertex結(jié)構(gòu)體的大小啊片。
使用一個(gè)像這樣的結(jié)構(gòu)體只锻,不僅能提供可讀性更高的代碼同時(shí)也是我們可以輕松的擴(kuò)展結(jié)構(gòu)體。如果我們想要增加另一個(gè)頂點(diǎn)屬性紫谷,我們把它可以簡(jiǎn)單的添加到結(jié)構(gòu)體中齐饮,由于它的可擴(kuò)展性,渲染代碼不會(huì)被破壞笤昨。
渲染
我們需要為Mesh類定義的最后一個(gè)函數(shù)祖驱,是它的Draw函數(shù)。在真正渲染前我們希望綁定合適的紋理瞒窒,然后調(diào)用glDrawElements捺僻。可因?yàn)槲覀儚囊婚_始不知道這個(gè)網(wǎng)格有多少紋理以及它們應(yīng)該是什么類型的崇裁,所以這件事變得很困難匕坯。所以我們?cè)撛鯓釉谥髦性O(shè)置紋理單元和采樣器呢?
解決這個(gè)問題拔稳,我們需要假設(shè)一個(gè)特定的名稱慣例:每個(gè)diffuse紋理被命名為texture_diffuseN,每個(gè)specular紋理應(yīng)該被命名為texture_specularN葛峻。N是一個(gè)從1到紋理允許使用的最大值之間的數(shù)“捅龋可以說术奖,在一個(gè)網(wǎng)格中我們有3個(gè)diffuse紋理和2個(gè)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;
使用這樣的慣例,我們能定義我們?cè)谥髦行枰募y理采樣器的數(shù)量采记。如果一個(gè)網(wǎng)格真的有(這么多)紋理佣耐,我們就知道它們的名字應(yīng)該是什么。這個(gè)慣例也使我們能夠處理一個(gè)網(wǎng)格上的任何數(shù)量的紋理挺庞,通過定義合適的采樣器開發(fā)者可以自由使用希望使用的數(shù)量(雖然定義少的話就會(huì)有點(diǎn)浪費(fèi)綁定和uniform調(diào)用了)晰赞。
像這樣的問題有很多不同的解決方案,如果你不喜歡這個(gè)方案选侨,你可以自己創(chuàng)造一個(gè)你自己的方案掖鱼。
最后的繪制代碼:
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-元素每個(gè)紋理類型,把它鏈接到紋理類型字符串來獲取合適的uniform名晨仑。然后查找合適的采樣器位置褐墅,給它位置值對(duì)應(yīng)當(dāng)前激活紋理單元,綁定紋理洪己。這也是我們需要在Draw方法是用shader的原因妥凳。我們添加material.到作為結(jié)果的uniform名,因?yàn)槲覀兺ǔ0鸭y理儲(chǔ)存進(jìn)材質(zhì)結(jié)構(gòu)體(對(duì)于每個(gè)實(shí)現(xiàn)也許會(huì)有不同)
注意答捕,當(dāng)我們把diffuse和specular傳遞到字符串流(stringstream
)的時(shí)候逝钥,計(jì)數(shù)器會(huì)增加,在C++自增叫做:變量++拱镐,它會(huì)先返回自身然后加1艘款,而++變量,先加1再返回自身沃琅,我們的例子里哗咆,我們先傳遞原來的計(jì)數(shù)器值到字符串流,然后再加1益眉,下一輪生效晌柬。
你可以從這里得到Mesh類的源碼。
Mesh類是對(duì)我們前面的教程里討論的很多話題的的簡(jiǎn)潔的抽象郭脂。在下面的教程里空繁,我們會(huì)創(chuàng)建一個(gè)模型,它用作乘放多個(gè)網(wǎng)格物體的容器朱庆,真正的實(shí)現(xiàn)Assimp的加載接口盛泡。