我的OpenGL学习进阶之旅介绍顶点缓冲区对象VBO和元素数组缓冲区对象EBO,并对比使用VBO和不使用VBO绘制三角形的效果

Posted 欧阳鹏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的OpenGL学习进阶之旅介绍顶点缓冲区对象VBO和元素数组缓冲区对象EBO,并对比使用VBO和不使用VBO绘制三角形的效果相关的知识,希望对你有一定的参考价值。

在上一篇博客 【我的OpenGL学习进阶之旅】顶点属性、顶点数组 https://ouyangpeng.blog.csdn.net/article/details/121388737
中,我们介绍了 顶点属性顶点数组

这一篇博客,我们来介绍下 顶点缓冲区对象VBO元素数组缓冲区对象EBO,并对比使用VBO和不使用VBO绘制三角形的效果

一、顶点缓冲区对象

1.1 顶点缓冲区对象介绍

使用顶点数组指定的顶点数据保存在客户内存中。在进行glDrawArrays或者glDrawElements等绘图调用时,这些数据必须从客户内存复制到图形内存。

但是,如果我们没有必要在每次绘图调用时都复制顶点数据,而是在图形内存中缓存这些数据,那就好得多了。

这种方法可以显著地改进渲染性能,也会降低内存和功耗,对于手持设备相当重要。这是顶点缓冲区对象发挥作用的地方。

顶点缓冲区对象使OpenGL ES 3.0 应用程序可以在高性能的图形内存中分配和缓存顶点数据,并从这个内存进行渲染,从而避免在每次绘制图元的时候重新发送数据。不仅是顶点数据,描述图元的顶点索引、作为glDrawElements参数传递的元素索引也可以缓存。

OpenGL ES 3.0 支持两类缓冲区对象,用于指定顶点和图元数据:数据缓冲区对象(VBO)元素数组缓冲区对象(EBO或者叫IBO)

  • GL_ARRAY_BUFFER标志
    指定的数组缓冲区对象用于创建保存顶点数据的缓冲区对象
  • GL_ELEMENT_ARRAY_BUFFER标志
    指定的元素数组缓冲区对象用于创建保存图元索引的缓冲区对象。

OpenGL ES 3.0 的其他缓冲区对象类型在后面的博客再做介绍,本篇博客我们重点关注用于指定顶点属性的对象和元素数组。

建议:为了得到最佳性能,我们建议OpenGL ES 3.0 应用程序对顶点属性数据和元素索引使用顶点缓冲区对象。

1.2 示例代码

在使用缓冲对象渲染之前,需要分配缓冲区对象并将顶点数据和元素索引上传到响应的缓冲区对象。
下面的样板代码演示了这一操作。

//  Generate VBO Ids and load the VBOs with data
// 创建 2 个 VBO(EBO 实际上跟 VBO 一样,只是按照用途的另一种称呼)
glGenBuffers(2, vboIds);

// 绑定第一个 VBO,拷贝顶点数组到显存
// GL_STATIC_DRAW 标志标识缓冲区对象数据被修改一次,使用多次,用于绘制。
glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 绑定第二个 VBO(EBO),拷贝图元索引数据到显存
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIds[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);


上面代码创建两个缓冲区对象:

  • 一个用于保存实际的顶点属性数据
  • 另一个用于保存组成图元的元素索引

在这个例子中,

  1. 调用glGenBuffers命令获取vboIds中两个未用的缓冲区对象名称
  2. 然后,vboIds中返回的未使用的缓冲区对象名称用于创建一个数组缓冲区对象 VBO 和一个元素数组缓冲区对象 EBO
    • 数组缓冲区对象 VBO 用于保存一个或者多个图元的顶点属性数据。
    • 元素数组缓冲区对象 EBO保存一个或者多个图元的所有。
  3. 实际数组或者元素数据用glBufferData指定。注意,GL_STATIC_DRAW作为一个参数传递给glBufferData,该值用于描述应用程序如何访问缓冲区。

1.2.1 glGenBuffers

void glGenBuffers (GLsizei n, GLuint *buffers);

