샘플: Teapot

Teapot 샘플은 samples/Teapot/ 디렉터리의 NDK 설치 루트 디렉터리 아래에 있습니다. 이 샘플에서는 OpenGL 라이브러리를 사용하여 대표적인 Utah teapot을 렌더링합니다. 특히 게임 및 유사한 애플리케이션을 네이티브 애플리케이션으로 구현하는 데 필요한 네이티브 도우미 함수 모음인 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

자바 측 구현

TeapotNativeActivity 파일은 GitHub의 NDK 저장소 루트 디렉터리 아래 teapots/classic-teapot/src/com/sample/teapot에 있습니다. 이 파일은 활동 수명 주기 이벤트를 처리하고, ShowUI() 함수를 사용하여 화면에 텍스트를 표시하는 팝업 창을 만들고, updateFPS() 함수를 사용하여 프레임 속도를 동적으로 업데이트합니다. 다음 코드는 앱의 활동을 시스템 탐색 메뉴 없이 몰입적인 전체 화면에서 구현하므로 렌더링된 Teapot 프레임을 표시하는 데 전체 화면을 사용 가능하다는 점에서 유용할 수 있습니다.

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);
}

네이티브 측 구현

이 섹션에서는 C++에서 구현된 Teapot 앱 부분을 살펴봅니다.

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 관련 수명 주기가 처리되고 EGL 컨텍스트 상태(생성됨/손실됨)가 Android 수명 주기 이벤트와 연결됩니다. 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_;

앱은 다양한 Android 수명 주기 이벤트 및 EGL 컨텍스트 상태 변화에 따라 다음 함수를 호출하여 Engine 클래스를 통해 ndk_helper에서 제공하는 다양한 기능을 사용합니다.


void LoadResources();
void UnloadResources();
void DrawFrame();
void TermDisplay();
void TrimMemory();
bool IsReady();

그 후 다음 함수가 자바 측을 콜백하여 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;
}

다음에는 이 함수가 다시 자바 측으로 호출되어 네이티브 측에서 렌더링된 화면에 텍스트 상자가 겹쳐지도록 띄우고 프레임 수를 보여줍니다.

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;