在英特尔® 凌动™ 处理器上将 OpenGL* 游戏移植到 Android* (第一部分)

将游戏和其他使用大量 3D 图形的应用从 OpenGL 标准移植到 Google Android 设备(包括构建在英特尔® 凌动™ 微架构上的设备)存在巨大的机遇,因为基于 OpenGL 的游戏、游戏引擎和其他传统软件易于获得;OpenGL 便于移植;而且 Android 可提供对 OpenGL ES 和 C/C++ 的支持。 甚至,许多基于 OpenGL 的游戏和引擎的可用性与开源软件一样,如 Id Software 的 Quake 系列。 本文包括两部分的内容,通过详述在英特尔凌动处理器上将早期版本的 OpenGL 所构建的应用的渲染组件移植到 Android 中存在的障碍来介绍如何开始这样的项目。 这些应用可以是游戏、游戏引擎或使用 OpenGL 构建 3D 场景或图形用户界面(GUI)的任何一种软件。 此外,还包括从台式机操作系统(如 Windows* 和 Linux* )以及使用嵌入式版本的 OpenGL ES(无论包括或不包括 windowing 系统)的应用移植 OpenGL 代码。

本文的第一部分介绍了如何通过软件开发套件(SDK)或 Android 原生开发工具套件(NDK)在 Android 平台上使用 OpenGL ES,以及如何确定选择何种方法。 本部分还介绍了各种 SDK 和 NDK 中的 OpenGL ES 示例应用,以及 Java* 原生接口(JNI),这支持您结合使用 Java 和 C/C++ 组件。 此外,还讨论了如何确定使用 OpenGL ES 版本 1.1 还是 2.0。

第二部分将讨论您在开始这样的项目之前,应该了解的移植 OpenGL 游戏存在的障碍,包括 OpenGL 扩展、浮点支持、纹理压缩格式和 GLU 库的区别。 此外,还介绍了借助 OpenGL ES 如何为英特尔凌动处理器设置 Android 的开发系统,以及如何获得 Android 虚拟设备模拟工具的最佳性能。

Android 上的图形

您可以通过四种不同的方式在 Android 平台上渲染图形,每种方式都有其长处和不足之处(见表 1)。 本文并未对四种方式都进行介绍。 只有两种方式适用于从其他平台移植 OpenGL 代码: 面向 OpenGL ES 的 SDK 包装程序类,以及可用于在 C/C++ 中开发 OpenGL ES 的 NDK。 关于其他两种方法,SDK Canvas 应用编程接口(API)是一款强大的 2D API,可支持您结合 OpenGL ES 使用,但是仅限于 2D 且需要使用新的代码。

Android 的 Renderscript API 最初并不支持 OpenGL ES,已在 API 等级 16 (Jelly Bean) 中被弃用,因而不能在新的项目中使用。 现在,最适合 Renderscript 的是可以提升计算密集型算法的性能且不需要分配大量内存或传输大量数据的应用,如仿真游戏物理引擎的计算。

表 1.在 Android 上渲染图形的四种方式

方法 规定
SDK Canvas API 仅支持 Java 和 2D 图形
面向 OpenGL ES 的 SDK 包装程序类 可从 Java 调用 OpenGL ES 1.1 和 2.0(但是有 JNI 开销)
NDK OpenGL ES OpenGL ES 1.1 和 2.0(包括从 Java 中调用原生 C/C++)
面向 OpenGL ES 的 Renderscript OpenGL ES 支持已在 API 等级 16 中弃用

