Android显示系统(06)- OpenGL ES - VBO和EBO和VAO

一、前言:

之前代码,我们都是直接在java代码中定义顶点数组,然后,将顶点数据存储在FloatBuffer对象,最后,传递给GPU。如下所示:

java 复制代码
public class Triangle {
    private FloatBuffer mVertexBuffer;
    // 定义的三角形顶点坐标数组
    private final float[] mTriangleCoords = new float[]{
            0.0f, 0.2f, 0.0f,   // 顶部
            -0.5f, -0.5f, 0.0f, // 左下角
            0.5f, -0.5f, 0.0f   // 右下角
    };

    public Triangle(Context context) {
        // 为顶点坐标分配DMA内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mTriangleCoords.length * 4);
        // 设置字节顺序为本地字节顺序(会根据硬件架构自适应大小端)
        byteBuffer.order(ByteOrder.nativeOrder());
        // 将字节缓冲区转换为浮点缓冲区
        mVertexBuffer = byteBuffer.asFloatBuffer();
        // 将顶点三角形坐标放入缓冲区
        mVertexBuffer.put(mTriangleCoords);
        // 设置缓冲区的位置指针到起始位置
        mVertexBuffer.position(0);
// ... 删除不相关代码
    }
}

但是,这个FloatBuffer顶点数组的存储对象是在我们CPU管理的主内存当中,一种叫做DMA的内存(Direct Memory Access),它可以直接供底层 GPU 使用,不受 Java 垃圾回收的影响,不需要CPU参与。

而我们绘制三角形时候怎么做的呢?看代码:

java 复制代码
public class GLRenderTest implements GLSurfaceView.Renderer {
    private Triangle mTriangle;
    // ... 删除非关键代码
    @Override
    public void onDrawFrame(GL10 gl){
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
        mTriangle.draw();
    }
}

看得出我们会在onDrawFrame当中调用draw方法,并且,onDrawFrame是每一帧都要被调用,如果视频帧率是60fps,那么一秒钟就要拷贝60次数据,显然非常低效。

那么,有没有更好的方法呢?当然有,要么我写文章干啥呢?( ̄▽ ̄)"~~,当然有,因此聪明的工程师们已经帮我们搞出来了本文主角VBO\EBO\VAO。

二、VBO:

  • VAO(vertex-array object)顶点数组对象,用来管理VBO。
  • VBO(vertex buffer object)顶点缓冲对象,用来缓存用户传入的顶点数据。
  • EBO(element buffer object)索引缓冲对象,用来存放顶点索引数据。

来,分开看看:

1、概念:

先在GPU中分配一个存储空间VBO,然后,一次性从主内存拷贝数据到GPU的VBO当中;

1)使用VBO的好处:

  • 减少了主内存到GPU的数据传输次数,原来是每一帧都拷贝,现在只拷贝一次;
  • 数据保存在GPU当中,可以重复使用;
  • 还可以在GPU中对数据进行转换,减少CPU负载;

2)VBO使用步骤:

  • 生成并绑定VBO:

    • 使用 glGenBuffers() 生成一个 VBO 对象。

    • 使用 glBindBuffer() 将 VBO 绑定到指定的缓冲区类型(如 GL_ARRAY_BUFFER)。

  • 分配内存并传递数据:

    • 使用 glBufferData() 分配内存空间并传递顶点数据到 VBO。
    • 可以使用 glBufferSubData() 更新部分数据或者使用 glMapBuffer() 来映射缓冲区进行数据修改。
  • 设置顶点属性指针:

    • 在绑定 VBO 后,使用 glVertexAttribPointer() 来告诉OpenGL如何解释顶点数据。
    • 使用 glEnableVertexAttribArray() 启用顶点属性数组。
  • 解绑VBO:

    • 在完成数据传递后,使用 glBindBuffer(GL_ARRAY_BUFFER, 0) 来解绑 VBO。

3)关键API:

glGenBuffers(GLsizei n, GLuint *buffers)

该函数用于生成 VBO 对象的名称。

  • 参数 n 指定要生成的 VBO 对象的数量。
  • 参数 buffers 是一个指向 GLuint 类型的数组,用于存储生成的 VBO 对象的名称。

glDeleteBuffers(GLsizei n, const GLuint *buffers)

用于删除通过 glGenBuffers 生成的 VBO 对象。

  • 参数 n 指定要删除的 VBO 对象的数量。
  • 参数 buffers 是一个指向 GLuint 类型的数组,包含要删除的 VBO 对象的名称。

glBindBuffer(GLenum target, GLuint buffer)

用于绑定一个 VBO 对象到指定的缓冲区类型。

  • 参数 target 指定要绑定的缓冲区类型,如 GL_ARRAY_BUFFER 表示顶点属性数据缓冲区。
  • 参数 buffer 是要绑定的 VBO 对象的名称。

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

用于分配内存空间并传递数据到 VBO。

  • target 指定要分配数据的缓冲区类型。
  • size 指定要分配的数据大小。
  • data 是指向要传递数据的指针。
  • usage 表示数据在未来的使用方式,如 GL_STATIC_DRAW 表示数据将被修改一次,但使用多次。

glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer)

