1.1 顶点缓冲区
将绘制物体的顶点数据保存在内存中,在调用glDrawArrays或者glDrawElements等绘制方法前需要调用相应的方法将数据送入显存,I/O开销大,性能不够好。
若采用顶点缓冲区对象存放顶点数据,则不需要在每次绘制前都将顶点数据复制进显存,而是在初始化顶点缓冲区对象时一次性将顶点数据送入显存,每次绘制时直接使用显存中的数据,可以大大提高渲染性能。
1.1.1 基本知识
OpenGL ES 3.0中支持两种类型的顶点缓冲区对象,分别为数组缓冲区对象(Array Buffer)和元素数组缓冲区对象(Element Array Buffer),具体情况如下:
数组缓冲区对象对应的类型为GL_ARRAY_BUFFER,一般用于存放待绘制物体的顶点相关数据,如顶点坐标、纹理坐标、法向量等。
元素数组缓冲区对象对应的类型为GL_ELEMENT_ARRAY_BUFFER,一般用于存放图元的组装索引数据,在使用索引法进行绘制时使用。
了解了两种类型的顶点缓冲区对象后,下面接着介绍缓冲区对象的一些常用操作方法。主要包括创建缓冲区的方法glGenBuffers、绑定缓冲区的方法glBindBuffer、向缓冲区送入数据的方法glBufferData和glBufferSubData、删除缓冲区的方法glDeleteBuffers以及查询指定缓冲区相关信息的方法glGetBufferParameteriv等,这些方法都可以通过GLES30类调用。
- glGenBuffers方法
glGenBuffers方法用于创建缓冲对象,是在使用每个自定义缓冲前都需要调用的,其方法签名如下。
1 public static void glGenBuffers (int n, IntBuffer buffers)
2 public static void glGenBuffers (int n, int[] buffers, int offset)
第一个用于创建缓冲对象的glGenBuffers方法共有2个参数,其中参数n为需要创建的缓冲区数量,参数buffers为用于存放创建的n个缓冲区编号的IntBuffer。
第二个用于创建缓冲对象的glGenBuffers方法共有3个参数,其中参数n为需要创建的缓冲区数量,参数buffers为用于存放创建的n个缓冲区编号的数组,参数offset为buffers数组所需的偏移量。
说明
请读者注意,通过此方法获得的缓冲区编号是0以外的无符号整数,0号缓冲OpenGL ES内部保留使用,不用于自定义缓冲。
- glBindBuffer方法
glBindBuffer方法用于绑定当前缓冲区对象,第一次调用glBindBuffer方法绑定缓冲区对象时,缓冲区对象以默认状态分配;如果分配成功,则分配的对象绑定为当前缓冲区对象。该方法签名如下。
1 public static void glBindBuffer (int target, int buffer)
说明
参数target用于描述需绑定的缓冲区类型,其可能的取值如表1-1所列;参数buffer为需要绑定的缓冲区的编号。
提示
表1-1中的target参数值,不仅仅用于glBindBuffer方法中,接下来将要介绍的glBufferData和glBufferSubData方法中都将用到,请读者注意。
- glBufferData方法
glBufferData方法一般用于向指定缓冲中送入数据,也可以用于对指定缓冲进行相关的存储空间初始化,其签名方法如下。
1 public static void glBufferData (int target, int size, Buffer data, int usage)
说明
参数target用于描述指定的缓冲区类型(如表1-1所列);参数size用于给出缓冲区的大小(单位为字节);参数data为需要送入缓冲的数据,若没有数据要送入缓冲区,其值可以为null;参数usage用于指定缓冲区的用途,其可能的取值如表1-2所列。
提示
请读者注意,usage参数仅仅用于辅助性描述指定缓冲的用途,在有些情况下可以帮助渲染管线优化操作,并不是强制性的。如在应用程序中将缓冲区用途设置为GL_STATIC_DRAW,但还是可以对缓冲数据进行多次修改。一般来说,设置的用途如果和实际使用匹配的话将更有利于渲染管线工作,因此并不建议读者随便设置。
- glBufferSubData方法
glBufferSubData方法一般用于向指定缓冲中送入部分数据进行初始化或者更新,其方法签名如下。
1 public static void glBufferSubData (int target, int offset, int size, Buffer data)
说明
参数target用于描述指定的缓冲区类型(如表1-1所列);参数offset用于给出缓冲区被修改的数据的起始内存偏移量;参数size用于给出缓冲区中数据被修改的字节数;参数data为需要送入缓冲的数据。
- glDeleteBuffers方法
glDeleteBuffers方法用于删除指定的缓冲区对象,其方法签名如下。
1 public static void glDeleteBuffers (int n, IntBuffer buffers)
2 public static void glDeleteBuffers (int n, int[] buffers, int offset)
第一个用于删除指定缓冲区对象的glDeleteBuffers方法共有两个参数,其中参数n为将要被删除的缓冲区对象数量,参数buffers为存储了n个要删除的缓冲区编号的IntBuffer。
第二个用于删除指定的缓冲区对象的glDeleteBuffers方法共有3个参数,其中参数n为将要被删除的缓冲区对象数量,参数buffers为存储了n个要删除缓冲区编号的数组,参数offset为数组所需的偏移量。
说明
在实际开发中,不再需要的缓冲区应该尽早用glDeleteBuffers方法删除,以便及时释放资源,提高系统的运行效率。
- glGetBufferParameteriv方法
前面介绍了缓冲区对象的创建、绑定以及初始化等相关方法,在运行中还可以调用glGetBufferParameteriv方法查询指定缓冲区的信息,其方法签名如下。
1 public static void glGetBufferParameteriv (int target, int pname, IntBuffer params)
2 public static void glGetBufferParameteriv (int target, int pname, int[] params, int offset)
说明
参数target用于描述指定的缓冲区类型(如表1-1所列);参数pname为要查询的信息项目,其可能的取值如表1-3所列;params参数用于存放查询的结果。
提示
当第一次调用glBindBuffer方法绑定缓冲区对象时,GL_BUFFER_SIZE初始值为0。GL_BUFFER_USAGE的详细取值如表1-2所列,初始值为GL_STATIC_DRAW。
1.1.2 一个简单的案例
了解了顶点缓冲区的基本知识以后,下面将给出一个使用了顶点缓冲区的简单案例——Sample1_1,其具体运行效果如图1-1所示。
提示
运行本案例时,当手指在屏幕上上下左右滑动时,相应的物体也会绕x轴和y轴旋转。
了解了本小节案例的运行效果后,就可以进行案例的开发了,具体步骤如下。
(1)首先用3dsMax生成两个基本物体(茶壶和软管),贴好纹理,并导出生成obj文件放入项目的assets目录下待用。
(2)开发出搭建场景的基本代码,包括加载物体、摆放物体、计算光照等。这些代码与上卷中许多案例中的基本套路完全一致,这里不再赘述。
(3)顶点缓冲区对象主要用于存储物体的顶点数据,以备在绘制时使用,与其相关的操作都在代表加载物体的LoadedObjectVertexNormalTexture类中,具体内容如下。
1 package com.bn.Sample1_1;
2 ……//此处省略了导入一些相关类的代码,请读者自行查看随书中的源代码
3 public class LoadedObjectVertexNormalTexture{
4 ……//此处省略了部分成员变量声明的代码,请读者自行查看随书中的源代码
5 int maPositionHandle; //顶点位置属性引用
6 int maNormalHandle; //顶点法向量属性引用
7 int maTexCoorHandle; //顶点纹理坐标属性引用
8 int mVertexBufferId; //顶点坐标数据缓冲编号
9 int mNormalBufferId; //顶点法向量数据缓冲编号
10 int mTexCoorBufferId; //顶点纹理坐标数据缓冲编号
11 public LoadedObjectVertexNormalTexture(
12 MySurfaceView mv,float[] vertices,float[] normals,float texCoors[]){
13 //调用初始化顶点数据的方法
14 initVertexData(vertices,normals,texCoors);
15 //调用初始化着色器的方法
16 initShader(mv);
17 }
18 public void initVertexData(float[] vertices,float[] normals,float texCoors[]){
19 int[] buffIds=new int[3]; //缓冲编号数组
20 GLES30.glGenBuffers(3, buffIds, 0); //生成3个缓冲
21 mVertexBufferId=buffIds[0]; //顶点坐标数据缓冲编号
22 mNormalBufferId=buffIds[1]; //顶点法向量数据缓冲编号
23 mTexCoorBufferId=buffIds[2]; //顶点纹理坐标数据缓冲编号
24 vCount=vertices.length/3; //计算顶点数量
25 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
//创建顶点坐标数据缓冲
26 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
27 FloatBuffer mVertexBuffer = vbb.asFloatBuffer();//转换为Float型缓冲
28 mVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据
29 mVertexBuffer.position(0); //设置缓冲区起始位置
30 //绑定到顶点坐标数据缓冲
31 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);
32 //向顶点坐标缓冲中送入数据
33 GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER,
34 vertices.length*4, mVertexBuffer, GLES30.GL_STATIC_DRAW);
35 ……//此处纹理坐标数据相关代码与上述代码相似,请读者自行查看随书中的源代码
36 ……//此处法向量数据相关代码与上述代码相似,请读者自行查看随书中的源代码
37 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0); //绑定到系统默认缓冲
38 }
39 public void initShader(MySurfaceView mv){/*代码省略*/}
40 public void drawSelf(int texId){ /*此处省略drawSelf方法,将在后面详细介绍*/}
41 }
- 第1~第17行为成员变量的声明以及构造器代码,这部分代码大部分与上卷介绍obj模型加载的案例相同,主要是增加了声明缓冲区编号的3个成员变量。
- 第19~第23行通过调用glGenBuffers方法创建了三个缓冲,并将获得的缓冲编号存储到了对应的成员变量中。
- 第25~第29行创建了内存缓冲,并将顶点坐标数据存入内存缓冲中,以备后面将内存缓冲中的顶点坐标数据送入顶点缓冲区。
- 第30~第34行首先绑定了顶点坐标缓冲,然后将内存缓冲中的顶点坐标数据送入对应的顶点缓冲区中,以备在绘制物体时使用。
(4)使用了顶点缓冲区之后,绘制物体的drawSelf方法有一些变化,那就是在每次绘制物体时不需要将顶点数据重复送入渲染管线了,而是直接使用前面步骤中初始化时存放到顶点缓冲区中的相关数据进行绘制,具体代码如下。
1 public void drawSelf(int texId) {
2 GLES30.glUseProgram(mProgram); //指定使用某套着色器程序
3 //将最终变换矩阵传入渲染管线
4 GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
5 //将基本变换矩阵传入渲染管线
6 GLES30.glUniformMatrix4fv(muMMatrixHandle, 1, false, MatrixState.getMMatrix(), 0);
7 //将光源位置传入渲染管线
8 GLES30.glUniform3fv(maLightLocationHandle, 1, MatrixState.lightPositionFB);
9 //将摄像机位置传入渲染管线
10 GLES30.glUniform3fv(maCameraHandle, 1, MatrixState.cameraFB);
11 GLES30.glEnableVertexAttribArray(maPositionHandle); //启用顶点位置数据数组
12 GLES30.glEnableVertexAttribArray(maNormalHandle); //启用法向量数据数组
13 GLES30.glEnableVertexAttribArray(maTexCoorHandle); //启用纹理坐标数据数组
14 //绑定到顶点坐标数据缓冲
15 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);
16 //指定顶点位置数据使用对应缓冲
17 GLES30.glVertexAttribPointer (maPositionHandle,3, GLES30.GL_FLOAT, false,3*4,0);
18 //绑定到顶点法向量数据缓冲
19 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mNormalBufferId);
20 //指定顶点法向量数据使用对应缓冲
21 GLES30.glVertexAttribPointer (maNormalHandle, 3, GLES30.GL_FLOAT, false, 3*4,0);
22 //绑定到顶点纹理坐标数据缓冲
23 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mTexCoorBufferId);
24 //指定顶点纹理坐标数据使用对应缓冲
25 GLES30.glVertexAttribPointer(maTexCoorHandle, 2, GLES30.GL_FLOAT, false, 2*4,0 );
26 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0); //绑定到系统默认缓冲
27 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //激活纹理
28 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId); //绑定纹理
29 GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount); //绘制加载的物体
30 }
- 第2~第13行指定使用某套着色器程序,将最终变换矩阵、基本变换矩阵、光源位置、摄像机位置等传入渲染管线,并启用顶点位置数据数组、法向量数据数组、纹理坐标数据数组。这部分代码与前面很多案例中的代码相同。
- 第14~第26行首先绑定到顶点坐标数据缓冲,并指定绘制时的顶点位置数据从此缓冲中获取,接着绑定到顶点法向量数据缓冲,并指定绘制时顶点法向量数据从此缓冲中获取。然后绑定到顶点纹理坐标数据缓冲,并指定绘制时顶点纹理坐标数据从此缓冲中获取。最后绑定到系统默认缓冲,这一步初学者容易忘记,请读者注意。
- 第27~第29行激活并绑定了所需的纹理,然后进行物体的绘制,与前面很多案例相同。
提示
从上述案例可以看出,将普通的每次绘制时送入顶点数据的应用修改为使用顶点缓冲一次送入数据的版本很容易。只需要在初始化时将数据送入对应的顶点缓冲,在绘制时指定使用即可,整体代码变化不大。但运行效率会有明显差别,因此在实际开发中读者应该尽量使用缓冲。