在 Android* 平台上设置原生 OpenGL ES*

Setting up Native OpenGL ES* on Android* Platforms final.docx
BSD2.0.txt
ParticleSystemNDK.zip
README.txt

摘要

本文具体介绍了在 Android 平台上创建原生 OpenGL ES 2.0 图形应用的基本步骤。 首先将讨论使用 Java* API 设置 OpenGL ES 2.0 应用的流程,随后将介绍在共享库中使用原生渲染来转换应用。

注:为了尽可能向更多的受众提供支持,本文中未使用 IDE。 所有的项目创建、维护和部署均将通过 Google 提供的命令行工具集执行。

项目设置

如要创建默认的 Android 项目,当出现命令提示符时执行下列命令。

android 创建项目 --target <TID> \
  --name <ProjName> \
  --path <FolderPath> \
  --activity <ActivityName> \
  --package <Package>

注: 具体的目标 ID 将取决于如何配置 Android SDK。 出现命令提示符时通过运行 android list targets 检索可用目标列表。

下列设置将用于该项目:

--name DemoProject
--activity DemoProjectActivity
--package demo.project

Android 实用程序生成的项目是默认的“Hello World”项目。 在项目的根目录中时,如果出现命令提示符,通过运行 ant debug 来确认项目是否正常初始化。

添加 OpenGL ES 支持

Android 可直接向 Java 曝露 OpenGL ES API 以便应用开发人员使用。 我们将使用设置 OpenGL 的分类和方法来利用 Java API 或执行原生渲染。

因此,我们将会预先讨论使用 Java API 渲染简单场景的详细情况。 接下来,我们将会对示例应用进行修改以使用原生渲染。

卸载标题栏

运行 Hello World 应用时,该应用的标题栏在运行中可见。 一般而言,这种运行状况在编写 OpenGL ES 应用时不需要。

通过在 AndroidManifest.xml 文件中添加适当的活动标记主题属性可禁用该运行状况。

android:theme=�@android:style/Theme.NoTitleBar.Fullscreen�

或者,也可以在活动类的 onCreate 方法内运行下列代码。

requestWindowFeature(Window.FEATURE_NO_TITLE);

getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                     WindowManager.LayoutParams.FLAG_FULLSCREEN);

更换默认视图

Hello World 项目使用了 setContentView 的超载,向 XML 资源中添加了资源标识符以压缩并设置为当前的活动视图。 必须使用能够渲染 OpenGL ES 文本的视图接口来更换该操作。 Android 可提供 Java 类 GLSurfaceView,它可以压缩渲染接口和 EGL 显示。

您可以将 “res/layout/main.xml”文件从项目中删除,因为该视图可通过编程方式创建。

GLSurfaceView

适用于 GLSurfaceView 的两种导入方法是 setEGLContextClientVersion 和 setRenderer。

setEGLContextClientVersion 方法可设置需要使用的 ES 版本。 对于该项目,我们使用了 OpenGL ES 2.0。

渲染至环境由从 GLSurfaceView.Renderer 继承的类实例来控制。渲染器示例通过调用 setRenderer 方法来设置。

必须将 Activity 类的 onPause 和 onResume 覆盖才能够在 GLSurfaceView 类上分别调用方法来暂停/恢复渲染线程。

下列是完整的包含 GLSurfaceView 的 DemoActivity 类:

public class DemoActivity extends Activity 
{
	private GLSurfaceView view;

	@Override
	public void onCreate(Bundle savedInstanceState) 
{
		super.onCreate(savedInstanceState);

		view = new GLSurfaceView();

		// Tell EGL to use a ES 2.0 Context
		view.setupEGLContextClientVersion(2);

		// Set the renderer
		view.setRenderer(new DemoProjectRenderer());

		setContentView(view);
	}

	@Override
	protected void onPause() 
{
		super.onPause();
		view.onPause();
	}

	@Override
	protected void onResume() 
{
		super.onResume();
		view.onResume();
	}
}

GLSurfaceView.Renderer

