連載
» 2011年10月24日 00時00分 公開

モバイルARアプリ開発“超”入門(2):NyARToolKitでマーカー型ARのAndroidアプリを作る (2/3)

[朝香貴寛,TIS株式会社]

サンプルアプリのメイン処理の中身

 それでは、ソースコードの中を見ていきます。アプリの大まかな処理の流れは図5のようになっています。

図5 アプリの処理フロー 図5 アプリの処理フロー

 以降では、図中の番号に沿って説明していきます。

【1】アプリの初期化処理

 アプリが起動したときに最初に呼ばれるのが、jp.androidgroup.nyartoolkit.NyARToolkitAndroidActivity.java.onCreate()です。AndroidアプリはAndroidManifest.xmlで指定されたActivity継承クラスのonCreate()が最初に呼ばれます。

@Override
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
 
    // Renderer for metasequoia model
    // String[] modelName = new String[2];
    // modelName[0] = "droid.mqo";
    // modelName[1] = "miku01.mqo";
    // float[] modelScale = new float[] {0.01f, 0.03f};
    // mRenderer = new ModelRenderer(getAssets(), modelName, modelScale);
    // mRenderer.setMainHandler(mHandler);
 
    // Renderer of min3d
    _initSceneHander = new Handler();
    _updateSceneHander = new Handler();
 
    //
    // These 4 lines are important.
    //
    Shared.context(this);
    scene = new Scene(this);
    scene.backgroundTransparent(true);
    mRenderer = new Renderer(scene);
    Shared.renderer(mRenderer);
 
    requestWindowFeature(Window.FEATURE_PROGRESS);
 
    Window win = getWindow();
    win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
 
    setContentView(R.layout.main);
    mSurfaceView = (SurfaceView) findViewById(R.id.camera_preview);
    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
 
    mSurfaceView.setKeepScreenOn(true);
 
    // don't set mSurfaceHolder here. We have it set ONLY within
    // surfaceChanged / surfaceDestroyed, other parts of the code
    // assume that when it is set, the surface is also set.
    SurfaceHolder holder = mSurfaceView.getHolder();
    holder.addCallback(this);
    holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}

 ここでは、初めにmin3dで使われるSceneオブジェクトへ3Dモデルを読み込みを行うときやモデルのアップデート時に使われるHandlerを生成しています。

_initSceneHander = new Handler();
_updateSceneHander = new Handler();

 Sceneクラスとは、3Dモデル表示のための情報を持つクラスです。実際には、ここで生成したHandlerを通して呼ばれる処理は何もしていないのですが、インスタンスを作成しておかなければmin3dライブラリ内でエラーが発生します。

 次に、3Dモデル表示のためのSceneインスタンスと3Dモデルのレンダラーを生成しています。

Shared.context(this);
scene = new Scene(this);
scene.backgroundTransparent(true);
mRenderer = new Renderer(scene);
Shared.renderer(mRenderer);

 Sharedクラスはアプリケーションのcontextや3DモデルレンダラのインスタンスHolderです。最後に、カメラのプレビューを表示するためのviewの初期化処理を行っています。

SurfaceHolder holder = mSurfaceView.getHolder();
holder.addCallback(this);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

 SurfaceViewクラスはカメラのプレビューを表示するビューで、SurfaceHolderにプレビューが表示されたときやプレビューサイズが変化したときに呼ばれるコールバックを設定しています。

