【OpenGL】其它绘制方式

顶点缓冲对象 VBO 保存缓冲区中的顶点数据,顶点数组对象 VAO 保存顶点属性,索引缓冲对象 EBO 保存顶点属性的索引。虽然不使用 VAO 也可以正常绘制,但出于规范还是手动管理 VAO 比较好,使用 EBO 来绘制图元可以节省顶点开销,EBO 关联 VAO,VAO 再关联 VBO。

顶点数组对象

统计一下到目前为止我们用到的技术,使用 VBO 管理缓冲区中的顶点数据,使用 shader 设置渲染过程,使用 glVertexAttribPointer 链接顶点属性与顶点数据的对应关系。虽然能正常渲染,但我们少了一步,就是使用顶点数组对象 VAO。

VAO 叫顶点数组对象,但它其实保存的是顶点属性,准确的叫法应该是顶点属性对象。顶点缓冲对象 VBO 管理数据,而”顶点属性对象“(非官方叫法)管理属性,然后把 VAO 与 VBO 对应起来,让 VAO 取到 VBO 的数据,之后使用 VAO 进行渲染。如何把 VAO 与 VBO 联系起来,就是前面使用的链接顶点属性指针,因为 VAO 管理的就是顶点属性。

我们前面没有使用 VAO 为什么也能正常渲染,按网上的说法是有一个默认的 VAO,亦有说法是使用 VAO 之外的全局状态。OpenGL 是基于状态来渲染的,无论哪种说法,都说明顶点属性一定是以状态的方式存在的,而不是单独存在的。虽然不使用 VAO 也能渲染,但建议还是使用 VAO;一方面这是官方要求的,有些 OpenGL 版本不使用 VAO 可能会报错;另一方面,自己来管理 VAO 更符合 OpenGL 一切皆对象的标准,否则顶点属性看起来好像不存在一样。

使用顶点数组对象之后,顶点属性就和顶点数据一样,使用一个对象来管理。和 VBO 一样,VAO 也要经过创建对象,绑定对象和填充数据三步。

1
2
3
4
5
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GL_FLOAT), 0);
glBindVertexArray(0);

VAO 的填充数据就是绑定顶点属性指针,即指定顶点属性的值从哪里得到,进行这一步之前需要保证已经有 VBO 绑定到顶点缓冲,因为顶点属性是从顶点缓冲取值的。

绑定 VAO 之后,绘制时就不需要再链接顶点属性了,直接绑定一个 VAO 即可。glBindVertexArray(0) 用于解绑 VAO,如果不解绑,则绘制时就不需要再次绑定了;但解绑是一个好习惯,特别是有多个对象的时候,使用的时候绑定,使用完解绑,这样才不会出现混乱。另外,绘制之前必须开启顶点属性,开启顶点属性必须在绑定 VAO 之后,因为任何对顶点属性的操作都是基于 VAO,就像任何针对顶点数据的操作都是基于 VBO 一样。如果没有绑定一个 VAO,则对顶点属性的任何操作都是无效的,之前之所以有用,是因为OpenGL 默认绑定了一个 VAO,而现在我们已经绑定然后手动解绑了,则不会有默认绑定的 VAO 了。

使用 VAO 和 VBO 的完整程序如下。

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
#include "Shader.h"

Shader shader;
GLuint VBO;
GLuint VAO;

void init();
void render();

int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

glutInitWindowPosition(400, 400);
glutInitWindowSize(400, 400);
glutCreateWindow(argv[0]);

if (GLEW_OK != glewInit())
{
std::cout << "glew init failded!" << std::endl;
return 1;
}
std::cout << (char*)glGetString(GL_VERSION) << std::endl;

init();
glClearColor(0.2f, 0.3f, 0.5f, 1.0f);
glutDisplayFunc(render);
glutMainLoop();

return 0;
}

void init()
{
//初始化 VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
GLfloat vertices[][3] = {
{ -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
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GL_FLOAT), 0);
glBindVertexArray(0);

//初始化 Shader
shader.init("vertex.vs", "fragment.fs");
}

void render()
{
glClear(GL_COLOR_BUFFER_BIT);

//使用 shader
shader.use();

//绑定 VAO
glBindVertexArray(VAO);

//开始绘制
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);

glutSwapBuffers();
}

使用 VAO 还有一个好处就是我们可以使用多个 VAO 和 多个 VBO 进行灵活组合;比如有两个模型,一个模型对应一个 VAO,在特定时候绑定第一个 VAO 就只绘制第一个模型,绑定第二个 VAO 则只绘制第二模型。下面的例子我们使用两个 VAO 来绘制两个三角形,一个 VAO 对应一个 VBO,然后使用按键事件来切换绘制。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include "Shader.h"

Shader shader;
GLuint VBO;
GLuint VBO2;
GLuint VAO;
GLuint VAO2;
GLuint currentVAO;

