OpenGL ES
Android包含支持高性能2D和3D图形绘制开源库(OpenGL),尤其是OpenGL ES API. OpenGL是一个跨平台的图形API,它为硬件处理3D图形指定了一个标准的软件接口。OpenGL ES是一种用于嵌入式设备的OpenGL规范。Android支持多个版本的OpenGL ES接口:
- OpenGL ES 1.0和1.1 - Android 1.0或更高版本所支持的API规范。
- OpenGL ES 2.0 - Android 2.2(API 8)或更高版本所支持的API规范。
- OpenGL ES 3.0 - Android 4.3(API 18)或更高版本所支持的API规范。
- OpenGL ES 3.1 - Android 5.0(API 21)或更高版本所支持的API规范。
注意:设备支持OpenGL ES 3.0 API需要设备制造商提供图形管道的实现。有些运行Android 4.3或更高版本的设备可能不支持OpenGL ES 3.0 API。获取关于运行时检查设备所支持的OpenGL版本的信息,请继续阅读下文。
基础(The Basics)
Android框架API和原生开发工具(NDK)都支持OpenGL。本文重点讲Android框架关于OpenGL的接口。获取更多关于NDK的信息,参看Android NDK。
Android framework有两个基础类可以让你通过OpenGL ES API创建和操作图形:GLSurfaceView 和GLSurfaceView.Renderer。如果想在你的Android应用程序中使用OpenGL,了解如何在Activity中实现这些类是你的第一个目标。
GLSurfaceView
这是一个可以使用OpenGL API方法来绘制和操作对象的View,功能类似SurfaceView。你可以通过创建GLSurfaceView的实例并向其添加渲染器Renderer来使用这个类。但是,如果你想捕获触屏事件,你得将继承GLSurfaceView类并实现触摸监听器。OpenGL实现监听器的示例,Responding to Touch Events.
GLSurfaceView.Renderer
这个接口定义了在GLSurfaceView上绘图所需的方法。你得在一个单独的类提供接口的实现,并调用GLSurfaceView.setRenderer()将其绑定到GLSurfaceView实例中。
GLSurfaceView.Renderer接口需要实现如下方法:
- onSurfaceCreated():当创建GLSurfaceView时,系统立即调用这个方法。通过这个方法实现一些需要马上处理的事,如设置OpenGL的环境参数或初始化OpenGL绘图对象。
- onDrawFrame(): GLSurfaceView每次重绘时系统就调用这方法。使用此方法作为绘制(和重绘)图形对象的主要执行步骤。
- onSurfaceChanged():当GLSurfaceView发生几何改变时调用这方法,包括GLSurfaceView的大小或设备屏幕方向的改变。比如,当设备从竖直方向变成水平方向时会调用这方法。在GLSurfaceView容器中使用该方法来响应改变。
OpenGL ES包(OpenGL ES packages)
一旦使用GLSurfaceView和GLSurfaceView.Renderer为OpenGL ES创建了视图容器,你就可以开始使用如下的类调用OpenGL API:
- OpenGL ES 1.0/1.1 API Packages
- oandroid.opengl - 这个包为OpenGL ES1.0/1.1提供了一个比javax.microedition.khronos包更好性能静态接口类。
- GLES10
- GLES10Ext
- GLES11
- GLES11Ext
- ojavax.microedition.khronos.opengles - 这个类提供了OpenGL ES 1.0/1.1的标准实现。
- GL10
- GL10Ext
- GL11
- GL11Ext
- GL11ExtensionPack
- OpenGL ES 2.0 API Class
- oandroid.opengl.GLES20 - 这个包为OpenGL ES 2.0提供了一些接口,Android 2.2(API 8)或更高版支持。
- OpenGL ES 3.0/3.1 API Packages
- oandroid.opengl - 这个包为OpenGL ES 3.0/3.1提供了一些接口类,其中3.0版本从Android4.3(API 18)开始支持。3.1版本从Android 5.0(API 21)开始支持。
- GLES30
- GLES31
- GLES31Ext (Android Extension Pack)
如果你想用OpenGL ES马上开始编写一个app,请参考Displaying Graphics with OpenGL ES 类。
声明OpenGL的条件(Declaring OpenGL Requirements)
如果你想在应用程序中使用并非所有设备都有效的OpenGL特性。你得在AndroidManifest.xml文件中包含这些条件。这里有最常见的OpenGL声明:
- OpenGL ES 版本条件 - 如果你的应用程序需要一个指定版本的OpenGL ES,你必须通过在清单文件中添加如下设置来声明这个条件:
为OpenGL ES 2.0
<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
添加此声明会导致Google Play限制应用程序安装在不支持OpenGL ES 2.0的设备。如果你的应用程序专用于支持OpenGL ES 3.0的设备,你还可以在清单中这样指定:
为OpenGL ES 3.0
<!-- Tell the system this app requires OpenGL ES 3.1. -->
<uses-feature android:glEsVersion="0x00030001" android:required="true" />
为OpenGL ES 3.1
<!-- Tell the system this app requires OpenGL ES 3.1. -->
<uses-feature android:glEsVersion="0x00030001" android:required="true" />
Note:OpenGL ES 3.x API 向后兼容2.0 API,这意味着在你的应用程序中OpenGL的实现可以更灵活。通过在清单文件中声明OpenGL ES 2.0 API条件后,你可以默认使用这个API版本,同时可以在程序运行时检查设备是否支持Open GL ES 3.x的特性,如果支持就使用OpenGL ES 3.0的特性。
- 纹理压缩条件 - 如果你的应用程序使用了纹理压缩格式,你得在清单文件中使用<supports-gl-texture>声明应用程序所支持压缩的格式。获取更多关于纹理压缩格式的信息,请阅读下文的Texture compression support.
设备不支持至少一个压缩类型声明的用户中,在清单中声明纹理压缩条件会隐藏你的应用程序。获取更多关于Google Play过滤纹理压缩格式的工作,参看Google Play and texture compression filtering部分的<supports-gl-texture>文档。
绘制对象的映射坐标(Mapping Coordinates For Drawn Objects)
Android设备显示图形的一个问题是屏幕会显示不同的尺寸和形状。OpenGL假设一个正方形在默认的统一坐标系中,将正方形的坐标绘制到典型的非方形屏幕上,并假使它是完全正方形的。
图1.左边是默认的OpenGL坐标系统,右边用来映射的典型设备屏幕。
上图左边显示OpenGL假设的统一坐标,右边显示这些坐标如何真正的映射到水平放置的设备上。为了解决这个问题,你可以应用OpenGL的投影模式和相机视图转换坐标来使你的图形对象在任何显示器上都具有正确的比例。
为了应用投影和相机视图,你可以创建一个投影矩阵和相机矩阵,并将它们应用到OpenGL的渲染管道中。投影矩阵通过重新计算图形的坐标来实现Android屏幕设备的正确显示。相机视图矩阵创建一个转换器并根据眼睛所看到的位置渲染对象。
OpenGL ES 1.0 的投影和相机视图(Projection and camera view in OpenGL ES 1.0)
在ES 1.0 API中,你可以通过创建投影矩阵和相机视图矩阵并将它们添加到OpenGL环境中来应用投影和相机。
1.投影矩阵 - 使用设备屏幕的几何形状创建投影矩阵,以便重新计算并以正确比例绘制对象坐标。下面的示例代码展示如何修改GLSurfaceView.Renderer实现的onSurfaceChanged()方法,实现根据屏幕的宽高比创建投影矩阵,并将其应用于OpenGL渲染环境中。
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
// make adjustments for screen ratio
float ratio = (float) width / height;
gl.glMatrixMode(GL10.GL_PROJECTION); // set matrix to projection mode
gl.glLoadIdentity(); // reset the matrix to its default state
gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7); // apply the projection matrix
}
2.相机转换矩阵 - 一旦使用投影矩阵调整好坐标系统后,你还得应用相机视图。下面的示例代码展示如何修改 GLSurfaceView.Renderer实现的onDrawFrame()方法,以便应用一个视图模型和使用GLU.gluLookAt()工具创建模拟相机位置的视图转换器。
public void onDrawFrame(GL10 gl) {
...
// Set GL_MODELVIEW transformation mode
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity(); // reset the matrix to its default state
// When using GL_MODELVIEW, you must set the camera view
GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
...
}
OpenGL ES 2.0或更高版本的投影和相机视图
在ES 2.0 和3.0 API中,使用投影和相机视图,你得先添加一个矩阵成员到图形对象的顶点着色器上。添加矩阵成员后,你就可以使用投影和相机视图矩阵到你的对象了。
1.添加矩阵到顶点着色器 - 为投影矩阵创建一个作为着色器位置乘数的变量。下面是顶点着色器的示例代码,其中包括允许让你应用投影和相机视图矩阵到使用着色器的对象坐标上的uMVPMatrix成员。
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of objects that use this vertex shader.
"uniform mat4 uMVPMatrix; \n" +
"attribute vec4 vPosition; \n" +
"void main(){ \n" +
// The matrix must be included as part of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition; \n" +
"} \n";
Note:上面的示例在定点着色器中定义了一个单一的转换矩阵成员,可以将其结合投影矩阵和相机视图矩阵应用。取决于你的应用程序所需,你可能想单独地在顶点着色器中声明投影矩阵和相机视图矩阵成员,以便可以独立的改变它们。
2.访问着色器矩阵 - 在顶点着色器创建一个hook(一个变量)之后,你就可以访问这个变量来应用投影和相机视图矩阵。下面的示例代码展示如何修改 GLSurfaceView.Renderer实现的onSurfaceCreated()方法,来访问如上定义在顶点着色器中矩阵变量。
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
...
}
3.创建投影和相机视图矩阵 - 生成投影和应用于图形对象的视图矩阵。下面的示例代码展示如何修改 GLSurfaceView.Renderer实现的onSurfaceCreated()方法和onSurfaceChanged()方法,实现创建相机视图矩阵和基于设备屏幕宽高比的投影矩阵。
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// Create a camera view matrix
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;
// create a projection matrix from device screen geometry
Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
4.应用投影和相机视图矩阵 - 为了应用投影和相机视图转换,将两个矩阵相乘,然后将它们设置给顶点着色器。下面的示例代码展示如何修GLSurfaceView.Renderer实现的onDrawFrame()方法,来实现结合投影矩阵和上述代码所创建的相机视图,然后将它们应用于OpenGL渲染的图形对象。
public void onDrawFrame(GL10 unused) {
...
// Combine the projection and camera view matrices
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
// Apply the combined projection and camera view transformations
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);
// Draw objects
...
}
获取OpenGL ES 2.0应用投影和相机视图的完整代码,参看: Displaying Graphics with OpenGL ES类。
形状面和环绕(Shape Faces and Winding)
在OpenGL中,形状的表面由三维空间的三个点或以上定义而成。一组三个或以上的三维点(在OpenGL中称为顶点)具有正面和背面。如何知道哪一面是正面哪一面是背面呢?这是一个不错的问题。答案不得不用环绕或你定义形状的点的方向来回答。
图1.图文的坐标列表按逆时针绘制。
在例子中,三角形的点被定义在一个逆时针方向绘制的顺序中。绘制这些坐标的顺序定义了形状的环绕方向。在OpenGL中,默认逆时针绘制的面是正面。图1你所看到的OpenGL渲染出来的形状是正面,其他面是背面。
为什么了解哪一面是正面如此重要呢?这个回答得用OpenGL常用的特性–面剔除来解释。面剔除是OpenGL环境允许你忽略图形背面(不需要计算和绘制背面)渲染管道,节省时间、内存还有处理周期的选项。
// enable face culling feature
gl.glEnable(GL10.GL_CULL_FACE);
// specify which faces to not draw
gl.glCullFace(GL10.GL_BACK);
如果你在不知道正面背面的情况下就使用面剔除的特性,OpenGL图形看起来可能会变形,也可能全部不显示。所以,总是以逆时针绘制顺序定义你的OpenGL形状的坐标。
Note:将OpenGL环境的顺时针方向作为正面也是存在的,但是这么做会导致需要额外更多的代码,以及你在需要寻求帮助的时候会让你搞混OpenGL的开发。所以不推荐这么做。
OpenGL的版本和设备兼容性
Android从1.0就支持OpenGL ES 1.0和1.1 API规范。从Android 2.2(API 8)开始,框架支持OpenGL ES 2.0 API规范。OpenGL ES 2.0支持大多数Android设备,所以比较推荐使用这版本开发应用程序。Android 4.3(API 18)或更高版本支持OpenGL ES 3.0 API。获取关于Android设备支持指定版本的OpenGL ES的信息,请参阅OpenGL ES版本仪表板。
使用OpenGL ES 1.0/1.1 API与使用2.0或更高版本API开发图形程序是非常不一样的。1.x版本的API有更多简便的方法和稳定的图形管道,而OpenGL ES 2.0和3.0 API提供更多使用OpenGL取色器直接控制管道的方法。你应该仔细考虑图形的需求,然后选择最适合应用程序的API版本。
OpenGL ES 3.0 API 相比2.0 API,提供了更多特性以及更好的性能表现,同时也向后兼容。这意味着你可以编写针对OpenGL ES 2.0的应用程序,并有条件地包括OpenGL ES 3.0图形特性(如果可用的话)。
支持纹理压缩(Texture compression support)
纹理压缩通过减少内存的使用和提高内存带宽使用效益来提升OpenGL应用程序的性能。Android框架提供ETC1压缩格式作为标准的特性,包括ETC1Util工具类和etc1tool压缩工具(存放于本地Android SDK的tool/目录下)。
注意:ETC1格式虽然是大多数Android设备支持的格式,但是并不能保证它是有效的。通过调用 ETC1Util.isETC1Supported()方法来检查应用程序是否支持ETC1.
Note:ETC1纹理压缩格式不支持透明度纹理的压缩。如果你的应用程序有用到透明度的部分,你应该检查设备是否有其他的纹理压缩格式。
ETC2/EAC纹理压缩格式在OpenGL ES 3.0 API中就能保证它的有效性。这种格式提供更高视觉质量的压缩比率,同时也支持压缩透明度。
在ETC之后,Android设备基于GPU芯片和OpenGL的实现实现了支持各种纹理压缩格式。你应该检索设备所支持的纹理压缩类型以便决定选出应用程序应该使用的压缩类型。为了决定你设备所支持的格式,您必须查询设备并查看OpenGL扩展名,这些名称标识设备支持哪些纹理压缩格式(以及其他OpenGL功能)。一些常见支持的纹理压缩格式如下:
- ATITC (ATC) - ATI纹理压缩 (ATITC or ATC) 普遍设备有效,在没有透明度通道的条件下,支持稳定的RGB纹理压缩速率。OpenGL扩展名表示如下:
- GL_AMD_compressed_ATC_texture
- GL_ATI_texture_compression_atitc
- PVRTC - PowerVR 纹理压缩 (PVRTC)普遍设备有效,在没有透明度通道的条件下,支持2位和4位单位像素纹理。OpenGL扩展名表示如下:
- GL_IMG_texture_compression_pvrtc
- S3TC (DXTn/DXTC) - S3 纹理压缩 (S3TC) 有几种格式的变化 (DXT1 to DXT5) ,不太普遍. 支持有透明度管道的4位或8位纹理压缩。OpenGL扩展名表示如下:
- GL_EXT_texture_compression_s3tc
一些设备只支持 DXT1格式,OpenGL扩展名表示如下:
- GL_EXT_texture_compression_dxt1
3DC - 3DC 纹理压缩 (3DC) 不太普遍,支持有透明度管道的RGB纹理. OpenGL扩展名表示如下:
- GL_AMD_compressed_3DC_texture
注意:纹理压缩格式并非所有设备都支持。支持这些压缩格式因手机厂商而异。获取关于如何在特殊设备上决定使用哪种纹理压缩格式,请继续阅读下文。
Note:一旦决定了你的应用程序所要支持的纹理压缩格式,确保在清单文件中使用<supports-gl-texture>来声明它们。使用这些声明并通过外部服务如Google Play来启动过滤工作,以便app只安装到你声明且被支持的设备上。
决定OpenGL的扩展名(Determing OpenGL entensions)
OpenGL的实现在Android设备上支持的OpenGL ES API的扩展方面有所不同。这些扩展包含纹理压缩,也包含其他OpenGL特性设置的扩展。
要确定特定设备上支持什么纹理压缩格式和其他OpenGL扩展:
1.在目标设备上运行以下代码以确定支持哪些纹理压缩格式:
String extensions = javax.microedition.khronos.opengles.GL10.glGetString(
GL10.GL_EXTENSIONS);
注意:这个返回结果因设备而异。你得运行在多种设备上以便获取哪种压缩类型是最常见的。
2.回顾方法返回的结果以便确定设备支持哪种OpenGL扩展名。
Android扩展包(AEP)
AEP确保你的应用程序支持一套标准化的OpenGL扩展以及一套上下文所描述的OpenGL 3.1规范。建议将一些功能一致的扩展集成到一起来跨平台使用,这样可以让开发者充分使用移动设备的GPU。
AEP还改进了对图像,着色器存储缓冲区和片段着色器中的原子计数器的支持。
为了app使用AEP,你得在应用程序的清单文件添加一些AEP需要的条件。另外,还要对应的平台版本支持对它的使用。
清单文件所需对AEP的声明如下:
<uses feature android:name="android.hardware.opengles.aep"
android:required="true" />
将FEATURE_OPENGLES_EXTENSION_PACK作为参数传入hasSystemFeature(String)方法,来确认平台是否支持AEP。如下代码片段的演示:
boolean deviceSupportsAEP = getPackageManager().hasSystemFeature
(PackageManager.FEATURE_OPENGLES_EXTENSION_PACK);
如果方法返回true,则表明设备支持AEP。
获取更多AEP信息,参考 Khronos OpenGL ES Registry.
检查OpenGL ES的版本
Android设备有几种有效的OpenGL ES版本。你可以在应用程序的清单文件中指定最小的API版本,但是有可能你也想使用最新版本的API特性。比如,OpenGL ES 3.0 API向后兼容2.0版本的API,所以你可以使用OpenGL ES 3.0的特性来编写应用程序。如果3.0 API不能使用,再继续使用2.0的API。
在使用一个更高版本的OpenGL ES特性之前,你得在应用程序上检查这个版本的API是否在这个设备上有效。可以有两种方式检查:
1.尝试创建一个高版本的OpenGL ES的上下文(EGLContext)然后检查结果。
2.创建最低支持的OpenGL上下文并检查版本的值。
下面的示例代码通过创建EGLContext并检查结果来检查是否3.0 OpenGL ES版本有效。
private static double glVersion = 3.0;
private static class ContextFactory implements GLSurfaceView.EGLContextFactory {
private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
public EGLContext createContext(
EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
Log.w(TAG, "creating OpenGL ES " + glVersion + " context");
int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, (int) glVersion,
EGL10.EGL_NONE };
// attempt to create a OpenGL ES 3.0 context
EGLContext context = egl.eglCreateContext(
display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
return context; // returns null if 3.0 is not supported;
}
}
如果createContext()方法返回null,你应该在代码中创建OpenGL ES 2.0的上下文来替代3.0然后使用这个API。
下面的代码示例如何先通过创建最低支持的上下文来检测OpenGL ES版本,然后检查版本的字符串:
// Create a minimum supported OpenGL ES context, then check:
String version = javax.microedition.khronos.opengles.GL10.glGetString(
GL10.GL_VERSION);
Log.w(TAG, "Version: " + version );
// The version format is displayed as: "OpenGL ES <major>.<minor>"
// followed by optional content provided by the implementation.
使用这种方式,如果你发现设备支持更高版本的API,你得先销毁低版本的OpenGL ES的上下文,然后使用所支持的高版本的API创建一个新的上下文。
选择OpenGL API版本(Choosing an OpenGL API Version)
OpenGL ES 1.0 API 版本(以及1.1的扩展),2.0版本以及3.0版本都为创建3D游戏,可视化UI提供了高性能的图形绘制接口。OpenGL ES 2.0和3.0的图形编程有很大的相似之处,3.0版本其实就是多了一些特性的2.0 版本集合库。OpenGL ES1.0/1.1的编程与OpenGL ES 2.0和3.0就相当的不同了,所以开发者在开始使用这些APIs开发之前应该仔细认真考虑如下的因素:
- 性能- 通常OpenGL ES 2.0和3.0都提供了比ES 1.0/1.1更快的图形性能。然而,由于不同的硬件厂商对OpenGL ES图形管道的实现,性能也因你运行OpenGL应用程序的Android设备而异。
- 设备兼容 - 开发者应该考虑设备的类型,Android的版本以及OpenGL ES的版本是否在他们的目标用户上有效。获取更多关于OpenGL跨设备兼容的内容,可参看上文。
- 代码的简便性 - OpenGL ES 1.0/1.1 API提供了稳定的功能管道和便利的功能,不过这些功能在OpenGL ES 2.0、3.0 APIs已经失效了。对OpenGL ES新手的开发人员可能会发现版本1.0 / 1.1的编码更快,更方便。
- 图形控制 - OpenGL ES 2.0和3.0 API通过使用着色器提供完全可编程的管道来提供更高程度的控制。通过更直接地控制图形处理管道,开发人员可以创建1.0 / 1.1 API很难生成的效果。
- 纹理支持 - OpenGL ES 3.0 API 拥有最好的纹理压缩支持,因为它保证了支持透明度的ETC2压缩格式的有效性。1.x和2.0 API的实现通常包含ETC1的支持,然而,纹理格式不支持透明度,所以你得针对设备提供的其他压缩格式资源。
当性能,兼容性,简便性,图形控制以及其他因素影响了你的选择,你就应该基于什么才是最好的用户体验的思想去选择一个OpenGL API版本。