第4章 OpenGL
Android通过使用开放图形库(OpenGL)对高性能的2D和3D图形处理提供支持,尤其是,OpenGL ES API。OpenGL是一个跨平台图形处理的API,并且定义了标准的软件接口用来调用处理3D图形的硬件。OpenGL ES是专门为嵌入式设备而开发的OpenGL。从Android 1.0就已经开始支持OpenGL ES 1.0 和1.1 API,而从Android 2.2(API 8)开始,Android提供了对OpenGL ES 2.0API的支持。注意:Android框架所提供的API和J2ME JSR239 OpenGL ES API非常相似,但是并不完全相同。如果你对J2ME JSR239很熟悉,那么请注意他们之间的不同之处
4.1 基础
Android的框架API和本地开发组件(NDK)都提供对OpenGL的支持。本文着重讲解Android的框架接口。若想获取更多关于NDK的信息,请访问Android NDK. 在Android框架下有两种基础的类让你使用OpenGL ES API创建和操作图形。它们是 GLSurfaceView 和 GLSurfaceView.Renderer。如果你是想在你的Android应用中使用OpenGL,那么你首先要做的就是如何在activity中来实现这这两个类。
GLSurfaceView:
继承自View的类,可以通过使用OpenGL API来绘制和操作管理,这个在功能上和SurfaceView非常相似。使用这个类的话,你需要先创建一个GLSurfaceView实例并且给它添加你的Renderer。然而,如果你想捕获触屏事件,那则需要继承GLSurfaceView 类并实现对触屏动作的监听。
GLSurfaceView.Renderer:
这个接口定义了在OpenGL的GLSurfaceView绘制图形的方法。你需要新建一个类来实现这个接口并且通过 GLSurfaceView.setRenderer()方法来添加到GLSurfaceView实例里。
GLSurfaceView.Renderer接口需要实现以下方法:
◆onSurfaceCreated():系统在创建GLSurfaceView时候会调用这个方法,且只会调用一次。只需要执行一次的动作可以定义在这个方法里,比如说,设置OpenGL环境参数或者初始化OpenGL图形对象。
◆onDrawFrame():当系统每次需要重新绘制GLSurfaceView时候会调用此方法,同时,此方法也是绘制图形对象的主要执行点。
◆onSurfaceChanged():当GLSurfaceView几何图形改变时,系统调用此方法。这包括GLSurfaceView尺寸大小的改变,设备横竖屏幕切换时候的改变。系统调用此方法来响应GLSurfaceView容器的变化。
4.1.1 OpenGL包
一旦你有确定了使用GLSurfaceView还是GLSurfaceView.Renderer,那么你能使用以下类开始调用OpenGL API了:
1.OpenGL ES 1.0/1.1 API包
android.opengl-
这个包为OpenGL ES 1.0/1.1提供了一个静态接口,他能提供比javax.microedition.khronos包更好的性能。
2.OpenGL ES 2.0API类
android.opengl.GLES20-这个包提供了OpenGL ES 2.0 的接口,记住他是从Android2.2(API Level 8)开始的。
4.2 声明OpenGL 需求
请注意不是所有设备都支持OpenGL,如果你的应用程序使用OpenGL特性,你必须在AndroidManifest.xml文件中包含这些要求。以下是最常见的OpenGL声明。
OpenGL ES 版本需求:
如果你的应用程序仅支持OpenGL ES 2.0,你必须在manifest文件中做出如下声明要求:如代码清单4-1所示:
<!--告诉系统APP需求OpenGL ES 2.0. --> <uses-feature android:glEsVersion="0x00020000" android:required="true" />
代码清单4-1
添加了以上声明的原因是Google Play会限制你的应用程序不会被安装在不支持OpenGL ES 2.0.的设备上。
纹理压缩环境:
如果你的应用程序使用了纹理压缩格式,你必须在manifest文件中声明你的应用程序支持的格式,使用<supports-gl-texture>
。更多详细的纹理压缩格式,我们在本章后面会讲到。如果用户设备不支持至少一种你声明的纹理压缩格式,那么你的程序就会被隐藏,不会让用户安装,这是理所当然的。
4.3 为绘制对象映射坐标
在Android设备上显示图形的一个基本问题就是它们的屏幕大小和形状。OpenGL假定是一个方形。那么映射到屏幕设备上的效果如图4-1所示:
图4-1 默认的OpenGL坐标系(左)映射到一个典型的Android设备屏幕(右)
上面的插图显示了统一的坐标系统假定左边为一个OpenGL画面,右边则是这些坐标实际上映射到一个典型的设备屏幕上的效果。为了解决这个问题,你可以使用OpenGL投影模式和相机视图来变换坐标,这样你的图形对象在任何显示下有正确的比例。为了使用投影和摄影视图,你创建一个投影矩阵并一个摄影视图举证,并把它们应用到OpenGL渲染管道。投影矩阵重新计算你图形的坐标,它们会正确的映射到Android设备屏幕上。摄像机视图矩阵从一个指定视角位置创建一个转换用来渲染对象。
4.3.1 在OpenGL ES 1.0中投影和相机视图
在ES 1.0 API中,你使用投影和相机视图通过创建每一个矩阵并添加它们到OpenGL环境中。
1. 投影矩阵
创建一个投影矩阵使用使用几何形状的设备屏幕,以重新计算对象坐标,绘制正确的比例。下面例子代码示例了在onSurfaceChanged()方法中如何根据屏幕长宽比来修改投影矩阵,如代码清单4-2所示:
public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width, height); // 调整屏幕比例 float ratio = (float) width / height; gl.glMatrixMode(GL10.GL_PROJECTION); // 设置投影矩阵模式 gl.glLoadIdentity(); // 重置矩阵到默认状态 gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7); // 应用投影矩阵 }
代码清单4-2
2. 相机转换矩阵
一旦你使用投影矩阵调整了系统坐标系,你必须也使用相机视图。以下代码展示了如何在onDrawFrame()方法中修改一个模型视图并使用GLU.gluLookAt()实用工具来创建一个模拟摄像机位置的转换,如代码清单4-3所示:
public void onDrawFrame(GL10 gl) { ... //设置GL_MODELVIEW 转换模式 gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); //重置矩阵到默认状态 // 当使用GL_MODELVIEW时,你必须设置相机视图 GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f); ... }
代码清单4-3
4.3.2 在OpenGL ES 2.0中投影和相机视图
在ES 2.0 API中,你使用投影和相机视图通过首先添加一个矩阵成员到你图形对象的顶点着色器。
1. 添加矩阵到顶点着色器
为视图投影矩阵创建一个变量并让它作为着色器的位置的乘数。在以下顶点着色器的示例代码中,包含了uMVPMatrix
成员允许你使用投影和相机视图矩阵到对象坐标,如代码清单4-4所示:
private final String vertexShaderCode = //这个矩阵成员变量提供一个钩子来操纵使用这个顶点着色对象的坐标, "uniform mat4 uMVPMatrix; \n" + "attribute vec4 vPosition; \n" + "void main(){ \n" + //矩阵必须必须被包括与gl_Position的一部分 // 注意,uMVPMatrix系数前提必须首先对于矩阵乘法的乘积是正确的 " gl_Position = uMVPMatrix * vPosition; \n" + "} \n";
代码清单4-4
注意:上面的例子在顶点着色器中定义了一个单一的变换矩阵成员,是为了让你结合投影矩阵和相机视图矩阵。根据应用程序的需求, 在你的顶点着色器中你可能想要定义单独的投影矩阵和照相机视图矩阵成员,这样你就可以独立的改变他们。
2. 访问着色器矩阵
在顶点着色使用投影和相机视图后创建一个钩子(hook),你能访问应用投影和相机视图矩阵的变量。 以下代展示了如何修改onSurfaceCreated()方法来实现在顶点着色中访问矩阵变量。如代码清单4-5所示:
public void onSurfaceCreated(GL10 unused, EGLConfig config) { ... muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); ... }
代码清单4-5
3. 创建投影和照相机视图矩阵
生成投影和视图矩阵应用的图形对象,我们可看代码清单4-6所示:
public void onSurfaceCreated(GL10 unused, EGLConfig config) { ... // 创建一个摄像头视图矩阵 Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f); } public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); float ratio = (float) width / height; //从设备屏幕创建一个投影矩阵 Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7); }
代码清单4-6
4. 应用投影和相机视图矩阵
为了使用投影和相机视图转换,矩阵相乘,然后把结果设置它们到顶点着色器中。以下代码展示了如何在onDrawFrame()方法中结合投影矩阵和相机视图来创建图形对象,如代码清单4-7所示:
public void onDrawFrame(GL10 unused) { ... // 结合投影和相机视图矩阵 Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0); // 应用被结合的投影和相机视图转换 GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); // 绘制对象 ... }
代码清单4-7
4.4 形状面向和顶点顺序
在OpenGL中,形状的一个表面在三维空间中是一个通过3个或更多点来定义的表层。在三维空间中三个或更多点一组被称为顶点。他有正面也背面。你又如何知道这面是正面还是背面呢?好问题。答案,就是你定义形状中点的方向。下面让我们看下图4-2:
图4-2 图解一个坐标列表,并转化为一个逆时针方向绘制顺序
在上面的图中,三角形的点的绘制顺序是逆时针的。默认情况下,在OpenGL中逆时针画的是正面。知道哪个面是正面是很重要的。因为OpenGL有一个常用功能叫做“面剔除”功能。“面剔除”是一个选项,它允许OpenGL环境渲染管道忽略(不计算或者不绘制)形状的背面,这样可以节省时间,内存和处理周期,如下面的代码清单4-8所示:
// 打开“面剔除”功能 gl.glEnable(GL10.GL_CULL_FACE); //制定哪个面不绘制 gl.glCullFace(GL10.GL_BACK);
代码清单4-8
如果你试图使用“面剔除”功能,但不知道你目前形状中是正面还是背面,你的OpenGL图形会看起来有点薄,或者根本就不会露面。所以,总是在OpenGL中定义坐标的逆时针绘制顺序。注意:你可以可以设置顺时针为正面,但这样做可能需要更多的代码,并且对于习惯使用默认逆时针为正面的程序员来说,你可能会让它们混淆。花费不必要的沟通功夫。
4.5 OpenGL版本和设备兼容性
OpenGL ES 1.0和1.1 API规范在Android1.0开始就被支持了。从Android2.2开始,系统开始支持OpenGL ES 2.0 API规范。OpenGL ES 2.0支持大多数设备并对于新开发的应用程序来说官方推荐使用2.0版本。
4.5.1 纹理压缩支持
纹理压缩可以通过减少OpenGL的内存需求显著提高应用程序的性能,使更有效的利用内存。Android框架提供了支持ETC1压缩格式作为标准特性,包括一个ETC1Util工具类和theetc1tool压缩工具(位于Android SDK在< SDK > /tools/)。ETC格式大部分Android设备,但是它不能保证都是可用的。所以需要在设备上检查ETC格式是否支持,可以使用ETC1Util.isETC1Supported()这个方法来判断。注意:ETC1的纹理压缩格式不支持纹理与一个alpha通道。如果您的应用程序需要的纹理与一个alpha通道,你应该研究其他可用的纹理压缩格式来适用于你的设备。除了ETC1格式,Android设备有不同的纹理压缩支持基于GPU芯片和OpenGL实现。你应该研究你目标设备支持的纹理压缩格式并确定你的应用程序应该支持的压缩类型。为了确定什么纹理格式都支持在一个给定的设备,你必须查询设备和检阅OpenGL扩展名,它确定什么纹理压缩格式(和其他OpenGL特性)是该设备所支持的,一些广泛支持的纹理压缩格式如下:
ATITC (ATC):
ATI纹理压缩(ATITC或ATC)可以在各种各样的设备和支持固定率压缩为RGB纹理有或没有一个alpha通道。这种格式可由代表几个OpenGL扩展名称,例如:
GL_AMD_compressed_ATC_texture
GL_ATI_texture_compression_atitc
PVRTC:
PowerVR纹理压缩(PVRTC)是可在各种各样的设备和支持2位和4位每个像素纹理有或没有一个alpha通道。这个格式是代表以下OpenGL扩展名:
GL_IMG_texture_compression_pvrtc
S3TC (DXTn/DXTC)
S3纹理压缩(S3TC)有几个格式变化(DXT1到DXT5)和更广泛使用。格式支持RGB纹理与4位alpha或8位alpha通道。这种格式可由代表几个OpenGL扩展名称,例如:
GL_OES_texture_compression_S3TC
GL_EXT_texture_compression_s3tc
GL_EXT_texture_compression_dxt1
GL_EXT_texture_compression_dxt3
GL_EXT_texture_compression_dxt5
3DC
3DC纹理压缩(3 dc)是一种广泛使用的格式,支持一个alpha RGB纹理通道。这个格式是代表以下OpenGL扩展名:
GL_AMD_compressed_3DC_texture
警告: 这些纹理压缩格式不支持在所有设备。支持这些格式可以因制造商和设备而更改。下面将介绍如何确定纹理压缩格式。注意:一旦你决定你应用支持的纹理压缩格式,确定你在manifest中使用了<supports-gl-texture>声明它们。使用这个声明当然用于GooglePlay中来过滤一些设备的。
4.5.2 确定 OpenGL扩展
实现OpenGL的差异可以随Android设备方面的扩展支持OpenGL ES的API。这些扩展包括纹理压缩,但通常还包括其他扩展OpenGL的特性集。在一个特定的设备中确定哪些纹理压缩格式,和其他支持OpenGL的扩展:
1. 在你的目标设备中运行下面的代码来确定支持的纹理压缩格式:
String extensions = javax.microedition.khronos.opengles.GL10.glGetString(GL10.GL_EXTENSIONS);
警告: 这个调用的结果是根据具体设备的。你应该在几个目标设备中运行此方法来确定通用的压缩类型。
2. 检阅输出的方法来确定设备支持的OpenGL扩展。
4.6 选择一个OpenGL API版本
OpenGL ES API版本1.0(和1.1扩展)和2.0版本都提供高性能的图形界面,用于创建3D游戏,可视化用户界面。OpenGL ES 1.0/1.1 API与ES 2.0图形编程明显不同,因此在开发之前开发人员应该仔细考虑以下因素:
性能
一般来说,2.0提供了更快的图形性能比1.0/1.1 API。然而,性能差异可以取决于你的Android设备上运行的应用程序,由于不同的OpenGL图形的实现管道而处理不一样。
设备兼容性
开发人员应该考虑设备的类型,Android版本和OpenGL ES版本对于他们的用户来说必须可用。更多信息在不同设备上OpenGL兼容性,在本书的开始部分我们已经讲述过。
编码方便性
OpenGL ES 1.0/1.1 API比2.0更为简单,如果你想要快速编码或简单优先的话,你可以考虑使用1.0/1.1
图形控制
OpenGL ES 2.0的API提供了更高程度的控制并通过使用着色器提供了一个完全可编程的管道。很多效果是1.0/1.1不能实现的。
虽然性能、兼容性、编码方便、图形控制和其他因素可能会影响你的决定,但最终你的出发点应该是为用户提供最好的用户体验,以此来选择一个OpenGL API版本。