从 GLSurfaceView.Renderer 继承的类负责向 OpenGL 环境中绘制。 GLSurfaceView.Renderer 曝露了三种必须在继承的类中执行的摘要方法: onSurfaceCreated、onSurfaceChanged 和 onDrawFrame。

Android 应用暂停或挂起时,OpenGL 环境以及所有与其相关的资源都将被破坏。 因此,针对应用的初始化和资源加载应在 onSurfaceCreated 功能中处理。

注:有一种方法 — setPreserveEGLContextOnPause — 可以让 EGL 跨越暂停/恢复界限来尝试和保存环境。 但是请注意,这种功能依赖于硬件对多 EGL 环境的支持,因此,调用它并不能确保一定将环境保存。

演示项目将使用 Java OpenGL ES API 来清除颜色缓冲区的品红色。

针对演示项目从 GLSurfaceView.Renderer 得出的类如下所示:

class DemoProjectRenderer implements GLSurfaceView.Renderer 
{

	public void onDrawFrame(GL10 gl) 
{
		gl.glClear(gl.GL_COLOR_BUFFER_BIT);
	}

	public void onSurfaceChanged(GL10 gl, int width, int height) 
{		
		gl.glViewport(0, 0, width, height);
	}

	public void onSurfaceCreated(GL10 gl, EGLConfig config) 
{
gl.glClearColor(1.0f, 0.0f, 1.0f, 1.0f);
	}
}

AndroidManifest.xml

清单文件应通过在清单元素下添加下列 XML 元素来指定对 OpenGL ES 2 的需求。

<uses-feature android:glEsVersion=``0x00020000'' android:required=``true'' />

迁移至原生渲染

无需重新实现现有的代码部分来使用 Java 接口,可以通过从 Java 类调用原生代码来利用现有的 C/C++ 代码库。 原生代码已编译至共享库,并使用 APK 文件部署。

所有的原生代码都位于项目根目录中的 jni 文件夹下。

JNI 基础

原生代码通过 JNI 框架曝露至 Java 各类。 如欲了解 JNI 框架的具体解释,请参阅 http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

必须在载入程序中遵守 JNI 命名规则,以便能够正确找到原生方法。 所有的原生方法:

  • 必须以前缀 Java_ 开始
  • 然后使用完全限定的 mangled 类名
  • 然后使用完全限定的 mangled 方法名

例如,以下是一个简单的原生 C 函数:

void nativeLibraryFunction(int width, int height);

如要在不修改现有函数的情况下将该方法暴露至 Java 代码,请加入 JNI 头文件并创建可调用现有原生方法的封装方法。

#include <jni.h>
JNIEXPORT void JNICALL Java_demo_project_LibraryClass_LibraryFunctionWrapper(JNIEnv* env, jobject obj, jint width, jint height)
{
    nativeLibraryFunction(width, height);
}

上述函数假定面向项目的软件包是 demo.project,并将使用名为 LibraryClass 的 Java 类来暴露原生函数。

在所有导出的 JNI 函数中第一个参数属于“JNIEnv*.”类型。该参数有多种功能,其中最重要的是支持原生代码回调至 Java 代码。 本文中暂不讨论这种运行状况。

对于静态函数, jobject 参数是调用 Java 类的参数。

其他的参数类型从 JNI 框架暴露。 对于内置 C 类型,JNI 框架可自动控制转换。 对于阵列和复杂的类型,请参考文档。

在 Java 端上必须导入共享库。 这可通过调用至 System.loadLibrary 来实现。这一般在面向该类的静态初始化器中完成。

此外,类必须声明适用于导出方法的接口。 这些方法为静态,并声明为本地。

class LibraryClass
{
    static
{
        System.loadLibrary("demo");
    }
    public static native void LibraryFunctionWrapper(int width, int height);
}

重要的一点是,LibraryClass 名称与导出的原生方法暴露的名称一致。 此外,需要将导出的功能的定义标记为本地。

此处,库应遵守目标平台上的共享库规则。 对于基于 Unix 的目标(如 Android),库名称应以 “lib”为前缀并附加 “.so”。 对于基于 Windows* 的目标,应附加扩展名“dll”。

