AR空間を構築する

「現実」であるカメラ画像の上に重ねるための、AR空間を構築することを考えよう。AR空間は、自分を中心とした球体の空間として捉えることができるだろう。この球体中に種々のオブジェクトを配置する。そしてiPhoneのカメラ機能を使って、その一部を切り取って表示するわけだ。

このようなことから、AR空間を実現するための技術としては、3D空間を取り扱うことが求められる。その実装としては、OpenGLを使うのが適当だろう。そこで、まずはサンプルアプリにOpenGL環境を組み込むとしよう。サンプルのプロジェクトには、ARViewというUIViewのサブクラスがある。このビューでOpenGLの表示を行うものとする。

UIViewでOpenGLの表示を行うには、まずビューのレイヤーとしてCAEAGLLayerを利用する。そして、OpenGLのコンテキストを表すEAGLConextを作成してやればいい。その後は、OpenGLの関数を使って設定を行っていく。OpenGLをiOSで用いることは、本連載の第18回から始まる「タワーディフェンスゲームの作り方」で詳しく解説しているので、そちらも参照してほしい。

ここでは、OpenGL環境のセットアップを行っているソースコードを紹介しよう。初期化メソッドから呼び出すことになるメソッドである。

List 1.

+ (Class)layerClass
{
    return [CAEAGLLayer class];
}

- (void)_init
{
    // レイヤーの設定をする
    CAEAGLLayer*    eaglLayer;
    eaglLayer = (CAEAGLLayer*)self.layer;
    eaglLayer.opaque = NO;
    eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
            [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
            kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
            nil];

    // OpenGLコンテキストを作成する
    _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
    [EAGLContext setCurrentContext:_context];

    // フレームバッファを作成する
    glGenFramebuffersOES(1, &_defaultFramebuffer);
    glGenRenderbuffersOES(1, &_colorRenderbuffer);

    glBindFramebufferOES(GL_FRAMEBUFFER_OES, _defaultFramebuffer);
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, _colorRenderbuffer);
    [_context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:(CAEAGLLayer*)self.layer];
    glFramebufferRenderbufferOES(
            GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, _colorRenderbuffer);

    glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &_backingWidth);
    glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &_backingHeight);
}

まずlayerClassメソッドを上書きし、利用するレイヤークラスを指定している。そして_initメソッドの中では、レイヤーの設定およびOpenGLコンテキストの作成を行っている。これでUIViewとOpenGLが関連付けられた。後は、フレームバッファの作成などのOpenGLの設定である。このメソッドは、initWithFrame:またはinitWithCoder:といったUIViewの初期化メソッドから呼び出されることになる。

センサと表示を連動させる

次に、ARViewと各種センサを連動させることを考えよう。加速度、電子コンパス、ジャイロといったセンサの使い方は、前回解説した。

前回のソースコードでは、センサから取得した計測値はARAppDelegateが受け取ることになっていた。そこで、この値をARViewに設定できるようにしてやろう。そのために、ARViewに次のようなプロパティを追加する。

List 2.

@interface ARView : UIView
{
    // デバイスの位置情報
    CMAcceleration  _gravity;
    float           _heading;
    float           _roll;
    float           _pitch;
    float           _yaw;

    ...
}

// プロパティ
@property (nonatomic) CMAcceleration gravity;
@property (nonatomic) float heading;
@property (nonatomic) float roll;
@property (nonatomic) float pitch;
@property (nonatomic) float yaw;

...

加速度の値を保持するためのCMAcceleration型のgravity、電子コンパスの値を保持するためのheading、ジャイロセンサーの値を保持するためのroll、pitch、yawを、それぞれインスタンス変数とプロパティとして追加した。ARAppDelegateは、センサの計測値を受け取ったら、これらのプロパティの値に設定してやればいい。