【2】カメラプレビューの開始処理とプレビュー画像のキャプチャ

 SurfaceViewを使用するためにはSurfaceHolder.CallbackインターフェイスのsurfaceChanged()、 surfaceCreated()、surfaceDestroyed()のコールバックメソッドを実装したクラスのインスタンスをSurfaceHolderにセットする必要があります。これら3つのコールバックは以下のタイミングで呼ばれます。

  • surfaceCreated():SurfaceViewが作成されたとき
  • surfaceChanged():SurfaceViewのサイズやピクセルフォーマットが変化したとき
  • surfaceDestroyed():SurfaceViewが廃棄されたとき

 surfaceChanged()は初めのSurfaceView生成時にも呼ばれます。このサンプルでは、surfaceChanged()でカメラデバイスのインスタンスを生成し、プレビューを起動しています。

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
……(省略)……
 
    if (mCameraDevice == null) {
 
        /*
         * To reduce startup time, we start the preview in another thread.
         * We make sure the preview is started at the end of surfaceChanged.
         */
        Thread startPreviewThread = new Thread(new Runnable() {
            public void run() {
                try {
                    mStartPreviewFail = false;
                    startPreview();
                } catch (Exception e) {
                    // In eng build, we throw the exception so that test
                    // tool
                    // can detect it and report it
                    if ("eng".equals(Build.TYPE)) {
                        throw new RuntimeException(e);
                    }
                    mStartPreviewFail = true;
                }
            }
        });
        startPreviewThread.start();
 
        // Make sure preview is started.
        try {
            startPreviewThread.join();
            if (mStartPreviewFail) {
                showCameraErrorAndFinish();
                return;
            }
        } catch (InterruptedException ex) {
            // ignore
        }
    }
……(省略)……

 surfaceChanged()ではカメラデバイスのインスタンスmCameraDeviceがnullだった場合、startPreiviewThread内でカメラデバイスのインスタンスを生成し、startPreview()を呼び出すことでカメラプレビューの開始処理を行っています。その後、startPreviewThread.join()でカメラプレビューが開始されていることを確認しています。

 startPreview()ではAndroidアプリでカメラを利用するための一連の流れが記述されていますが、この中で重要となってくるのが、下記のコールバック設定です。

mCameraDevice.setOneShotPreviewCallback(mPreviewCallback);

 生成したカメラデバイスのインスタンスにandroid.hardware.Camera.PreviewCallbackインターフェイスを実装したプライベートクラスPreviewCallbackをセットすることで、カメラプレビューをキャプチャできるようになります。キャプチャした画像データはPreviewCallback.onPreviewFrame()に渡されます。

 そして、このキャプチャ画像を元に、ARマーカーがあるかどうか判定しています。

 また、PreviewCallbackによるカメラプレビューのキャプチャ取得方法には3種類ありますが、サンプルではプレビューが開始されたときにキャプチャを一度だけ取得するsetOneShotPreviewCallback()を利用しています。そして、onPreviewFrame内からHandlerを通してPreviewをリスタートしています。

 PreviewCallbackで継続的にキャプチャ画像を取得することも可能ですが、そうするとメモリ不足が起こりやすくなるため、このように処理していると考えられます。

if (!mFirstTimeInitialized) {
    mHandler.sendEmptyMessage(FIRST_TIME_INIT);
} else {
    initializeSecondTime();
}

 また、このタイミングでHandlerを通して、3Dモデル表示用のView mGLSurfaceViewの生成を行っています。

 なお、surfaceChangedはアプリの縦横が変化した際にも呼ばれるため、アプリの初回起動時と縦横変更時の処理をmFirstTimeInitializedフラグで分けています。

【3】カメラプレビューの上に3Dモデルを重畳

 NyARToolkitAndroidActivity.surfaceChanged()の中でstartPreviewを呼び出しカメラプレビューの表示やキャプチャの開始処理を行った後に、次のような処理が実行されています。

if (!mFirstTimeInitialized) {
    mHandler.sendEmptyMessage(FIRST_TIME_INIT);
} else {
    initializeSecondTime();
}

 この部分はアプリの向きを取得するためのOrientationListenerの設定と3Dモデル表示用のViewを生成しています。初回起動時とアプリ再開時で処理は分かれていますが、OrientationListenerのインスタンスを生成するかどうかの違いのみで、3D表示用Viewが生成されていなければ、どちらも最終的にはinitializeGLSurfaceView()を呼びます。

 initializeGLSurfaceView()では、カメラ画像からマーカーの有無の判定や変換行列の取得を行うARToolkitDrawerクラスのインスタンスの生成と3Dモデルを表示するためのViewを生成しています。

 ARToolkitDrawerの詳細に関しては後述するとして、3Dモデル表示用Viewの処理を先に見てみます。