参数说明:

  • n
    返回的缓冲区对象名称数量
  • buffers
    指向n个条目的数组指针,该数组是分配的缓冲区对象返回的位置

glGenBuffers分配n个缓冲区对象名称,并在buffers中返回它们。glGenBuffers返回的缓冲区对象名称是0以外的无符号整数。0值由OpenGL ES保留,不表示缓冲区对象。企图修改或者查询缓冲区对象0的缓冲区对象状态将产生一个错误。

1.2.2 glBindBuffer

glGenBuffers命令用于指定当前缓冲区对象。第一次通过调用glBindBuffer绑定缓冲区对象名称时,缓冲区对象以默认状态分配;如果分配成功,则分配的对象绑定为目标的当前缓冲区对象。

void glBindBuffer (GLenum target, GLuint buffer);

参数说明:

  • target
    可以设置为以下目标中的任何一个:
#define GL_ARRAY_BUFFER                   0x8892
#define GL_ELEMENT_ARRAY_BUFFER           0x8893
#define GL_COPY_READ_BUFFER               0x8F36
#define GL_COPY_WRITE_BUFFER              0x8F37
#define GL_PIXEL_PACK_BUFFER              0x88EB
#define GL_PIXEL_UNPACK_BUFFER            0x88EC
#define GL_TRANSFORM_FEEDBACK_BUFFER      0x8C8E
#define GL_UNIFORM_BUFFER                 0x8A11
  • buffer
    分配给目标作为当前对象的缓冲区对象

注意,在用glBindBuffer绑定之前,分配缓冲区对象名称并不需要glGenBuffers。应用程序可以用glBindBuffer指定一个未使用的缓冲区对象。然而,我们建议OpenGL ES应用程序调用glGenBuffers,并使用glGenBuffers返回的缓冲区对象名称,而不是指定它们自己的缓冲区对象名称。

和缓冲区对象相关的状态分量入下:

#define GL_BUFFER_SIZE                    0x8764
#define GL_BUFFER_USAGE                   0x8765
  • GL_BUFFER_SIZE
    引用由 glBufferData 指定的缓冲区对象数据的大小。在用glBindBuffer首次绑定缓冲区对象时,初始值为0。
  • GL_BUFFER_USAGE
    这是对应用程序如何使用存储在缓冲区对象中的数据的提示。详细的说明如下表所示。初始值为GL_STATIC_DRAW
#define GL_STREAM_READ                    0x88E1
#define GL_STREAM_COPY                    0x88E2
#define GL_STATIC_READ                    0x88E5
#define GL_STATIC_COPY                    0x88E6
#define GL_DYNAMIC_READ                   0x88E9
#define GL_DYNAMIC_COPY                   0x88EA

如上所述,GL_BUFFER_USAGE 是对OpenGL ES的一个提示,而不是保证。因此,应用程序可以分配一个缓冲区对象数据,将其使用方式设置为 GL_STATIC_DRAW,并且频繁修改它。

1.2.2 glBufferData

顶点数组数据或者元素数组数据存储用glBufferData命令创建和初始化。

void glBufferData (GLenum target, GLsizeiptr size, const void *data, GLenum usage);

参数说明:

  • target
    可以设置为以下目标中的任何一个:
#define GL_ARRAY_BUFFER                   0x8892
#define GL_ELEMENT_ARRAY_BUFFER           0x8893
#define GL_COPY_READ_BUFFER               0x8F36
#define GL_COPY_WRITE_BUFFER              0x8F37
#define GL_PIXEL_PACK_BUFFER              0x88EB
#define GL_PIXEL_UNPACK_BUFFER            0x88EC
#define GL_TRANSFORM_FEEDBACK_BUFFER      0x8C8E
#define GL_UNIFORM_BUFFER                 0x8A11
  • size
    缓冲区数据存储大小,以字节数表示
  • data
    应用程序提供的缓冲区数据的指针
  • usage
    应用程序将如何使用缓冲区对象中存储的数据的提示。

glBufferData将根据size的值保留相应的数据存储。data参数可以设置为NULL值,表示保留的数据存储不进行初始化。如果data是一个有效的指针,则其内容将复制到分配的数据存储。

