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,大家掌握了基本概念和使用之后,其实也就掌握了一种产品性能优化的思路。

相关推荐
User_undefined25 分钟前
uniapp Native.js 调用安卓arr原生service
android·javascript·uni-app
安小牛27 分钟前
android anr 处理
android
璀璨星輝1 小时前
安全删除硬件并弹出媒体(弹出显卡)问题处理
媒体
刘争Stanley3 小时前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏
野蛮的大西瓜3 小时前
BigBlueButton视频会议 vs 华为云会议的详细对比
人工智能·自动化·音视频·实时音视频·信息与通信·视频编解码
sickworm陈浩4 小时前
Java 转 Kotlin 系列:究竟该不该用 lateinit?
android·kotlin
野蛮的大西瓜4 小时前
文心一言对接FreeSWITCH实现大模型呼叫中心
人工智能·机器人·自动化·音视频·实时音视频·文心一言·信息与通信
野蛮的大西瓜5 小时前
BigBlueButton视频会议 vs 钉钉视频会议系统的详细对比
人工智能·自然语言处理·自动化·音视频·实时音视频·信息与通信·视频编解码
一只特立独行的程序猿6 小时前
Android音频中常用的BT Format和Profile介绍
android·音视频·bt profile·bt format
韩曙亮7 小时前
【FFmpeg】解封装 ① ( 封装与解封装流程 | 解封装函数简介 | 查找码流标号和码流参数信息 | 使用 MediaInfo 分析视频文件 )
ffmpeg·音视频·视频流·mediainfo·解封装·码流