VBO, Vertex Buffer Object 顶点缓冲对象

VBO的作用

在GPU内存中存储大量顶点的对象。避免多次从CPU传递数据到GPU从而影响性能。

使用VBO

创建VBO

创建一个缓冲对象。使用glGenBuffers函数生成一个带有缓冲ID的VBO对象

1
2
3
4
unsigned int VBO;//Buffer的ID
glGenBuffers(1,&VBO);//函数原型:void glGenBuffers(GLsizei n,GLuint * buffers);
//GLsizei n —— 声明的缓冲区数量
//GLuint buffer —— 缓冲区ID的指针

绑定VBO

用于确定缓冲区的类型。可以同时绑定多个缓冲,只要它们是不同的缓冲类型。使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上。
从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。

1
2
3
4
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//函数原型:void glBindBuffer(GLenum target,GLuint buffer);
// GLenum target —— 缓冲区类型
// GLuint buffer —— 缓冲区ID

缓冲区类型有:

  • GL_ARRAY_BUFFER VBO顶点缓冲区对象
  • GL_ELEMENT_ARRAY_BUFFER IBO索引缓冲区对象
  • GL_TEXTURE_BUFFER 图片缓冲对象
  • GL_UNIFORM_BUFFER uniform缓冲区对象
  • GL_TRANSFORM_FEEDBACK_BUFFER 变换反馈缓冲区对象
  • GL_PIXEL_PACK_BUFFER和GL_PIXEL_UNPACK_BUFFER 像素缓冲区对象
  • GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER 复制缓冲区
  • GL_DRAW_INDIRECT_BUFFER

向VBO中填充数据

glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。第四个参数指定了显卡绘制指定数据的方式。有三种形式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。
1
2
3
4
5
6
7
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

VAO, Vertex Array Object 顶点数组对象

VAO的作用

顶点(Vertex)可以包含多种数据,包括但不限于顶点位置、法线、切线、顶点颜色、uv坐标等等。

VAO用于链接顶点属性,指明VBO中哪一个部分对应顶点着色器的顶点属性

如下图,VAO2中第一个AttributePointer0指向了VBO2中的顶点位置,第二个AttributePointer1指向了VBO2中的顶点颜色。AttributePointer通过数据的Stride(数据步长)访问下一个元素。
VAO与VBO的关系.png

使用VAO

理解链接顶点属性

1
2
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
  • glVertexAttribPointer
    glVertexAttribPointer函数的参数:

第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。

第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。

第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。

下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。

第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。

最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。

至于具体获取哪一个VBO的数据则由在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的

  • glEnableVertexAttribArray
    唯一一个参数index,指定了要启用的顶点属性的索引。需要和顶点着色器配合使用。
    比如:
    在OpenGL代码中将颜色属性的index设为0,位置属性的index设为1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GLuint vbo, vao;
glGenBuffers(1, &vbo);
glGenVertexArrays(1, &vao);

glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);

// 假设数据中位置和颜色已经存储
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); // 为颜色属性设置格式
glEnableVertexAttribArray(0); // 启用颜色属性

glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position)); // 设置位置属性格式
glEnableVertexAttribArray(1); // 启用位置属性

glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑
glBindVertexArray(0); // 解绑

那么在对应的VS中,需要这样写:

1
2
3
4
5
6
7
8
9
10
11
#version 330 core

layout(location = 0) in vec3 color; // 颜色(使用 location 0)
layout(location = 1) in vec3 position; // 位置

out vec3 fragColor;

void main() {
gl_Position = vec4(position, 1.0);
fragColor = color;
}

创建VAO

通常一个模型有多个属性,逐个连接就难以管理了,这时候就需要使用VAO了。

1
2
unsigned int VAO; //类似VBO的创建
glGenVertexArrays(1, &VAO); //参数:数量、ID

绑定VAO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

//glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//glEnableVertexAttribArray(1);
//...
// 解绑VAO,之后使用时再绑定相应的VAO

// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

IBO, Index Buffer Object 索引缓冲对象(EBO, Element Buffer Object, 元素缓冲对象)

IBO的作用

用于指示哪些顶点会组成图元。从而减少VBO传输的数据大小。

IBO的使用

创建IBO

同VBO的创建

1
2
unsigned int EBO;
glGenBuffers(1, &EBO);

绑定IBO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};

unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形

0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //注意target类型是GL_ELEMENT_ARRAY_BUFFER
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

这样,在绘制时调用下面的函数:

1
2
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
  • glDrawElements
    第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。
    第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。
    第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。

将IBO绑定到VAO

为了更方便的切换绘制对象,可以将IBO绑定到VAO的最后一位。
(注:在解绑VAO时要注意不要先解绑IBO,否则会导致IBO丢失)
现在VBO、VAO和IBO的关系应该如下图:

代码部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);


// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

测试

学习完了来实践一下,我们来画一个彩色的矩形。
以下是矩形类:

  • 头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>

class TestRect
{
#pragma region ModelData
unsigned int VBO;
unsigned int IBO;
unsigned int VAO;

const unsigned int indices[6] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

const float vertices[24] = {
0.5f, 0.5f, 0.0f, 1.0f,0.0f,0.0f, // 右上角
0.5f, -0.5f, 0.0f, 0.0f,1.0f,0.0f, // 右下角
-0.5f, -0.5f, 0.0f, 0.0f,0.0f,1.0f,// 左下角
-0.5f, 0.5f, 0.0f, 0.0f,1.0f,1.0f // 左上角
};
#pragma endregion

#pragma region Material&Shader
unsigned int shaderProgram;
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aCol;\n"
"out vec3 fragColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
" fragColor = aCol;"
"}\0";
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 fragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(fragColor.xyz, 1.0f);\n"
"}\0";

#pragma endregion

public:
void SetBufferObjects();

void CompileShader();

void Draw();

void Delete();
};

cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include "TestRect.h"

void TestRect::SetBufferObjects()
{
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &IBO);

glBindVertexArray(VAO);
//Bind VBO
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
//Bind EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices), indices, GL_STATIC_DRAW);
//Set VAO
//Position
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
//Vertex Color
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

void TestRect::CompileShader()
{
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader,1,&vertexShaderSource,nullptr);
glCompileShader(vertexShader);
// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}

shaderProgram = glCreateProgram();
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}

void TestRect::Draw()
{
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
//glBindVertexArray(0);
}

void TestRect::Delete()
{
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &IBO);
glDeleteProgram(shaderProgram);
}