1.2.3 glBufferSubData

缓冲区对象存储的内容可以用glBufferSubData命令初始化或者更新。

void glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const void *data);
  • target
    可以设置为以下目标中的任何一个:
#define GL_ARRAY_BUFFER                   0x8892
#define GL_ELEMENT_ARRAY_BUFFER           0x8893
#define GL_COPY_READ_BUFFER               0x8F36
#define GL_COPY_WRITE_BUFFER              0x8F37
#define GL_PIXEL_PACK_BUFFER              0x88EB
#define GL_PIXEL_UNPACK_BUFFER            0x88EC
#define GL_TRANSFORM_FEEDBACK_BUFFER      0x8C8E
#define GL_UNIFORM_BUFFER                 0x8A11
  • offset
    缓冲区数据存储中的偏移
  • size
    被修改的数据存储字节数
  • data
    需要被复制到缓冲区对象数据存储的客户数据指针

在用glBufferData或者 glBufferSubData初始化或者更新缓冲区对象数据存储之后,客户数据存储不再需要,可以释放。

  • 对于静态的几何形状,应用程序可以释放客户数据存储,减少应用程序消耗的系统内存。
  • 对于动态几何形状,这可能无法做到。

1.2.4 glDeleteBuffers

在应用程序结束缓冲区对象的使用之后,可以用 glDeleteBuffers命令删除它们。

void glDeleteBuffers (GLsizei n, const GLuint *buffers);

参数说明:

  • n
    删除的缓冲区对象数量
  • buffers
    包含要删除的缓冲区对象的有n个元素的数组。

glDeleteBuffers删除缓冲区中指定的缓冲区对象。一旦缓冲区对象被删除,它就可以作为新的缓冲区对象重用,存储顶点属性或者不同图元的元素索引。

二、实战一下

我们看看使用和不使用缓冲区对象进行的图元绘制。

下面例子描述了使用和不使用顶点缓冲区对象进行的图元绘制。注意,设置顶点属性的代码非常相似。

2.1 代码展示

  • NativeTriangleVertextBufferObject.h
#pragma once

#include <BaseGLSample.h>

#define VERTEX_POS_SIZE       3 // x, y and z
#define VERTEX_COLOR_SIZE     4 // r, g, b, and a

#define VERTEX_POS_INDX       0
#define VERTEX_COLOR_INDX     1

class NativeTriangleVBO : public BaseGLSample 

public:
    NativeTriangleVBO() = default;

    virtual ~NativeTriangleVBO() = default;

    virtual void create();

    virtual void draw();

    virtual void shutdown();

private:

    // VertexBufferObject Ids
    GLuint vboIds[2];

    // x-offset uniform location
    GLuint offsetLoc;

    static void DrawPrimitiveWithoutVBOs(GLfloat *verticesParam, GLint vtxStride,
                                         GLint numIndices, GLushort *indicesParam);

    void DrawPrimitiveWithVBOs(GLint numVertices, GLfloat *vtxBuf,
                               GLint vtxStride, GLint numIndices, GLushort *indicesParam);
;



  • NativeTriangleVertextBufferObject.cpp
#include <cstring>
#include "NativeTriangleVertextBufferObject.h"

// 可以参考这篇讲解: https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/

// 3 vertices, with (x,y,z) ,(r, g, b, a)  per-vertex
static GLfloat vertices[3 * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE)] =
        
                // 逆时针 三个顶点
                -0.5f, 0.5f, 0.0f,       // v0
                1.0f, 0.0f, 0.0f, 1.0f,  // c0

                -1.0f, -0.5f, 0.0f,        // v1
                0.0f, 1.0f, 0.0f, 1.0f,  // c1

                0.0f, -0.5f, 0.0f,        // v2
                0.0f, 0.0f, 1.0f, 1.0f,  // c2
        ;

// Index buffer data
static GLushort indices[3] = 0, 1, 2;

