跳转到内容

OpenGL 编程/Android GLUT 包装器

来自 Wikibooks,开放世界中的开放书籍

我们的包装器:制作

如果您计划编写自己的 OpenGL ES 2.0 应用程序,以下是如何包装器执行此操作的一些提示

编写 Android 的 C/C++ 代码

[编辑 | 编辑源代码]

Android 的应用程序是用 Java 编写的,但它们可以使用 JNI(Java 本地接口)调用 C/C++ 代码,在 Android 中,JNI 被称为 NDK(原生开发工具包)。

您可以:

  • 编写 Java 包装器和 C++ 代码
    • 自 Android 1.5 起可用
    • C++ 代码可以与由 Java 创建的 OpenGL ES 上下文进行交互
    • 直接从 C++ 创建 OpenGL ES 2.0 上下文(使用 EGL)需要 Android 2.3/Gingerbread/API android-9
    • OpenGL ES 2.0 自 Android 2.0/API android-5 起可用
    • 示例:NDK 的 hello-gl2 示例
  • 依赖于内置的 "NativeActivity" java 包装器,只编写 C++ 代码
    • 自 Android 2.3/Gingerbread/API android-9 起可用
    • 使用 EGL 创建 OpenGL ES 上下文
    • 示例:NDK 的 native-activity 示例(它是 OpenGL ES 1.x,但可以轻松升级)

本地 Activity 详情

[编辑 | 编辑源代码]

Android 2.3/Gingerbread/API android-9 引入了 本地活动,它允许在不使用任何 Java 的情况下编写应用程序。

虽然示例提到了默认的 API 级别为 8,但它应该是 9

    <uses-sdk android:minSdkVersion="9" />

此外,确保您的清单包含

<application ...
        android:hasCode="true"

否则应用程序将无法启动。

您的入口点是 android_main 函数(而不是更常见的 mainWinMain)。为了可移植性,您可以在预处理器级别使用 -Dmain=android_main[1] 对其进行重命名。

构建系统

[编辑 | 编辑源代码]

包装器基于 native-activity 示例。它使用处理非阻塞 Android 事件处理的 "android_native_app_glue" 代码。

<!-- Android.mk -->
LOCAL_STATIC_LIBRARIES := android_native_app_glue
...
$(call import-module,android/native_app_glue)

由于我们不直接调用粘合代码(其入口点是 Android 使用的回调,而不是我们),因此 android_native_app_glue.o 可能会被编译器剥离,因此让我们调用其虚拟入口点

    // Make sure glue isn't stripped.
    app_dummy();

它使用 OpenGL ES 2.0(而不是示例的 OpenGL ES 1.X)

<!-- Android.mk -->
LOCAL_LDLIBS    := -llog -landroid -lEGL -lGLESv2

要使用 GLM,我们需要启用 C++ STL

<!-- Application.mk -->
APP_STL := gnustl_static

并宣传其安装位置

<!-- Android.mk -->
LOCAL_CPPFLAGS  := -I/usr/src/glm

现在我们可以声明我们的源文件 (tut.cpp)

<!-- Android.mk -->
LOCAL_SRC_FILES := main.c GL/glew.c tut.cpp


要运行构建系统

  • 编译 C/C++ 代码
ndk-build NDK_DEBUG=1 V=1
  • 准备 Java 构建系统(仅一次)
android update project --name wikibooks-opengl --path . --target "android-10"
  • 创建 .apk 包
ant debug
  • 安装它
ant installd
# or manually:
adb install -r bin/wikibooks-opengl.apk
  • 清理
ndk-build clean
ant clean

我们将这些命令包含在包装器 Makefile 中。

使用 EGL 创建 OpenGL ES 上下文

[编辑 | 编辑源代码]

我们需要告诉 EGL 创建一个版本为 2.0 的 OpenGL ES(而不是 1.x)。

首先,在请求可用上下文时

    const EGLint attribs[] = {
            ...
	    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
            EGL_NONE
    };
    ...
    eglChooseConfig(display, attribs, &config, 1, &numConfigs);

其次,在创建上下文时

    static const EGLint ctx_attribs[] = {
      EGL_CONTEXT_CLIENT_VERSION, 2,
      EGL_NONE
    };
    context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attribs);

(在 Java 代码中:)

setEGLContextClientVersion(2);
// or in a custom Renderer:
int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);

这是一个好的做法,但不是强制性的,在您的 AndroidManifest.xml 中声明 GLES 2.0 需求

<uses-feature android:glEsVersion="0x00020000"></uses-feature>
<uses-sdk android:targetSdkVersion="9" android:minSdkVersion="9"></uses-sdk>

当用户进入主页(或接到电话)时,您的应用程序会被暂停。当用户返回您的应用程序时,它会恢复,但 OpenGL 上下文可能会丢失。在这种情况下,您需要重新加载所有 GPU 端资源(VBO、纹理等)。有一个 Android 事件可以检测何时恢复您的应用程序。

