サンプル: Teapot

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_helperEngine クラスを介して提供する各種の機能を使用し、さまざまな 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;