1.4 映射缓冲区对象

前面几节已经介绍了顶点缓冲区对象、顶点数组对象以及一致缓冲区对象,通过使用这些技术可以在很大程度上提高绘制效率。本节将介绍在某些情况下可以进一步提高效率的映射缓冲区对象(Mapping Buffer Objects)。

1.4.1 基本知识与案例效果

本章前面的几个案例中,都是通过调用glBufferData方法或者glBufferSubData方法向缓冲区中送入数据或者更新数据的。采用这种策略时需要首先将数据在内存中准备好,然后再通过glBufferData方法或glBufferSubData方法将数据从内存复制到显存中。

这对于缓冲中数据不变或变化率很低的情况基本够用了,但是对于绘制过程中频繁变化的数据就显得效率不够高。本小节将介绍一种针对此问题的解决方案——映射缓冲区对象。通过使用映射缓冲区对象,可以在绘制过程中数据频繁变化的情况下进一步减少内存消耗并提高渲染效率。

提示 所谓映射缓冲区对象就是将显存中的存储映射到虚拟的内存地址上,使得开发人员可以使用如同访问内存一样的API访问显存以提高效率。

使用映射缓冲区对象主要涉及的方法有3个,具体内容如下。

❑ glMapBufferRange方法

glMapBufferRange方法用于将指定缓冲对应的显存映射到虚拟的内存地址上,并返回映射的结果,以便开发人员使用它来更新显存中的数据。如果出现错误或者发出无效请求,该方法将返回空,其具体方法签名如下。

      1    public static Buffer glMapBufferRange (int target, int offset, int length, int access)

说明 参数target用于描述需映射的缓冲区类型,可以设置的值如本章前面表1-1所列;参数offset为被映射的缓冲区数据存储中的偏移量;参数length为需要映射的缓冲区数据字节数;参数access为访问标志,可选的访问标志如表1-6所列。

表1-6 访问标志及说明

提示 表1-6中的访问标志在不冲突的情况下可以同时使用多个标志,使用多个标志时用“|”隔开。另外,实际开发中一般至少选用GL_MAP_READ_BIT与GL_MAP_WRITE_BIT中的一个,而其他选项则进一步根据需要选择即可。

❑ glUnmapBuffer方法

glUnmapBuffer方法用于解除缓冲区映射,其具体方法签名如下。

      1   public static boolean glUnmapBuffer (int target)

说明 参数target用于描述需解除映射的缓冲区类型,可以设置的值如本章前面表1-1所列。如果解除映射操作成功,则返回true,并且前面glMapBufferRange方法返回的映射范围在取消映射操作成功之后不再可用。如果顶点缓冲区对象数据存储中的数据在缓冲区映射之后已经破坏,则glUnmapBuffer方法返回false。

❑ glFlushMappedBufferRange方法

glFlushMappedBufferRange方法用于通知渲染管线被映射缓冲区中的数据已经被修改,类似于I/O操作时用于刷新数据的flush方法,其具体方法签名如下。

      1    public static void glFlushMappedBufferRange (int target, int offset, int length
      )

说明 参数target用于描述需刷新数据所属的被映射缓冲区类型;参数offset为被映射的缓冲区数据存储中的偏移量;参数length为需要刷新的被映射缓冲区数据字节数。需要注意的是,glFlushMappedBufferRange方法所操作的缓冲必须用glMapBufferRange方法在映射时选用了GL_MAP_FLUSH_EXPLICIT_BIT选项。

了解了映射缓冲区对象的基本知识以后,就可以进行案例的开发了。在开发案例之前,首先应该了解本节案例Sample1_4的运行效果,具体情况如图1-4所示。

▲图1-4 Sample1_4的运行效果图

说明 从图1-4中可以看出,运行过程中球体的上半部分在球体与立方体之间连续变换着。这是由于运行过程中程序不断进行缓冲区映射,并连续更新球体上半部分的顶点数据。

1.4.2 案例开发步骤

了解了映射缓冲区对象的基本知识与案例效果后,就可以进行代码的开发了。由于本案例中的很多类与前面案例中的很相似,因此这里仅给出本案例中具有特殊性及代表性的代码,具体内容如下。