将 OpenGL 应用移植到早期版本的 Android 较为困难,因为大多数传统的 OpenGL 代码是使用 C 或 C++ 进行编写的,而且 Android 仅支持 NDK 在 Android 1.5 中发布之前的 NDK(Cupcake)。 OpenGL ES 1.0 和 1.1 从开始便可提供支持,但是性能不一致,因为加速为可选操作。 但是,近年来 Android 取得了重大的进步。 向 Android 2.2 (Froyo) 中的 SDK 和修订版 3 中的 NDK 添加了 OpenGL ES 2.0 支持,在 NDK 修订版 7 中的 OpenGL ES 扩展添加了扩展支持。 现在,在所有新的 Android 设备上,加速的 OpenGL ES 1.1 和 2.0 是必备配置 — 尤其随着屏幕尺寸不断增大。 今天,Android 可为 Java 或 C/C++ 中 OpenGL ES 1.1 或 2.0 上构建的 3D 密集型应用提供一致、可靠的性能,且开发人员可选择多种方式让植入流程更轻松。

使用采用 OpenGL ES 包装程序类的 Android 框架 SDK

Android SDK 框架可为 Android 支持的三个版本的 OpenGL ES (1.0、1.1 和 2.0) 提供一套包装程序类。 这些分类支持 Java 代码在 Android 系统中轻松调用 OpenGL ES 驱动程序 — 即使驱动程序在本地执行。 如果您正在从新开始创建一个新的 OpenGL ES Android 游戏,或愿意将传统的 C/C++ 代码转换为 Java,则这可能是最简单的方法。 虽然 Java 的设计具备便携性,但是移植 Java 应用却较为困难,因为 Android 不能支持全套的现有 Java 平台、标准版(Java SE)或 Java 平台、微型版 (Java ME)分类、库或 API。 虽然 Android 的 Canvas API 支持 2D API,但是它仅可在 Android 上使用且无法与传统代码兼容。

Android SDK 提供的多种其他分类让使用 OpenGL ES 更加轻松,如 GLSurfaceViewTextureViewGLSurfaceView 与配合 Canvas API 使用的 SurfaceView 类相类似,但是它还具备其他一些特性 — 尤其对 OpenGL ES。 它可处理所需的嵌入式系统图形库(EGL)的初始化,并可分配渲染接口以便 Android 在屏幕的固定位置显示并进行渲染。 此外,它还具备一些追踪和调试 OpenGL ES 调用的有用特性。 通过执行面向 GLSurfaceView.Renderer() 接口的三种方法,您可以快速创建一个新的 OpenGL ES 应用,如表 2 所示。

表 2. 适用于 GLSurfaceView.Renderer 的基本方法

方法 描述
onSurfaceCreated() 在应用开始初始化时调用一次
onSurfaceChanged() 当 GLSurfaceView 的尺寸或方向发生变化时进行调用
onDrawFrame() 重复调用以渲染每帧图形场景

如果从 Android 4.0 开始,您可以使用 TextureView 类,不要使用 GLSurfaceView,以便为具备额外功能的 OpenGL ES 提供渲染接口,但是这需要使用更多代码。 TextureView 接口的运行方式与普通 Views 相同,并可用于渲染至屏幕外接口。 当将 TextureViews 合成至屏幕时,借助该功能,您可以使其迁移、转化、实现动画效果或混合。 此外,您也可以使用 TextureView 以结合使用 OpenGL ES 渲染和 Canvas API。

The 借助位图GLUtils 类,使用 Android Canvas API 为 OpenGL ES 创建纹理,或从 PNG、JPG 或 GIF 文件加载纹理将更轻松。 位图可用于为 Android 的 Canvas API 分配渲染接口。 GLUtils 类可将影响从位图转换为 OpenGL ES 纹理。 该集成支持您使用 Canvas API 渲染 2D 映像,然后将其用作配合 OpenGL ES 使用的纹理。 这对于创建 OpenGL ES 未提供的图形元素尤其有用 如 GUI widget 和文本字体。 但是当然,需要使用新的 Java 代码来利用这些特性。

