Teapot のサンプルは、NDK のインストール ルート ディレクトリの下の samples/Teapot/
ディレクトリにあります。このサンプルでは、OpenGL ライブラリを使用して CG の世界ではおなじみのユタ ティーポットをレンダリングします。このサンプルでは特に ndk_helper
ヘルパークラスを紹介していますが、これはゲームやそれに似たアプリをネイティブ アプリとして実装するために必要となるネイティブ ヘルパー関数の集まりです。このクラスは次のものを提供します。
- NDK 固有の動作のいくつかを処理する抽象化レイヤ
GLContext
- タップ検出など、NDK には含まれていない有用なヘルパー関数
- テクスチャ読み込みなどのプラットフォーム機能を JNI で呼び出すためのラッパー
AndroidManifest.xml
ここでのアクティビティ宣言は、NativeActivity
そのものではなく、そのサブクラスである TeapotNativeActivity
です。
<activity android:name="com.sample.teapot.TeapotNativeActivity" android:label="@string/app_name" android:configChanges="orientation|keyboardHidden">
最終的にビルドシステムでビルドされる共有オブジェクト ファイルの名前は、libTeapotNativeActivity.so
になります。ビルドシステムが接頭辞 lib
と拡張子 .so
を追加するため、マニフェストで最初に android:value
にセットする値には接頭辞も拡張子も付けません。
<meta-data android:name="android.app.lib_name" android:value="TeapotNativeActivity" />
Application.mk
NativeActivity
フレームワーク クラスを使用するアプリでは、このクラスが導入された Android API レベル 9 以上を指定する必要があります。NativeActivity
クラスの詳細については、ネイティブ アクティビティとアプリをご覧ください。
APP_PLATFORM := android-9
次の行で、ビルドシステムに対して、サポート対象のすべてのアーキテクチャ向けにビルドするよう指定します。
APP_ABI := all
このファイルでは次に、ビルドシステムに対して、使用する C++ ランタイム サポート ライブラリを指定します。
APP_STL := stlport_static
Java サイドの実装
TeapotNativeActivity
ファイルは、GitHub の NDK リポジトリ ルート ディレクトリの下の teapots/classic-teapot/src/com/sample/teapot
にあります。このファイルは、アクティビティのライフサイクル イベントを処理し、ポップアップ ウィンドウを作成して ShowUI()
関数を使ってテキストを画面に表示し、updateFPS()
関数を使ってフレームレートを動的に更新します。次のコードでは、レンダリングされたティーポット フレームを表示する際に画面全体が使用されるように、アプリのアクティビティを「全画面、没入型、システムのナビゲーション バーなし」として準備している点に注目してください。
Kotlin
fun setImmersiveSticky() { window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE ) }
Java
void setImmersiveSticky() { View decorView = getWindow().getDecorView(); decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); }
ネイティブ サイドの実装
このセクションでは、Teapot アプリのうち C++ で実装されている部分について説明します。
TeapotRenderer.h
下記の関数呼び出しは、ティーポットの実際のレンダリングを行います。ndk_helper
を使用して、マトリックスを計算したり、ユーザーがタップした場所に基づいてカメラを再配置したりします。
ndk_helper::Mat4 mat_projection_; ndk_helper::Mat4 mat_view_; ndk_helper::Mat4 mat_model_; ndk_helper::TapCamera* camera_;
TeapotNativeActivity.cpp
以下の数行では、ネイティブ ソースファイルに ndk_helper
をインクルードし、ヘルパークラスの名前を定義しています。
#include "NDKHelper.h" //------------------------------------------------------------------------- //Preprocessor //------------------------------------------------------------------------- #define HELPER_CLASS_NAME "com/sample/helper/NDKHelper" //Class name of helper function
ndk_helper
クラスの最初の用途は、EGL コンテキストの状態(created / lost)を Android のライフサイクル イベントに関連付けることで、EGL 関連のライフサイクルを処理することです。ndk_helper
クラスを使用すると、アプリはコンテキスト情報を保存できるため、システムは破棄されたアクティビティを復元できます。たとえば、ターゲット マシンを回転させた場合(この結果アクティビティが破棄され、その後すぐに新しい画面の向きで復元する)や、ロック画面が表示された場合に役立ちます。
ndk_helper::GLContext* gl_context_; // handles EGL-related lifecycle.
次に、ndk_helper
によりタップ操作を可能にします。
ndk_helper::DoubletapDetector doubletap_detector_; ndk_helper::PinchDetector pinch_detector_; ndk_helper::DragDetector drag_detector_; ndk_helper::PerfMonitor monitor_;
さらに、カメラを制御できるようにします(OpenGL のビュー フラスタム)。
ndk_helper::TapCamera tap_camera_;
このアプリではその後、NDK が提供するネイティブ API を使用して、デバイスのセンサーを使用するための準備をします。
ASensorManager* sensor_manager_; const ASensor* accelerometer_sensor_; ASensorEventQueue* sensor_event_queue_;
このアプリは、ndk_helper
が Engine
クラスを介して提供する各種の機能を使用し、さまざまな Android ライフサイクル イベントや EGL コンテキスト状態の変化に応じて、以下の関数を呼び出します。
void LoadResources(); void UnloadResources(); void DrawFrame(); void TermDisplay(); void TrimMemory(); bool IsReady();
以下の関数は、Java サイドにコールバックして UI の表示をアップデートします。
void Engine::ShowUI() { JNIEnv *jni; app_->activity->vm->AttachCurrentThread( &jni, NULL ); //Default class retrieval jclass clazz = jni->GetObjectClass( app_->activity->clazz ); jmethodID methodID = jni->GetMethodID( clazz, "showUI", "()V" ); jni->CallVoidMethod( app_->activity->clazz, methodID ); app_->activity->vm->DetachCurrentThread(); return; }
次に、下記の関数は Java サイドにコールバックし、ネイティブ サイドでレンダリングした画面上に重ねてテキスト ボックスを描画し、フレーム数を表示します。
void Engine::UpdateFPS( float fFPS ) { JNIEnv *jni; app_->activity->vm->AttachCurrentThread( &jni, NULL ); //Default class retrieval jclass clazz = jni->GetObjectClass( app_->activity->clazz ); jmethodID methodID = jni->GetMethodID( clazz, "updateFPS", "(F)V" ); jni->CallVoidMethod( app_->activity->clazz, methodID, fFPS ); app_->activity->vm->DetachCurrentThread(); return; }
アプリはシステム クロックを取得してそれをレンダラに渡し、リアルタイム クロックを使用したタイムベースのアニメーションを実現します。この情報は、速度が時間の関数として減少している状況において、運動量を計算する場合などに使用されます。
renderer_.Update( monitor_.GetCurrentTime() );
アプリは、レンダリングされたフレームを GLcontext::Swap()
関数を使って表示用のフロント バッファにフリップします。フリップ処理中に発生する可能性のあるエラーも処理します。
if( EGL_SUCCESS != gl_context_->Swap() ) // swaps buffer.
プログラムは、タップイベントを ndk_helper
クラスで定義されているジェスチャー検出機能に渡します。ジェスチャー検出機能は、複数回のタップ操作(ピンチ操作やドラッグなど)をトラックし、このようなイベントでトリガーされると通知を送信します。
if( AInputEvent_getType( event ) == AINPUT_EVENT_TYPE_MOTION ) { ndk_helper::GESTURE_STATE doubleTapState = eng->doubletap_detector_.Detect( event ); ndk_helper::GESTURE_STATE dragState = eng->drag_detector_.Detect( event ); ndk_helper::GESTURE_STATE pinchState = eng->pinch_detector_.Detect( event ); //Double tap detector has a priority over other detectors if( doubleTapState == ndk_helper::GESTURE_STATE_ACTION ) { //Detect double tap eng->tap_camera_.Reset( true ); } else { //Handle drag state if( dragState & ndk_helper::GESTURE_STATE_START ) { //Otherwise, start dragging ndk_helper::Vec2 v; eng->drag_detector_.GetPointer( v ); eng->TransformPosition( v ); eng->tap_camera_.BeginDrag( v ); } // ...else other possible drag states... //Handle pinch state if( pinchState & ndk_helper::GESTURE_STATE_START ) { //Start new pinch ndk_helper::Vec2 v1; ndk_helper::Vec2 v2; eng->pinch_detector_.GetPointers( v1, v2 ); eng->TransformPosition( v1 ); eng->TransformPosition( v2 ); eng->tap_camera_.BeginPinch( v1, v2 ); } // ...else other possible pinch states... } return 1; }
ndk_helper
クラスは、ベクタ演算ライブラリ(vecmath.h
)の利用も可能にします。ここでは、ベクタ演算ライブラリを使用してタップの座標を変換しています。
void Engine::TransformPosition( ndk_helper::Vec2& vec ) { vec = ndk_helper::Vec2( 2.0f, 2.0f ) * vec / ndk_helper::Vec2( gl_context_->GetScreenWidth(), gl_context_->GetScreenHeight() ) - ndk_helper::Vec2( 1.f, 1.f ); }
HandleCmd()
メソッドは、android_native_app_glue ライブラリから渡されたコマンドを処理します。メッセージの意味について詳しくは、android_native_app_glue.h
および .c
のソースファイル内のコメントをご覧ください。
void Engine::HandleCmd( struct android_app* app, int32_t cmd ) { Engine* eng = (Engine*) app->userData; switch( cmd ) { case APP_CMD_SAVE_STATE: break; case APP_CMD_INIT_WINDOW: // The window is being shown, get it ready. if( app->window != NULL ) { eng->InitDisplay(); eng->DrawFrame(); } break; case APP_CMD_TERM_WINDOW: // The window is being hidden or closed, clean it up. eng->TermDisplay(); eng->has_focus_ = false; break; case APP_CMD_STOP: break; case APP_CMD_GAINED_FOCUS: eng->ResumeSensors(); //Start animation eng->has_focus_ = true; break; case APP_CMD_LOST_FOCUS: eng->SuspendSensors(); // Also stop animating. eng->has_focus_ = false; eng->DrawFrame(); break; case APP_CMD_LOW_MEMORY: //Free up GL resources eng->TrimMemory(); break; } }
android_app_glue
がシステムから onNativeWindowCreated()
コールバックを受け取ると、ndk_helper
クラスは APP_CMD_INIT_WINDOW
を渡します。アプリは通常、ウィンドウの初期化(EGL 初期化など)を実行します。まだアクティビティの準備ができていないため、アプリはこの初期化をアクティビティ ライフサイクルの外部で行います。
//Init helper functions ndk_helper::JNIHelper::Init( state->activity, HELPER_CLASS_NAME ); state->userData = &g_engine; state->onAppCmd = Engine::HandleCmd; state->onInputEvent = Engine::HandleInput;