Gear VR Frameworkで視線選択を実装する

Gear VR Frameworkで視線選択を実装するにあたって、ユーザーがオブジェクトを見ているかどうかを判定する必要があります。これは`GVRPicker`と`IPickEvents`を使って簡単に実装できます。やることは次の4つです。

1. `IPickEvents`を実装したクラスを作成する
2. `GVRScene.getEventReceiver().addListener()`で1.のオブジェクトを登録する
3. `GVRSceneObject.attachCollider()`で当たり判定を設定する
4. `GVRPicker`のインスタンスを作成する

以下、順番に見ていきます。


## `IPickEvents`を実装したクラスを作成する

`IPickEvents`インターフェースはユーザーがオブジェクトを見始めた時、見ている間、オブジェクトから目を離した時、といったタイミングでのコールバック処理を定義します。

```java
class PickHandler implements IPickEvents {

    /**
     * オブジェクトを見始めた時に呼ばれる。
     *
     * @param sceneObj 見ているオブジェクト
     * @param pickInfo 見ている位置等の情報
     */
    public void onEnter(GVRSceneObject sceneObj, GVRPicker.GVRPickedObject pickInfo) {
    }

    /**
     * 何かを見ている間、毎フレーム呼ばれる。
     *
     * @param sceneObj 見ているオブジェクト。
     * @param pickInfo 見ている位置等の情報
     */
    public void onInside(GVRSceneObject sceneObj, GVRPicker.GVRPickedObject pickInfo) {
    }

    /**
     * オブジェクトから視線を外した時に呼ばれる。
     *
     * @param sceneObj 見ていたオブジェクト
     */
    public void onExit(GVRSceneObject sceneObj) {
    }

    /**
     * 何かを見始めた時に呼ばれる。
     *
     * @param picker
     */
    public void onPick(GVRPicker picker) {
    }

    /**
     * 何も見ているものがなくなった時に呼ばれる。
     *
     * @param picker
     */
    public void onNoPick(GVRPicker picker) {
    }
}
```

## `GVRScene.getEventReceiver().addListener()`で1.のオブジェクトを登録する

先程作成したクラスをシーン中の視線選択に使用するために、シーン内で発生したイベントを受け取るEventReceiverにインスタンスを登録します。

```java
scene.getEventReceiver().addListener(new PickHandler());
```

## `GVRSceneObject.attachCollider()`で当たり判定を設定する

シーン中のオブジェクトを視線選択するためには、オブジェクトに当たり判定が必要です。一番簡単なのはオブジェクトに設定されている頂点情報をそのまま当たり判定に使用する方法です。そのためには`GVRMeshCollider`を使用します。

```java
sceneObject.attachCollider(new GVRMeshCollider(gvrContext, false));
```

## `GVRPicker`のインスタンスを作成する

毎フレーム視線選択判定を行うようにするため、`GVRPicker`のインスタンスを作成します。

```java
GVRScene scene = gvrContext.getMainScene();
new GVRPicker(gvrContext, scene);
```

インスタンスを作成するだけというのがとても違和感あるのですが、ソースを覗くと`GVRPicker`のコンストラクタの内部で毎フレーム視線選択判定を行うように自身をフレームワークに登録する`GVRPicker.startListening()`を呼び出していることがわかります。なので、インスタンスを作成するだけで動作するのです。

`GVRPicker`には視線選択を開始・停止するメソッドが用意されておらず、`GVRPicker`をコンポーネントとして`GVRSceneObject`に`attachComponent`したり`detachComponent`したりすることで間接的に視線選択の開始・停止を行えます。しかしインスタンスを作成した直後に自動的にも視線選択を開始するようになっていたりと、明らかに実装がおかしいです。

おそらく本来の使い方の想定に近いのは、このようにカメラやシーンのルートオブジェクトのコンポーネントとしてアタッチさせる方法だと思うのですが……このようにするとうまく動作しません。

```java:動作しない例
scene.getMainCameraRig().getOwnerObject().attachComponent(new GVRPicker(ctx, scene));
scene.getRoot().attachComponent(new GVRPicker(ctx, scene));
```

---
これで一通りの実装ができました。最後にまとめて書いた例を紹介します。

