OpenGL ES 2.0 —— 顶点缓冲 VBO

 

前言

在前面绘制图形和纹理时, 我们的顶点坐标的使用流程如下

  • 定义 java 的 float[]
  • 将 float[] 写入 Native, 使用 FloatBuffer 描述
  • 在绘制时, 将 FloatBuffer 传入着色器

可以看到, 每次绘制都需要将 FloatBuffer 中的数据, 从 Native 拷贝到 GL 着色器所在的 GPU 内存中, 当顶点数据比较庞大时, 这也会是一笔非常大的开销

VBO 便是解决这个问题很好的途径

一. 什么是 VBO?

VBO 即 Vertex buffer object 顶点缓冲对象, 它通过在 GPU 中开辟一块内存专门用于存放顶点坐标数据的方式, 减少 GPU 绘制时从物理内存拷贝到 GPU 内存空间所带来的性能损耗

二. VBO 相关 API

一) 对象的创建

public class GLES20 {

    // C function void glGenBuffers ( GLsizei n, GLuint *buffers )

    public static native void glGenBuffers(
        int n,             // 缓冲对象数量
        int[] buffers,     // 传出参数, 用于保存创建好的 vbo id
        int offset         // 描述 buffers 的偏移量
    );

    // C function void glGenBuffers ( GLsizei n, GLuint *buffers )

    public static native void glGenBuffers(
        int n,                      // 缓冲对象数量
        java.nio.IntBuffer buffers  // Native 的 int array, 用于保存创建好的 vbo id
    );
    
}

可以看到, 两个方法中的 buffers 均为传出参数, 当 VBO 对象创建好之后, 便会将其句柄值存入 buffers, 这里称之为 vboId

二) 绑定与解绑

public class GLES20 {

    // C function void glBindBuffer ( GLenum target, GLuint buffer )

    public static native void glBindBuffer(
        int target, // 描述绑定的缓冲区类型
        int buffer  // vbo 的 id
    );
    
}
  • target 的类型有很多, 如下所示
  • buffer 传值为 0 表示, 解绑 vboId
target 值 缓冲区类型
GL_ARRAY_BUFFER 数组缓冲
GL_ELEMENT_ARRAY_BUFFER 元素数组缓冲
GL_COPY_READ_BUFFER 复制只读缓冲
GL_COPY_WRITE_BUFFER 复制可写缓冲
GL_PIXEL_PACK_BUFFER 像素打包缓冲
GL_PIXEL_UNPACK_BUFFER 像素解包缓冲
GL_TRANSFORM_FEEDBACK_BUFFER 变换反馈缓冲
GL_UNIFORM_BUFFER 一致变量缓冲

三) 数据写入

public class GLES20 {
    
    // C function void glBufferData ( GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage )

    public static native void glBufferData(
        int target,
        int size,
        java.nio.Buffer data,
        int usage
    );

    // C function void glBufferSubData ( GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid *data )

    public static native void glBufferSubData(
        int target,
        int offset,
        int size,
        java.nio.Buffer data
    );
    
}
  • glBufferData: 主要用于存储空间和数据的初始化
  • glBufferSubData: 主要用于数据的更新

target 如下

缓冲区用途可选参数值 参数说明
GL_STATIC_DRAW 在绘制时,缓冲区对象数据可以被修改一次,使用多次
GL_STATIC_READ 在 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用多次,且该数据可以冲应用程序中查询
GL_STATIC_COPY 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用多次,该数据将直接作为绘制图元或者指定图像的信息来源
GL_DYNAMIC_DRAW 在绘制时,缓冲区对象数据可以被重复修改、使用多次
GL_DYNAMIC_READ 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被重复修改、使用多次,且该数据可以从应用程序中查询
GL_DYNAMIC_COPY 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被重复修改、使用多次,该数据将直接作为绘制图元或者指定图像的信息来源
GL_STREAM_DRAW 在绘制时,缓冲区对象数据可以被修改一次,使用少数几次
GL_STREAM_READ 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用少数几次,且该数据可以从应用程序中查询
GL_STREAM_COPY 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用少数几次,且该数据将直接作为绘制图元或者指定图像的信息来源

四) 缓冲对象删除

public class GLES20 {
    
    // C function void glDeleteBuffers ( GLsizei n, const GLuint *buffers )

    public static native void glDeleteBuffers(
        int n,          
        int[] buffers,
        int offset
    );

