本节书摘来异步社区《OpenGL ES 3.x游戏开发(下卷)》一书中的第1章,第1.3节,作者: 吴亚峰 责编: 张涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.3 一致缓冲区对象
本丛书上卷第4章已经介绍过一致变量的相关知识,读者也已经了解到可以将多个一致变量的声明通过类似结构体形式的一致块来实现。对于一致块而言,宿主语言(如Java)将值传入渲染管线时就需要使用一致缓冲区对象,本节将介绍这方面的知识,同时还会给出一个简单的案例。
1.3.1 基本知识与案例效果
介绍具体的案例开发之前,有必要先了解一下与一致缓冲区对象相关的基础知识。主要包括存储形式和相关方法两个方面,具体内容如下。
1.存储形式
从前面的介绍中已经了解到,一致缓冲区是为了向一致块中传输数据服务的。一般情况下,一致块中会有多个不同类型的一致变量,这些变量在内存中存储时有一定的组织规律,具体内容如下。
bool、int、uint及float类型的一致块成员在内存中有特定的偏移量,分别作为单个bool、int、uint以及 float类型的分量。
基本类型bool、int、uint及float的向量保存在连续内存中,起始于特定的偏移量,第一个分量位于最低偏移量处。
C列R行的列主矩阵被当成一个C列浮点型向量的数组对待,每个向量包含R个分量。类似地,R行C列的行主矩阵可以看做一个R行浮点型向量数组,每个向量包含C个分量。无论是列向量还是行向量在内存中都是连续存储的。矩阵中两个向量之间的偏移量被称为列步幅或行步幅(GL_UNIFORM_MATRIX_STRIDE),可用glGetActiveUniformsiv方法查询其值。
标量、向量和矩阵按照元素的顺序存储在内存中,数组成员0位于最低偏移量处。数组元素之间的偏移量是一个常数,称为数组步幅(GL_UNIFORM_ARRAY_STRIDE),可以用glGetActiveUniformsiv方法查询其值。
2.相关方法
了解了一致块中各种一致变量在内存中的组织规律后,下面将进一步介绍与使用一致缓冲区相关的一些常用方法,主要包括用于获取一致块索引的glGetUniformBlockIndex方法、用于获取一致块名称的glGetActiveUniformBlockName方法、用于绑定一致缓冲区对象的glBindBufferRange方法等。具体内容如表1-4所列。
说明
表1-4中不仅介绍了有关一致缓冲区对象的相关方法,还包括一致块的相关方法,这主要是因为一致块与一致缓冲区对象相辅相成,缺一不可。由于在本书上卷第4章已经介绍了一致块的基础知识,在这里不再赘述,需要的读者请参考本书上卷中的相关内容。
表1-5给出了一致块属性及说明。
了解了一致缓冲区对象的基本知识以后,下面来看看本节案例Sample1_3的运行效果,以便在介绍案例的具体开发之前先有一个感性认识,具体情况如图1-3所示。
提示
运行本案例时,当手指在屏幕上上下左右滑动时,相应的物体也会绕x轴和y轴旋转。由于使用一致块与不使用一致块进行渲染得到的画面效果是相同的,只是执行效率不同,因此仅仅从运行效果上是体会不出本案例的特色的。
1.3.2 案例开发步骤
了解了一致缓冲区对象的基本知识以及本节案例的运行效果后,就可以进行代码的开发了。由于本案例中的很多类与上卷案例中的很相似,因此这里仅给出本案例中具有特殊性及代表性的代码,具体内容如下。
(1)首先介绍LoadedObjectVertexNormalTexture类中的initUBO方法,此方法中有用于获取一致块索引、获取一致块成员引用等一致块相关属性的代码,还有将一致块与一致缓冲区对象进行绑定的相关代码,具体内容如下。
1 public void initUBO() { //初始化一致缓冲的方法
2 blockIndex=GLES30.glGetUniformBlockIndex(mProgram, "MyDataBlock");
//获取一致块的索引
3 int[] blockSizes=new int[1]; //用于存储一致块尺寸的数组
4 GLES30.glGetActiveUniformBlockiv(mProgram, blockIndex, //获取一致块的尺寸
5 GLES30.GL_UNIFORM_BLOCK_DATA_SIZE, blockSizes, 0);
6 int blockSize=blockSizes[0]; //记录一致块的尺寸
7 //声明一致块内的成员名称数组
8 String[] names={"MyDataBlock.uLightLocation","MyDataBlock.uCamera"};
9 int[] uIndices=new int[names.length]; //声明对应的成员索引数组
10 GLES30.glGetUniformIndices(mProgram, names, uIndices, 0); //获取一致块内的成员索引
11 int[] offset=new int[names.length]; //用于记录一致块内成员偏移量的数组
12 GLES30.glGetActiveUniformsiv(mProgram, 2, //获取一致块内的成员偏移量
13 uIndices,0, GLES30.GL_UNIFORM_OFFSET, offset,0);
14 int[] uboHandles=new int[1]; //用于存储一致缓冲对象编号的数组
15 GLES30.glGenBuffers(1, uboHandles, 0); //创建一致缓冲对象
16 uboHandle=uboHandles[0]; //获取一致缓冲对象编号
17 //将一致缓冲对象绑定到一致块
18 GLES30.glBindBufferBase(GLES30.GL_UNIFORM_BUFFER,blockIndex,uboHandle);
19 //开辟存放一致缓冲所需数据的内存缓冲
20 ByteBuffer ubb = ByteBuffer.allocateDirect(blockSize);
21 ubb.order(ByteOrder.nativeOrder()); //设置字节顺序
22 FloatBuffer uBlockBuffer = ubb.asFloatBuffer(); //转换为Float型缓冲
23 float[] data=MatrixState.lightLocation; //获取光源位置
24 uBlockBuffer.position(offset[0]/BYTES_PER_FLOAT); //设置内存缓冲的位置到对应偏移量
25 uBlockBuffer.put(data); //将光源位置数据送入内存缓冲
26 float[] data1=MatrixState.cameraLocation; //获取摄像机位置
27 uBlockBuffer.position(offset[1]/BYTES_PER_FLOAT); //设置内存缓冲的位置到对应偏移量
28 uBlockBuffer.put(data1); //将摄像机位置数据送入内存缓冲
29 uBlockBuffer.position(0); //设置缓冲起始偏移量为0
30 //将光源位置、摄像机位置总数据内存缓冲中的数据送入一致缓冲
31 GLES30.glBufferData(GLES30.GL_UNIFORM_BUFFER,
32 blockSize,uBlockBuffer,GLES30.GL_DYNAMIC_DRAW);
33 }
- 第2~第6行的功能为获取一致块的索引值和一致块的尺寸,并记录其索引值和尺寸。
- 第7~第13行首先声明一致块内的成员名称数组,然后获取并记录一致块内成员变量对应的索引值和偏移量。
- 第14~第18行首先创建一致缓冲对象,并记录一致缓冲对象的编号,然后调用glBindBufferBase方法将一致缓冲对象与一致块进行绑定。
- 第19~第29行首先开辟存放一致缓冲所需数据的内存缓冲,设置字节顺序并转换为Float型缓冲,然后分别设置光源位置、摄像机数据在内存缓冲中的偏移量,将光源位置数据与摄像机位置数据送入内存缓冲,最后将内存缓冲起始的偏移量设置为0。
- 第30~第32行将存放光源位置与摄像机位置总数据的内存缓冲中的数据送入一致缓冲。
说明
设置数据在内存缓冲中的偏移量时,需要将offset除以BYTES_PER_FLOAT,该常量为每个浮点数的字节数。这是因为从着色器中获取的数据尺寸、偏移量等都是以字节为单位计算的,而本案例使用的内存缓冲中的位置、数据等是以float型计算的,若不进行换算则会出错。
(2)前面介绍了LoadedObjectVertexNormalTexture类中用于初始化一致缓冲对象的方法initUBO,接下来要介绍的是LoadedObjectVertexNormalTexture类的绘制方法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.glBindBufferBase(GLES30.GL_UNIFORM_BUFFER,blockIndex,uboHandle);
9 GLES30.glEnableVertexAttribArray(maPositionHandle); //启用顶点位置数据数组
10 GLES30.glEnableVertexAttribArray(maNormalHandle); //启用顶点法向量数据数组
11 GLES30.glEnableVertexAttribArray(maTexCoorHandle);//启用顶点纹理坐标数据数组
12 GLES30.glVertexAttribPointer(maPositionHandle,3, //将顶点位置数据送入渲染管线
13 GLES30.GL_FLOAT, false, 3*4,mVertexBuffer);
14 GLES30.glVertexAttribPointer (maNormalHandle, 3, //将顶点法向量送入渲染管线
15 GLES30.GL_FLOAT, false,3*4,mNormalBuffer);
16 GLES30.glVertexAttribPointer (maTexCoorHandle, 2, //将顶点纹理坐标数据送入渲染管线
17 GLES30.GL_FLOAT, false,2*4, mTexCoorBuffer );
18 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //激活纹理
19 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId); //绑定纹理
20 GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount); //绘制加载的物体
21 }
- 第2~第6行首先指定使用某套着色器程序,接着将总变换矩阵、基本变换矩阵送入渲染管线。
第7行为调用glBindBufferBase方法将一致块与一致缓冲对象进行绑定。 - 第8~第17行为启动顶点位置数据数组、法向量数据数组与纹理坐标数据数组,并将顶点位置数据、法向量数据以及纹理坐标数据送入渲染管线。
- 第18~第20行为激活和绑定纹理,并以三角形方式绘制加载的物体。
(3)介绍完Java部分的代码,下面就应该介绍着色器了。由于本案例中的片元着色器上卷很多案例中的类似,因此这里仅给出比较有代表性的顶点着色器,具体代码如下。
1 #version 300 es
2 uniform mat4 uMVPMatrix; //总变换矩阵
3 uniform mat4 uMMatrix; //基本变换矩阵
4 in vec3 aPosition; //顶点位置
5 in vec3 aNormal; //顶点法向量
6 in vec2 aTexCoor; //顶点纹理坐标
7 out vec4 ambient; //用于传递给片元着色器的环境光最终强度
8 out vec4 diffuse; //用于传递给片元着色器的散射光最终强度
9 out vec4 specular; //用于传递给片元着色器的镜面光最终强度
10 out vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标
11 uniform MyDataBlock{ //一致块
12 vec3 uLightLocation; //光源位置
13 vec3 uCamera; //摄像机位置
14 } mb; //一致块实例名
15 void pointLight( //定位光光照计算的方法
16 ......//此处省略了定位光光照计算方法的几个入口参数,需要的读者请参考随书
17 ){
18 ambient=lightAmbient; //直接得出环境光的最终强度
19 vec3 normalTarget=aPosition+normal; //计算变换后的法向量
20 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
21 newNormal=normalize(newNormal); //对法向量规格化
22 //计算从表面点到摄像机的向量
23 vec3 eye= normalize(mb.uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
24 //计算从表面点到光源位置的向量vp
25 vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
26 ......//此处省略了与前面案例中定位光光照计算方法相同的部分代码,需要的读者请参考随书
27 }
28 void main(){
29 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置
30 vec4 ambientTemp, diffuseTemp, specularTemp; //存放环境光、散射光、镜面光的临时变量
31 pointLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,//光照计算
32 mb.uLightLocation,vec4(0.15,0.15,0.15,1.0),vec4(0.9,0.9,0.9,1.0),
vec4(0.4,0.4,0.4,1.0));
33 ambient=ambientTemp; //将环境光传递给片元着色器
34 diffuse=diffuseTemp; //将散射光传递给片元着色器
35 specular=specularTemp; //将镜面光传递给片元着色器
36 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器
37 }
说明
上述代码中第11~第14行创建了名称为“MyDataBlock”的一致块,其实例名为“mb”。在主方法中,通过“mb. ULightLocation”访问一致块的成员变量uLightLocation;在计算光照的pointLight方法中,通过“mb. UCamera”访问一致块的成员变量uCamera。其余有关光照和顶点位置计算的相关代码与前面案例中的相同,这里不再赘述。
提示
从本案例中可以看出,通过把在绘制过程中一直不变或变化率很低的一致变量组织进一致块,并创建与之对应的一致缓冲,同时将一致块中的一致变量所需数据存入一致缓冲中,可以简化绘制时的一些操作,不必像原来那样每次绘制时都将每个一致变量的值单独重复传入渲染管线了,有利于提高绘制效率,降低开发成本。