VBO、VAO和EBO
阅读原文时间:2021年05月15日阅读:1

对于经历过fixed pipeline的我来讲,VBO的出现对于渲染性能提升让人记忆深刻。完了,暴露年龄了~

//immediate mode
glBegin(GL_TRIANGLES);
    glNormal3f(...);
    glVertex3f(...);
glEnd();

//display list
list = glGenLists(1);
glNewList(list, GL_COMPILE);
    glBegin(GL_TRIANGLES);
        glNormal3f(...);
        glVertex3f(...);
    glEnd();
glEndList();

glCallList(list);

//vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(...);
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(...);
glDrawArrays(...);

上面的代码是远古时期的OpenGL绘制图元的执行流程,不懂也不用追究了,因为实在太老了。

接下来我们进入正题。

VBO标识的是显卡中的一块存储区域,我们可以从内存中向它传送顶点数据(空间位置,纹理坐标,法线等等),然后在draw的时候作为vertex attribute进行使用。

void init()
{
    GLfloat position[] =        //空间位置
    {
        -0.8f, -0.8f, 0.0f,
         0.8f, -0.8f, 0.0f,
         0.0f,  0.8f, 0.0f
    };
    GLfloat color[] =            //颜色
    {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };

    GLuint vbo[2] = {0};
    glCreateBuffers(2, vbo);                //创建buffer对象
    //把buffer object绑定到GL_ARRAY_BUFFER(binding target),表示这个buffer object用来存放vertex attributes,现在它就是一个VBO啦
    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
    glBindBuffer(GL_ARRAY_BUFFER, 0);//为GL_ARRAY_BUFFER绑定一个无效的对象,防止后续的手贱操作,同时提醒你4.5版本引入DSA之后,可以在不bind的情况下直接操纵object了
    glNamedBufferData(vbo[0], 9 * sizeof(GLfloat), position, GL_STATIC_DRAW);        //向vertex buffer上传数据
    glNamedBufferData(vbo[1], 9 * sizeof(GLfloat), color, GL_STATIC_DRAW);
}

至此,我们创建好了两个VBO并分别存放了空间位置数据和颜色数据。

我们的shader仍然是最简单的shader:

//vertex shader
#version 460 core

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;

layout(location = 0) out vec3 vs_out_color;

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

//fragment shader
#version 460 core

layout(location = 0) in vec3 fs_in_color;

layout(location = 0) out vec4 frag_color;

void main(void)
{
    frag_color = vec4(fs_in_color, 1.0);
}

接下来的问题就是我们如何把VBO与vertex attribute (location) 关联起来,这时候VAO就闪亮登场了。

GLuint vao = 0;

void init()
{
    ...    //set up vbo

    glCreateVertexArrays(1, &vao);            //创建vao
    //启用vertex attribute 0 和 1,其中0和1分别与vertex shader中的position(location = 0)和color(location = 1)对应
    glEnableVertexArrayAttrib(vao, 0);
    glEnableVertexArrayAttrib(vao, 1);

    glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 3);    //设置vbo的binding point
    glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLfloat) * 3);

    glVertexArrayAttribBinding(vao, 0, 3);        //设置vertex attribute的binding point,须与对应的vbo bind到同一个binding point上
    glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0);     //指定vertex attribute的顶点规范,相当于告诉OpenGL如何解析对应的vbo数据,之后vertex shader就能够拿到正确的vertex attribute
    glVertexArrayAttribBinding(vao, 1, 5);
    glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
}

可以看到,自从4.5版本增加了DSA,API的执行顺序不是那么的重要了,因为调用OpenGL的命令需要显式的指定handle,而不是把这个handle绑定到当前的OpenGL Context(如上述代码,每次我们都传入了vao)。另外对于状态的从属关系,也更加明确了。

我们把vertex attribute和对应的vbo绑定到了同一个binding point上,相当于告诉OpenGL,vertex attribute的数据来自哪个vbo。这里我故意把binding point的值分别设置为3和5(其实可以设置为0和1),是担心有同学会把vertex attribute binding point和vertex attribute index弄混淆了。

