OpenGL ES 索引缓冲区(4)
简述
本节会介绍索引缓冲区,索引缓冲区和顶点缓冲区类似,也是显存上的一段内存,只不过上面的数据用处不同,索引缓冲区故名思义里面的数据是用于索引,主要作用是用于复用顶点缓冲区里的数据。
我们之前说过OpenGL渲染都是渲染三角形,如果我们想渲染一个正方形,就要通过渲染两个三角形,拼接成一个正方形,那么这两个三角形有两个顶点是重合的,如果没有索引缓冲区,两个三角形则需要六个顶点,而实际上一个正方形只有四个顶点,这里有两个顶点时数据冗余。仅仅一个正方形就有这么多冗余,那么一个复杂的游戏场景就会浪费非常多的内存。
下面我们就以渲染一个正方形为例来使用索引缓冲区。
接口使用
索引缓冲区的接口使用方式和顶点缓冲区类似,只不过参数不同,这里使用的GL_ELEMENT_ARRAY_BUFFER表示索引缓冲区。
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, bufferId);
GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER,
size,
data,
type);
glDrawElements是用于渲染索引缓冲区,第一个和其他DrawCall一样,第二个参数是count,表示有多少顶点需要渲染,第三个参数是索引缓冲区参数类型,必须是GLES30.GL_UNSIGNED_SHORT或者GL_UNSIGNED_BYTE,第四个参数是offset。
GLES30.glDrawElements(GLES30.GL_TRIANGLES, count, type, offset)
不使用索引缓冲区渲染正方形
配置顶点数据
顶点数据如下,6个顶点。我我们可以发现第3个和第5个是一样的,第2个和第4个是一样的。
private float[] vertexArray = new float[] {
-0.25f, -0.25f, 0.0f,
0.25f, -0.25f, 0.0f,
-0.25f, 0.25f, 0.0f,
0.25f, -0.25f, 0.0f,
-0.25f, 0.25f, 0.0f,
0.25f, 0.25f, 0.0f,
};
配置着色器
着色器和我们三角形demo的时候基本是一样的,通过一个统一变量来控制颜色。
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
配置顶点缓冲区数据和布局
onSurfaceCreated中填充顶点缓冲区的数据以及加载着色器的逻辑和绘制三角形的时候一样。
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除颜色
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 创建顶点缓冲区
int[] idBuffer = new int[1];
GLES30.glGenBuffers(1, idBuffer, 0);
vertexBufferId = idBuffer[0];
// 顶点缓冲区数据填充
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
vertexArray.length * 4,
vertexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
// shader
shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}
onDrawFrame方法中其他的基本和绘制三角形的时候一样,参数布局其实也是一样的,变化的只有glDrawArrays中count变成了6个顶点。
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// 使能着色器程序
GLES30.glUseProgram(shaderProgramId);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
GLES30.glEnableVertexAttribArray(positionLocation);
// 告诉GPU顶点缓冲区的布局情况,即那些数据的意义是什么。
GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 0, 0);
// 配置统一变量,用于CPU和GPU通信的
int colorLocation = GLES30.glGetUniformLocation(shaderProgramId, "vColor");
GLES30.glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
// 调用DrawCall绘制三角形
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 6);
// 清除配置
GLES30.glDisableVertexAttribArray(positionLocation);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
GLES30.glUseProgram(0);
}
效果
渲染图像看起来不是正方形,而是长方形,是因为OpenGL的坐标比例是按照屏幕显示的比例,我们的手机屏幕是长方形的,所以按照比例也是长方形的。
使用索引缓冲区
我们前面看到了渲染这个正方形我们使用了6个顶点,而其中有2个顶点是重复的,我们使用索引缓冲区可以复用顶点。
配置顶点数据
顶点数据如下,indexArray为索引缓冲区的数据,必须要short或者byte类型。
这里索引缓冲区就是表示第一个三角形使用的顶点缓冲区中0,1,2号顶点,第二个三角形使用顶点缓冲区中1,2,3号顶点。
private float[] vertexArray = new float[] {
-0.25f, -0.25f, 0.0f,
0.25f, -0.25f, 0.0f,
-0.25f, 0.25f, 0.0f,
0.25f, 0.25f, 0.0f,
};
private short[] indexArray = new short[] {
0,1,2,
1,2,3
};
配置顶点缓冲区数据和布局
着色器和之前一样,顶点缓冲区配置修改如下,除了申请顶点缓冲区之外还申请了索引缓冲区。
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除颜色
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 申请两个缓冲区,一个作为顶点缓冲区,一个座位索引缓冲区
int[] idBuffer = new int[2];
GLES30.glGenBuffers(2, idBuffer, 0);
vertexBufferId = idBuffer[0];
elementBufferId = idBuffer[1];
// 顶点缓冲区数据填充
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
vertexArray.length * 4,
vertexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
// 索引缓冲区数据填充,类型必须为short/byte
ShortBuffer indexBuffer = ByteBuffer.allocateDirect(indexArray.length * 4).order(ByteOrder.nativeOrder()).asShortBuffer();
indexBuffer.put(indexArray);
indexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
GLES30.glBufferData(
GLES30.GL_ELEMENT_ARRAY_BUFFER,
indexArray.length * 4,
indexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
// shader
shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}
通过GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId)绑定索引缓冲区,然后调用glDrawElements来进行绘制。
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// 使能着色器程序
GLES30.glUseProgram(shaderProgramId);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
GLES30.glEnableVertexAttribArray(positionLocation);
// 告诉GPU顶点缓冲区的布局情况,即那些数据的意义是什么。
GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 0, 0);
// 配置统一变量,用于CPU和GPU通信的
int colorLocation = GLES30.glGetUniformLocation(shaderProgramId, "vColor");
GLES30.glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
// 绑定索引缓冲区
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
// 调用DrawCall绘制三角形
GLES30.glDrawElements(GLES30.GL_TRIANGLES, 6, GLES30.GL_UNSIGNED_SHORT, 0);
// 清除配置
GLES30.glDisableVertexAttribArray(positionLocation);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
GLES30.glUseProgram(0);
}
效果
和之前一样。
小结
索引缓冲区里面就是存储了一系列索引,用于复用顶点缓冲区。
有个问题,大家可能会想之前用6个顶点也不过18个数,使用了顶点缓冲区后变为12个,但是索引缓冲区还有6个,好像没有节省多少内存。
但是实际上顶点可能包含了非常多的数据,比如我们之前用它来存颜色,它还可以存纹理等额外数据,实际应用场景一个顶点可能有非常多数据,所以复用可节省的内存是非常可观的。