FrameLayout frame = (FrameLayout) findViewById(R.id.frame);
mGLSurfaceView = new GLSurfaceView(this);
mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
mGLSurfaceView.setZOrderOnTop(true);
mGLSurfaceView.setRenderer(mRenderer);
frame.addView(mGLSurfaceView);

 GLSurfaceViewは、AndroidでOpenGL ESを用いて3Dモデルをレンダリングする際に用いられるViewのクラスです。ここでARを実現するために重要となるのが、以下の2点です。

  1. FrameLayoutの利用
  2. mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT)

 FrameLayoutはAndroidアプリの表示に利用するレイアウト方式の1つで、複数のViewオブジェクトを重ねて表示できるものです。そして、mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT)を指定することで、OpenGL ESのViewの背景を透明にできます。この2つを組み合わせることでカメラプレビューの上に3Dモデルを表示できるようになります。

 また、GLSurfaceViewに3Dモデルを描画するためには3Dモデルのレンダラーをセットする必要があります。ここではonCreate()で生成したmin3d.core.Rendererクラスのインスタンスをセットしています。

【4】表示する3Dモデルを登録

 GLSurfaceViewにセットするRendererクラスは以下の3つのメソッドを実装している必要があります。

  • onSurfaceCreated():レンダリングの開始時と、OpenGL ES描画コンテキストが再生成された時に呼び出される
  • onSurfaceChanged():サーフェスがサイズを変更したときに呼び出される
  • onDrawFrame():再描画ごとに呼び出される

 min3d.core.Rendererでは、onSurfaceCreated()内でScene.init()を呼び、onCreate()で生成したSceneインスタンスの初期化処理を実行しています。

 初期化処理はActivityで実装しているISceneControllerインターフェイスのメソッドinitScene()に記述された処理が実行されます。また、このサンプルでは利用されていませんが、getInitSceneRunnable()を利用すればスレッドセーフな初期化処理ができます。

 Sceneを初期化しているinitScene()では、マーカー上へ表示する3Dモデルのデータ登録やOpenGL ESの光源の位置、カメラ位置を設定しています。

 モデルの登録方法は以下のようにします。

IParser parser;
AnimationObject3d animationObject3d = null;
 
parser = Parser.createParser(Parser.Type.MD2,
getResources(), "jp.androidgroup.nyartoolkit:raw/droid", false);
parser.parse();
 
animationObject3d = parser.getParsedAnimationObject();
animationObject3d.rotation().z = -90.0f;
animationObject3d.scale().x = animationObject3d.scale().y =
animationObject3d.scale().z = 1.0f;
scene.addChild(animationObject3d);
animationObject3d.setFps(30);

 ここでは、「res/raw」ディレクトリ以下に配置しているdroidというアニメーション付きMD2形式の3Dモデルファイルを読み込み、MD2モデルパーサでmin3d用3Dモデルクラスに変換しています。

 また、min3dではMD2形式以外にもWaveFront、3ds形式のパーサが用意されていますが、WaveFrontパーサの方はエラーハンドリングなどまだ問題が残っているため利用の際には注意が必要です。

【5】カメラ画像からマーカーの有無や変換行列を取得し3Dモデルを表示

 カメラ画像からマーカーの有無や表示する3Dモデルをマーカーの座標系へ変換するための行列の取得などをしているのが、ARToolkitDrawerクラスです。

 NyARToolkitAndroidActivity.initializeGLSurfaceView()で、「res/raw」ディレクトリにある実カメラのパラメータcamera_paraやマーカーデータpatthiro、pattkanjiを引数に インスタンスが生成されます。

 このとき、ARToolkitDrawer.initialization()が呼ばれ、検出するマーカー数やマーカーの検出に利用されるマーカーパターンを保持します。

 マーカーの判定とマーカー座標系の取得はARToolkitDrawer.draw()で行っています。

 ARToolkitDrawer.draw()はPreviewCallback.onPreviewFrame()内で呼ばれます。引数にはキャプチャ画像データが渡され、その画像を基にマーカーを検出します。

 ARToolkitDrawer.draw()の大まかな処理の流れは以下のようになっています。

  1. 画像データのフォーマットを変換
  2. マーカー検出
  3. 変換行列の取得
  4. 判定したマーカー情報をレンダラーへ通知

 画像データのフォーマット変換をしているのが、以下の処理です。

