OpenGL 學習到模型加載的時候,介紹了一個模型導入庫(Open Asset Import Library,Assimp)的用法。初學的時候覺得稍微有些復雜,故借由這篇blog來簡單地捋一下其中的細節。
首先,當我們使用Assimp導入模型的時候,它通常會將整個模型加載到一個場景(Scene)對象,這個對象包含了導入模型的所有數據。具體結構如下圖所示(這個圖結構十分重要,需要充分理解):
- Scene 場景。Scene場景有3個成員,分別是mRootNode(場景根節點),mMeshes(場景中所有網格Mesh的集合),mMaterials(網格的材質屬性)。
- Node節點。場景的根節點(Root Node, mRootNode指向)包含了子節點,同時每個節點中有一系列指向場景對象中具體mMeshes成員的索引(Scene下的mMeshes數組儲存了真正的Mesh對象,節點中的mMeshes數組保存的只是場景中網格數組的索引)。此外,這個子結構讓我們可以使用遞歸的方式來處理節點(稍后講到)。
- Mesh網格,一個Mesh 網格是可以看做渲染的一個單位,它包含了頂點數據,法線,紋理坐標(只是坐標數據,不是紋理對象),面等數組,以及材質索引(指向Scene中的mMaterials)
- Face面,面數組由面組成(廢話),概念上,一個面表示的是物體的渲染圖元(primitive)(三角形,方形,點)。在這里,我們則讓它包含了組成圖元的頂點索引(也就是指向Mesh里面的頂點數據啦),從而我們可以使用EBO來繪制圖形。
- 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,首先將在着色器里面紋理采樣器(sampler2D)的值設置為對應的紋理單元值(0-15),然后激活一個對應的紋理單元,接着我們將要綁定的紋理綁定(glBindTexture())起來。上面Draw函數干的就是這事兒,並且最后根據索引(EBO),當前激活的着色器等繪制圖元,
Model類下一節講。