用于告诉 OpenGL 如何解释顶点数据。

  • index 指定顶点属性的索引。
  • size 指定每个顶点属性的组件数量。
  • type 指定顶点属性数据的类型。
  • normalized 表示是否对非浮点型数据进行归一化。
  • stride 表示相邻两个顶点属性之间的字节偏移量。
  • pointer 指定顶点数据在缓冲区中的偏移量。

glEnableVertexAttribArray(GLuint index)

启用指定索引的顶点属性数组。

  • index 是要启用的顶点属性数组的索引。

2、修改之前的代码:

代码都是在com/example/glsurfaceviewdemo/Triangle.java修改的。

1)生成并绑定 VBO:

Triangle 类中,你需要生成并绑定一个 VBO 来存储顶点数据。这应该在构造函数中完成。

java 复制代码
 private int mVboId; // 类成员变量
// 生成并绑定 VBO
int[] vbos = new int[1];
GLES30.glGenBuffers(1, vbos, 0);
mVboId = vbos[0];
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);

2)传递顶点数据到 VBO:

替代在构造函数中直接将顶点数据传递给 FloatBuffer,你应该将顶点数据传递到GPU的 VBO 中。这可以通过 glBufferData 实现。

java 复制代码
 // 传递顶点数据到 VBO
GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, byteBuffer.capacity(), byteBuffer, GLES30.GL_STATIC_DRAW);
  • GLES30.GL_ARRAY_BUFFER: 这是一个缓冲区对象类型标识符,表示我们正在操作的是一个顶点数组缓冲区对象。在这里,我们将顶点数据存储在一个顶点数组缓冲区中。
  • byteBuffer.capacity(): 这里计算了顶点数据数组的总字节数。mTriangleCoords 是包含顶点坐标的浮点数组,每个浮点数占4个字节(float类型为32位,即4字节),所以乘以4得到总字节数。
  • byteBuffer: 这是存储顶点数据的缓冲区对象。在这里,我们使用 mVertexBuffer 存储了顶点数据,它是一个 FloatBuffer 类型的对象。
  • GLES30.GL_STATIC_DRAW: 这个标志告诉OpenGL ES如何处理缓冲区的数据。GL_STATIC_DRAW 表示数据将被设置一次,但将被多次使用。这个标志有助于OpenGL ES优化内存使用和性能。

注意:我们对所有的VBO操作都应该放在glLinkProgram后面,因为,所有的Shader都link之后,再操作VBO;

3)设置顶点属性指针:

draw() 方法中,你需要设置顶点属性指针以从 VBO 中读取顶点数据(就是告诉OpenGL如何解释顶点数据)。这可以通过 glVertexAttribPointerglEnableVertexAttribArray 实现。

java 复制代码
public void draw() {
    GLES30.glUseProgram(mProgram);

    // 确保绑定 VBO (保险措施)
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId[0]);

    // 设置顶点属性指针
    int positionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
    GLES30.glEnableVertexAttribArray(mPositionHandle);
    GLES30.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES30.GL_FLOAT, false, 0, 0);

    // 设置颜色句柄和绘制三角形
    int colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
    GLES30.glUniform4fv(colorHandle, 1, mColor, 0);
    GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, mTriangleCoords.length / COORDS_PER_VERTEX);

    // 禁用顶点属性数组
    GLES30.glDisableVertexAttribArray(mPositionHandle);

    // 解绑 VBO
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
}

通过在每次绘制结束后禁用顶点属性数组和解绑 VBO,您可以确保在下一次绘制之前不会使用或修改之前设置的数据,从而避免潜在的渲染问题。

4)修改后完整代码:

java 复制代码
package com.example.glsurfaceviewdemo;

import android.content.Context;
import android.opengl.GLES30;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL;

public class Triangle {
    // 顶点数据是float类型,因此,使用这个存储
    private FloatBuffer mVertexBuffer;
    // VBO存储顶点数据
    private int mVboId;

    private int mProgram;
    // 定义的三角形顶点坐标数组
    private final float[] mTriangleCoords = new float[]{
            0.0f, 0.2f, 0.0f,   // 顶部
            -0.5f, -0.5f, 0.0f, // 左下角
            0.5f, -0.5f, 0.0f   // 右下角
    };

    public Triangle(Context context) {
        // 为顶点坐标分配DMA内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mTriangleCoords.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder()); // 设置字节顺序为本地字节顺序(会根据硬件架构自适应大小端)
        mVertexBuffer = byteBuffer.asFloatBuffer(); // 将字节缓冲区转换为浮点缓冲区
        mVertexBuffer.put(mTriangleCoords); // 将顶点三角形坐标放入缓冲区
        mVertexBuffer.position(0);

        // 2.加载并编译vertexShader和fragmentShader
        String vertexShaderCode = ShaderController.loadShaderCodeFromFile("triangle_vertex.glsl", context);
        String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("triangle_fragment.glsl", context);

        // 3.创建一个OpenGL程序,并链接程序
        mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode);

