我最喜欢的英文单词之一是 research,拆开来 research = re-search,字面意思"重新搜寻",隐含的意思是,研究不是探索新领域,而是故地重寻。好比在海滩寻找珍珠,未必要常常换地方,而是反复翻拣那些已经被自己或者别人翻开过多遍的石头。
昨天记下 Hello Triangle 的笔记后,重新翻阅 ES 2.0 spec §2.8, Vertex Arrays,注意到一些之前忽视的字眼:
Vertex data is placed into arrays stored in the client's address space ... The command
void VertexAttribPointer(...)
describes the locations and organizations of these arrays. ...pointer
specifies the location in memory of the first value of the first element of the array being specified.
The commandvoid DrawArrays(...)
constructs a sequence of geometric primitives by successively transferring elements first through first + count − 1 of each enabled array to the GL.
Vertex attribute array(例如,包含顶点坐标)存在于应用程序/CPU 地址空间,它被复制/传送到 GPU 的时机,不是在 VertexAttribPointer()
时,而是在 DrawArrays()
时。我重新调整了程序结构,把 attribute array 的 setup 从 render()
挪到了 render_init()
:
C
static void render_init()
{
GLuint prog;
if (!load_program("vertex_shader.glsl.c", "frag_shader.glsl.c", &prog))
{
...
}
render_state.program = prog;
// Vertex attributes wont be copied to GPU until `glDrawArrays()` is called
// therefore the vertices array is allocated in static space
static const float vertices[] = {
-.5f, -.5f, //
.5f, -.5f, //
0, .5f, //
};
GLint attr_index = glGetAttribLocation(prog, "a_pos");
glEnableVertexAttribArray(attr_index);
glVertexAttribPointer(attr_index, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glClearColor(.2f, .3f, .3f, 1.f);
glUseProgram(prog);
}
static void render()
{
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
特别需要注意第 13 行的 static
修饰符!可以试试去掉 static
,程序跑起来后,倒没如我预料地崩溃,只是窗口绘制区域一片空白。
洁净精微,OpenGL API 者也!!
VBO
如上所述,vertex attribute 数据可以保存在 CPU 地址空间内,在进行 GL 渲染(例如调用 DrawArrays()
)时复制到 GPU。ES 2.0 提供了另一种机制,将 vertex attribute 直接保存到 GPU 存储空间内,避免在 GL 渲染时的数据复制。这一机制称为 Vertex Buffer Objects(VBO)。
在上面代码的基础上修改,这里再次贴出完整代码以便对比:
C
static void render_init()
{
GLuint prog;
if (!load_program("vertex_shader.glsl.c", "frag_shader.glsl.c", &prog))
{
...
}
render_state.program = prog;
const float vertices[] = {
-.5f, .5f, //
-.5f, -.5f, //
.5f, -.5f, //
-.5f, .5f, //
.5f, -.5f, //
.5f, .5f, //
};
GLuint vbo;
glGenBuffers(1, &vbo);
// As soon as `glBufferData()` is called vertex data is copied to VBOs
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
GLint attr_index = glGetAttribLocation(prog, "a_pos");
glEnableVertexAttribArray(attr_index);
glVertexAttribPointer(attr_index, 2, GL_FLOAT, GL_FALSE, 0, 0);
glClearColor(.2f, .3f, .3f, 1.f);
glUseProgram(prog);
}
static void render()
{
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
首先,这里绘制了 2 个三角形组成一个正方形,共 6 个顶点。
VBO 的设置过程是,
- 调用
glGenBuffers()
创建 VBO 对象(代码第 21 行) - 调用
glBindBuffer()
将 VBO 对象绑定到 vertex attribute(第 24 行) - 调用
glBufferData()
将顶点数据复制到 VBO(第 25 行)
这几个函数的参数查询文档,这里不赘述。
这里强调与 vertex attribute array 方式相区别的地方。
第一,Vertex attribute array 数据通过 glVertexAttribPointer()
函数"绑定"到 vertex attribute,数据直到进行 GL 渲染时才复制到 GPU。相比之下,使用 VBO 时,通过 glBufferData()
函数调用,即刻将数据复制进 GPU。
故此,使用 VBO 时,将数据复制到 VBO/GPU 后,应用程序/CPU 空间内的原数据不再被需要。因此,上面代码中 vertices
数组并未定义为 static
(第 11 行),而是分配在 render_init()
的栈上(自动变量)。在 render_init()
返回后,vertices
数组的内存空间被回收,但这不会造成问题,因为 vertex attribute 数据已经复制进了 VBO。
第二,使用 VBO 时,依然需要调用 glVertexAttribPointer()
告诉 OpenGL 关于 vertex attribute 的数据布局,如数据类型等等。但这个函数的最后一个参数 pointer
不再是指向实际数据的指针,而是表示 vertex attribute 的起始数据偏移量,单位为字节。见代码第 29 行。
程序运行效果如下图:
ELEMENT_ARRAY_BUFFER
VBO
上一段示例代码中绘制了 2 个三角形,这两个三角形有 2 个顶点重合(坐标相等)。OpenGL 提供了另一种 VBO,可以消除 attribute array 中的重复值。其基本思路是,attribute array 中保存唯一(无重复)的顶点属性值,如顶点坐标;与此同时,用另一个数组提供要绘制的几何体的顶点,这个数组的每个元素是一个索引,指向 attribute array 中的一项。
对于前面由 2 个共边的三角形构成的正方形,共有 4 个顶点,而绘制 2 个三角形则共需要 6 个顶点:
C
const float vertices[] = {
-.5f, .5f, // 0
-.5f, -.5f, // 1
.5f, -.5f, // 2
.5f, .5f, // 3
};
const GLushort indices[] = {
0, 1, 2, //
0, 2, 3, //
};
OpenGL 将数组中的构成一个顶点的数据合称为一个 element。顶点索引数组称为 element array。Element array 也可以复制到 VBO 中,其设置方式与 vertex array VBO 类似,区别只在 glBindBuffer()
和 glBufferData()
的 target
参数值要指定为 GL_ELEMENT_ARRAY_BUFFER
(Vertex array 则为GL_ARRAY_BUFFER
):
C
GLuint array_vbo, elem_vbo;
{
GLuint a1[2];
glGenBuffers(2, a1);
array_vbo = a1[0];
elem_vbo = a1[1];
}
glBindBuffer(GL_ARRAY_BUFFER, array_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elem_vbo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
使用 element array VBO 时,绘制图形的 GL 函数要改成 glDrawElements()
:
C
static void render()
{
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
}
运行效果与前面一样。完整代码见 gitlab.com/sihokk/lear... 。