跳轉到內容

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 程式設計

瀏覽和下載 完整程式碼
華夏公益教科書