Android显示系统(01)- 架构分析
Android显示系统(02)- OpenGL ES - 概述
Android显示系统(03)- OpenGL ES - GLSurfaceView的使用
Android显示系统(04)- OpenGL ES - Shader绘制三角形
Android显示系统(05)- OpenGL ES - Shader绘制三角形(使用glsl文件)
Android显示系统(06)- OpenGL ES - VBO和EBO和VAO
Android显示系统(07)- OpenGL ES - 纹理Texture
Android显示系统(08)- OpenGL ES - 图片拉伸
Android显示系统(09)- SurfaceFlinger的使用
Android显示系统(10)- SurfaceFlinger内部结构
Android显示系统(11)- 向SurfaceFlinger申请Surface
Android显示系统(12)- 向SurfaceFlinger申请Buffer
Android显示系统(13)- 向SurfaceFlinger提交Buffer
一、前言:
之前代码,我们都是直接在java代码中定义顶点数组,然后,将顶点数据存储在FloatBuffer
对象,最后,传递给GPU。如下所示:
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参与。
而我们绘制三角形时候怎么做的呢?看代码:
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 来存储顶点数据。这应该在构造函数中完成。
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
实现。
// 传递顶点数据到 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
实现。
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)修改后完整代码:
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(); // 将字节缓冲区转换为浮点缓冲区
mVertexBuff