(1)首先介绍的是BallAndCube类中用于初始化顶点数据的initVertexData方法,在该方法中向顶点坐标数据缓冲中送入数据时就采用了映射缓冲区,具体内容如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

      1    public void initVertexData() {                    //初始化顶点数据的方法
      2         int[] buffIds=new int[3];                    //用于存放缓冲id的数组
      3         GLES30.glGenBuffers(3, buffIds, 0);          //生成3个缓冲id
      4         mVertexBufferId=buffIds[0];                  //顶点坐标数据缓冲 id
      5         mTexCoorBufferId=buffIds[1];                 //顶点纹理坐标数据缓冲 id
      6         mIndicesBufferId=buffIds[2];                 //顶点索引数据缓冲id
      7         ……//此处省略了用于初始化纹理坐标缓冲区和索引缓冲区的代码,需要的读者请参考随书
      8         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVertexBufferId); //绑定顶点坐标数据缓冲
      9         GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, vertices.length*4,
      10                 null, GLES30.GL_STATIC_DRAW);       //开辟缓冲存储
      11        vbb1=(ByteBuffer)GLES30.glMapBufferRange(    //映射顶点坐标数据缓冲
      12         GLES30.GL_ARRAY_BUFFER,                     //缓冲类型
      13         0,                                          //偏移量
      14         vertices.length*4,                          //长度(以字节计)
      15         GLES30.GL_MAP_WRITE_BIT|GLES30.GL_MAP_INVALIDATE_BUFFER_BIT); //访问标志
      16        if(vbb1==null){return; }                      //若映射失败则返回
      17        vbb1.order(ByteOrder.nativeOrder());         //设置字节顺序
      18        mVertexMappedBuffer=vbb1.asFloatBuffer();    //转换为Float型缓冲
      19        mVertexMappedBuffer.put(vertices);           //向映射缓冲区中放入顶点坐标数据
      20        mVertexMappedBuffer.position(0);             //设置缓冲区起始位置
      21        if(GLES30.glUnmapBuffer(GLES30.GL_ARRAY_BUFFER)==false){return; }//解除缓冲映射
      22        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0); //绑定到系统默认缓冲
      23    }

说明 上述代码中最有代表性的就是第8~第21行,其中没有使用传统的glBufferData方法向顶点坐标数据缓冲中送入数据,而是使用glMapBufferRange方法先进行缓冲映射,然后再直接将数据送入映射后的缓冲中。需要注意的是,每次映射并更新数据完毕后,都需要使用glUnmapBuffer方法解除映射,否则渲染管线在绘制时,无法正常使用被映射缓冲区中的数据。

(2)了解了用于初始化顶点数据的initVertexData方法后,接下来要介绍的是用于在运行过程中连续计算顶点坐标的方法calVertices和插值方法insertValue,具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

      1    public void calVertices(int count, boolean flag){      //计算顶点坐标数据的方法
      2         for(int i=0; i<vertices.length/2; i++){            //遍历顶点
      3               curBallForCal[i]=insertValue(vertices[i], verticesCube[i], span, count,
                      flag);    //调用插值方法
      4         }
      5         synchronized(lock){            //加锁同步,避免多线程并发操作可能带来的问题
      6             curBallForDraw=Arrays.copyOf(curBallForCal, curBallForCal.length);
                    //复制数据
      7    }}
      8    public float insertValue(float start, float end, float span, int count, boolean isB
            allToCubeY){ //插值方法
      9         float result=0;
      10        if(isBallToCubeY){                            //如果是球到立方体的变化
      11              result=start+count*(end-start)/span;    //进行顶点坐标插值计算
      12        }else{                                        //如果是立方体到球的变化
      13              result=end-count*(end-start)/span;      //进行顶点坐标插值计算
      14        }
      15        return result;                                //返回插值结果
      16    }

❑ 第1~第7行为在运行过程中不断被调用以计算当前顶点坐标位置为从球到立方体或从立方体到球变化服务的calVertices方法。此方法每次被调用时遍历每个顶点坐标,根据count参数以及flag参数值调用insertValue方法完成插值计算。需要特别注意的是第5~第7行,每次计算完成后,将数据复制进绘制用数组时都需要加锁,以避免复制的同时进行绘制造成画面撕裂的问题。

❑ 第8~第15行为在球与立方体之间进行顶点坐标插值计算的insertValue方法,从代码中可以看出此方法采用的是线性插值。

(3)接下来详细介绍用于更新顶点数据到映射缓冲区的方法updateMapping,其功能为根据接收到的顶点坐标数据更新顶点缓冲区对象中的数据,具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

      1    public void updateMapping(float[] currVertex){
      2         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,
      3                            mVertexBufferId);      //绑定到顶点坐标数据缓冲
      4         vbb1=(ByteBuffer)GLES30.glMapBufferRange(GLES30.GL_ARRAY_BUFFER, 0,
      5               currVertex.length*4, GLES30.GL_MAP_WRITE_BIT|
      6              GLES30.GL_MAP_INVALIDATE_BUFFER_BIT); //进行缓冲区映射
      7         if(vbb1==null){return; }                   //若映射失败则返回
      8         vbb1.order(ByteOrder.nativeOrder());      //设置字节顺序
      9         mVertexMappedBuffer=vbb1.asFloatBuffer(); //转换为Float型缓冲
      10        mVertexMappedBuffer.put(currVertex);      //向映射的缓冲区中放入顶点坐标数据
      11        mVertexMappedBuffer.position(0);          //设置缓冲区起始位置
      12        if(GLES30.glUnmapBuffer(GLES30.GL_ARRAY_BUFFER)==false){return; }//解除映射
      13    }