    // C function void glDeleteBuffers ( GLsizei n, const GLuint *buffers )

    public static native void glDeleteBuffers(
        int n,
        java.nio.IntBuffer buffers
    );

}

删除的方法与创建相互对应

五) 缓冲查询

public class GLES20 {
   
   // C function void glGetBufferParameteriv ( GLenum target, GLenum pname, GLint *params )

    public static native void glGetBufferParameteriv(
        int target,
        int pname,
        int[] params,
        int offset
    );

    // C function void glGetBufferParameteriv ( GLenum target, GLenum pname, GLint *params )

    public static native void glGetBufferParameteriv(
        int target,
        int pname,
        java.nio.IntBuffer params
    );

}

glGetBufferParameteriv 方法用于查询指定缓冲区的信息

  • target: 与写入时的 target 一致
  • pname: 表述要查询的信息
  • int[] params + int offset/IntBuffer params: 传出参数, 用于保存查询结果

pname 的可选项如下

缓冲区查询信息 说明
GL_BUFFER_SIZE 缓冲区以字节计的大小
GL_BUFFER_USAGE 缓冲区用途
GL_BUFFER_MAPPED 是否为映射缓冲区
GL_BUFFER_ACCESS_FLAGS 缓冲区访问标志
GL_BUFFER_MAP_LENGTH 缓冲区映射长度
GL_BUFFER_MAP_OFFSET 缓冲区映射偏移量

三. VBO 使用示例

VBO 使用的示例, 我们在纹理的绘制基础上进行修改

一) VBO 的创建与初始化

public class TextureRenderer implements GLSurfaceView.Renderer {
    
    private float[] mVertexCoordinate;
    private float[] mTextureCoordinate;
    private FloatBuffer mVertexBuffer;
    private FloatBuffer mTextureBuffer;
    
    private int mVboId;
    
    ......
    
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        ......
        // 创建 vbo(Vertex Buffer Object)
        int vboSize = 1;
        int[] vboIds = new int[vboSize];
        // 1. 创建缓冲对象
        GLES20.glGenBuffers(
                vboSize,      // n: 缓冲区数量
                vboIds,       // buffers: 传出参数, 用于保存创建好的 vbo id
                0             // offset: 描述 buffers 的偏移量
        );
        mVboId = vboIds[0];
        // 2. 绑定缓冲对象
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
        
        // 3. 为缓冲对象开辟缓冲区
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, (mVertexCoordinate.length + mTextureCoordinate.length) * 4,
                null, GLES20.GL_STATIC_DRAW);
        // 4.1 为缓冲区写入数据
        GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0,
                mVertexCoordinate.length * 4, mVertexBuffer);
        // 4.2 将纹理坐标写入 vbo
        GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mVertexCoordinate.length * 4,
                mTextureCoordinate.length * 4, mTextureBuffer);
                
        // 5. 解绑缓冲对象
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
    }
    
}

二) 将 VBO 数据写入顶点着色器

public class TextureRenderer implements GLSurfaceView.Renderer {
    
    @Override
    public void onDrawFrame(GL10 gl) {
        ......
        
        // 写入裁剪矩阵数据
        GLES20.glUniformMatrix4fv(uVertexMatrix, 1, false, mClipMatrix, 0);
        // 绑定 vbo
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
        // 写入顶点坐标数据
        GLES20.glEnableVertexAttribArray(aVertexPosition);
        GLES20.glVertexAttribPointer(aVertexPosition, 2, GLES20.GL_FLOAT, false, 8, 0);
        // 写入纹理坐标数据
        GLES20.glEnableVertexAttribArray(aTexturePosition);
        GLES20.glVertexAttribPointer(aTexturePosition, 2, GLES20.GL_FLOAT, false, 8,
                mVertexCoordinate.length * 4);
        // 解绑 vbo
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
        
        ......
    }
    
}

可以看到 onDrawFrame 中的 glVertexAttribPointer 方法最后一个参数与之前不同, 它并没有直接传入一个 FloatBuffer 而是传入 VBO 数据的偏移量, 这样就可以直接使用 VBO 中的数据了

总结

VBO 的使用在 OpenGL ES 中十分的广泛, 即使顶点数据动态变更了, 也可以通过 glBufferSubData 可以快捷的更新 GPU 内存中的数据, 保证渲染管线的高效执行

参考文献