注: 对于复杂的函数声明,Java 可提供实用程序 javah,为原生函数生成头文件定义(考虑到 Java 类文件)。

现在,您可从代码中的任何位置将该方法调用为静态 Java 方法。 对于具体的示例,DemoProject 现可迁移至原生渲染。

迁移初始化代码

在项目 jni 文件夹中创建 C 文件。 此处,该文件为“nativelibrary.c.”。

文件将会使用所有的原生 OpenGL ES 调用。

void InitializeOpenGL() 
{
	glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
}
void resizeViewport(int newWidth, int newHeight) 
{
	glViewport(0, 0, newWidth, newHeight);
}
void renderFrame() 
{
	glClear(GL_COLOR_BUFFER_BIT);
}

假定这些方法已存在于项目或静态库中。 如要使用这些方法 JNI,需要为这三种方法中的每一种方法创建 JNI 封装方法。 这些方法可采用另一种 C 文件 jniExports.c。

JNIEXPORT void JNICALL Java_demo_project_SomeClass_init(JNIEnv*) 
{
	InitializeOpenGL();
}
JNIEXPORT void JNICALL Java_demo_project_SomeClass_resize(JNIEnv*, jint width, jint height) 
{
	resizeViewport(width, height);
}
JNIEXPORT void JNICALL Java_demo_project_SomeClass_render(JNIEnv*) 
{
	renderFrame();
}

在 Java 端上,导出的方法在库类中被声明。

class LibraryClass 
{
	static 
{
		System.loadLibrary("demo");
	}

	public static native void init();
	public static native void resize(int width, int height);
	public static native void render();
}

现在,GLSurfaceView.Renderer 派生类已进行了修改以调用原生方法:

class DemoProjectRenderer extends GLSurfaceView.Renderer 
{
	public void onDrawFrame(GL10 gl) 
{
		LibraryClass.render();
	}

	public void onSurfaceChanged(GL10 gl, int width, int height) 
{
		LibraryClass.resize(width, height);
	}

	public void onSurfaceCreated(GL10 gl, EGLConfig config) 
{
		LibraryClass.init();
	}
}

配置原生 Build

Android.mk

在 “jni”文件夹中,创建了 “Android.mk”文件以指导 NDK 编译原生模块。 在粒子示例项目中使用的示例 Android.mk 如下所示:

# Tell Android where to locate source files
# "my-dir" is a macro function which will return the path of the current 
directory(where Android.mk resides)
LOCAL_PATH := $(call my-dir)

# Clear contents of the LOCAL_* variables
include $(CLEAR_VARS)

# All the source files to include in this module
LOCAL_SRC_FILES := nativeLibrary.c \
                   jniExports.c 

# The name of the module
LOCAL_MODULE := libdemo

# Compilation flags
LOCAL_CFLAGS := -Werror

# Static libraries to link with
LOCAL_LDLIBS := -llog –landroid -lGLESv2

# Build the module
include $(BUILD_SHARED_LIBRARY)

从命令提示符运行 ndk-build 命令以构建共享库。 这一步应在使用 ant 构建 Android 应用之前完成。

读取 APK 中的资产

访问资产是迁移至本地渲染需要特别关注的一个领域。 资产管理器从 Java 传送至原生代码。 例如,在活动 onCreate 方法中,添加了下列代码行:

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    assetManager = getAssets();
    LibraryClass.initializeAssetManager(assetManager);

    view = new GLSurfaceView();

    // Tell EGL to use a ES 2.0 Context
    view.setupEGLContextClientVersion(2);

    // Set the renderer
    view.setRenderer(new DemoProjectRenderer());

    setContentView(view);
}

JNIEXPORT void JNICALL Java_demo_project_LibraryClass_initializeAssetManager(JNIEnv* env, jobject obj, jobject assetManager)
{
    AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
}

AAssetManager_fromJava 函数由 NDK 提供,可获取原生指针至资产管理器实例。 APK 中存储的资产可通过 AAssetManager API 访问。 关于使用 AAssetManager API 读取文件的示例,请参阅粒子示例源代码中的 util/file.c。