另外需要注意的是glVertexArrayVertexBuffer最后一个参数指定了vbo中元素之间的stride,与glVertexAttribPointer不同的是,就算vbo中的元素是紧挨着的,也必须设置正确的stride值,而不能设置为0。因为在调用glVertexArrayVertexBuffer的时候,OpenGL对于vbo中的数据该如何解析丝毫不知情。而之所以你之前用到的glVertexAttribPointer的stride可以设置为0,是因为这个命令同时指定了每个元素的类型(比如GL_FLOAT)以及size(比如由3个GL_FLOAT组成),相当于OpenGL会自动帮我们去算正确的stride的值。有的同学可能会说,glVertexArrayAttribFormat不是指定了如何解析vbo中的数据吗,但是你有没有想过:glVertexArrayAttribFormat不一定在glVertexArrayAttribBinding之前调用,所以在调用glVertexArrayAttribBinding的时候OpenGL可能还不知道vbo的数据信息。

我们已经vao设置好了所有必要的信息了,现在用它进行render

void render()
{
    ...

    glBindVertexArray(vao);        //绑定vao
    glDrawArrays(GL_TRIANGLES, 0, 3 );    //draw
    glBindVertexArray(0);    //draw完成,将当前context下的Vertex Array绑定到一个无效的handle上
}

VAO先讲到这里,下面我们看一下EBO。

所谓EBO,就是把顶点索引数据保存到buffer中,然后用这些索引去vertex buffer中查找对应的顶点来绘制图元,以避免在vertex buffer中存放冗余的顶点信息。

//绘制一个矩形
void init()
{
    GLfloat position[] =
    {
        -0.5f, 0.5f,
    -0.5f, -0.5f,
        0.5f,  0.5f,
        0.5f, -0.5f,
    };
    GLfloat color[] =
    {
    1.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f,
    0.5f, 0.5f, 0.5f
    };

    GLubyte index[] =
    {
    0, 1, 3,
    2, 0, 3
    };

    glCreateBuffers(2, vbo);

    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glNamedBufferData(vbo[0], 8 * sizeof(GLfloat), position, GL_STATIC_DRAW);
    glNamedBufferData(vbo[1], 12 * sizeof(GLfloat), color, GL_STATIC_DRAW);

    glCreateBuffers(1, &ebo);        //创建buffer对象
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);    //将buffer对象当作GL_ELEMENT_ARRAY_BUFFER
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);    //为当前的OpenGL Context的EBO置为无效值
    glNamedBufferData(ebo, 6 * sizeof(GLubyte), index, GL_STATIC_DRAW);    //向element array buffer传输索引数据

    glCreateVertexArrays( 1, &vao );

    glEnableVertexArrayAttrib(vao, 0);
    glEnableVertexArrayAttrib(vao, 1);

    glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 2);
    glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLfloat) * 3);

    glVertexArrayAttribBinding(vao, 0, 3);
    glVertexArrayAttribFormat(vao, 0, 2, GL_FLOAT, GL_FALSE, 0);
    glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
    glVertexArrayAttribBinding(vao, 1, 5);
}

同学们只需要关注我加注释的那一段代码。可以发现,其实EBO的建立过程与VBO极为类似。

不过很遗憾,本来我以为可以同vbo一样,通过binding point或者其它手段建立EBO和VAO的联系,可惜没找到。所以我们在render的时候,除了要绑定VAO外,还要把用到的EBO绑定至当前的OpenGL Context。

void render()
{
    glBindVertexArray(vao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);        //绑定ebo
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (GLvoid*)(nullptr));
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

draw命令也不能用glDrawArrays,而是用glDrawElements。现在我确信不存在可以建立EBO和VAO之间联系的API了,因为glDrawElements的最后的两个参数分别表示EBO存放的数据类型和起始位置的字节偏移。如果存在这样的API,那么这两个参数的信息肯定是保存到了VAO中了(参照VBO和VAO)。

  1. VBO表示显卡中用于存放vertex attribute数据的一块缓存。
  2. VAO通过vertex attribute binding point建立vertex attribute index与VBO之间的联系,并且在render的时候,只需要绑定一个VAO即可进行draw,减少了状态切换,提升渲染性能。
  3. EBO以索引的形式对VBO中的顶点进行多次利用,但是无法建立EBO与VAO之间的联系,所以每次draw之前,需要显式的绑定EBO。