OpenGL 模型加载详解
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。这样是为了保持其与系统默认行为一致,不至于引起额外变量。