粒子示例代码

上述文本详细介绍了在 Android 平台上启用原生 OpenGL ES 工作负载的一套基础步骤。 如欲了解更完整的示例,适用于原生 OpenGL ES 2.0 粒子系统源代码可通过 https://software.intel.com/zh-cn/android/learn?&topic=20780 获得。 与文档相关的源代码部分将在下文中进行介绍。

ParticlesActivity.java

本类储存了主要的应用活动。 它负责创建 GLSurfaceView 派生类,以及通过调用 createAssetManager 将应用的资产管理器传送至原生 C 代码。

ParticlesView.java

在粒子示例中,从 GLSurfaceView 中继承了一个明确的类来设置 OpenGL ES 客户端版本。 此外,接口渲染作为内部类来实现。

ParticlesLib.java

本类可导入共享库并声明示例使用的三种原生方法: init、step 和 createAssetManager。

Shared Library Code

示例中使用的共享库的代码位于 jni 目录下。

导出的 C 函数钩子位于 jni/gl_code.cpp 中。它们可暴露初始化、帧步骤和资产创建函数。 这些方法的实现位于 jni/App.c 中。

示例中使用的粒子系统位于 jni/Particles.c and jni/Particles.h 中。

位于示例中的其他文件是适用于数学函数、简单几何创建和纹理加载的实用程序方法。 该代码位于 jni/util 目录下。

值得特别关注的一个文件是读取资产数据的简单实用程序函数,它可在 util/file.c 中找到。

关于作者

Chris Kirkpatrick 是就职于英特尔软件与服务事业部的软件应用工程师,在个人设备外形(Personal Form Factors)部提供英特尔显卡解决方案支持。 他拥有俄勒冈州立大学计算机科学学士学位。

声明

本文件中包含关于英特尔产品的信息。 本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。 英特尔不承担任何其他责任。英特尔在此作出免责声明:本文件不构成英特尔关于其产品的使用和/或销售的任何明示或暗示的保证,包括不就其产品的(i)对某一特定用途的适用性、(ii)适销性以及(iii)对任何专利、版权或其他知识产权的侵害的承担任何责任或作出任何担保。

除非经过英特尔的书面同意认可,英特尔的产品无意被设计用于或被用于以下应用:即在这样的应用中可因英特尔产品的故障而导致人身伤亡。

英特尔有权随时更改产品的规格和描述而毋需发出通知。 设计者不应信赖任何英特产品所不具有的特性,设计者亦不应信赖任何标有“保留权利”或“未定义”的说明或特性描述。 对此,英特尔保留将来对其进行定义的权利,同时,英特尔不应为因其日后更改该等说明或特性描述而产生的冲突和不相容承担任何责任。 此处的信息可能随时更改,恕不另行通知。 请勿使用本文信息完成一项产品设计。

本文件所描述的产品可能包含使其与宣称的规格不符的设计缺陷或失误。 这些缺陷或失误已收录于勘误表中,可索取获得。

在发出订单之前,请联系当地的英特尔营业部或分销商以获取最新的产品规格。

索取本文件中或英特尔的其他材料中提的、包含订单号的文件的复印件,可拨打1-800-548-4725,或登陆 http://www.intel.com/design/literature.htm

在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下方能得到优化。 诸如SYSmark和MobileMark等测试均系基于特定计算机系统、硬件、软件、操作系统及功能。 上述任何要素的变动都有可能导致测试结果的变化。 请参考其他信息及性能测试(包括结合其他产品使用时的运行性能)以对目标产品进行全面评估。

对本文件中包含的软件源代码的提供均依据相关软件许可而做出,任何对该等源代码的使用和复制均应按照相关软件许可的条款执行。

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

OpenGL 是注册商标,OpenGL ES 标识是 Silicon Graphics Inc. 的注册商标,需获 Khronos 的许可方能使用。

英特尔公司© 2013 年版权所有。 保留所有权利。

*其他的名称和品牌可能是其他所有者的资产。

有关编译器优化的更完整信息,请参阅优化通知