1. Assimp
目前為止,我們已經可以繪製一個物體,並新增不同的光照效果了。但是我們的頂點資料太過簡單,只能繪製簡單的立方體。但是房子汽車這種不規則的形狀我們的頂點資料就很難定製了。索性,這部分並不需要我們苦逼的開發人員去考慮。成熟的3D建模工具可以將設計師設計的模型匯出模型檔案,藉助模型載入庫就可以將他們轉化為頂點資料。
2. 模型載入庫
一個非常流行的模型匯入庫是Assimp,它是Open Asset Import Library(開放的資產匯入庫)的縮寫。Assimp能夠匯入很多種不同的模型檔案格式(並也能夠匯出部分的格式),它會將所有的模型資料載入至Assimp的通用資料結構中。當Assimp載入完模型之後,我們就能夠從Assimp的資料結構中提取我們所需的所有資料了。由於Assimp的資料結構保持不變,不論匯入的是什麼種類的檔案格式,它都能夠將我們從這些不同的檔案格式中抽象出來,用同一種方式訪問我們需要的資料。
當使用Assimp匯入一個模型的時候,它通常會將整個模型載入進一個場景(Scene)物件,它會包含匯入的模型/場景中的所有資料。Assimp會將場景載入為一系列的節點(Node),每個節點包含了場景物件中所儲存資料的索引,每個節點都可以有任意數量的子節點。Assimp資料結構的(簡化)模型如下:
這個結構圖是一個模型在assimp中的基礎結構,如果看不懂也沒關係,到後面我們會頻繁的使用它。
3. 網格
在上一節中,我們知道了Assimp中的基本單元式Mesh或者Model。這一節中我們就先定義一個自己的Mesh類。
4. Mesh
Mesh應該作為一個最基本的繪製單元,那麼他應該自己維護VAO、VBO、EBO這些資料。並且他應該具備自動繫結資料及定義針對自身的Mesh自動設定資料格式等功能。
之前我們都是一個float陣列來表示,這是因為GL在繫結VAO的時候需要的是一個連續的記憶體,我們透過指定資料其實地址和資料長度就可以告訴GL如何去繫結資料。但是陣列看起來並不是一個直觀的形式,我們希望能找到一個更加明瞭的形式來方便我們檢視資料。慶幸的是,結構體中的記憶體地址是連續的。我們將陣列中的數值替換成結構體,這樣我們可以清楚地區分出不同頂點。
此外,陣列我們也可以進一步簡化一下:陣列的長度是一個定長,需要在一開始就指定陣列長度,而且陣列的元素個數也需要計算才可以獲得。c++中提供了一個很好的擴充套件就是向量vector。
這裡我們直接放一下Mesh類的程式碼,注意現在我們還沒有將他寫成一個通用的Mesh類,而是針對當前箱子模型的片段著色器寫的一個Mesh類。
1 #ifndef Mesh_h 2 #define Mesh_h 3 4 ///system framework 5 #include <vector> 6 7 ///third party framework 8 #include <glm/glm.hpp> 9 #include <glm/gtc/matrix_transform.hpp> 10 #include <glm/gtc/type_ptr.hpp> 11 12 ///custom framework 13 #include "Shader.h" 14 15 using namespace std; 16 17 struct Mesh_Vertex { 18 glm::vec3 Position; 19 glm::vec3 Normal; 20 glm::vec2 TexCoords; 21 }; 22 23 ///紋理結構體(標明已經載入的紋理的紋理ID及紋理對應型別) 24 struct Mesh_Texture { 25 unsigned int t_id; 26 string type; 27 }; 28 29 class Mesh { 30 public: 31 32 vector<Mesh_Vertex> vertices; 33 vector<unsigned int> indices; 34 vector<Mesh_Texture> textures; 35 unsigned int VAO; 36 37 Mesh(vector<Mesh_Vertex> aVertices, vector<unsigned int> aIndices, vector<Mesh_Texture> aTextures) { 38 vertices = aVertices; 39 indices = aIndices; 40 textures = aTextures; 41 setupMesh(); 42 } 43 44 Mesh(vector<Mesh_Vertex> aVertices, vector<unsigned int> aIndices) { 45 vertices = aVertices; 46 indices = aIndices; 47 setupMesh(); 48 } 49 50 Mesh() { 51 52 } 53 54 void Draw(Shader shader) { 55 for (int i = 0; i < textures.size(); ++i) { 56 ///首先啟用指定位置的紋理單元 57 glActiveTexture(GL_TEXTURE0 + i); 58 string name; 59 string type = textures[i].type; 60 if (type == "diffuse") { 61 name = "material.diffuse"; 62 } else if (type == "specular") { 63 name = "material.specular"; 64 } 65 shader.setInt(name, i); 66 glBindTexture(GL_TEXTURE_2D,textures[i].t_id); 67 } 68 DrawWithoutConfigImage(); 69 ///結束頂點陣列物件的繫結 70 glBindVertexArray(0); 71 glActiveTexture(GL_TEXTURE0); 72 } 73 74 void DrawWithoutConfigImage() { 75 glBindVertexArray(VAO); 76 glDrawElements(GL_TRIANGLES, (int)indices.size(), GL_UNSIGNED_INT, 0); 77 } 78 79 void ReleaseMesh() { 80 ///釋放物件 81 glDeleteVertexArrays(1, &VAO); 82 glDeleteBuffers(1, &VBO); 83 glDeleteBuffers(1, &EBO); 84 } 85 86 private: 87 unsigned int VBO,EBO; 88 89 void setupMesh(){ 90 glGenVertexArrays(1,&VAO); 91 glGenBuffers(1,&VBO); 92 glGenBuffers(1,&EBO); 93 glBindVertexArray(VAO); 94 glBindBuffer(GL_ARRAY_BUFFER,VBO); 95 glBufferData(GL_ARRAY_BUFFER,vertices.size() * sizeof(Mesh_Vertex),&vertices[0],GL_STATIC_DRAW); 96 glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Mesh_Vertex),(void *)(offsetof(Mesh_Vertex, Position))); 97 glEnableVertexAttribArray(0); 98 glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Mesh_Vertex),(void *)(offsetof(Mesh_Vertex, Normal))); 99 glEnableVertexAttribArray(1); 100 glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,sizeof(Mesh_Vertex),(void *)(offsetof(Mesh_Vertex, TexCoords))); 101 glEnableVertexAttribArray(2); 102 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 103 glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); 104 glBindVertexArray(0); 105 glBindBuffer(GL_ARRAY_BUFFER, 0); 106 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0); 107 } 108 }; 109 110 #endif
我們看到我們只是將原來在main.mm中的資料繫結過程移到了Mesh類中,其他的地方基本沒有什麼變化。
5. 通用Mesh類
觀察我們上面的程式碼,我們唯一不通用的地方就是紋理繫結的時候。如果想通用,就要求我們的片段著色器中的紋理命名應該是可以用一個通式表達出來的形式。如:
那麼如果是這樣的,我們的繫結部分就可以改造成這個樣子:
這裡還是簡單的解釋一下我們的Mesh類工作的流程。
- 1.初始化時傳入頂點資料、索引資料、紋理資料(這裡我們確定了繪製什麼、如何繪製的問題)。
- 2.自動繫結VAO、EBO。獲取到可複用的模型物件。
- 3.繪製時每次都重新繫結GL當前啟用的紋理單元,並按照索引繪製模型。
幾個可以重點解釋的地方:
- 1.傳入Mesh類的實際為已經提交給GL的紋理的ID。在外界的時候我們載入影像後,GL中即已存在該紋理的一份複製,我們可以透過GL返回給我們的ID找到對應的資料。在想要使用的時候只要將指定位置的紋理單元啟用後將對應的ID繫結在該紋理單元上即可讓啟用的紋理單元上的資料指向指定紋理資料,而後再將片段著色器中紋理繫結為指定紋理單元即可。
- 2.GL中可用的紋理單元是有限的,故而我們要反覆使用紋理單元,所以在每次使用前應重新繫結紋理紋理資料。當然這是相對的,如果你使用的紋理單元足夠少而不用複用的話,你也可以只繫結一次。具體還是要視情況而定。
- 3.在每一次Mesh繪製完畢後,我們要記得恢復當前啟用的紋理位置為GL_TEXTURE0。這樣是為了保持其與系統預設行為一致,不至於引起額外變數。