同样,当用户按下后退按钮时,应用程序会被销毁,但它仍然驻留在内存中,并且可以重新启动。

对于我们的包装器,我们认为 GLUT 应用程序通常不是为恢复 OpenGL 上下文而设计的,更不用说重置所有静态分配的变量了。因此,当上下文丢失时,应用程序会完全退出 - 就像在桌面上的应用程序窗口关闭时一样。

Android 事件

[编辑 | 编辑源代码]

即使我们编写的是本地代码,我们的应用程序仍然是通过 Java 进程启动的,使用 android.app.NativeActivity 内置活动。该进程负责接收设备事件并将其转发到我们的应用程序。

工作流程

  • Android 操作系统将事件发送到 NativeActivity Java 进程
  • Java 活动框架调用相应的活动回调函数(例如,例如 protected void onLowMemory()
  • NativeActivity 在 android_app_NativeActivity.cpp 中调用其 JNI 匹配函数(例如,void onLowMemory_native(...)
  • android_app_NativeActivity.cppandroid_native_app_glue.c 中调用匹配的 NativeCode 回调(例如,void onLowMemory(...)
  • android_native_app_glue.c 通过 C pipe(2)(例如,APP_CMD_LOW_MEMORY)写入消息,并立即返回,以防止 Java 进程卡住(否则用户将被提示将其杀死)
  • 在我们的本地应用程序中,我们会定期检查事件队列并调用 android_native_app_glue.cprocess_cmd(或 process_input
  • 向上返回一层到 android_native_app_glue.cprocess_cmd 在其中执行事件前和事件后的通用钩子,并在中间调用我们的应用程序 onAppCmd 回调
  • 返回到我们的应用程序中,onAppCmd 钩子(例如,engine_handle_cmd)最终处理事件!

资源/资产

[编辑 | 编辑源代码]

Android 应用程序通常会从其 .apk 文件(实际上是一个 Zip 存档)中提取资源(例如着色器或网格)。

  • 资源位于 res/ 子文件夹中(例如 res/layout/);有 Android 函数可以根据它们的类型加载它们
  • 资产位于 assets/ 文件夹中,并通过更传统的目录结构进行访问

这对 GLUT 应用程序来说并不常见,因此让我们尝试以透明的方式提供资源

  • 使用 fopen/open 的包装器
    • 使用 LD_PRELOAD 加载,例如 zlibc
    • 使用内核 ptrace 钩子
  • 在我们的 .cpp 文件中重新定义 fopen
  • 预先提取文件

使用 fopen/open 包装器实现起来很麻烦,因为我们的应用程序是通过 JNI 调用的。这意味着我们不能在设置 LD_PRELOAD 后只 execv 另一个应用程序。相反,我们需要启动子进程,将所有 Android 事件转发到它,并设置 IPC 以共享 android_appALooper 数据结构。ptrace 也需要一个子进程。

在本地重新定义 fopen 将适用于 C 的 fopen,但不适用于 C++ 的 cout

预先提取资产需要额外的磁盘空间来存储文件,但这是更合理的解决方案。

访问资产

[编辑 | 编辑源代码]

开发人员一直在努力在 NDK 中轻松访问资源

  • Android API:您可以通过 JNI 调用 Android Java 函数,但获取文件描述符需要使用非官方函数,并且只适用于未压缩文件;使用 Java 缓冲区操作来代替非常繁琐的 C/C++
  • libzip:您可以轻松地使用 libzip 访问 .apk,但您需要在构建系统中集成该库
  • NDK API:在 Android 2.3/Gingerbread/API android-9 中,终于有一个 NDK api 来访问资源

让我们使用 NDK API。它对开发人员来说也不透明(没有 fopen/cout 替代),但使用起来相当容易。

有点棘手的是,从我们的本地活动中的 Java/JNI 中获取 AssetManager。

注意:我们将使用稍微简化的 JNI C++ 语法(而不是 C 语法)。

首先,我们的本地活动在其自己的线程中运行,因此在 android_main 中检索 JNI 句柄时需要谨慎

    JNIEnv* env = state_param->activity->env;
    JavaVM* vm = state_param->activity->vm;
    vm->AttachCurrentThread(&env, NULL);

然后让我们获取调用 NativeActivity 实例的句柄

    jclass activityClass = env->GetObjectClass(state_param->activity->clazz);

我们还需要决定在哪里提取文件。我们将使用应用程序的标准缓存目录

    // Get path to cache dir (/data/data/org.wikibooks.OpenGL/cache)
    jmethodID getCacheDir = env->GetMethodID(activityClass, "getCacheDir", "()Ljava/io/File;");
    jobject file = env->CallObjectMethod(state_param->activity->clazz, getCacheDir);
    jclass fileClass = env->FindClass("java/io/File");
    jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;");
    jstring jpath = (jstring)env->CallObjectMethod(file, getAbsolutePath);
    const char* app_dir = env->GetStringUTFChars(jpath, NULL);

    // chdir in the application cache directory
    LOGI("app_dir: %s", app_dir);
    chdir(app_dir);
    env->ReleaseStringUTFChars(jpath, app_dir);

现在我们可以获取 NativeActivity AssetManager

#include <android/asset_manager.h>
    jobject assetManager = state_param->activity->assetManager;
    AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);

实际的提取很简单:浏览所有文件并逐个将它们复制到磁盘上

    AAssetDir* assetDir = AAssetManager_openDir(mgr, "");
    const char* filename = (const char*)NULL;
    while ((filename = AAssetDir_getNextFileName(assetDir)) != NULL) {
	AAsset* asset = AAssetManager_open(mgr, filename, AASSET_MODE_STREAMING);
	char buf[BUFSIZ];
	int nb_read = 0;
	FILE* out = fopen(filename, "w");
	while ((nb_read = AAsset_read(asset, buf, BUFSIZ)) > 0)
	    fwrite(buf, nb_read, 1, out);
	fclose(out);
	AAsset_close(asset);
    }
    AAssetDir_close(assetDir);

现在,应用程序可以使用普通的 fopen/cout 访问所有文件。

此技术适用于我们的教程,但不适用于大型应用程序。在这种情况下,您可以:

  • 请求 SD 卡的写入权限,并将文件提取到那里(这是 SDL Android 端口所做的),
  • 使用一个围绕文件访问的包装器,它在 Android 上使用 AssetManager(注意,它只读访问)

通过设置

        <activity ...
                android:screenOrientation="portrait"

您的应用程序仅在纵向模式下运行,与设备方向或形状无关。这并不推荐,但对于某些游戏可能有用。

为了更有效地处理方向,您理论上需要检查 onSurfaceChanged 事件。android_app_NativeActivity.cpp 包装器中的 onSurfaceChanged_native 处理程序似乎没有在方向更改时适当创建 onNativeWindowResized 事件,因此我们将定期监控它。

/* glutMainLoop */

    int32_t lastWidth = -1;
    int32_t lastHeight = -1;

    // loop waiting for stuff to do.
    while (1) {

        ...

	int32_t newWidth = ANativeWindow_getWidth(engine.app->window);
	int32_t newHeight = ANativeWindow_getHeight(engine.app->window);
	if (newWidth != lastWidth || newHeight != lastHeight) {
	    lastWidth = newWidth;
	    lastHeight = newHeight;
	    onNativeWindowResized(engine.app->activity, engine.app->window);
	    // Process new resize event :)
	    continue;
	}

现在我们可以处理事件了。

static void onNativeWindowResized(ANativeActivity* activity, ANativeWindow* window) {
    struct android_app* android_app = (struct android_app*)activity->instance;
    LOGI("onNativeWindowResized");
    // Sent an event to the queue so it gets handled in the app thread
    // after other waiting events, rather than asynchronously in the
    // native_app_glue event thread:
    android_app_write_cmd(android_app, APP_CMD_WINDOW_RESIZED);
}

注意:可以处理 APP_CMD_CONFIG_CHANGED 事件,但它发生在屏幕调整大小之前,因此太早获取新的屏幕大小。

Android 只能在缓冲区交换后检测到新的屏幕大小,因此让我们滥用另一个钩子来获取调整大小事件。

/* android_main */
    state_param->activity->callbacks->onContentRectChanged = onContentRectChanged;

...

static void onContentRectChanged(ANativeActivity* activity, const ARect* rect) {
    LOGI("onContentRectChanged: l=%d,t=%d,r=%d,b=%d", rect->left, rect->top, rect->right, rect->bottom);
    // Make Android realize the screen size changed, needed when the
    // GLUT app refreshes only on event rather than in loop.  Beware
    // that we're not in the GLUT thread here, but in the event one.
    glutPostRedisplay();
}

输入事件

[编辑 | 编辑源代码]

我们从 native-activity 示例中重复使用 engine_handle_input

当事件未直接处理时,return 0 非常重要,以便 Android 系统进行处理。例如,我们通常让 Android 处理返回按钮。

NativeActivity 框架似乎不会发送适当的重复事件:按键在完全相同的时间被按下和释放,并且重复次数始终为 0。因此,似乎无法处理来自 Hacker's Keyboard 的箭头键,除非重写框架的一部分。

运动(触摸屏)和键盘事件通过相同的通道处理。

为了允许没有键盘的用户使用箭头键,我们在左下角实现了一个虚拟键盘 (VPAD),它在触摸屏上激活。我们努力避免将 VPAD 事件与现有的运动事件混合在一起,反之亦然。

参考资料

[编辑 | 编辑源代码]
  1. 这是 SDL 用于 Windows 的 WinMain 的技术。

内置 NativeActivity 的源代码

< OpenGL 编程

浏览和下载 完整代码
华夏公益教科书