说明 上述方法在运行中定时被调用,用于将计算出来的新的顶点坐标数据更新到对应的缓冲中,供渲染管线在绘制时使用,主要套路与前面initVertexData方法中的对应部分相同。

(4)了解了在绘制过程中不断被调用以便计算与更新顶点坐标的相关方法后,下面来了解一下定时执行BallAndCube类中calVertices方法的线程类——UpdateThread,其具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的UpdateThread.java。

      1    package com.bn.Sample1_4;                                    //声明包名
      2    public class UpdateThread extends Thread{
      3          ……//此处省略了一些定义成员变量和构造器的代码,读者可自行查阅随书附带中的源代码
      4          public void run(){
      5                while(true){
      6                    mv.mBallAndCube.calVertices(count, isBallCube); //计算顶点坐标数据
      7                     try{
      8                         count++;                             //计数器加1
      9                         if(count%mv.mBallAndCube.span==0){   //若达到一轮变化所需步骤
      10                             count=0;                        //重置计数器
      11                             isBallCube=! isBallCube;         //重置标志位
      12                         }
      13                        Thread.sleep(40);                    //休眠40毫秒
      14                   }catch(Exception e){
      15                         e.printStackTrace();
      16    }}}

说明 上述UpdateThread类非常简单,主要是在其run方法中定时调用calVertices方法更新顶点坐标数据。同时每次更新后会检查是否达到一轮变化所需的总步骤数,若达到了则将步骤计数器置0,并将变化方向标志位isBallCube(用于表示变化方向是从球到立方体还是立方体到球)置返。

(5)上一步介绍了定时执行BallAndCube类中calVertices方法的线程类——UpdateThread,接下来介绍BallAndCube类中的绘制方法drawSelf,其具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

      1    public void drawSelf(int texId){
      2         MatrixState.rotate(xAngle, 1, 0, 0);                  //绕x轴旋转
      3         MatrixState.rotate(yAngle, 0, 1, 0);                  //绕y轴旋转
      4         MatrixState.rotate(zAngle, 0, 0, 1);                  //绕z轴旋转
      5         GLES30.glUseProgram(mProgram);                        //指定使用某套着色器程序
      6         //将最终变换矩阵传入渲染管线
      7         GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
      8         GLES30.glEnableVertexAttribArray(maTexCoorHandle);      //启用纹理数据数组
      9         //绑定到顶点纹理坐标数据缓冲
      10        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mTexCoorBufferId);
      11        //指定顶点纹理坐标数据使用对应缓冲
      12        GLES30.glVertexAttribPointer(maTexCoorHandle, 2, GLES30.GL_FLOAT, false,2*4,0);
      13        //启用顶点位置数据数组
      14        GLES30.glEnableVertexAttribArray(maPositionHandle);
      15        //绑定到顶点位置坐标数据缓冲
      16        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVertexBufferId);
      17        //指定顶点位置坐标数据使用对应缓冲
      18        GLES30.glVertexAttribPointer(maPositionHandle,  3, GLES30.GL_FLOAT,  false,3*4,0);
      19        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);              //激活纹理
      20        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);       //绑定纹理
      21        synchronized(lock){                       //同步加锁
      22            updateMapping(curBallForDraw);        //更新顶点坐标数据缓冲中的顶点数据
      23        }
      24        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, mIndicesBufferId); //绑定索引缓冲
      25        GLES30.glDrawElements(GLES30.GL_TRIANGLES,  //以三角形方式执行绘制
      26                  iCount, GLES30.GL_UNSIGNED_INT, 0);
      27        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,0); //绑定到系统默认索引缓冲
      28        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0);      //绑定到系统默认数组缓冲
      29    }

❑ 第2~第4行设置物体绕x、y、z轴旋转指定的角度。

❑ 第5~第18行首先指定使用某套着色器程序,然后将最终变换矩阵送入渲染管线,接着绑定了顶点位置、纹理坐标缓冲,并指定顶点位置、纹理坐标使用对应的缓冲,同时还启用了顶点坐标以及纹理坐标数据数组。

❑ 第19~第23行激活并绑定了指定的纹理,然后同步加锁后调用updateMapping方法更新顶点数据。这里的同步加锁与前面calVertices方法中是对应的,目的是为了避免更新缓冲中数据的同时数据数组curBallForDraw被其他线程访问。

❑ 第24~第28行首先绑定索引缓冲对象,并以三角形方式执行绘制,最后绑定到系统默认的索引与数组缓冲。