decodeYUV420SP(buf, data, width, height, 1);

 PreviewCallbackで受け取ったデータはYUVフォーマットで、そのままでは扱えないためRGB形式に変換しています。この変換処理は時間がかかる場合があるためjniを使ってネイティブメソッドで処理をしています。

 マーカーの検出を行っているのが以下の処理です。

found_markers = nya.detectMarkerLite(raster, 100);

 nyaはARToolkitDrawer.createNyARTool()で生成されたNyARDetectMarkerクラスのインスタンスで、initialization()で取得したカメラ情報やマーカーパターン情報が登録されています。

 NyARDetectMarker.detectMarkerLite()はラスタ画像とその画像を2値化するためのしきい値を引数に与えて呼び出すことで、検出したマーカー数が戻り値として取得できます。

 これにより、ARマーカーの検出が可能となります。

 2値化のしきい値は0?255が指定できます。このサンプルでは100が指定されていますが、マーカーが明るい場所にある場合は、しきい値を上げるなど調節するといいでしょう。

 画像からマーカーが検出できたら、そのマーカーに合わせて3Dモデルを表示するための変換行列を取得します。それを行っているのが以下の処理です。

for (int i = 0; i < found_markers; i++) {
    if (nya.getConfidence(i) < 0.60f)
        continue;
    try {
        ar_code_index[i] = nya.getARCodeIndex(i);
        NyARTransMatResult transmat_result = ar_transmat_result;
        nya.getTransmationMatrix(i, transmat_result);
        toCameraViewRHf(transmat_result, resultf[i]);
        isDetect = true;
    } catch (NyARException e) {
        Log.e("AR draw", "getCameraViewRH failed", e);
        return;
    }
}
mRenderer.objectPointChanged(found_markers, ar_code_index, resultf, cameraRHf);

 nya.getConfidence()は検出したマーカーの一致度を取得しています。一致度は0?1の値を採り、一致度が低い場合は誤検出の可能性があるため無視し、一致度が高ければ検出したマーカーの種類と変換行列を取得し3Dモデルを表示するようにしています。

 マーカーの種類はNyARDetectMarkerのインスタンス生成時にIDが振られて管理されており、nya.detectMarkerLite()でマーカーを検出した際に、検出したマーカーと登録マーカーのIDが、ひも付けられているため次のように取得できます。

ar_code_index[i] = nya.getARCodeIndex(i);

 これにより複数種類のマーカーを登録し、マーカーごとに異なる3Dモデルを表示することが可能となっています。

 また、検出したマーカーに対する変換行列は以下の処理で取得できます。

nya.getTransmationMatrix(i, transmat_result);

 このメソッドは検出した マーカーのインデックスと変換行列を格納するオブジェクトtransmat_resultを渡し、呼び出すことでtransmat_resultに変換行列が格納されます。

 マーカーの種類と変換行列が取得できたら、以下でマーカーに、ひも付く3Dモデルを描画するようにレンダラーに伝えます。

mRenderer.objectPointChanged(found_markers, ar_code_index, resultf, cameraRHf);

 Renderer.objectPointChanged()の中では、以下のようにscene.children().get(ar_code_index[i])で検出したマーカーに対応した、3Dモデルのオブジェクトを取得し、o.isvisible(true)とすることで、3Dモデルがレンダリングされるように設定しています。

Object3d o = _scene.children().get(ar_code_index[i]);
if (!o.isVisible()) {
    if (o.animationEnabled())
        ((AnimationObject3d) o).play();
    o.isVisible(true);
    o.matrix(resultf[i]);
}

 次ページでは、ここで中身を紹介したサンプルアプリをカスタマイズしてオリジナルにするために、マーカーやを作ったり、BlenderでWaveFront形式の3Dモデルを作ったりして使う方法を解説します。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。