        // 生成并绑定 VBO (一定要在createGLProgram之后)
        int[] vbos = new int[1];
        GLES30.glGenBuffers(1, vbos, 0);
        mVboId = vbos[0];
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);

        // 传递顶点数据到 VBO
        GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, byteBuffer.capacity(), byteBuffer, GLES30.GL_STATIC_DRAW);
    }

    // 定义的fragment的颜色数组,表示每个像素的颜色
    private final float[] mColor = new float[]{0.0f, 1.0f, 0.0f, 1.0f};
    // 顶点着色器的位置句柄
    private int mPositionHandle = 0;
    // 片元着色器的位置句柄
    private int mColorHandle = 0;
    private final int COORDS_PER_VERTEX = 3;

    public void draw() {
        // 使用program
        GLES30.glUseProgram(mProgram);
        // 确保绑定 VBO (保险措施)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);

        // 设置顶点属性指针
        mPositionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
        GLES30.glEnableVertexAttribArray(mPositionHandle);
        // 最后一个参数 0 是为绑定的 缓冲区对象(VBO) 指定偏移,而在没有 VBO 的情况下,0 是顶点缓冲区(mVertexBuffer)的直接内存地址
        GLES30.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES30.GL_FLOAT, false, 0, 0);

        // 获取片元着色器的颜色句柄
        mColorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
        // 设置绘制三角形的颜色
        GLES30.glUniform4fv(mColorHandle, 1, mColor, 0);

        // 绘制三角形
        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, mTriangleCoords.length / COORDS_PER_VERTEX);

        // 禁用顶点属性数组
        GLES30.glDisableVertexAttribArray(mPositionHandle);
        // 解绑 VBO
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
    }
}

三、EBO:

1、概念:

EBO 是 Element Buffer Object 的缩写,也称为索引缓冲对象。EBO可以帮助减少重复顶点数据的存储,提高性能并减少内存占用。

1)使用EBO的好处:

使用 EBO 的基本思想是,你可以定义一组顶点,并使用索引数组来指定这些顶点在绘制时的顺序。通过使用 EBO,你可以在绘制时只指定顶点一次,然后通过索引数组多次引用这些顶点,从而避免重复存储相同的顶点数据。

2)EBO使用步骤:

  • 生成并绑定 EBO

    • 使用 glGenBuffers 生成一个缓冲区对象的标识符。

    • 使用 glBindBuffer 绑定该缓冲区对象为 GL_ELEMENT_ARRAY_BUFFER 类型,表示这个缓冲区将用于存储索引数据。

    java 复制代码
    int[] ebo = new int[1];
    GLES30.glGenBuffers(1, ebo, 0);
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
  • 传递索引数据到 EBO

    • 定义索引数据,通常是一个数组,指定顶点数组中顶点的索引顺序。

    • 创建一个 ByteBuffer 或者 IntBuffer 来存储索引数据,并将数据传递给 EBO。

    java 复制代码
    short[] indices = new short[] { 0, 1, 2 };
    ByteBuffer indexBuffer = ByteBuffer.allocateDirect(indices.length * 2);
    indexBuffer.order(ByteOrder.nativeOrder());
    ShortBuffer shortIndexBuffer = indexBuffer.asShortBuffer();
    shortIndexBuffer.put(indices);
    GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.capacity(), indexBuffer, GLES30.GL_STATIC_DRAW);
  • 绘制时使用 EBO:

    • 在绘制时,使用 glDrawElements 替代 glDrawArrays,并指定绘制的图元类型、索引数据的数量以及数据类型。

    java 复制代码
    GLES30.glDrawElements(GLES30.GL_TRIANGLES, indices.length, GLES30.GL_UNSIGNED_SHORT, 0);
  • 解绑和清理

    • 在不再需要 EBO 时,解绑缓冲区对象并删除它。

    java 复制代码
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑 EBO
    GLES30.glDeleteBuffers(1, ebo, 0); // 删除 EBO

2、修改之前的代码:

将代码比较聚合的部分提取成了单独的方法,便于以后迭代开发,完整代码如下:

文件路径:com/example/glsurfaceviewdemo/Triangle.java

java 复制代码
package com.example.glsurfaceviewdemo;

import android.content.Context;
import android.opengl.GLES30;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;

import javax.microedition.khronos.opengles.GL;

public class Triangle {
    private final int COORDS_PER_VERTEX = 3;
    private FloatBuffer mVertexBuffer;
    private int[] mIndices = new int[]{0, 1, 2}; // EBO索引数据
    private int mVboId;
    private int mEboId;
    private int mProgram;
    // 定义的三角形顶点坐标数组
    private final float[] mTriangleCoords = new float[]{
        0.0f, 0.2f, 0.0f,   // 顶部
        -0.5f, -0.5f, 0.0f, // 左下角
        0.5f, -0.5f, 0.0f   // 右下角
    };

    public Triangle(Context context) {
        // 初始化顶点数据
        initVertexBuffer();
        // 加载并编译着色器
        initShaders(context);
        // 下面对VBO和EBO的操作,一定要在createGLProgram之后
        // 生成并绑定 VBO
        initVbo();
        // 生成并绑定 EBO
        initEbo();
    }

    // 初始化顶点数据
    private void initVertexBuffer() {
        // 为顶点坐标分配DMA内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mTriangleCoords.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
        mVertexBuffer = byteBuffer.asFloatBuffer();
        mVertexBuffer.put(mTriangleCoords);
        mVertexBuffer.position(0);
    }

