OpenGLESでのマウスピッキング

前回までのソースコードで、AR空間上にオブジェクトを浮かべて表示することができた。となると、次はこのオブジェクトをタッチしたときに、何らかの情報を表示したくなるだろう。タッチしたオブジェクトが通常のUIViewであれば、CocoaのAPIを使えばいい。だが、今回はOpenGLを使って描画しているので、タッチ判定は別の方法で行う必要がある。

つまり、画面上の座標から、そこに表示されているOpenGLのポリゴンを取得する必要があるのだ。これは、マウスピッキングと呼ばれるテクニックになる。フルのOpenGLであれば、マウスピッキングを行うために、セレクションモードと呼ばれる状態が用意されている。だが、iOSで提供されているOpenGLESでは、セレクションモードを利用することができない。したがって、別の手段を考える必要がある。

そこでここでは、ARオブジェクトをOpenGLを使って描画するときに、同時にそのスクリーン上での座標も計算してしまうという方法を採用しよう。その座標を保存しておき、ユーザが画面をタッチしたときに判定のために使うのだ。ある意味、直接的で単純な方法だ。このやり方は、ARオブジェクトが矩形領域であり、その数があまり多くなく、かつz座標をそれほど考慮しなくてもいい場合に有効だろう。

ちなみに今回は取り上げないが、ARオブジェクトのスクリーン上での座標を計算することは、他の目的にも使うことができるだろう。たとえば、正面を向くオブジェクトの描画である。現状のソースコードでは、ARオブジェクトは中心の視点の方を向いており、画面とは正体していない。したがって、文字をテクスチャとして描画すると、歪んでしまうことになる。これを防ぐには、まずARオブジェクトのスクリーン上での座標を取得する。その後モデルビュー行列をクリアし、二次元座標としてオブジェクトを描画してやればいい。こうすれば、三次元空間上にありながら、常にスクリーンの正面を向いているように描画できる。ARアプリで情報を表示したいときなどには役に立つテクニックだろう。

スクリーン上での座標の計算

では、ARオブジェクトのスクリーン座標の計算をやってみよう。まず、計算結果を保持しておくためのインスタンス変数を追加しておく。_translatedRectsという名前にしておこう。

List 1.

@interface ARView : UIView
{
    ...
    GLfloat         _translatedRects[16][8];

    ...
}

16x8の配列にした。これは、ARオブジェクトが16個あり、かつ矩形領域なので4つの頂点で定義できるからである。頂点は、スクリーン座標なので、xとy成分だけ保存しておけばオッケーだ。

座標の計算は、ARオブジェクトの描画を行った直後に行う。つまり、OpenGL座標の回転や移動を行って、glDrawArraysを呼んだその次ということになる。必要なのは、その時点でのモデルビューマトリックスだ。この行列が、ARオブジェクトの頂点の変換を行うものだからだ。モデルビューマトリックスは、glGetFloatv関数にGL_MODELVIEW_MATRIXを指定することで取得できる。

この行列を取得できたら、ARオブジェクトの頂点座標の変換を計算しよう。モデルビューマトリックスは4x4行列として返ってくるので、頂点座標も4x4行列として用意する。計算したら、変換された座標のx、y成分だけをインスタンス変数に入れておこう。

List 2.

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

        // 現在のモデルビューマトリックスを取得する
        GLfloat mm[16];
        glGetFloatv(GL_MODELVIEW_MATRIX, mm);

        // 変換する頂点を4x4行列で用意する
        GLfloat ov[] = {
            vleft, vbottom, 0, 1.0f,
            vright, vbottom, 0, 1.0f,
            vleft, vtop, 0, 1.0f,
            vright, vtop, 0, 1.0f,
        };

        // モデルビューマトリックスを用いて変換する
        for (j = 0; j < 4; j++) {
            // 頂点行列の一行を取得する
            GLfloat v[4];
            v[0] = ov[j * 4];
            v[1] = ov[j * 4 + 1];
            v[2] = ov[j * 4 + 2];
            v[3] = ov[j * 4 + 3];

            // 変換の計算を行う
            GLfloat mv[4];
            mv[0] = v[0] * mm[0] + v[1] * mm[4] + v[2] * mm[8] + v[3] * mm[12];
            mv[1] = v[0] * mm[1] + v[1] * mm[5] + v[2] * mm[9] + v[3] * mm[13];
            mv[2] = v[0] * mm[2] + v[1] * mm[6] + v[2] * mm[10] + v[3] * mm[14];
            mv[3] = v[0] * mm[3] + v[1] * mm[7] + v[2] * mm[11] + v[3] * mm[15];

            // x、y座標を保存する
            _translatedRects[i][j * 2] = mv[0];
            _translatedRects[i][j * 2 + 1] = mv[1];
        }

これでスクリーン上の座標が求まったことになる。

タッチ判定

ここまでできてしまえば、タッチ判定は簡単だ。単純に、タッチした座標と変換された矩形領域を比較するだけで済んでしまう。

タッチイベントを捕まえるために、touchesEnded:withEvent:を上書きしよう。この中では、まずタッチ座標を取得する。Quartz座標からOpenGL座標へも変換しておこう。

そして、矩形領域と比較する。矩形領域は4つの頂点で定義されるが、その中にある点が含まれるかどうか確認するだけならば、左下と右上の座標だけあればいい。そこで、左下を表す(x0, y0)と、右上を表す(x3, y3)を取り出して、タッチ座標と比較しよう。矩形領域に含まれると判断したら、あらかじめ用意しておいたラベルに文字を表示させてみた。

List 3.

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    // ラベルをクリアする
    _label.text = @"";

    // タッチした座標を取得する
    CGPoint point;
    point = [[touches anyObject] locationInView:self];

    // OpenGL座標系に変換する
    float   x, y;
    x = (point.x - 160.0f) / 160.0f;
    y = (240.0f - point.y) / 160.0f;

    // 変換された矩形領域と比較する
    int i;
    for (i = 0; i < 16; i++) {
        // 矩形の頂点を取得する
        float   x0, y0, x3, y3;
        x0 = _translatedRects[i][0];
        y0 = _translatedRects[i][1];
        x3 = _translatedRects[i][6];
        y3 = _translatedRects[i][7];

        // タッチした座標が矩形領域に含まれる場合
        if (x > x0 && x < x3 && y > y3 && y < y0) {
            // ラベルにメッセージを表示する
            _label.text = [NSString stringWithFormat:@"Touched No. %d rect", i];

            break;
        }
    }
}

実行結果は次のようになる。ARオブジェクトをタッチすると、画面下のラベルにその番号が表示されるはずだ。

これで、位置情報系のARアプリに求められる機能は一通り揃ったことになるだろう。あとは、適切なサーバなどに接続して、その位置と方位の情報を取得し、画面上に表示してやればいい。

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