位图类主要是配合 Canvas API 使用,当将它用于为 OpenGL ES 加载纹理时,有一些严重的限制。 Canvas API 遵循适用于 alpha 值混合处理的 Porter-Duff 规格,位图通过以 premultiplied 格式(A、R*A、G*A、B*A)进行存储来优化每个像素 alpha 值的映像。 这适合 Porter-Duff 但不适合 OpenGL,后者需要使用非 premultiplied(ARGB)格式。 这意味着位图类仅可配合完全不透明(或没有每个像素的 alpha 值)的纹理使用。 一般而言,三维游戏需要使用带有每个像素 alpha 值的纹理,在这种情况下,您必须避免使用位图,而从字节阵列或通过 NDK 来加载纹理。

另一个问题是位图仅支持从 PNG、JPG 或 GIF 格式加载映像,但是一般情况下,OpenGL 游戏使用由 GPU 解码的压缩纹理格式,且通常仅专用于 GPU 架构,如 ETC1 和 PVRTC。 位图GLUtils 不支持任何专用的压缩纹理格式或 mipmapping。 因为这些纹理被大部分的 3D 游戏频繁使用,这为使用 SDK 将传统的 OpenGL 游戏移植到 Android 带来严重的障碍。 直到 Google 解决了这些问题,最好的解决方法是避免使用位图GLUtils 类来加载纹理。 本文中将进一步讨论纹理格式,"纹理压缩格式。”

Android ApiDemos 包含一个名为 StaticTriangleRenderer 的示例应用,可证明如何使用面向 OpenGL ES 1.0 的 GLES10 封装、GLSurfaceView位图GLUtils 类从 PNG 资源加载不透明纹理。 名为 GLES20TriangleRenderer 的类似版本使用面向 OpenGL ES 2.0 的 GLES20 封装类。 如果您正在使用封装类处理 Android 框架 SDK,这些示例应用可为开发 OpenGL ES 游戏奠定良好的基础。 请勿使用名为 TriangleRenderer 的原始版本,因为它为名为 javax.microedition.khronos.opengles 的 Java 使用了面向较旧版本的 OpenGL ES 绑定的封装。 Google 创建了新的绑定,从而能够为专门面向 Android 的 OpenGL ES 提供静态接口。 这些静态绑定可提供更出色的性能,实现更多的 OpenGL ES 特性,并可提供更接近于 OpenGL ES 配合 C/C++ 使用的编程模型 — 这有益于代码的重新使用。

Android 框架 SDK 可通过 Google 和 Khronos 提供的面向 Java 的多种绑定为 OpenGL ES 提供支持,如表 3 所示。

表 3. 面向 OpenGL ES 的封装类总结和示例应用

面向 OpenGL ES 的 Java 绑定 描述 示例应用
javax.microedition.khronos.egl Khronos standard definition
javax.microedition.khronos.opengles Khronos standard definition TriangleRenderer, Kube, CubeMap
android.opengl Android 专用静态接口

Android 专用静态绑定可提供更出色的性能,如果可行,应使用它而非 Khronos 绑定。 静态绑定可为 Android 上应用开发可用的所有版本的 OpenGL ES 提供相应的封装类。 表 4 对这些类进行了总结。

表 4. 面向 OpenGL ES 的 Android 封装类总结和示例应用

API 版本 Java 类 示例应用
OpenGL ES 1.0 android.opengl.GLES10 StaticTriangleRenderer, CompressedTexture
OpenGL ES 1.0 android.opengl.GLES10Ext
OpenGL ES 1.1 android.opengl.GLES11
OpenGL ES 1.1 android.opengl.GLES11Ext
OpenGL ES 2.0 android.opengl.GLES20 GLES20TriangleRenderer, BasicGLSurfaceView, HelloEffects

这些封装类支持传统的 C/C++ 代码中的大部分 OpenGL ES 调用仅通过使用适当 API 版本的封装类为 OpenGL ES 函数和符号名加前缀来转换为 Java。 参阅表 5 中的示例。

表 5.将 OpenGL ES 调用从 C 编辑为 Java 的示例