    // 加载并编译着色器
    private void initShaders(Context context) {
        String vertexShaderCode = ShaderController.loadShaderCodeFromFile("triangle_vertex.glsl", context);
        String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("triangle_fragment.glsl", context);
        mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode);
    }
    private void initVbo() {
        int[] vbos = new int[1];
        GLES30.glGenBuffers(1, vbos, 0);
        mVboId = vbos[0];
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);
        mVertexBuffer.position(0);
        // 指定顶点属性指针,从 VBO 读取数据
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,       // 缓冲区目标:顶点缓冲区
            mVertexBuffer.capacity() * 4, // 总字节大小(mVertexBuffer.capacity()个float,每个占4字节)
            mVertexBuffer,                // 数据源:顶点缓冲区
            GLES30.GL_STATIC_DRAW         // 缓冲区类型:静态数据
        );

    }
    // 生成并绑定 EBO
    private void initEbo() {
        // 生成并绑定 EBO (一定要在createGLProgram之后)
        int[] ebos = new int[1];
        GLES30.glGenBuffers(1, ebos, 0);
        mEboId = ebos[0];
        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, mEboId);

        // 传递索引数据到 EBO
        ByteBuffer indexBuffer = ByteBuffer.allocateDirect(mIndices.length * 4); // 每个索引是int,4字节
        indexBuffer.order(ByteOrder.nativeOrder());
        IntBuffer intIndexBuffer = indexBuffer.asIntBuffer();
        intIndexBuffer.put(mIndices);
        intIndexBuffer.position(0);
        GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.capacity() * 1, indexBuffer, GLES30.GL_STATIC_DRAW);
    }

    public void draw() {
        // 定义的fragment的颜色数组,表示每个像素的颜色
        float[] color = new float[]{0.0f, 1.0f, 0.0f, 1.0f};
        // 使用program对象
        GLES30.glUseProgram(mProgram);
        // 绑定 EBO
        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, mEboId);

        // 获取顶点属性的位置
        int positionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
        GLES30.glEnableVertexAttribArray(positionHandle);

        // 绑定 VBO(存储顶点坐标数据)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);

        // 设置顶点属性指针(告诉OpenGL如何解释顶点数据缓冲区中的数据)
        GLES30.glVertexAttribPointer(
                positionHandle,    // 顶点属性位置句柄,指示OpenGL应该将这些数据连接到哪个着色器属性
                COORDS_PER_VERTEX, // 每个顶点包含的坐标数
                GLES30.GL_FLOAT,   // 数据类型
                false,             // 是否数据应该被标准化,通常用于整数类型的数据
                0,                 // 步长,指定在连续的顶点属性之间的偏移量,如果所有属性是紧密排列在一起的,可以设置为0
                0);                // 0 是为绑定的 缓冲区对象(VBO) 指定偏移,否则,顶点缓冲区(mVertexBuffer)的直接内存地址

        // 设置片元着色器属性指针
        int colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
        GLES30.glUniform4fv(colorHandle, 1, color, 0);

        // 绘制三角形
        GLES30.glDrawElements(GLES30.GL_TRIANGLES, mIndices.length, GLES30.GL_UNSIGNED_INT, 0);

        // 禁用顶点属性数组
        GLES30.glDisableVertexAttribArray(positionHandle);
        // 解绑 EBO(索引缓冲区)
        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
        // 解绑 VBO(顶点缓冲区)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
    }

    // 释放资源
    public void release() {
        GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 释放VBO
        GLES30.glDeleteBuffers(1, new int[]{mEboId}, 0); // 释放EBO
        GLES30.glDeleteProgram(mProgram);
    }
}

同时,添加一个资源回收的方法release(),在不需要绘制的时候,回收GPU中分配的资源:

调用地方在GLRenderTest中的onDestroy()当中;

文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

java 复制代码
public class GLRenderTest implements GLSurfaceView.Renderer {
    private Triangle mTriangle;
    // ... 删除非关键代码
    public void onDestroy() {
        mTriangle.release();
    }
}

GLRenderTest中的onDestroy()GLSurfaceViewTestonDestroy()被调用;

文件路径:com/example/glsurfaceviewdemo/GLSurfaceViewTest.java

java 复制代码
public class GLSurfaceViewTest extends GLSurfaceView {
    private GLRenderTest mGlRenderTest;
    // ... 删除非关键代码

    public void onDestroy() {
        mGlRenderTest.onDestroy();
    }
}

GLSurfaceViewTestonDestroy()将会在MainActivityonDestroy()中被调用:

java 复制代码
public class MainActivity extends AppCompatActivity {
    private GLSurfaceViewTest mGlSurfaceViewTest;
	// ... 删除非关键代码

    @Override
    protected void onDestroy() {
        super.onDestroy();

        mGlSurfaceViewTest.onDestroy();
    }
}

3、运行结果:

四、VAO:

1、概念:

1)基本概念:

VAO(Vertex Array Object)是OpenGL中的一个顶点数组对象,用于存储顶点数据的配置信息。

在现代 OpenGL 中,绘制一个几何图形通常需要以下步骤:

  1. 顶点数据存储:将顶点数据存储在 GPU 的缓冲区对象(如 VBO, Vertex Buffer Object)中;
  2. 索引数据存储:如果使用索引绘制,则需要将索引数据存储在 EBO(Element Buffer Object)中;
  3. 顶点属性配置 :通过调用 glVertexAttribPointer 配置顶点数据在缓冲区中的存储布局(例如位置、法线、纹理坐标等);
  4. 绑定状态:每次绘制时需要重新绑定这些缓冲区对象,并重新设置对应的顶点属性。