void NativeTriangleVBO::create() 
    GLUtils::printGLInfo();
    // 顶点着色器
    VERTEX_SHADER = GLUtils::openTextFile(
            "vertex/vertex_shader_hello_triangle_vbo.glsl");
    // 片段着色器
    FRAGMENT_SHADER = GLUtils::openTextFile(
            "fragment/fragment_shader_hello_triangle2.glsl");
    mProgram = GLUtils::createProgram(&VERTEX_SHADER, &FRAGMENT_SHADER);

    offsetLoc = glGetUniformLocation(mProgram, "u_offset");

    if (!mProgram) 
        LOGD("Could not create program")
        return;
    

    vboIds[0] = 0;
    vboIds[1] = 0;

    // 设置清除颜色
    glClearColor(1.0f, 1.0f, 1.0f, 0.0f);


void NativeTriangleVBO::draw() 
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(mProgram);

    // without VBOs
    glUniform1f(offsetLoc, 0.0f);
    DrawPrimitiveWithoutVBOs(vertices,
                             sizeof(GLfloat) * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE),
                             3, indices);

    // with VBOs
    // Offset tge vertex positions so both can be seen
    glUniform1f(offsetLoc, 1.0f);
    DrawPrimitiveWithVBOs(3, vertices,
                          sizeof(GLfloat) * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE),
                          3, indices);


// verticesParam   - pointer to a buffer that contains vertex attribute data
// vtxStride  - stride of attribute data / vertex in bytes
// numIndices - number of indicesParam that make up primitive drawn as triangles
// indicesParam    - pointer to element index buffer.
void NativeTriangleVBO::DrawPrimitiveWithoutVBOs(GLfloat *verticesParam,
                                                 GLint vtxStride, GLint numIndices,
                                                 GLushort *indicesParam) 
    FUN_BEGIN_TIME("NativeTriangleVBO::DrawPrimitiveWithoutVBOs")
        GLfloat *vtxBuf = verticesParam;

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        glEnableVertexAttribArray(VERTEX_POS_INDX);
        glEnableVertexAttribArray(VERTEX_COLOR_INDX);

        glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
                              GL_FLOAT, GL_FALSE, vtxStride, vtxBuf);

        vtxBuf += VERTEX_POS_SIZE;

        glVertexAttribPointer(VERTEX_COLOR_INDX, VERTEX_COLOR_SIZE,
                              GL_FLOAT, GL_FALSE, vtxStride, vtxBuf);

        glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT, indicesParam);

        glDisableVertexAttribArray(VERTEX_POS_INDX);
        glDisableVertexAttribArray(VERTEX_COLOR_INDX);
    FUN_END_TIME("NativeTriangleVBO::DrawPrimitiveWithoutVBOs")


void NativeTriangleVBO::DrawPrimitiveWithVBOs(GLint numVertices, GLfloat *vtxBuf,
                                              GLint vtxStride, GLint numIndices,
                                              GLushort *indicesParam) 
    FUN_BEGIN_TIME("NativeTriangleVBO::DrawPrimitiveWithVBOs")
        GLuint offset = 0;

        // vboIds[0] - used to store vertex attribute data
        // vboIds[l] - used to store element indicesParam
        if (vboIds[0] == 0 && vboIds[1] == 0) 
            //Only allocate on the first draw
            glGenBuffers(2, vboIds);

            glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
            glBufferData(GL_ARRAY_BUFFER, vtxStride * numVertices,
                         vtxBuf, GL_STATIC_DRAW);

            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIds[1]);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                         sizeof(GLushort) * numIndices,
                         indicesParam, GL_STATIC_DRAW);
        

        glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIds[1]);

        glEnableVertexAttribArray(VERTEX_POS_INDX);
        glEnableVertexAttribArray(VERTEX_COLOR_INDX);

        glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
                              GL_FLOAT, GL_FALSE, vtxStride, (const void *) offset);

        offset += VERTEX_POS_SIZE * sizeof(GLfloat);
        glVertexAttribPointer(VERTEX_COLOR_INDX, VERTEX_COLOR_SIZE,
                              GL_FLOAT, GL_FALSE, vtxStride, (const void *) offset);

        glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT, nullptr);

        glDisableVertexAttribArray(VERTEX_POS_INDX);
        glDisableVertexAttribArray(VERTEX_COLOR_INDX);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    FUN_END_TIME("NativeTriangleVBO::DrawPrimitiveWithVBOs")