```java:実装例
import android.util.Log;

import org.gearvrf.GVRAndroidResource;
import org.gearvrf.GVRContext;
import org.gearvrf.GVRMain;
import org.gearvrf.GVRMeshCollider;
import org.gearvrf.GVRPicker;
import org.gearvrf.GVRRenderData;
import org.gearvrf.GVRScene;
import org.gearvrf.GVRSceneObject;
import org.gearvrf.IPickEvents;

public class Main extends GVRMain {
    private static final String TAG = "Main";

    /**
     * オブジェクトに視線を合わせたり外したりしたときに呼ばれる処理。
     */
    private class PickHandler implements IPickEvents {

        /**
         * オブジェクトを見始めた時に呼ばれる。
         *
         * @param sceneObj 見ているオブジェクト
         * @param pickInfo 見ている位置等の情報
         */
        public void onEnter(GVRSceneObject sceneObj, GVRPicker.GVRPickedObject pickInfo) {
            Log.d(TAG, "onEnter: " + sceneObj);
            sceneObj.getTransform().setScale(1.1f, 1.1f, 1.1f); // ちょっと大きくする
        }

        /**
         * 何かを見ている間、毎フレーム呼ばれる。
         *
         * @param sceneObj 見ているオブジェクト。
         * @param pickInfo 見ている位置等の情報
         */
        public void onInside(GVRSceneObject sceneObj, GVRPicker.GVRPickedObject pickInfo) {
//            Log.d(TAG, "onInside: ");
        }

        /**
         * オブジェクトから視線を外した時に呼ばれる。
         *
         * @param sceneObj 見ていたオブジェクト
         */
        public void onExit(GVRSceneObject sceneObj) {
            Log.d(TAG, "onExit: ");
            sceneObj.getTransform().setScale(1.0f, 1.0f, 1.0f); // 元の大きさに戻す
        }

        /**
         * 何も見ているものがなくなった時に呼ばれる。
         *
         * @param picker
         */
        public void onNoPick(GVRPicker picker) {
            Log.d(TAG, "onNoPick: ");
        }

        /**
         * 何かを見始めた時に呼ばれる。
         *
         * @param picker
         */
        public void onPick(GVRPicker picker) {
            Log.d(TAG, "onPick: ");
        }
    }

    @Override
    public void onInit(GVRContext ctx) throws Throwable {
        super.onInit(ctx);

        GVRScene scene = ctx.getMainScene();
        scene.getEventReceiver().addListener(new PickHandler());
        new GVRPicker(ctx, scene);

        // 視線選択カーソル
        GVRSceneObject cursor = new GVRSceneObject(ctx, 0.5f, 0.5f, ctx.getAssetLoader().loadTexture(new GVRAndroidResource(ctx, R.raw.cursor)));
        cursor.getTransform().setPosition(0, 0, -10);
        cursor.getRenderData().setRenderingOrder(GVRRenderData.GVRRenderingOrder.TRANSPARENT);
        scene.getMainCameraRig().addChildObject(cursor);

        // 選択ボタン
        GVRSceneObject btn1 = new GVRSceneObject(ctx, 1, 1, ctx.getAssetLoader().loadTexture(new GVRAndroidResource(ctx, R.raw.btn1)));
        btn1.setName("btn1");
        btn1.getTransform().setPosition(-3, 0, -10);
        btn1.attachComponent(new GVRMeshCollider(ctx, false));
        ctx.getMainScene().addSceneObject(btn1);

        GVRSceneObject btn2 = new GVRSceneObject(ctx, 1, 1, ctx.getAssetLoader().loadTexture(new GVRAndroidResource(ctx, R.raw.btn2)));
        btn2.setName("btn2");
        btn2.getTransform().setPosition(0, 0, -10);
        btn2.attachComponent(new GVRMeshCollider(ctx, false));
        ctx.getMainScene().addSceneObject(btn2);

        GVRSceneObject btn3 = new GVRSceneObject(ctx, 1, 1, ctx.getAssetLoader().loadTexture(new GVRAndroidResource(ctx, R.raw.btn3)));
        btn3.setName("btn3");
        btn3.getTransform().setPosition(3, 0, -10);
        btn3.attachComponent(new GVRMeshCollider(ctx, false));
        ctx.getMainScene().addSceneObject(btn3);
    }
}
```

## `IPickEvents`の呼ばれる順番

`IPickEvents`のメソッドは、以下の順番で呼ばれます。

1. `onEnter`
2. `onPick`
3. `onInside` (複数回)
4. `onExit`
5. `onNoPick`