为了简化这些繁琐的绑定和配置过程,VAO(顶点数组对象)被引入。VAO 是一个对象,它可以记录所有与顶点属性相关的状态,比如:

  • 顶点缓冲区对象(VBO)的绑定;
  • 元素缓冲区对象(EBO)的绑定;
  • 顶点属性的启用与配置;
  • 调用各种绘制命令时需要的状态。

有了 VAO 后,只需将顶点属性的配置和绑定动作写在 VAO 中,后续只需绑定 VAO 即能快速恢复这些状态。

2)使用VAO的好处:

  • 绘制不同物体时,不用重置VBO;
  • 可以提高性能和效率;

3)VAO使用步骤:

  • 生成VAO并绑定到GPU;
  • 生成并绑定一个或者多个VBO;
  • VBO ID自动会保存到VAO当中;
  • 拷贝数据到VBO;
  • 连接VAO与Shader;

2、关键API:

void glGenVertexArrays(GLsizei n, GLuint *arrays)

glGenVertexArrays 用于生成一个或多个 VAO 对象的 ID。

  • 参数说明

    • n:要生成的 VAO 的数量;
    • arrays:传递一个 GLuint 数组(一般使用 int[]),用于存储生成的 VAO 的 ID。
  • 用法

    • 一般是使用一个存储 VAO ID 的变量,比如:

      java 复制代码
      int[] vao = new int[1];
      GLES30.glGenVertexArrays(1, vao, 0);
      // vao[0] 存储第一个生成的 VAO 的 ID。
    • 可以生成多个 VAO:

      java 复制代码
      int[] vaos = new int[2];
      GLES30.glGenVertexArrays(2, vaos, 0);
      // vaos[0] 和 vaos[1] 分别存储两个 VAO 的 ID

void glBindVertexArray(GLuint array)

glBindVertexArray 用于绑定一个 VAO,使其成为活动状态。

  • 参数说明

    • array:要绑定的 VAO 的 ID。当 array 为 0 时表示解绑当前 VAO。
  • 用法

    • 每次需要切换 VAO 或启用某个 VAO 时使用:

      java 复制代码
      GLES30.glBindVertexArray(vao[0]);
    • 如果不再需要任何 VAO,解除绑定:

      java 复制代码
      GLES30.glBindVertexArray(0);

3、应用实例:

以下是一个绘制三角形的完整示例,说明如何使用 VAO 简化状态管理。


Java 示例代码
java 复制代码
// 顶点数据(一个简单的三角形)
private final float[] vertices = {
    0.0f,  0.5f, 0.0f,  // 顶点1:顶部
   -0.5f, -0.5f, 0.0f,  // 顶点2:左下角
    0.5f, -0.5f, 0.0f   // 顶点3:右下角
};

// VAO 和 VBO 的 ID
private int[] vao = new int[1];
private int[] vbo = new int[1];

public void initOpenGL() {
    // Step 1: 生成 VAO
    GLES30.glGenVertexArrays(1, vao, 0);

    // Step 2: 绑定 VAO
    GLES30.glBindVertexArray(vao[0]);

    // Step 3: 生成并绑定 VBO
    GLES30.glGenBuffers(1, vbo, 0);
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo[0]);

    // Step 4: 传递顶点数据到 VBO
    FloatBuffer vertexBuffer = ByteBuffer
            .allocateDirect(vertices.length * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer();
    vertexBuffer.put(vertices);
    vertexBuffer.position(0);
    GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, vertexBuffer.capacity() * 4, 
                          vertexBuffer, GLES30.GL_STATIC_DRAW);

    // Step 5: 配置顶点属性
    int positionHandle = GLES30.glGetAttribLocation(shaderProgram, "vPosition"); // 激活的顶点属性变量名
    GLES30.glEnableVertexAttribArray(positionHandle);
    GLES30.glVertexAttribPointer(positionHandle, 3, GLES30.GL_FLOAT, false, 0, 0);

    // Step 6: 解绑 VAO 和 VBO(可选,避免对当前缓冲区的后续操作)
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
    GLES30.glBindVertexArray(0);
}

public void draw() {
    // Step 1: 使用 Shader Program
    GLES30.glUseProgram(shaderProgram);

    // Step 2: 绑定 VAO (恢复顶点属性状态)
    GLES30.glBindVertexArray(vao[0]);

    // Step 3: 绘制三角形
    GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vertices.length / 3);

    // Step 4: 解绑 VAO(可选)
    GLES30.glBindVertexArray(0);
}

代码解析

  • 初始化阶段
    • 生成 VAO 和 VBO。
    • 绑定 VAO 后,可以在其上下文中完成所有顶点缓冲区数据设置和属性配置(顶点数据、顶点属性等)。
    • 配置完成后可以解绑 VAO。
  • 绘制阶段
    • 不需要再重新绑定顶点缓冲区或重新配置顶点属性,只需绑定 VAO。
    • 绑定 VAO 后会恢复之前配置的所有缓冲区和顶点属性的状态,按需要调用绘制操作。
  • 优势
    • 简化绘图流程:只需一条 glBindVertexArray() 就可以切换到指定的绘制状态;
    • 支持多个 VAO 方便管理:可以通过多个 VAO 分别记录不同的绘制状态。

效果示意图