void NativeTriangleVBO::shutdown() 
    // Delete program object
    GLUtils::DeleteProgram(mProgram);

    glDeleteBuffers(2, &vboIds[0]);

  • vertex/vertex_shader_hello_triangle_vbo.glsl
#version 300 es                            
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec4 a_color;

uniform float u_offset;

out vec4 v_color;

void main()

    v_color = a_color;
    gl_Position = a_position;
    gl_Position.x += u_offset;

  • fragment/fragment_shader_hello_triangle2.glsl
#version 300 es
// 表示OpenGL ES着色器语言V3.00

// 声明着色器中浮点变量的默认精度
precision mediump float;

// 声明由上一步顶点着色器传入进来的颜色值
in vec4 v_color;

// 声明一个输出变量fragColor,这是一个4分量的向量,
// 写入这个变量的值将被输出到颜色缓冲器
out vec4 o_fragColor;

void main()

	o_fragColor = v_color;

2.2 效果展示

2.3 性能对比

正如你在这例子中所看到的,使用定点缓冲区对象非常容易,比起顶点数组,所需要的额外工作非常少。考虑到这种功能提供的性能提升,支持顶点缓冲区对象的少量额外工作很值得。

从下面的日志可以看出来,使用VBO绘制全部都是0ms,不适用VBO绘制会经常出现需要1ms的情况。

因此加载顶点属性的两种方式对比:

  • 使用客户顶点数组
  • 使用顶点缓冲区对象VBO

顶点缓冲区对象VBO优于顶点数组,因为它们能够减少CPU和GPU之间复制的数据量,从而获得更好的性能。

2021-12-05 14:32:42.979 8743-8788/com.oyp.openglesdemo E/NDK_JNI_LOG_TAG: [NativeTriangleVertextBufferObject.cpp][DrawPrimitiveWithVBOs][100]: [NativeTriangleVBO::DrawPrimitiveWithVBOs] func start
2021-12-05 14:32:42.979 8743-8788/com.oyp.openglesdemo E/NDK_JNI_LOG_TAG: [NativeTriangleVertextBufferObject.cpp][DrawPrimitiveWithVBOs][139]: [NativeTriangleVBO::DrawPrimitiveWithVBOs] func cost time 0ms
2021-12-05 14:32:42.996 8743-8788/com.oyp.openglesdemo E/NDK_JNI_LOG_TAG: [NativeTriangleVertextBufferObject.cpp][DrawPrimitiveWithoutVBOs][73]: [NativeTriangleVBO::DrawPrimitiveWithoutVBOs] func start
2021-12-05 14:32:42.997 8743-8788/com.oyp.openglesdemo E/NDK_JNI_LOG_TAG: [NativeTriangleVertextBufferObject.cpp][DrawPrimitiveWithoutVBOs][94]: [NativeTriangleVBO::DrawPrimitiveWithoutVBOs] func cost time 1ms
2021-12-05 14:32:42.997 8743-8788/com.oyp.openglesdemo E/NDK_JNI_LOG_TAG: [NativeTriangleVertextBufferObject.cpp][DrawPrimitiveWithVBOs][100]: [NativeTriangleVBO::DrawPrimitiveWithVBOs] func start
2021-12我的OpenGL学习进阶之旅介绍顶点缓冲区对象VBO和元素数组缓冲区对象EBO,并对比使用VBO和不使用VBO绘制三角形的效果

我的OpenGL学习进阶之旅介绍一下 映射缓冲区对象和复制缓冲区对象

我的OpenGL学习进阶之旅介绍一下 图元的类型:三角形直线和点精灵

我的OpenGL学习进阶之旅介绍一下OpenGL ES的图元装配:坐标系统透视分割视口变化

我的OpenGL学习进阶之旅介绍一下OpenGL ES的图元装配:坐标系统透视分割视口变化

我的OpenGL学习进阶之旅统一变量和属性