このようにしてARViewにセンサの値が渡ってきたら、それをOpenGLの表示と連動させる。これは、OpenGLの座標を回転させることに相当する。得られたセンサの値のうちどれを使うかは、アプリケーションの目的によって変わるだろう。今回は、電子コンパスと加速度を使うことにしてみた。電子コンパスで得られた方位の角度でy軸の回転を行い、加速で得られる値でx軸の回転を行う。電子コンパスの値を使うので、実際の方位との連携がとりやすい。地図上のデータを、AR空間にマッピングしたいときに適切だろう。

OpenGLの描画ルーチンは、ディスプレイリンクを使うことで呼び出すことにしよう。drawView:というメソッドを作り、これをディスプレイリンクから呼び出すように設定する。drawView:の中では、まず始めに、センサの値による回転を行うことになる。

List 3.

- (void)startAnimation
{
    // ディスプレイリンクを有効化する
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView:)];
    [_displayLink setFrameInterval:1];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)drawView:(id)sender
{
    ...

    // 加速度による回転を行う
    glRotatef(_gravity.z * -90.0f, 1.0f, 0, 0);

    // 電子コンパスによる回転を行う
    glRotatef(_heading, 0, 1.0f, 0);

    ...

startAnimationメソッドは、ディスプレイリンクの有効化を行うものだ。このメソッドを呼ぶことで、drawView:が定期的に呼び出されることになる。

drawView:メソッドから、OpenGLの描画に関わるすべてを行う。まず行うのは、センサと連動した視点の回転だ。加速度の値はx、y、z軸ごとに取得することができるが、このうちz軸成分のみをつかって、OpenGLのx軸回転を行う。つまり、ユーザが下を見ているか上を見ているかという、上下の動きを反映させている。次に、電子コンパスの値を使って、OpenGLのy軸回転を行う。これは、ユーザが北を見ているか東を見ているかという、その場での左右の回転を反映させることになる。

オブジェクトを表示する

視点の回転を行ったら、後は種々のオブジェクトを表示すればいい。ここは通常のOpenGLのソースコードになるだろう。AR空間上にポリゴンを配置していけばいい。このサンプルでは、水平線上にぐるりと16個の正方形を表示させてみた。

List 4.

- (void)drawView:(id)sender
{
    ...

    // ポリゴンを作成する
    int i;
    for (i = 0; i < 16; i++) {
        // 現在の行列を保存する
        glPushMatrix();

        // オブジェクトの位置を決定する
        glRotatef(360.0f / 16 * i, 0, 1.0f, 0);
        glTranslatef(0, 0, -2.0f);

        // ポリゴンの頂点を設定する
        GLfloat vleft, vright, vtop, vbottom;
        vleft = -0.2f;
        vright = 0.2f;
        vtop = -0.2f;
        vbottom = 0.2f;
        GLfloat squareVertices[] = {
            vleft, vbottom,
            vright, vbottom,
            vleft, vtop,
            vright, vtop,
        };
        glVertexPointer(2, GL_FLOAT, 0, squareVertices);

        // ポリゴンの色を設定する
        const GLubyte squareColors[] = {
            16 * i, 255 - (16 * i), 255, 255,
            16 * i, 255 - (16 * i), 255, 255,
            16 * i, 255 - (16 * i), 255, 255,
            16 * i, 255 - (16 * i), 255, 255,
        };
        glColorPointer(4, GL_UNSIGNED_BYTE, 0, squareColors);

        // ポリゴンを描画する
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        // 以前の行列に戻す
        glPopMatrix();
    }

    ...

これでオブジェクトが配置されることになる。

サンプルアプリを起動すると、カメラが表示される。デバイスを手にもって地面と垂直に立てると、正方形が横に並んでいるはずだ。そのままくるくると回転すると、カメラ画像の移動に合わせて正方形も移動する。上下を見れば、正方形は画面外に消えていくだろう。これが、基本的なAR空間となっているのだ。

ここまでのソースコード: AR-3.zip