在上面的例子中,屏幕会绘制一个绿色的三角形,是因为对应的 vertices 是定义一个三角形的数据,整个渲染过程简化为:

  1. 初始化 VAO 后,只需 glBindVertexArray(vao[0]) 即能自动配置所有顶点缓冲区和属性;
  2. 解除绑定或切换 VAO 后,可以为其他几何图形绘制新的 VAO 状态。

注意事项

  1. VAO 不直接存储数据
    • VAO 本质上是一个状态管理对象,记录与顶点属性相关的绑定状态。
    • 数据实际存储在绑定的 VBO 或 EBO 中。
  2. 解绑谨慎
    • 在配置完成后解绑 VAO 是一个好习惯,避免意外修改 VAO 状态。
    • 在绘制阶段不需要频繁解绑 VAO,除非需要切换到其他 VAO。
  3. 上下文限制
    • 如果在多种顶点数据形式之间切换(如多个物体需要不同的顶点数据),可以使用多个 VAO 分别记录不同的状态。

利用 glGenVertexArraysglBindVertexArray 的 VAO 机制,可以提升代码结构的清晰性,同时也减少了顶点缓冲区的重复绑定和配置操作。

4、修改之前代码:

之前我们已经使用了VBO和EBO,现在其基础上增加VAO:

java 复制代码
package com.example.glsurfaceviewdemo;

import android.content.Context;
import android.opengl.GLES30;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

public class Triangle {
    private final int COORDS_PER_VERTEX = 3;
    private FloatBuffer mVertexBuffer;
    private int[] mIndices = new int[]{0, 1, 2}; // EBO索引数据
    private int mVboId;
    private int mEboId;
    private int mVaoId; // 添加 VAO ID
    private int mProgram;

    // 定义三角形顶点坐标数组
    private final float[] mTriangleCoords = new float[]{
        0.0f, 0.2f, 0.0f,  // 顶点 1:顶部
        -0.5f, -0.5f, 0.0f,  // 顶点 2:左下角
        0.5f, -0.5f, 0.0f   // 顶点 3:右下角
    };

    public Triangle(Context context) {
        // 初始化顶点数据
        initVertexBuffer();
        // 加载并编译着色器
        initShaders(context);
        // 使用 VAO,对 VBO 和 EBO 的绑定进行封装(简化 draw)
        initVao();
    }

    // 初始化顶点数据
    private void initVertexBuffer() {
        // 为顶点坐标分配内存
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mTriangleCoords.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
        mVertexBuffer = byteBuffer.asFloatBuffer();
        mVertexBuffer.put(mTriangleCoords);
        mVertexBuffer.position(0);
    }

    // 加载并编译着色器
    private void initShaders(Context context) {
        String vertexShaderCode = ShaderController.loadShaderCodeFromFile("triangle_vertex.glsl", context);
        String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("triangle_fragment.glsl", context);
        mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode);
    }

    // 初始化 VAO,封装 VBO 和 EBO 的绑定
    private void initVao() {
        // 生成 VAO
        int[] vaos = new int[1];
        GLES30.glGenVertexArrays(1, vaos, 0);
        mVaoId = vaos[0];
        GLES30.glBindVertexArray(mVaoId); // 绑定 VAO

        // 初始化 VBO
        initVbo();

        // 初始化 EBO
        initEbo();

        // 配置顶点属性
        int positionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
        GLES30.glEnableVertexAttribArray(positionHandle); // 启用顶点属性
        GLES30.glVertexAttribPointer(
                positionHandle,
                COORDS_PER_VERTEX, // 每个顶点的分量个数
                GLES30.GL_FLOAT,   // 数据类型
                false,
                0,
                0  // 偏移量(VBO 内部偏移位置)
        );

        // 解绑 VAO(可选,防止后续操作误改 VAO 状态)
        GLES30.glBindVertexArray(0);
    }

    // 生成并绑定 VBO
    private void initVbo() {
        int[] vbos = new int[1];
        GLES30.glGenBuffers(1, vbos, 0);
        mVboId = vbos[0];
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId); // 绑定 VBO
        GLES30.glBufferData(
                GLES30.GL_ARRAY_BUFFER,
                mVertexBuffer.capacity() * 4,  // 顶点缓冲大小(字节)
                mVertexBuffer,                 // 顶点数据
                GLES30.GL_STATIC_DRAW          // 静态缓冲区
        );
    }

    // 生成并绑定 EBO
    private void initEbo() {
        int[] ebos = new int[1];
        GLES30.glGenBuffers(1, ebos, 0);
        mEboId = ebos[0];
        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, mEboId); // 绑定 EBO

        // 分配并传递索引数据
        ByteBuffer indexBuffer = ByteBuffer.allocateDirect(mIndices.length * 4); // 使用 IntBuffer
        indexBuffer.order(ByteOrder.nativeOrder());
        IntBuffer intIndexBuffer = indexBuffer.asIntBuffer();
        intIndexBuffer.put(mIndices);
        intIndexBuffer.position(0);

        GLES30.glBufferData(
                GLES30.GL_ELEMENT_ARRAY_BUFFER,
                indexBuffer.capacity(),
                indexBuffer,
                GLES30.GL_STATIC_DRAW
        );
    }

    public void draw() {
        // 设置片元着色器的颜色
        float[] color = new float[]{0.0f, 1.0f, 0.0f, 1.0f};

        // 使用 Shader Program
        GLES30.glUseProgram(mProgram);

        // 绑定 VAO(自动恢复顶点属性、VBO 和 EBO 的绑定状态)
        GLES30.glBindVertexArray(mVaoId);

        // 设置片段着色器的颜色值
        int colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
        GLES30.glUniform4fv(colorHandle, 1, color, 0);

        // 绘制三角形
        GLES30.glDrawElements(GLES30.GL_TRIANGLES, mIndices.length, GLES30.GL_UNSIGNED_INT, 0);

        // 解绑 VAO(可选,防止影响其他绘图操作)
        GLES30.glBindVertexArray(0);
    }

    // 释放资源
    public void release() {
        GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 删除 VBO
        GLES30.glDeleteBuffers(1, new int[]{mEboId}, 0); // 删除 EBO
        GLES30.glDeleteVertexArrays(1, new int[]{mVaoId}, 0); // 删除 VAO
        GLES30.glDeleteProgram(mProgram); // 删除 shader program
    }
}
  1. 添加了 VAO 支持

    • 增加了 mVaoId 用于存储 VAO 的 ID。
    • initVao 方法中,生成 VAO 并绑定后,将 VBO 和 EBO 的绑定、顶点属性配置都记录到 VAO。
  2. 修改了 VBO 和 EBO 的绑定方式

    • 原代码中,VBO 和 EBO 在 draw() 中每次都需要绑定。改为在 initVao() 中通过 VAO 封装一次,在绘制时只需绑定 VAO。
  3. 绘制时只需绑定 VAO 而不是分别绑定 VBO 和 EBO

    • 原代码需要分别绑定 VBO 和 EBO,然后设置顶点属性。改为直接绑定 VAO,简化绘制代码。
  4. 新增 VAO 的删除操作

    • release 方法中,新增 VAO 的清理代码:

      java 复制代码
      GLES30.glDeleteVertexArrays(1, new int[]{mVaoId}, 0);
  5. 代码简化

    • 绘制过程只需绑定 VAO 和调用绘制函数。无需每次重新绑定和配置缓冲区,便于代码维护。

