早期的OpenGL为了将模型的顶点数据传送到显卡,需要逐个顶点进行(冗余处理的问题),如果还需要额外的信息(纹理坐标和法线)的话,当模型比较复杂时,将导致大量函数的调用,传输开销是相当大的!为了解决这个问题引入了VBO(Vertex Buffer Object),VBO可以将顶点数据保存在显存中,绘制时直接从显存中取数据,减少了数据传输的开销。
顶点属性(Vertex Attribute),是关于顶点坐标和顶点纹理、顶点法线以及其他信息的统称。
顶点的格式:
typedef struct { float x; float y; float z; }Vec3; typedef struct { float x; float y; } typedef struct { Vec v; Vec2 vt; Vec3 vn; }Vertex;
从以上可知,通过VBO我们可以将顶点属性数据保存在显存中,当绘制时问题又来了,需要调用好几个函数,过程挺复杂的。为了解决这个问题,OpenGL又引入了VAO(Vertex Array Object)来关联VBO中的数据 (VAO详解0,VAO详解1),有了VAO,任何数组形式的GL函数调用都会添加到VAO的绘制列表当中(直到解除VAO绑定),当需要绘制的时候,我们仅需要重新绑定VAO,那么之前创建的绘制列表将会重新激活,使得绘制代码更加简洁。
举个例子:假设顶点着色器中定义了以下三个变量,用于保存顶点属性
attribute vec3 vertex_position; attribute vec2 vertex_texture_coord; attribute vec3 vertex_normal;
在C++代码中定义一个init函数,用于加载shader、模型、纹理,以及最重要的创建VBO和VAO和关联它们
// 初始化 void init() { pid = load_shader_program("vertex.glsl", "fragment.glsl"); // 获取vertex_position、vertex_texture_coord和vertex_nromal的位置 GLuint vertex_position_loc = glGetAttribLocation(pid, "vertex_position"); GLuint vertex_texture_coord_loc = glGetAttribLocation(pid, "vertex_texture_coord"); GLuint vertex_normal_loc = glGetAttribLocation(pid, "vertex_normal"); // 加载纹理 texture_id = create_texture_2d("texture.bmp"); // 加载模型 loadModel("cube.obj", vertices); // 设置相机初始位置 camera.set_position(0.0f, 0.0f, 2.0f); // 创建VBO GLuint vbo_id; glGenBuffers(1, &vbo_id); glBindBuffer(GL_ARRAY_BUFFER, vbo_id); // 传输数据 glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * vertices.size(), &vertices[0], GL_STATIC_DRAW); // 创建VAO GLuint vao_id; glGenVertexArrays(1, &vao_id); glBindVertexArray(vao_id); // 启用顶点属性 glEnableVertexAttribArray(vertex_position_loc); glEnableVertexAttribArray(vertex_texture_coord_loc); glEnableVertexAttribArray(vertex_normal_loc); glBindBuffer(GL_ARRAY_BUFFER, vbo_id); // vertex_position_loc与顶点数据映射 glVertexAttribPointer(vertex_position_loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0); // vertex_texture_coord_loc与纹理数据映射 glVertexAttribPointer(vertex_texture_coord_loc, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (0+sizeof(Vec3))); // vertex_normal_loc与法线数据映射 glVertexAttribPointer(vertex_normal_loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)(0+sizeof(Vec3) + sizeof(Vec2))); }
glBufferData用于向VBO传递数据:第二个参数表示数据的大小,第三个参数表示指向数据的指针;
glVertexAttribPointer用于将顶点着色器中的attribute变量与VBO中的数据进行绑定:第一个参数表示属性的位置,第二个参数表示分量的个数,第三个参数表示数据类型,第四个参数表示是否归一化,第五个参数表示连续顶点属性之间的偏移量,第六个参数表示组件的第一个分量在对应的数组顶点属性中的偏移量
例子中VBO存储的数据格式如下:
VBO = {v1.x, v1.y, v1.z, vt1.x, vt1.y, vt1.z, vn1.x, vn1.y, vn1.z, v2.x, v2.y, v2.z, vt2.x, vt2.y, vt2.z, vn2.x, vn2.y, vn2.z, ... ... vn.x, vn.y, vn.z, vtn.x, vtn.y, vtn.z, vnn.x, vnn.y, vnn.z,}
以取到法线数据为例,法线数据是vn1.xyz到vnn.xyz,从VBO的格式可知,vn1.x在VBO的 0+sizeof(Vec3)+sizeof(Vec2)位置,vn1.x到vn2.x之间相差了sizeof(Vertex)大小,这就是glVertexAttribPointer第五和第六个参数的由来。
最后绘制的时候,我们只需要调用以下代码即可
glBindVertexArray(vao_id); glDrawArrays(GL_QUADS, 0, (GLsizei)vertices.size());