OpenGL 程式設計/Android GLUT 包裝器
我們的包裝器:製作
如果您計劃編寫自己的 OpenGL ES 2.0 應用程式,以下是如何包裝器執行此操作的一些提示
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,但可以輕鬆升級)
Android 2.3/Gingerbread/API android-9 引入了 本地活動,它允許在不使用任何 Java 的情況下編寫應用程式。
雖然示例提到了預設的 API 級別為 8,但它應該是 9。
<uses-sdk android:minSdkVersion="9" />
此外,確保您的清單包含
<application ...
android:hasCode="true"
否則應用程式將無法啟動。
您的入口點是 android_main 函式(而不是更常見的 main 或 WinMain)。為了可移植性,您可以在預處理器級別使用 -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 建立一個版本為 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 上下文而設計的,更不用說重置所有靜態分配的變量了。因此,當上下文丟失時,應用程式會完全退出 - 就像在桌面上的應用程式視窗關閉時一樣。
即使我們編寫的是原生代碼,我們的應用程式仍然是透過 Java 程序啟動的,使用 android.app.NativeActivity 內建活動。該程序負責接收裝置事件並將其轉發到我們的應用程式。
工作流程
- Android 作業系統將事件傳送到 NativeActivity Java 程序
- Java 活動框架呼叫相應的活動回撥函式(例如,例如
protected void onLowMemory()) - NativeActivity 在 android_app_NativeActivity.cpp 中呼叫其 JNI 匹配函式(例如,
void onLowMemory_native(...)) android_app_NativeActivity.cpp在android_native_app_glue.c中呼叫匹配的 NativeCode 回撥(例如,void onLowMemory(...))android_native_app_glue.c透過 Cpipe(2)(例如,APP_CMD_LOW_MEMORY)寫入訊息,並立即返回,以防止 Java 程序卡住(否則使用者將被提示將其殺死)- 在我們的本地應用程式中,我們會定期檢查事件佇列並呼叫
android_native_app_glue.c的process_cmd(或process_input) - 向上返回一層到
android_native_app_glue.c,process_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_app 和 ALooper 資料結構。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 事件與現有的運動事件混合在一起,反之亦然。
- ↑ 這是 SDL 用於 Windows 的
WinMain的技術。
- http://developer.android.com/sdk/ : Android SDK 主頁
- NDK 安裝目錄中的
docs/NDK-BUILD.html:構建過程的詳細資訊 - http://developer.android.com/guide/topics/manifest/manifest-element.html : AndroidManifest.xml 參考
- http://developer.android.com/guide/developing/device.html : 使用 USB 將裝置連線到裝置的官方文件
- http://developer.android.com/reference/android/app/NativeActivity.html : 關於無 Java 應用程式的官方文件
- http://developer.android.com/sdk/ndk/ : Android NDK,修訂版 5(2010 年 12 月) 引入了原生活動
- http://blog.tewdew.com/post/6852907694/using-jni-from-a-native-activity : 從原生活動中使用 JNI
內建 NativeActivity 的原始碼