VAO 与 VBO、EBO 的关系

  • VBO:存储顶点数据(顶点位置、颜色、纹理等)。
  • EBO:存储索引数据,用于优化重复顶点的绘制。
  • VAO:记录 VBO 的绑定状态、EBO 的绑定状态以及顶点属性的设置,使得这些状态可以在 VAO 中统一管理,简化渲染流程。

使用 VAO 之后,整个渲染流程更加高效,避免了重复绑定和配置缓冲区的操作。

5、问题与思考:

针对刚才的修改大家不妨思考两个问题:
问题一:

为什么draw里面只解绑了vao没有解绑vbo和ebo?
问题二:

为什么initVao里面最后要调用GLES30.glBindVertexArray(0)来解绑VAO?如果解绑了,构造函数中调用完这个函数之后,等draw函数被调用的时候不就没有绑定VAO吗?

----------------------------------------------Two minutes later-------------------------------------

下面是对刚才提出的两个问题的详细解答,同时解释 OpenGL 的状态管理机制以及为什么在特定场景下需要解绑 VAO 和其他对象。


问题一:为什么 draw 方法里只解绑了 VAO,没有解绑 VBO 和 EBO?

原因:

当 VAO 被绑定时,它会记录所有与顶点属性、VBO 和 EBO 绑定相关的状态 。在 draw 方法中通过绑定 VAO (GLES30.glBindVertexArray(mVaoId)),OpenGL 自动恢复与 VAO 相关的所有状态,包括之前绑定的 VBO 和 EBO。因此:

  • draw 方法中,我们只需要绑定/解绑 VAO,无需再单独解绑 VBO 和 EBO,因为它们的状态已经包含在 VAO 的绑定关系中。
  • 像 VBO 和 EBO 的绑定状态在解绑 VAO 时一起解绑了,VBO 和 EBO 不需要单独解绑。

进一步解释:VAO、VBO 和 EBO 的解绑机制

  • VAO 的绑定状态 :当绑定 VAO (glBindVertexArray) 时,VBO 和 EBO 绑定的状态会被记录到 VAO 中。
    • 绑定一个新的 VAO 后,VBO 和 EBO 的状态会被替换成新的 VAO 的绑定关系。
    • 当解绑 VAO(glBindVertexArray(0))时,与当前 VAO 相关的状态会被清除,但不会影响 GPU 中数据本身。
  • 因此,解绑 VBO(glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0))和 EBO(glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0))在使用 VAO 的场景中是多余的。

原代码设计的正确性(对应问题一)

draw 函数中:

java 复制代码
// 只解绑 VAO
GLES30.glBindVertexArray(0);
  • 当解绑 VAO 后,OpenGL 会自行解除所有与这个 VAO 绑定的状态(包括 VBO 和 EBO)。
  • 没有解绑 VBO 和 EBO 的原因是:绘制和状态管理都被 VAO 完全封装了,只需要管理 VAO 的绑定/解绑。
  • 如果明确解绑 VBO 和 EBO,效果不会改变,但代码显得冗余,不符合 VAO 的设计意图。

问题二:为什么 initVao 方法里最后要调用 GLES30.glBindVertexArray(0) 来解绑 VAO?解绑后 draw 时不久没绑定 VAO 吗?

