一、前言:
之前代码,我们都是直接在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 后,使用
-
解绑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如何解释顶点数据)。这可以通过 glVertexAttribPointer
和 glEnableVertexAttribArray
实现。
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 类型,表示这个缓冲区将用于存储索引数据。
javaint[] ebo = new int[1]; GLES30.glGenBuffers(1, ebo, 0); GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
-
-
传递索引数据到 EBO:
-
定义索引数据,通常是一个数组,指定顶点数组中顶点的索引顺序。
-
创建一个 ByteBuffer 或者 IntBuffer 来存储索引数据,并将数据传递给 EBO。
javashort[] 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
,并指定绘制的图元类型、索引数据的数量以及数据类型。
javaGLES30.glDrawElements(GLES30.GL_TRIANGLES, indices.length, GLES30.GL_UNSIGNED_SHORT, 0);
-
-
解绑和清理:
-
在不再需要 EBO 时,解绑缓冲区对象并删除它。
javaGLES30.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()
在GLSurfaceViewTest
的onDestroy()
被调用;
文件路径:com/example/glsurfaceviewdemo/GLSurfaceViewTest.java
java
public class GLSurfaceViewTest extends GLSurfaceView {
private GLRenderTest mGlRenderTest;
// ... 删除非关键代码
public void onDestroy() {
mGlRenderTest.onDestroy();
}
}
GLSurfaceViewTest
的onDestroy()
将会在MainActivity
的onDestroy()
中被调用:
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 中,绘制一个几何图形通常需要以下步骤:
- 顶点数据存储:将顶点数据存储在 GPU 的缓冲区对象(如 VBO, Vertex Buffer Object)中;
- 索引数据存储:如果使用索引绘制,则需要将索引数据存储在 EBO(Element Buffer Object)中;
- 顶点属性配置 :通过调用
glVertexAttribPointer
配置顶点数据在缓冲区中的存储布局(例如位置、法线、纹理坐标等); - 绑定状态:每次绘制时需要重新绑定这些缓冲区对象,并重新设置对应的顶点属性。
为了简化这些繁琐的绑定和配置过程,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 的变量,比如:
javaint[] vao = new int[1]; GLES30.glGenVertexArrays(1, vao, 0); // vao[0] 存储第一个生成的 VAO 的 ID。
-
可以生成多个 VAO:
javaint[] 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 时使用:
javaGLES30.glBindVertexArray(vao[0]);
-
如果不再需要任何 VAO,解除绑定:
javaGLES30.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
是定义一个三角形的数据,整个渲染过程简化为:
- 初始化 VAO 后,只需
glBindVertexArray(vao[0])
即能自动配置所有顶点缓冲区和属性; - 解除绑定或切换 VAO 后,可以为其他几何图形绘制新的 VAO 状态。
注意事项
- VAO 不直接存储数据 :
- VAO 本质上是一个状态管理对象,记录与顶点属性相关的绑定状态。
- 数据实际存储在绑定的 VBO 或 EBO 中。
- 解绑谨慎 :
- 在配置完成后解绑 VAO 是一个好习惯,避免意外修改 VAO 状态。
- 在绘制阶段不需要频繁解绑 VAO,除非需要切换到其他 VAO。
- 上下文限制 :
- 如果在多种顶点数据形式之间切换(如多个物体需要不同的顶点数据),可以使用多个 VAO 分别记录不同的状态。
利用 glGenVertexArrays
和 glBindVertexArray
的 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
}
}
-
添加了 VAO 支持:
- 增加了
mVaoId
用于存储 VAO 的 ID。 - 在
initVao
方法中,生成 VAO 并绑定后,将 VBO 和 EBO 的绑定、顶点属性配置都记录到 VAO。
- 增加了
-
修改了 VBO 和 EBO 的绑定方式:
- 原代码中,VBO 和 EBO 在
draw()
中每次都需要绑定。改为在initVao()
中通过 VAO 封装一次,在绘制时只需绑定 VAO。
- 原代码中,VBO 和 EBO 在
-
绘制时只需绑定 VAO 而不是分别绑定 VBO 和 EBO:
- 原代码需要分别绑定 VBO 和 EBO,然后设置顶点属性。改为直接绑定 VAO,简化绘制代码。
-
新增 VAO 的删除操作:
-
在
release
方法中,新增 VAO 的清理代码:javaGLES30.glDeleteVertexArrays(1, new int[]{mVaoId}, 0);
-
-
代码简化:
- 绘制过程只需绑定 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 的绑定状态都会完全恢复。
具体场景解释:initVao
和 draw
的逻辑分工
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。
整体原则:
- 在使用 VAO 时,尽量只操作 VAO,而尽可能少直接操作 VBO 和 EBO。
- 初始化阶段解绑 VAO 是为了保护其状态,确保状态修改的独立性。
- 绘制阶段只需要绑定和解绑 VAO 即可,VBO 和 EBO 的状态由 VAO 管理,不需要再手动解绑。
五、总结:
本文主要介绍了VBO、EBO、VAO,大家掌握了基本概念和使用之后,其实也就掌握了一种产品性能优化的思路。