C 语言 Java 语言
glDrawArrays(GL_TRIANGLES, 0, count) GLES11.glDrawArrays(GLES11.GL_TRIANGLES, 0, count)
glDrawArrays(GL_TRIANGLES, 0, count) GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, count)

使用上述封装类将 OpenGL 游戏移植至 Android 有三大局限: 需要使用大量 Java 虚拟机和 JNI 的开销以及操作将传统的 C/C++ 代码转换为 Java。 Java 是解释语言,Android 上所有 Java 代码都在 Dalvik 虚拟机上运行,因此比编译的 C/C++ 代码运行地更慢。 因为 OpenGL ES 驱动程序总是在本地运行,所以每次通过这些封装调用 OpenGL ES 函数都会造成 JNI 开销,这限制了游戏图形渲染的性能。 您的应用发出的 OpenGL ES 调用越多,对 JNI 开销造成的而影响越大。 所幸,OpenGL ES 的设计可帮助最大限度地减少一般在性能关键型渲染循环中需要的调用数量。 如果性能很重要,您可以随时选择使用 NDK 将性能关键型代码迁移至 C/C++。 但是当然,如果您的代码开始就是 C/C++,最好先使用 NDK。

是否需要将 C/C++ 代码转换为 Java 取决于您项目的具体情况。 如果 C/C++ 代码的量相对较小且易于理解,而且您不希望提高性能功耗,那么可以将其转换至 Java。 如果 C/C++ 代码的量较大且不易理解,而且您不希望增加功耗,则可以考虑使用 NDK。

使用 Android NDK

Google 于 2009 年 6 月添加了 NDK 以允许应用使用本地运行的 C/C++ 代码,这可比 Java 提供更高的性能。 因为大部分传统的 OpenGL 代码使用 C 或 C++ 编写,NDK 可提供更简单的路径来移植应用,尤其当 C/C++代码的量太大以至于将其全部转换为 Java 不实际的时候。 这是 Google 决定公开释放 NDK 的主要原因,这可让将 OpenGL 游戏移植到 Android 更轻松。 由于这些优势,NDK 成为在 Android 上实现需要较快图形速度的应用的主要方法。

借助 NDK,您可以将 C/C++ 代码编写至 Linux 共享对象库,这些库静态连接至您的 Android 应用。 库使用 GNU 工具构建。这些工具包含在 Google 提供的 NDK 发行软件包中,您可以使用 Eclipse* 集成开发环境或命令行接口在 Windows、Mac OS* X 或 Linux 开发系统上运行该工具。 该工具链支持三种处理器架构: ARM、英特尔凌动 (x86) 和 MIPS。 虽然 C/C++ 的全部性能可用,但是大部分的 Linux API 不可用。 事实上,直接支持的 API 仅包括 OpenGL ES、OpenMAX* AL、OpenSL ES*、zlib 和 Linux 文件 I/O,Google 称其为稳定 API。 但是,根据需求将会提供有关如何将其他的 Linux 库移植到您的 NDK 项目中的文档。

NDK 允许您根据应用的情况灵活地在 Java 和 C/C++ 之间对代码分区。 NDK 支持从名为 Java Native Interface 的 Java 调用 C/C++ 代码。 但是,JNI 调用将出现大量的开销,因此,使用原生代码对应用分区以便最大程度地减少通过 JNI 的调用量非常重要。 一般而言,大部分的 OpenGL ES 应保持以 C/C++ 编写以便获得最佳性能和易于移植,但是如要使用 GLSurfaceView 和其他 SDK 类来管理应用生命周期事件和支持其他游戏函数,可以编写新的 Java 代码。 Android 开始支持面向英特尔凌动处理器的 JNI,以 NDK 修订版 6b 开始。

