在OpenGL(一) OpenGL管线 与 可编程管线流程中,提到加载VBO、IBO的相关技术,本篇详细说一下。实际应用时,我们是不可能手写顶点和索引点。通常模型是使用3dMax或Maya制作,然后在OpenGL程序中 加载模型 。本文着重分析这些文件的格式以及 加载模型 的流程和方法。
大体流程
加载模型 的主要流程是:
- 读取模型文件内容
- 解析 vbo(vertex buffer object) 和 ibo(index buffer object) 信息。其中vbo包括顶点的位置、纹理坐标、法线。多个vbo可以打包成一个vao,具体vao的事情后面会有文章单独讨论,这里可以将其理解为是同样的概念。ibo主要是面对应的顶点围成面序列数组、顶点数,索引数。
- 将vbo和ibo传给GPU,根据shader绘制对应图形。
模型存储结构
在了解如何 加载模型 之前,先要明确模型文件的存储方式。在众多的格式中以obj格式比较通用,它内部是以文本形式表达的。以最简单的obj模型为例,一个Quad的信息可能是这样:
# This file uses centimeters as units for non-parametric coordinates. mtllib Quad.mtl g default v -0.500000 -0.500000 0.000000 v 0.500000 -0.500000 0.000000 v -0.500000 0.500000 0.000000 v 0.500000 0.500000 0.000000 vt 0.000000 0.000000 vt 1.000000 0.000000 vt 0.000000 1.000000 vt 1.000000 1.000000 vn 0.000000 0.000000 1.000000 vn 0.000000 0.000000 1.000000 vn 0.000000 0.000000 1.000000 vn 0.000000 0.000000 1.000000 s 1 g Quad usemtl initialShadingGroup f 1/1/1 2/2/2 3/3/3 f 3/3/3 2/2/2 4/4/4
简单解释一下文件的含义:
- 以#开始的行为注释行
- usemtl和mtllib表示的材质相关数据。
- o 引入一个新的object
- v 表示顶点位置
- vt 表示顶点纹理坐标
- vn 表示顶点法向量
- f 表示一个面,面使用1/2/8这样格式,表示顶点位置/纹理坐标/法向量的索引,这里索引的是前面用v,vt,vn定义的数据 注意这里Obj的索引是从1开始的,而不是0。
通过二进制读取文件的方式,我们可以很容易的读取到顶点信息,并将其保存到内存结构中。这里有个要注意的地方,就是顶点是动态组建出来的。例如这种面:
f 5/5/9 6/6/10 7/7/11
是可能由少数顶点信息组建出的面。因此顶点索引需要动态生成,而不仅仅是看顶点信息的个数。换句话说,顶点索引是由面信息决定的。因此,为了保证ibo与vbo的对应性,在组建vbo数组时,应该从组成的面入手,按照顺序动态创建顶点,或记录已存在顶点的索引,生成对应的ibo,以保证最大限度的复用顶点。与此同时,还应该记录vbo和ibo的数量,其中vbo的数量用于计算生成的内存大小,ibo数量用于绘制时计算面的构成。
数据解析
绘制一个Mesh的时候,最好构建一个类结构,一个顶点的结构体可能是这样:
struct Vertex { glm::vec3 Position; glm::vec3 Normal; glm::vec2 TexCoords; }; class Mesh { Public: vector<Vertex> vertices; vector<GLuint> indices; vector<Texture> textures; Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> texture); Void Draw(Shader shader); private: GLuint VAO, VBO, IBO; void setupMesh(); public : Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures) { this->vertices = vertices; this->indices = indices; this->textures = textures; this->setupMesh(); } private: void setupMesh() { glGenVertexArrays(1, &this->VAO); glGenBuffers(1, &this->VBO); glGenBuffers(1, &this->IBO); 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->IBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint), &this->indices[0], GL_STATIC_DRAW); // 设置顶点坐标指针 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)0); // 设置法线指针 glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),(GLvoid*)offsetof(Vertex, Normal)); // 设置顶点的纹理坐标 glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),(GLvoid*)offsetof(Vertex, TexCoords)); glBindVertexArray(0); } }
辅助库实现
现在市面上有一个很流行的模型加载库,叫做Assimp
(Open Asset Import Library)。Assimp可以导入几十种不同格式的模型文件(同样也可以导出部分模型格式)。你可以在官网下载。我们可以对其做一些封装,来辅助我们 加载模型
#include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> void loadModel(string path) { Assimp::Importer import; const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); if(!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl; return; } this->processNode(scene->mRootNode, scene); } void processNode(aiNode* node, const aiScene* scene) { // 添加当前节点中的所有Mesh for(GLuint i = 0; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; this->processMesh(mesh, scene); } // 递归处理该节点的子孙节点 for(GLuint i = 0; i < node->mNumChildren; i++) { this->processNode(node->mChildren[i], scene); } } Mesh processMesh(aiMesh* mesh, const aiScene* scene) { vector<Vertex> vertices; vector<GLuint> indices; vector<Texture> textures; for(GLuint i = 0; i < mesh->mNumVertices; i++) { glm::vec3 vector; vector.x = mesh->mVertices[i].x; vector.y = mesh->mVertices[i].y; vector.z = mesh->mVertices[i].z; vertex.Position = vector; vertices.push_back(vertex); } for(GLuint i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; for(GLuint j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); } if(mesh->mTextureCoords[0]) { glm::vec2 vec; vec.x = mesh->mTextureCoords[0][i].x; vec.y = mesh->mTextureCoords[0][i].y; vertex.TexCoords = vec; } else vertex.TexCoords = glm::vec2(0.0f, 0.0f); return Mesh(vertices, indices, textures); }
通过这样的实现可以构建 加载模型 到OpenGL中。更详细的实现可以参考这里。
GPU绘制
这部分与绘制一个普通的三角形就没有区别了,从内存中读取vbo或vao,并且读取ibo,上传到GPU中,根据上一步获得的ibo数量进行绘制。代码可以简化为:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,this->VAO); glDrawElements(GL_TRIANGLES,this->indices,GL_UNSIGNED_INT,0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
其对应的shader为:
attribute vec3 pos; attribute vec2 texcoord; attribute vec3 normal; uniform mat4 M; uniform mat4 V; uniform mat4 P; varying vec4 V_Color; void main() { gl_Position=P*V*M*vec4(pos,1.0); }
总结
通过以上这些步骤,就可以将一个模型文件加载并绘制出来。建议将其封装起来,因为这部分逻辑很常用,而且没有经常改动的必要。
关注我的微信公众号,获取更多优质内容