void init();
void render();
void onKeyDown(unsigned char key, GLint x, GLint y);

int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

glutInitWindowPosition(400, 400);
glutInitWindowSize(400, 400);
glutCreateWindow(argv[0]);

if (GLEW_OK != glewInit())
{
std::cout << "glew init failded!" << std::endl;
return 1;
}
std::cout << (char*)glGetString(GL_VERSION) << std::endl;

init();
glClearColor(0.2f, 0.3f, 0.5f, 1.0f);
glutDisplayFunc(render);
glutKeyboardFunc(onKeyDown);
glutMainLoop();

return 0;
}

void init()
{
GLfloat vertices[][3] = {
{ -0.5f, -0.5f, 0.0f },
{ 0.5f, -0.5f, 0.0f },
{ 0.0f, 0.5f, 0.0f }
};
GLfloat vertices2[][3] = {
{ 0.0f, 0.0f, 0.0f },
{ 0.5f, 0.0f, 0.0f },
{ 0.25f, 0.5f, 0.0f }
};

//初始化 VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glGenBuffers(1, &VBO2);
glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2), vertices2, GL_STATIC_DRAW);

//初始化 VAO
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GL_FLOAT), 0);

glGenVertexArrays(2, &VAO2);
glBindVertexArray(VAO2);
glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GL_FLOAT), 0);
currentVAO = VAO;

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

//初始化 Shader
shader.init("vertex.vs", "fragment.fs");
}

void render()
{
glClear(GL_COLOR_BUFFER_BIT);

//使用 shader
shader.use();

//绑定 VAO
glBindVertexArray(currentVAO);

//开始绘制
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);

glutSwapBuffers();
}

void onKeyDown(unsigned char key, GLint x, GLint y)
{
if (key == 'w')
{
currentVAO = VAO;
glutPostRedisplay();
}
else if (key == 's')
{
currentVAO = VAO2;
glutPostRedisplay();
}
}

这个例子只是演示如何使用多个 VAO 或 VBO 而已,事实上这个程序的功能并不需要两个 VAO 和 VBO,只需要在 VBO 中保存六个顶点数据,然后绘制的时候选择绘制前三个顶点还是后三个顶点即可。

索引缓冲对象

到目前为止,我们绘制图元都是直接取顶点属性进行绘制,顶点属性再从顶点缓冲取数据;还有一种方式就是给每个顶点一个索引,然后绘制时取顶点索引,再根据索引取到顶点属性,再从顶点缓冲取数据。索引缓冲对象的名称是 EBO(Element Buffer Object)IBO(Index Buffer Object)

EBO,VBO,VAO 三者的关系如下。

image

使用 EBO 的一个好处就是可以节省开销,比如我们要绘制一个矩形,就必须绘制两个三角形(OpenGL 主要处理三角形,事实上在计算机领域三角形是最基本的图元),因此需要六个顶点,但事实上有两个顶点是重复的,如果我们直接使用顶点数据,就不得不定义重复的顶点数据。

使用 EBO 和 VBO,VAO 一样,也是三步曲,创建对象,绑定对象,填充数据。

1
2
3
GLuint EBO;
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

然后在绘制的时候使用 glDrawElements 来替代 glDrawArrays。

1
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (GLvoid*)0);
  • 第一个参数是绘制图元的类型;
  • 第二个参数是要绘制的顶点数;
  • 第三个参数是索引的类型;
  • 最后一个参数是索引的偏移值。

    如果没把索引保存到 EBO,则这个参数则传索引数组,GL_ELEMENT_ARRAY_BUFFER 没有绑定 EBO 对象时,这个索引数组才起作用。

还有一个要注意的地方是 EBO,VBO,VAO 三者的绑定顺序。

  • 填充 EBO 数据时需要用到顶点属性,所以 VAO 要在填充 EBO 数据之前绑定。
  • 链接顶点属性需要用到顶点缓冲,所以 VBO 要在链接顶点属性之前绑定。

绘制矩形

我们使用索引缓冲对象来绘制一个矩形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//创建 VAO VBO EBO
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glGenVertexArrays(1, &VAO);

//绑定 VAO
glBindVertexArray(VAO);
//绑定 VBO 并填充数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//链接顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
//绑定 EBO 并填充数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

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

顶点数据和索引数据:

1
2
3
4
5
6
7
8
9
10
11
12
GLfloat vertices[][3] =
{
{ -0.5f, -0.5f, 0.0f },
{ 0.5f, -0.5f, 0.0f },
{ 0.5f, 0.5f, 0.0f },
{ -0.5f, 0.5f, 0.0f }
};
GLuint indices[][3] =
{
{ 0, 1, 2 },
{ 0, 2, 3 }
};

绘制矩形

可以使用 glPolygonMode 来设置绘制方式为线框模式,更容易看出这是由两个三角形组成的。

1
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

如果要设置回填充模式,将参数设置回 GL_FILL 即可。

线框模式