3.1 OpenGL ES 3.x概述
随着4G时代的到来,Android与iPhone已经成为消费者购买智能手机的主要选择。而由于基于Android的智能手机性能优良、价格合适,因此Android智能手机得到了大多数用户的青睐。
随着Android系统版本及硬件水平的提升,OpenGL ES的版本也由原先支持自定义渲染管线的OpenGL ES 2.0逐渐升级为同样是支持自定义渲染管线的OpenGL ES 3.x。OpenGL ES 3.x新特性的添加使渲染的3D场景光影效果更加真实,从而能够创造全新的用户体验。
3.1.1 OpenGL ES 3.x简介
现今较为知名的3D图形API有OpenGL、DirectX以及OpenGL ES,其各自的应用领域如下。
❑ DirectX主要应用于Windows下游戏的开发,在此领域基本上一统天下。
❑ OpenGL的应用领域较为广泛,适用于UNIX、Mac OS、Linux以及Windows等几乎所有的操作系统,可以开发游戏、工业建模以及嵌入式设备应用。
❑ OpenGL ES是专门针对嵌入式设备而设计的,其实际是OpenGL的剪裁版本,去除了OpenGL中许多不是必须存在的特性,如GL_QUADS(四边形)与GL_POLYGONS(多边形)绘制模式以及glBegin(开始)/glEnd(结束)操作等。
经过多年的发展,目前市面上使用的OpenGL ES主要分为两个版本,基本情况如下。
❑ 一个是OpenGL ES 2.0,其采用的是可编程渲染管线,渲染能力大大提高。OpenGL ES 2.0要求设备中必须有相应的GPU硬件支持,不支持在设备上用软件模拟实现(不用担心,现在几百元的低端设备也可以支持了)。如图3-1所示的都市赛车6就是使用了OpenGL ES 2.0渲染技术。
▲图3-1 都市赛车6场景
❑ 另一个是诞生不久的OpenGL ES 3.x(目前主要包括3.0、3.1、3.2),其采用的也是可编程渲染管线。其中OpenGL ES 3.0要求Android设备中必须有4.3或以上的Android版本和相应的GPU硬件支持,而OpenGL ES 3.1要求Android设备中必须有5.0或以上的Android版本和相应的GPU硬件支持。如图3-2所示的狂野飙车8极速凌云采用的就是OpenGL ES 3.0渲染技术。
▲图3-2 狂野飙车8场景
说明
图3-1为使用OpenGL ES 2.0渲染的都市赛车6场景,从该图中可以看出使用OpenGL ES 2.0可以渲染出比较真实的路面及光影效果。图3-2为使用OpenGL ES 3.0渲染的狂野飙车8场景,在该图可以看出使用OpenGL ES 3.0渲染出的游戏画面更加逼真、细腻。
通过都市赛车6及狂野飙车8的效果图上说明了OpenGL ES 2.0与OpenGL ES 3.x渲染能力的差异。接下来将通过一款足球系列游戏FIFA,再次比较OpenGL ES 2.0与OpenGL ES 3.x渲染能力的差异。使用OpenGL ES 2.0与OpenGL ES3.x渲染的FIFA效果分别如图3-3和图3-4所示。
▲图3-3 OpenGL ES 2.0的渲染效果
▲图3-4 OpenGL ES 3.x的渲染效果
说明
由上述两幅渲染效果图比较可以看出,在视觉上OpenGL ES 3.x渲染的3D游戏场景比OpenGL ES 2.0系列渲染的3D游戏更加逼真。
接着再介绍两款使用OpenGL ES 3.x开发的3D游戏:聚爆(Implosion)与孤胆车神:维加斯,具体效果分别如图3-5和图3-6所示。这两款3D游戏均是大场景游戏,画面细腻、真实。
▲图3-5 聚爆
▲图3-6 孤胆车神:维加斯
说明
从图3-5聚爆以及图3-6孤胆车神:维加斯的效果图中可以看出,由于OpenGL ES 3.x渲染技术有更多的缓冲区对象、增加了GLSL ES 3.x着色语言、增加计算着色器支持等,使游戏在细腻程度、真实感等方面均有了质的变化。
介绍完上述聚爆和孤胆车神:维加斯之后,不得不介绍大名鼎鼎的Gameloft旗下的两款著名3D游戏,地牢猎手5和混沌与秩序2:救赎。这两款3D游戏一经发布便广受游戏爱好者的好评,其效果分别如图3-7和图3-8所示。
▲图3-7 地牢猎手5
▲图3-8 混沌与秩序2:救赎
使用OpenGL ES 3.x不仅可以开发类似地牢猎手5和混沌与秩序2:救赎等大场景的3D游戏,还可以开发较为真实的人物动作类型游戏,例如由日本游戏厂商KONAMI推出的足球经理新作—实况足球俱乐部经理,如图3-9所示。还有由美剧行尸走肉的版权方AMC电视台联合芬兰游戏开发商Next Games一同打造的The Walking Dead: No Man's Land(《行尸走肉:无人地带》),如图3-10所示。
▲图3-9 实况足球2016
▲图3-10 行尸走肉:无人地带
以前,想在智能手机上运行PC级的3D游戏,那简直是奢望。但是现在,那种原来不可能实现的愿望已经成为现实。随着手机硬件水平的不断提高、3D图形API的逐渐完善等多方面的因素,一些只能在PC上玩的大型游戏在Android手机上也可以安装并流畅运行了。
如EA的极品飞车,这款在PC上极其火爆的游戏,也已经成功移植到Android手机上了,效果如图3-11和图3-12所示。
▲图3-11 极品飞车13变速
▲图3-12 极品飞车14
说明
图3-11为EA开发的极品飞车13变速,图3-12为EA开发的极品飞车14热力追踪。这两款游戏均是以PC版的极品飞车13、14为原型,移植到Android手机中的。二者均使用的是OpenGL ES进行3D场景渲染。
3.1.2 初识OpenGL ES 3.0应用程序
前一小节简单地介绍了OpenGL ES 3.x与OpenGL ES 2.0的能力差异,在本小节将通过一个旋转三角形的简单案例介绍如何使用OpenGL ES 3.0进行3D场景的开发。该案例的运行效果分别如图3-13、图3-14和图3-15所示。
▲图3-13 运行效果图1
▲图3-14 运行效果图2
▲图3-15 运行效果图3
提示
请读者特别注意,基于OpenGL ES 3.0的3D应用程序不能保证可以在模拟器上运行,所以建议读者最好使用配置了GPU(GPU要求支持OpenGL ES 3.0)的真机(Android版本要求最低为4.3)运行本书中的案例。没有真机的读者想学习本书的内容可能需要购置或借用一部符合要求的真机了,关于GPU硬件的信息可以参考本章3.3节的相关内容。
说明
图3-13为3D空间中三角形初始状态的效果图,图3-14为3D空间中三角形绕x轴旋转大约100°的效果图,图3-15为绕x轴旋转大约180°的效果图。
前面已经介绍了本案例的运行效果,接下来将介绍本案例的具体开发步骤。
(1)在开发与本案例的功能直接相关的各种类之前,首先需要开发一个本书案例都需要用到的工具类ShaderUtil,其功能为将着色器(Shader)脚本加载进显卡并编译。最先开发的是该类中从着色器sh脚本中加载着色器内容的loadFromAssetsFile方法和检查每一步操作是否有错误的checkGlError方法,实现的具体代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/src/com/bn/Sample3_1目录下的ShaderUtil.java。
1 package com.bn.Sample3_1; //声明包名 2 import java.io.ByteArrayOutputStream; //相关类的引入 3 ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码 4 import android.util.Log; //相关类的引入 5 public class ShaderUtil { //加载顶点与片元着色器的类 6 public static int loadShader(int shaderType, String source ){}//加载指定着色器的方法 7 //创建着色器程序的方法 8 public static int createProgram(String vertexSource, String fragmentSource) { } 9 public static void checkGlError(String op) {//检查每一步操作是否有错误的方法 10 int error; // error变量 11 while ((error = GLES30.glGetError()) ! = GLES30.GL_NO_ERROR) { 12 Log.e("ES30_ERROR", op + ": glError " + error); //后台打印错误 13 throw new RuntimeException(op + ": glError " + error); //抛出异常 14 }} 15 //从sh脚本中加载着色器内容的方法 16 public static String loadFromAssetsFile(String fname, Resources r){ 17 String result=null; //声明一个String类型的字符串变量 18 try{ 19 InputStream in=r.getAssets().open(fname); //从assets文件夹中读取信息 20 int ch=0; //定义一个int型变量 21 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 22 while((ch=in.read())! =-1){ 23 baos.write(ch); //将获取的信息写入输出流中 24 } 25 byte[] buff=baos.toByteArray(); 26 baos.close(); //关闭输出流 27 in.close(); 28 result=new String(buff, "UTF-8"); //转化为UTF-8编码 29 result=result.replaceAll("\\r\\n", "\n"); 30 }catch(Exception e){ //捕获异常 31 e.printStackTrace(); //打印异常 32 } 33 return result; //返回结果 34 }}
❑ 第9-14行checkGlError方法的作用是在向GPU着色程序中加入顶点着色器或者片元着色器时,检查每一步操作是否有错误。这是由于在开发着色器脚本文件中的代码时,没有一个开发环境实时地进行编译、查错,因此开发一个检查错误的方法尤为重要。
❑ 第15-34行loadFromAssetsFile方法的作用为从项目根目录的assets文件夹下加载着色器代码脚本。其通过输入流将脚本信息读入,然后交给createProgram方法创建着色器程序。
(2)开发完加载着色器脚本内容的loadFromAssetsFile方法和检查错误的checkGlError方法后,下面开发的是加载着色器编码进入GPU并进行编译的loadShader方法与创建着色器程序的createProgram方法,具体代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/src/com/bn/Sample3_1目录下的ShaderUtil.java。
1 public static int loadShader(int shaderType, String source){//加载指定着色器的方法 2 int shader = GLES30.glCreateShader(shaderType); //创建一个shader,并记录其id 3 if (shader ! = 0) { //若创建成功则加载着色器 4 GLES30.glShaderSource(shader, source); //加载着色器的源代码 5 GLES30.glCompileShader(shader); //编译 6 int[] compiled = new int[1]; 7 //获取Shader的编译情况 8 GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0); 9 if (compiled[0] == 0) { //若编译失败则显示错误日志并删除此shader 10 Log.e("ES30_ERROR", "Could not compile shader " + shaderType + ":"); 11 Log.e("ES30_ERROR", GLES30.glGetShaderInfoLog(shader)); 12 GLES30.glDeleteShader(shader); 13 shader = 0; 14 }} 15 return shader; 16 } 17 //创建着色器程序的方法 18 public static int createProgram(String vertexSource, String fragmentSource) { 19 //加载顶点着色器 20 int vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource); 21 if (vertexShader == 0) {return 0; } 22 //加载片元着色器 23 int pixelShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource); 24 if (pixelShader == 0) {return 0; } 25 int program = GLES30.glCreateProgram(); //创建程序 26 if (program ! = 0) { //若程序创建成功则向程序中加入顶点着色器与片元着色器 27 GLES30.glAttachShader(program, vertexShader); //向程序中加入顶点着色器 28 checkGlError("glAttachShader"); 29 GLES30.glAttachShader(program, pixelShader); //向程序中加入片元着色器 30 checkGlError("glAttachShader"); 31 GLES30.glLinkProgram(program); //链接程序 32 int[] linkStatus = new int[1]; //存放链接成功program状态值的数组 33 GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0); 34 if (linkStatus[0] ! = GLES30.GL_TRUE) { //若链接失败则报错并删除程序 35 Log.e("ES30_ERROR", "Could not link program: "); 36 Log.e("ES30_ERROR", GLES30.glGetProgramInfoLog(program)); 37 GLES30.glDeleteProgram(program); //删除程序 38 program = 0; 39 }} 40 return program; //返回结果 41 }
❑ 第1-16行加载指定着色器的loadShader方法中:第2行通过调用glCreateShader方法创建了一个着色器;如果着色器创建成功,第3-14行则加载着色器的源代码,并编译着色器,同时检测编译的情况。若编译成功则返回着色器id,反之则删除着色器并且打印错误信息。
❑ 第19-24行为通过调用loadShader方法,分别加载顶点着色器与片元着色器的源代码进入GPU,并分别进行编译的代码。
❑ 第25-40行首先创建一个着色器程序,然后分别将相应的顶点与片元着色器添加到其中,最后将两个着色器链接为一个整体的着色器程序。
提示
到目前为止,读者可能还是对着色器一头雾水,不用担心,本章后面的内容将逐渐揭开着色器的面纱,这里读者有一个简单的印象即可。
(3)下面开始开发与本案例功能直接相关的类,首先需要开发的是本案例的主控制类Sample3_1Activity,该类继承自Activity,在程序开始时执行。该类的主要工作是创建MyTDView类的对象,然后调用setContentView方法跳转到相关界面,其代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/src/com/bn/Sample3_1目录下的Sample3_1 Activity.java。
1 package com.bn.Sample3_1; //声明包名 2 ......//此处省略了导入类的代码,读者可自行查看随书的源代码 3 public class Sample3_1Activity extends Activity{ //创建继承Activity的主控制类 4 MyTDView mview; //声明MyTDView类的引用 5 @Override 6 public void onCreate(Bundle savedInstanceState){//继承Activity后重写的方法 7 super.onCreate(savedInstanceState); //调用父类 8 //设置为竖屏模式 9 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 10 mview=new MyTDView(this); //创建MyTDView类的对象 11 mview.requestFocus(); //获取焦点 12 mview.setFocusableInTouchMode(true); //设置为可触控 13 setContentView(mview); //跳转到相关界面 14 } 15 @Override 16 public void onResume(){ //继承Activity后重写的onResume方法 17 super.onResume(); 18 mview.onResume(); //通过MyTDView类的对象调用onResume方法 19 } 20 @Override 21 public void onPause(){ //继承Activity后重写的onPause方法 22 super.onPause(); 23 mview.onPause(); //通过MyTDView类的对象调用onPause方法 24 }}
❑ 第4行为本类的成员变量,主要是声明MyTDView类的引用。
❑ 第6-14行为继承Activity后重写的onCreate方法,在该方法中主要是创建MyTDView类的对象,然后设置MyTDView获得焦点以及可触控,最后调用setContentView方法跳转到相关界面。
❑ 第15-19行为继承Activity后重写的onResume方法,在该方法中首先调用父类的onResume方法,然后调用MyTDView类对象的onResume方法。
❑ 第20-24行为继承Activity后重写的onPause方法,该方法中首先调用父类的onPause方法,然后调用MyTDView类对象的onPause方法。
(4)开发完本案例的主控制类Sample3_1Activity后,接下来需要开发的是本案例中用于创建三角形的类Triangle。该类的主要功能为初始化顶点数据、初始化着色器、设置相应的平移矩阵及旋转矩阵。首先介绍本类的基本框架,理解该框架有助于读者对本类整体结构的理解,其代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/src/com/bn/Sample3_1目录下的Triangle.java。
1 package com.bn.Sample3_1; //声明包名 2 import java.nio.ByteBuffer; //相关类的引入 3 ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码 4 import android.opengl.Matrix; //相关类的引入 5 public class Triangle{ 6 public static float[] mProjMatrix = new float[16]; //4*4投影矩阵 7 public static float[] mVMatrix = new float[16]; //摄像机位置朝向的参数矩阵 8 public static float[] mMVPMatrix; //总变换矩阵 9 int mProgram; //自定义渲染管线着色器程序id 10 int muMVPMatrixHandle; //总变换矩阵引用 11 int maPositionHandle; //顶点位置属性引用 12 int maColorHandle; //顶点颜色属性引用 13 String mVertexShader; //顶点着色器代码脚本 14 String mFragmentShader; //片元着色器代码脚本 15 static float[] mMMatrix = new float[16]; //具体物体的3D变换矩阵 16 FloatBuffer mVertexBuffer; //顶点坐标数据缓冲 17 FloatBuffer mColorBuffer; //顶点着色数据缓冲 18 int vCount=0; //顶点数量 19 float xAngle=0; //绕x轴旋转的角度 20 public Triangle(MyTDView mv){ //构造器 21 initVertexData(); //调用初始化顶点数据的initVertexData方法 22 intShader(mv); //调用初始化着色器的intShader方法 23 } 24 public void initVertexData(){ //自定义的初始化顶点数据的方法 25 vCount=3; //顶点数量为3 26 final float UNIT_SIZE=0.2f; //设置单位长度 27 float vertices[]=new float[]{ //顶点坐标数组 28 -4*UNIT_SIZE,0,0,0, -4*UNIT_SIZE,0,4*UNIT_SIZE,0,0}; 29 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4); 30 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序为本地操作系统顺序 31 mVertexBuffer = vbb.asFloatBuffer(); //转换为浮点(Float)型缓冲 32 mVertexBuffer.put(vertices); //在缓冲区内写入数据 33 mVertexBuffer.position(0); //设置缓冲区起始位置 34 float colors[]=new float[]{ //顶点颜色数组 35 1,1,1,0,0,0,1,0,0,1,0,0}; 36 ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4); 37 cbb.order(ByteOrder.nativeOrder()); //设置字节顺序为本地操作系统顺序 38 mColorBuffer = cbb.asFloatBuffer(); //转换为浮点(Float)型缓冲 39 mColorBuffer.put(colors); //在缓冲区内写入数据 40 mColorBuffer.position(0); //设置缓冲区起始位置 41 } 42 ……//此处省略了初始化着色器以及绘制图形的方法,将在下面介绍 43 public static float[] getFinalMatrix(float[] spec){ //产生最终变换矩阵的方法 44 mMVPMatrix=new float[16]; //初始化总变换矩阵 45 Matrix.multiplyMM(mMVPMatrix,0, mVMatrix,0, spec,0); 46 Matrix.multiplyMM(mMVPMatrix,0, mProjMatrix,0, mMVPMatrix,0); 47 return mMVPMatrix; //返回总变换矩阵 48 }}
❑ 第6-19行为本类所需成员变量的声明,主要是声明相关矩阵的引用、自定义渲染管线着色器程序的id、顶点位置和颜色属性的引用、顶点着色器以及片元着色器代码脚本字符串、顶点坐标和顶点着色数据缓冲、顶点的数量以及绕x轴旋转的角度。
❑ 第20-23行为本类的构造器,在该构造器中主要是调用initVertexData方法初始化顶点相关的数据,并且调用intShader方法初始化着色器。
❑ 第24-41行为初始化顶点数据的initVertexData方法。该方法中需要指定顶点的坐标数据以及顶点的颜色数据,将数据写入到对应的缓冲区中,并设置缓冲区的起始位置。
❑ 第43-48行为通过物体的3D变换矩阵、摄像机参数矩阵、投影矩阵计算产生最终总变换矩阵的方法。关于各种变换矩阵的问题读者不用担心,本书后面将有专门的章节详细介绍。
(5)开发完Triangle类的基本框架后,接下来将继续介绍上面省略的开发初始化着色器的方法initShader以及绘制三角形的方法drawSelf,具体代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/src/com/bn/Sample3_1目录下的Triangle.java。
1 public void intShader(MyTDView mv){ //创建并初始化着色器的方法 2 //加载顶点着色器的脚本内容 3 mVertexShader=ShaderUtil.loadFromAssetsFile("vertex.sh", mv.getResources()); 4 //加载片元着色器的脚本内容 5 mFragmentShader=ShaderUtil.loadFromAssetsFile("frag.sh", mv.getResources()); 6 //基于顶点着色器与片元着色器创建程序 7 mProgram = ShaderUtil.createProgram(mVertexShader, mFragmentShader); 8 //获取程序中顶点位置属性引用id 9 maPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition"); 10 //获取程序中顶点颜色属性引用id 11 maColorHandle= GLES30.glGetAttribLocation(mProgram, "aColor"); 12 //获取程序中总变换矩阵引用id 13 muMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"); 14 } 15 public void drawSelf(){ //自定义的绘制三角形的方法 16 GLES30.glUseProgram(mProgram); //使用指定的着色器程序进行渲染 17 Matrix.setRotateM(mMMatrix,0,0,0,1,0); //初始化变换矩阵 18 Matrix.translateM(mMMatrix,0,0,0,1); //设置沿z轴正向平移 19 Matrix.rotateM(mMMatrix,0, xAngle,1,0,0); //设置绕x轴旋转 20 GLES30.glUniformMatrix4fv(muMVPMatrixHandle,1, false, Triangle.GetFinalMatrix 21 (mMMatrix),0); //将变换矩阵传入渲染管线 22 GLES30.glVertexAttribPointer( //将顶点位置数据传送进渲染管线 23 maPositionHandle,3, GLES30.GL_FLOAT, false,3*4, mVertexBuffer); 24 GLES30.glVertexAttribPointer( //将顶点颜色数据传送进渲染管线 25 maColorHandle,4, GLES30.GL_FLOAT, false,4*4, mColorBuffer); 26 GLES30.glEnableVertexAttribArray(maPositionHandle); //启用顶点位置数据 27 GLES30.glEnableVertexAttribArray(maColorHandle); //启用顶点着色数据 28 GLES30.glDrawArrays(GLES30.GL_TRIANGLES,0, vCount); //执行绘制 29 }
❑ 第1-14行为初始化着色器的方法。在该方法中首先需要加载相应的着色器脚本,然后创建自定义的渲染管线着色器程序,并保留程序id于mProgram中。接着通过GLES30类调用相应的方法获取着色器程序中顶点坐标数据的引用、顶点颜色数据的引用以及总变换矩阵的引用。
❑ 第16行通过GLES30类调用glUseProgram方法给出着色器程序id指定使用的着色器程序。
❑ 第17-21行功能为初始化变换矩阵,设置沿z轴正方向的平移值以及绕x轴旋转的角度值。
❑ 第22-25行为通过调用GLES30类的glVertexAttribPointer方法,将顶点坐标数据以及顶点颜色数据传送进渲染管线,以备渲染时在顶点着色器中使用。
❑ 第26-28行为通过调用GLES30类的glEnableVertexAttribArray方法启用顶点位置数据以及启用顶点颜色数据,并通过调用GLES30类的glDrawArrays方法绘制三角形。
提示
本类中用到的有关着色器的知识、平移以及旋转变换的相关知识,将在后面的章节中逐步进行详细介绍。在本案例中由于知识不全,无法进行详细解释,读者有一个简单了解即可。
(6)开发完本案例的主控制类Sample3_1Activity以及三角形绘制类Triangle后,接下来将开发本案例中用于显示3D场景的类MyTDView。该类继承自GLSurfaceView,并且在该类中通过内部类的形式创建了场景渲染器,实现的具体代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/src/com/bn/Sample3_1目录下的MyTDView.java。
1 package com.bn.Sample3_1; //声明包名 2 import javax.microedition.khronos.egl.EGLConfig; //相关类的引入 3 ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码 4 import android.view.MotionEvent; //相关类的引入 5 public class MyTDView extends GLSurfaceView{//创建继承GLSurfaceView的MyTDView类 6 final float ANGLE_SPAN = 0.375f; //每次三角形旋转的角度 7 RotateThread rthread; //自定义线程类RotateThread的引用 8 SceneRenderer mRenderer; //自定义渲染器的引用 9 public MyTDView(Context context){ //构造器 10 super(context); 11 this.setEGLContextClientVersion(3); //使用OpenGL ES 3.0需设置该值为3 12 mRenderer=new SceneRenderer(); //创建SceneRenderer类的对象 13 this.setRenderer(mRenderer); //设置渲染器 14 this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); 15 } 16 private class SceneRenderer implements GLSurfaceView.Renderer{ 17 Triangle tle; //声明Triangle类的引用 18 public void onDrawFrame(GL10 gl){ //重写的onDrawFrame方法 19 GLES30.glClear( GLES30.GL_DEPTH_BUFFER_BIT//清除颜色缓存和深度缓存 20 | GLES30.GL_COLOR_BUFFER_BIT); 21 tle.drawSelf(); //通过Triangle的对象调用drawSelf绘制三角形 22 } 23 public void onSurfaceChanged(GL10 gl, int width, int height){ 24 GLES30.glViewport(0,0, width, height); //设置视口 25 float ratio = (float) width / height; //计算屏幕的宽度和高度比例 26 Matrix.frustumM(Triangle.mProjMatrix,0, -ratio, ratio, -1,1,1,10); 27 Matrix.setLookAtM(Triangle.mVMatrix,0,0,0,3,0f,0f,0f,0f,1.0f,0.0f); //设置摄像机 28 } 29 public void onSurfaceCreated(GL10 gl, EGLConfig config){ 30 GLES30.glClearColor(0,0,0,1.0f); //设置屏幕背景色 31 tle=new Triangle(MyTDView.this); //创建Triangle类的对象 32 GLES30.glEnable(GLES30.GL_DEPTH_TEST); //打开深度检测 33 rthread=new RotateThread(); //创建RotateThread类的对象 34 rthread.start(); //开启线程 35 }} 36 public class RotateThread extends Thread{ //自定义的内部类线程 37 public boolean flag=true; //设置循环标志位 38 @Override 39 public void run(){ //重写的run方法 40 while(flag){ //while循环 41 mRenderer.tle.xAngle=mRenderer.tle.xAngle+ ANGLE_SPAN; 42 try{ 43 Thread.sleep(20); //线程休眠20ms 44 }catch(Exception e){ //捕获并打印异常信息 45 e.printStackTrace(); 46 }}}}}
❑ 第6-8行为本类的成员变量,主要有每次旋转的角度、旋转线程类RotateThread对象的引用以及自定义渲染器SceneRenderer的引用。
❑ 第9-15行为本类的构造器,在该构造器中设置使用OpenGL ES 3.0版本,创建了SceneRenderer类的对象,设置了渲染器,并且设置渲染模式为主动渲染。
❑ 第18-22行为实现GLSurfaceView.Renderer接口后重写的onDrawFrame方法,在该方法中首先需要清除深度缓存和颜色缓存,然后通过Triangle类的对象调用drawSelf方法绘制三角形。
❑ 第23-28行为实现GLSurfaceView.Renderer接口后重写的onSurfaceChanged方法,在该方法中设置了视口、透视投影相关参数以及摄像机的位置。
❑ 第29-35行为实现GLSurfaceView.Renderer接口后重写的onSurfaceCreated方法,在该方法中设置了屏幕的背景颜色、创建了Triangle类的对象、开启了深度检测、创建了RotateThread类的对象并启动了该线程。
❑ 第36-46行为自定义的内部类线程,在该线程中通过while循环不断更改Triangle类中xAngle的值,使得三角形以一定的角速度绕x轴转动。
提示
本案例采用的用到变换矩阵时临时生成变换矩阵的方式并不方便,在本书后面的章节中,这些矩阵将会被集合到一个MatrixState工具类当中进行管理,使得开发人员在应用时更加方便。
(7)完成了Java代码的开发后,接着就可以用着色语言开发着色器了,着色器的代码可以存储在后缀名为“.sh”文件中,这些文件存放到项目的assets目录下。首先开发的是顶点着色器,其主要作用为执行顶点变换、纹理坐标变换等顶点的相关操作,具体代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/assets目录下的vertex.sh文件。
1 #version 300 es 2 uniform mat4 uMVPMatrix; //总变换矩阵 3 layout (location = 0) in vec3 aPosition; //顶点位置 4 layout (location = 1) in vec4 aColor; //顶点颜色 5 out vec4 vColor; //用于传递给片元着色器的out变量 6 void main(){ 7 gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点位置 8 vColor = aColor; //将接收的颜色值传递给片元着色器 9 }
说明
关于着色语言以及顶点着色器的作用将会在后面的章节中逐步详细介绍,这里对于顶点着色器中的代码不进行详细讲解,读者只简单了解即可。
(8)完成了顶点着色器代码的开发后,下面将开发的是片元着色器的代码,其主要作用为执行纹理的访问、颜色的汇总等操作,具体代码如下。
代码位置:见随书中源代码/第3章/Sample3_1/assets目录下的frag.sh文件。
1 #version 300 es 2 precision mediump float; //设置浮点精度 3 in vec4 vColor; //接收从顶点着色器过来的参数 4 out vec4 fragColor; //输出的片元颜色 5 void main(){ 6 fragColor = vColor; //给此片元赋颜色值 7 }
说明
关于着色语言以及片元着色器的作用将会在后面的章节中逐步详细介绍,这里对于片元着色器中的代码不进行详细讲解,读者也只要简单了解即可。另外,到这里为止读者可能学得不是很明白,没有关系,这个案例只是希望读者对基于OpenGL ES 3.0的3D应用程序有一个简单的认识,要想基本搞清楚至少需要耐着性子学完本书前5章的内容才可以。
OpenGL ES 3.0本身并不是基于任何特定操作系统平台的,基于其开发的应用程序不但可以运行在Android系统上,也可以运行在iOS、Symbian以及BlackBerry等平台上。
由于Android是目前市面上占有率最高的移动嵌入式平台,因此本书中的案例大部分是借助于Android平台来进行介绍的。但目标平台不是Android的读者也不用担心,OpenGL ES 3.0本身在各个平台上是通用的,直接相关于OpenGL ES 3.0的知识基于哪种平台进行学习关系并不大,学成之后可以在所有支持其的平台上使用。
还有一些读者可能会有另外一个疑问,书中的案例很多是使用Java语言开发的,性能没有影响吗?其实不必担心,原因如下:
❑ 本书的不少案例中在调用与OpenGL ES 3.0相关的API时虽然使用的是Java语言,但这部分API本质上并不是由Java实现的,Java调用时也是通过JNI直接调用的底层C库,因此这部分代码用Java开发还是用C开发性能上基本没有什么差异。
❑ 由于本质上是直接调用的底层C库,因此API与C版本的几乎没有差异,这样本书案例中的很多代码直接就可以在C版本的应用中使用。就算少量不一样的部分,移植也很容易,因为相似度非常高。
提示
想学习Android下NDK平台3D应用开发的读者也不用担心,本书后面有专门的章节介绍NDK下OpenGL ES 3.0 3D应用的开发。
3.1.3 OpenGL ES 3.1新特性简介
上一小节详细地介绍了使用OpenGL ES 3.0开发的3D应用程序,本小节将主要介绍OpenGL ES 3.x中关于OpenGL ES 3.1增加的部分新特性。要注意的是,OpenGL ES 3.1是3.0规范的小幅升级版,大部分支持3.0的硬件都可以支持3.1规范的新功能。
目前的OpenGL ES 3.0是基于OpenGL 3.x规范的子集,而OpenGL ES 3.1则是基于OpenGL 4.x规范的子集,同时向下兼容ES 3.0/2.0规范。OpenGL ES 3.1新特性主要包括如下几个部分。
❑ 计算着色器(Compute Shaders)。
其是新版的支柱性功能,来自OpenGL 4.3。通过计算着色器,应用可使用GPU执行通用目的的计算任务,并与图形渲染紧密相连,将大大增强移动设备的计算能力。
❑ 独立的着色器对象。
应用可为GPU的顶点、片元着色器阶段独立编程,无需明确的连接步骤即可将顶点、片元着色器程序混合匹配在一起。
❑ 增强的纹理功能。
主要包括多重采样纹理、模版纹理、纹理聚集等。
❑ 着色语言的改进。
包含新的算法和位字段(bitfield)操作,还有现代方式的着色器编程等。
❑ 向下兼容。
能够兼容OpenGL ES 2.0/3.0,编程人员可在已有基础上增加3.1特性。
说明
本书中所开发及讲解的案例无特别说明都是基于OpenGL ES 3.0的,当然,在本丛书的下卷会有专门的章节对OpenGL ES 3.1的新特性进行介绍。到那时读者会发现将OpenGL ES 3.0的案例升级为OpenGL ES 3.1版本是非常容易的。