NDK 支持 OpenGL ES 1.1 和 2.0 并可为两个版本提供样本应用,这些样本应用还可演示如何使用 JNI 将 C 函数与 Java 结合。 这些应用区别在于其代码在 Java 和 C 之间的分区以及线程化的方式。 它们均使用 NDK 和本地 C 代码,但是,native-media 样本应用的所有 OpenGL ES 代码都是在 Java 中完成,而 san-angelesnative-activity 的所有 OpenGL ES 代码都是在 C 中完成,hello-gl2 在 Java 和 C 之间分割了其 EGL 和 OpenGL ES 代码。我们应避免使用 hello-gl2 样本,不仅因为上述的分割,而且因为它无法为 OpenGL ES 2.0 接口优先配置 GLSurfaceView,它负责调用 setEGLContextClientVersion(2)。 请参见表 6。

表 6. NDK 中的 OpenGL ES 样本应用总结

使用的 API 示例应用 SDK/NDK 分区
OpenGL ES 1.1 san-angeles 所有 EGL 和 OpenGL ES 代码均为 C。
OpenGL ES 1.1 native-activity 所有代码均为 C 并使用 NativeActivity 类。
OpenGL ES 2.0 hello-gl2 EGL 设置位于 Java,OpenGL ES 代码为 C。
OpenGL ES 2.0 native-media 所有的 EGL 和 OpenGL ES 代码均为 Java。

虽然未使用 OpenGL ES,但是 bitmap-plasma 样本也非常有趣,因为它示范了如何使用 jnigraphics 库来执行本地函数,直接访问 Android 位图的像素。

注: 您可以从 http://developer.android.com/tools/sdk/ndk/index.html 下载 Android NDK。

活动生命周期事件和线程化

Android 要求所有对 OpenGL ES 的调用均从单线程执行,因为 EGL 环境仅可与单线程关联,非常不建议在主 UI 线程上渲染图形。 所以,最好的方法是专门为所有的 OpenGL ES 代码创建单独的线程并一直通过该线程执行。 如果您的应用使用了 GLSurfaceView,它可自动创建此专用 OpenGL ES 渲染线程。 在其他情况下,您的应用必须自己创建渲染线程。

san-angelesnative-activity 样本应用的所有 OpenGL ES 代码均在 C 中,但是 san-angeles 使用了一些 Java 和 GLSurfaceView 来创建渲染线程和管理活动生命周期,而 native-activity 样本未使用任何 Java 代码。 不要使用 GLSurfaceView,因为它在 C 中管理活动生命周期;使用 NativeActivity 类提供的渲染线程。 NativeActivity 是 NDK 提供的一种使用便捷的类,支持您使用本地代码执行活动生命周期处理程序,如 onCreate()onPause()onResume()。 一些 Android 服务和内容提供商无法直接从本地代码访问,但可以通过 JNI 获得。

native-activity 样本适合导入 OpenGL ES 游戏,因为它示范了如何使用 NativeActivity 类和 android_native_app_glue 静态库用本地代码处理生命周期活动。 该类提供了一个单独的面向 OpenGL ES 代码的渲染线程、一个渲染接口和一个界面上的窗口,因此,您无需使用 GLSurfaceViewTextureView 类。 该本地应用的主要入口点是 android_main(),它可在自己的线程中运行并拥有自己检索输入事件的事件循环。 遗憾的是,NDK 无法提供面向 OpenGL ES 2.0 的样本版本,但是您可以使用 2.0 代码在该样本中更换所有 1.1 代码。

使用 NativeActivity 的应用必须在 Android 2.3 或更高版本上使用,并在其清单文件中发出专门的声明,详见本文的第二部分。

Java 原生接口