原因:

initVao() 中调用 GLES30.glBindVertexArray(0) 是为了防止后续的任何 OpenGL 函数调用意外修改当前 VAO 的状态


详细解释:VAO 状态与解绑的目的

在 OpenGL 中,VAO 是一个状态对象,包含了顶点属性的配置、VBO 和 EBO 的绑定关系。也就是说,任何与顶点属性相关的操作(比如配置 glVertexAttribPointer、绑定新的缓冲区)都会作用在当前绑定的 VAO 上。因此,绑定 VAO 后你必须注意避免对现有状态的污染

未解绑会导致的问题:

如果你在 initVao 中没有解绑 VAO,后续的其他代码(初始化其他对象或者顶点数据)可能会无意中修改当前绑定的 VAO。例如:

  • 如果在未解除绑定的情况下再调用 glBindBuffer(GL_ARRAY_BUFFER, other_vbo) 配置另一个 VBO,这会直接影响当前 VAO;
  • 如果无意中调用了 glVertexAttribPointer(),也会污染已经配置好的顶点属性。

解绑的实际效果:

当调用 GLES30.glBindVertexArray(0) 时,相当于告诉 OpenGL 当前不再操作任何 VAO。以后对 VBO、EBO 或顶点属性的任何修改都不会影响当前 VAO。

draw() 中,VAO 仍然可以再次绑定:

解绑 VAO 并不会清除记录在 VAO 内部的状态。VAO 是一个离线的 "状态包",在以后需要时(比如 draw 方法中),你可以重新绑定:

java 复制代码
GLES30.glBindVertexArray(mVaoId);

再次绑定 VAO 后,之前的顶点属性设置、VBO 和 EBO 的绑定状态都会完全恢复。


具体场景解释:initVaodraw 的逻辑分工

  • initVao 是初始化阶段,在这一步完成 VAO 的状态记录,确保 VAO 捕捉到 VBO、EBO 和顶点属性的完整绑定逻辑。解绑 VAO 可以防止后续代码污染它。
  • draw 是绘制阶段。在需要绘制时,可以重新绑定 VAO 恢复其状态,无需重复设置 VBO、EBO 或顶点属性。

可以选择不解绑 VAO 吗?

技术上可以,但不是好习惯。

  • 如果在 initVao() 中不解绑 VAO,绘制功能仍然正常,但后续对其他顶点数据或渲染状态的修改可能意外污染当前 VAO。具体而言,其他地方的错误会难以追踪,因为开发者可能忘记哪些状态被 VAO 无意修改了
  • 解绑 VAO 是为了明确状态,特别是在复杂的渲染场景下(有多个对象且每个对象有自己 VAO 时)。解绑后可以较好地遵循"一个对象只负责自己的状态"的准则。

问题二的结论

  • GLES30.glBindVertexArray(0) 的目的是防止意外修改 VAO 状态
  • draw 方法可以重新绑定 VAO,此时 VAO 恢复记录的状态,不会丢失任何内容。
  • 在初始化阶段解绑 VAO 是一个良好的开发习惯,以避免潜在的调试难题。

总结:

问题一的小结:为什么 draw 中只解绑 VAO?

  • 因为 VAO 已经封装了所有与顶点属性、VBO 和 EBO 的绑定状态。在解绑 VAO 时,这些绑定状态会自动解除,不需要单独解绑 VBO 或 EBO。
  • VAO 的目的是简化 OpenGL 中复杂的绑定管理,手动解绑 VBO 或 EBO 会破坏这种机制。

问题二的小结:为什么初始化时要解绑 VAO?

  • 在初始化阶段解绑 VAO 是为了防止后续的其他逻辑修改 VAO 的绑定状态。
  • 解绑 VAO 后不会影响 VAO 内部记录的配置状态,可以随时在 draw() 中重新绑定并恢复这些状态。
  • 这是 OpenGL 编程中常见的安全实践,避免意外修改或错用 VAO。

整体原则:

  1. 在使用 VAO 时,尽量只操作 VAO,而尽可能少直接操作 VBO 和 EBO。
  2. 初始化阶段解绑 VAO 是为了保护其状态,确保状态修改的独立性。
  3. 绘制阶段只需要绑定和解绑 VAO 即可,VBO 和 EBO 的状态由 VAO 管理,不需要再手动解绑。

五、总结:

本文主要介绍了VBO、EBO、VAO,大家掌握了基本概念和使用之后,其实也就掌握了一种产品性能优化的思路。

相关推荐
nono牛1 小时前
ps -A|grep gate
android
未知名Android用户2 小时前
Android动态变化渐变背景
android
行业探路者2 小时前
二维码标签是什么?主要有线上生成二维码和文件生成二维码功能吗?
学习·音视频·语音识别·二维码·设备巡检
nono牛3 小时前
Gatekeeper 的精确定义
android
stevenzqzq5 小时前
android启动初始化和注入理解3
android
城东米粉儿6 小时前
compose 状态提升 笔记
android
粤M温同学7 小时前
Android 实现沉浸式状态栏
android
Android系统攻城狮7 小时前
Android16音频之获取Record状态AudioRecord.getState:用法实例(一百七十七)
音视频·android16·音频进阶
ljt27249606617 小时前
Compose笔记(六十八)--MutableStateFlow
android·笔记·android jetpack