如果您选择在 C/C++ 中实现大部分的应用,将很难避免为更大型且更专业的项目使用 Java 类。 例如,Android AssetManager 和 Resources API 仅可在 SDK 中使用,这是处理国际化和不同界面尺寸等的首选方式。 但是,JNI 可提供解决方法,因为它不仅允许 Java 代码调用 C/C++ 函数,还允许 C/C++ 代码调用 Java 类。 因此,虽然 JNI 会产生一些开销,但是请不要完全避免使用它。 它是访问仅可在 SDK 中使用的重要系统功能的最好方法 — 尤其当这些功能不是性能关键型功能时。 本文不对使用 JNI 进行完整介绍,但是下面列出了使用 JNI 从 Java 调用至 C/C++ 所需的三个基本步骤:

  1. 为 Java 类文件中的 C/C++ 函数添加一个声明作为本地类型。
  2. 为包含本地函数的共享对象库添加一个静态初始化器。
  3. 按照具体的命名方案向本地源文件中添加相应名称的功能。

注: 关于借助 NDK 使用 JNI 的更多信息,请参阅 http://developer.android.com/training/articles/perf-jni.html

选择 OpenGL ES 1.1 还是 2.0?

您应在 Android 上使用哪个版本的 OpenGL ES? 1.0 版的 OpenGL ES 已被 1.1 版取代,因此,真正需要选择的是 1.1 版和 2.0 版。 Khronos 和 Google 可能无限定地支持两种版本,但是在大部分情况下,OpenGL ES 2.0 优于 1.1。 凭借其 OpenGL 着色语言(GLSL)ES 着色器编程特性,它功能更全面并可提供更高的性能。 甚至,它可能会需要更少的代码和内存来处理纹理。 但是,Khronos 和 Google 仍然继续支持 1.1 版的原因是,它更像台式机和控制台游戏世界数十年来一直使用的初始 OpenGL 1.x。 因此,将旧版的游戏移植到 OpenGL ES 1.1 比 2.0 更轻松;而且游戏版本越旧,这种情况越适用。

如果移植的游戏没有着色器代码,则您可以选择 OpenGL ES 1.1 也可以选择 2.0,但是使用 1.1 版可能更轻松。 如果您的游戏已经有了着色器代码,那么肯定应该选择 OpenGL ES 2.0,尤其是考虑到近来的 Android 版本都大量地使用了 2.0 版。 根据 Google,截至 2012 年 10 月,访问 Google Play 网站的 Android 设备中有超过 90% 的设备支持 OpenGL ES 1.1 和 2.0 两种版本。

注: 更多信息,请参阅 http://developer.android.com/about/dashboards/index.html

结论

You can implement graphics rendering on Android with OpenGL ES through the Android SDK, the NDK, or a combination of both using the JNI. SDK 方法需要在 Java 中编码,且最适合新应用的开发,但是 NDK 对以 C/C++ 移植传统 OpenGL 更实用。 大部分的游戏移植项目需要结合使用 SDK 和 NDK 两种组件。 对于新项目,应选择 OpenGL ES 2.0 而非 1.1 版 — 除非您的传统 OpenGL 代码太旧而无法使用任何 GLSL 着色器代码。

该系列的第二部分将讨论您在开始这样的项目之前必须了解的移植 OpenGL 游戏存在的障碍,包括 OpenGL 扩展的差别、浮点支持、纹理压缩格式和 GLU 库。 此外,还介绍了借助 OpenGL ES 如何为英特尔凌动处理器设置 Android 的开发系统,以及如何获得 Android 虚拟设备模拟工具的最佳性能。

关于作者

Clay D. Montgomery 是在嵌入式系统上开发面向 OpenGL 的驱动程序和应用的主要开发人员。 他曾在 STB 系统、VLSI 技术、飞利浦半导体、诺基亚、德州仪器、AMX 以及作为独立顾问从事跨平台图形加速器硬件、图形驱动程序、APIs 和 OpenGL 应用的设计。 他曾参与 Freescale i.MX 和 TI OMAP* 平台以及 Vivante、AMD 和 PowerVR* 首个 OpenGL ES、OpenVG* 和 SVG 驱动程序和应用的开发。 他开发了在嵌入式 Linux 上开发 OpenGL ES 并开设了研讨班进行教授,且是 Khronos Group 多家公司的代表。

更多相关信息