FirstVRのジェスチャー認識システムを作る

#はじめに
この記事はFirstVR(筋変位センサ搭載コントローラ)を使ってジェスチャー認識システムを作るよ！という記事です。
「FirstVRのサンプルは一通り触ってみたけど、ジェスチャー認識とかどうやってゲームに組み込んでいったらいいかわからない＞＜」という方が、自分でジェスチャー認識のシステムを作り、ゲームに組み込めるようになるのを目的としています。
![AGDRec_20181029_1959422.gif](https://qiita-image-store.s3.amazonaws.com/0/307844/5bf198e9-dce8-d9d9-6442-b700e14953f3.gif)

↑のシステムを作っていきたいと思います。

##前準備
SDKをダウンロードしていない方は https://dev.first-vr.com/downloads?locale=ja からダウンロードしてください。
![sdk.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/e6b3f937-d7fe-2bef-99e0-98fac74b61af.png)
SDKの中身はプロジェクトファイルになっているので、Unityでそのまま開いて大丈夫です。
開けたらScript,Sprite,Sceneフォルダをそれぞれ作り、新しいSceneを１つ追加します。
Scene名は何でも良いですが、こちらではGestureSettingという名前にしておきます。
![newScene.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/f03c1067-8972-d4a0-6f1a-6ab00d03db13.png)
それが出来たら、ProjectのFVR > Prefab　フォルダの中から **ConnectionCheck** と **FVRContainer** の２つをHierarchyにドラッグ&ドロップします。
![prefab.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/60a377a9-6ba5-26f4-b17c-3bb067e4da98.png)
ConnectionCheckのRenderModeはデフォルトではWorldSpaceになっていますが、今回は**ScreenSpace-Overlay**に変えておきます。
ついでにUIScaleModeも**ScaleWithScreenSize**に変えておきます。
![connection.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/a2c457a2-045b-f921-bf7c-773749dfac8c.png)

変えた直後はImageサイズが小さくて画面に表示されないと思うので、ConnectionCheck以下のvisualとTextを自分が見やすいサイズ、位置に調整してください。

visualの設定だけ目安として載せておきます。
![visual.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/85b7f58f-e08c-fd47-06a6-c7fcb780764d.png)

続いて、ジェスチャー認識を確認するための画像を用意します。
画像は何でも良いですが、記事ではこの画像を使用します。
![hand_tex.png](https://qiita-image-store.s3.amazonaws.com/0/307844/f2c1e642-c367-9a53-785e-8c4d3024e8c6.png)
上の画像をプロジェクトに入れたら、SpriteEditorで画像をいい感じに分割しましょう。
SpriteModeをMultipleにしたらSpriteEditorを押します。
![SpriteEditor.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/a5b4c834-8fa1-7399-d2d4-28aa8e8f876c.png)
![hand_texSprite.png](https://qiita-image-store.s3.amazonaws.com/0/307844/565cbe2a-6801-d4ab-417d-4b06fa0159f7.png)
Spriteの範囲でクリックしてからドラッグすると範囲選択ができるので、この黒枠のように一つ一つのSpriteを囲み、Applyを押すと、画像が区切られているはずです。

それが出来たら新しくCanvasを作り、UIScaleModeをScaleWithScreenSizeに変えます。
![Screen_2.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/7185f9ad-bd43-4293-b967-9f886eca78af.png)
作ったCanvasの下に
１．Button２つ
２．グー、パーのImageを２つずつ
３．ジェスチャーチェック用のImageを１つ
をそれぞれ作ってください。
![Canvasika.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/1ed7ff6f-072c-47fc-54c4-97be4c31554e.png)

RockButton…ボタンです。押したらキャリブレーションを開始させます。
RockImg…白い手のImageです。キャリブレーションの時間を計るのに使います。
RockSettingCalibImg…色が付いた方の手のImageです。キャリブレーションの時間を計るのに使います。

Paperの方も同じような意味で名付けています。

CheckImg…一番したにある手です。ジェスチャー認識ができているかをチェックします。

(Canvasのすぐ下の階層のRock、Paperには特に意味はなく、それぞれのジェスチャーに関連しているオブジェクトをまとめて管理しやすくしているだけなので、気にしなくても大丈夫です)
![Screen_3.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/cda40b37-2ad1-84a0-9546-6224c0471de5.png)
２番のグー、パーのImageは下に白い手、上に色が付いた手の画像がくるように、ぴったりと重ねて配置してください。
![SettingCalibImg.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/e539556a-8572-9fe4-4f05-8f847d82cf28.png)
SettingCalibImg(色が付いている方のImage)の**ImageType**をSimpleから**Filled**に変えます。
すると色々とFill~という項目が出てきたと思いますが、そのうちのFillMethodをRadial360から**Vertical**に変更してください。
![AGDRec_20181029_130054.gif](https://qiita-image-store.s3.amazonaws.com/0/307844/54b9c34d-9ee7-1f7b-0548-5c678f662b17.gif)
FillAmountのバーをいじった時に、こんな感じになれば成功です。
この設定をもう片方にもしておいてください。

ここまでで前準備は終わりです。
##本題
ではさっそくジェスチャー認識をするコードを書いていきます。
先ほど作ったScriptフォルダの中に、C#スクリプトを作ってください。名前はSetGestureとしておきます。
作れたらダブルクリックでスクリプトを開きます。

```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using FVRlib;

public class SetGesture : MonoBehaviour {

    //-------------------------------------------
    // public
    //-------------------------------------------

    // キャリブレーションの待機時間表示用Image
    [Tooltip("0:グー,1:パー")]
    public Image[] calibHandImg;

    // ジェスチャーをチェックするときのImage
    [Space(10)]
    public Image checkImg;

    // ジェスチャーをチェックするときに使うSprite
    [Tooltip("0:グー,1:パー")]
    public Sprite[] checkHandSp;

    // ジェスチャー登録のボタン
    [Space(10),Tooltip("0:グー,1:パー")]
    public Button[] setGestureBtn;

    //-------------------------------------------
    // private
    //-------------------------------------------

    // FVR
    FVRGesture gesture;
    FVRConnection fvr;

    // キャリブレーションの時間
    float roundLength = 0;

    //-------------------------------------------
    // 関数
    //-------------------------------------------
    void Start ()
    {
        // FVRConnectionを取得
        fvr = FindObjectOfType(typeof(FVRConnection)) as FVRConnection;

        // 新しいカスタムジェスチャーを作成
        gesture = fvr.gestureManager.RegisterCustomGesture("RockGesture");

        // キャリブレーションの時間を設定
        roundLength = fvr.gestureManager.calibrationRoundLength;

    }
	
	void Update ()
    {
        if (gesture.held) checkImg.sprite = checkHandSp[0];
        else checkImg.sprite = checkHandSp[1];
	}

    /// <summary>
    /// グーを設定
    /// </summary>
    public void SetRockGesture()
    {
        StartCoroutine(Calibrate(true));
    }

    /// <summary>
    /// パーを設定
    /// </summary>
    public void SetPaperGesture()
    {
        StartCoroutine(Calibrate(false));
    }

    /// <summary>
    /// 登録したジェスチャーをリセット
    /// </summary>
    public void ResetGesture()
    {
        fvr.gestureManager.ResetPatternData(gesture);
    }

    /// <summary>
    /// キャリブレーション
    /// </summary>
    /// <param name="isTarget">ターゲットジェスチャーかどうか</param>
    /// <returns></returns>
    IEnumerator Calibrate(bool isTarget)
    {
        //キャリブレーション
        if (isTarget) fvr.gestureManager.SetTargetData(gesture);
        else fvr.gestureManager.SetNonTargetData(gesture);

        //キャリブレーション中は他ボタンを押せないようにする
        for (int i = 0; i < setGestureBtn.Length; i++) setGestureBtn[i].interactable = false;

        // キャリブレーションしている間の時間を計測
        float calibTime = 0;

        // キャリブレーションの時間を計算、視覚化
        while(gesture.registering)
        {
            calibTime += Time.deltaTime;

            if (isTarget) calibHandImg[0].fillAmount = calibTime / roundLength;
            else calibHandImg[1].fillAmount = calibTime / roundLength;

            yield return null;
        }

        // 終わったらボタンを再度押せるようにする
        for(int i = 0; i < setGestureBtn.Length; i++)
        {
            calibHandImg[i].fillAmount = 0;
            setGestureBtn[i].interactable = true;
        }
    }
}
```
中にはこのコードを書いてください。

順に説明していきます。

###FVRlib

```C#
using FVRlib;
```

FVRの接続やジェスチャ認識の判定で今回使っているFVRConnectionやFVRGestureは、FVRlibという名前空間のメンバーです。
そのため、こいつをusingしておかないと誰お前？と怒られます。

###RegisterCustomGesture
```C#
gesture = fvr.gestureManager.RegisterCustomGesture("RockGesture");
```
新しいジェスチャーインスタンスを作成しています。
![RegisterCustomGesture.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/ec641de7-1b24-6ef0-1548-4760768831ca.png)
![List.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/304ba1e3-81a1-e813-b5c5-6f582cd744bf.png)
API( http://reference.first-vr.com/ja/annotated.html )を見ると、カスタムジェスチャーを作成し、それぞれのListに追加してくれているようです。
また複数のジェスチャーを認識したい場合は、nameの引数を変えてそれぞれを識別させるみたいですね。

###SetTargetData、SetNonTargetData
```C#
fvr.gestureManager.SetTargetData(gesture);
fvr.gestureManager.SetNonTargetData(gesture);
```
ジェスチャーデータの登録をしてくれます。
![setData.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/cab929a6-b0f9-a82a-12f1-b1df4c0cdf03.png)
気になるのがターゲットデータ、非ターゲットデータという部分。一体なんぞや？という方が多いと思います。
こちらは機械学習でよく使われる単語らしく、その意味としては**”対象となっているもの/予測したい内容/答”**です。
なので、**ターゲット＝狙って行うジェスチャー、ノンターゲット＝それ以外のジェスチャー**という意味ですね。
上のコードだと　狙って行うジェスチャーはグー、それ以外のジェスチャーとしてパーを登録しています。

今回はグー、パーのみですが、これがもしグー、チョキ、パーになった場合はどう実装するかというと。

```C#
FVRGesture[] gesture = new FVRGesture[3];

void Start()
{
    gesture[0] = fvr.gestureManager.RegisterCustomGesture("Rock");
    gesture[0] = fvr.gestureManager.RegisterCustomGesture("Paper");
    gesture[0] = fvr.gestureManager.RegisterCustomGesture("Scissors");
}
```

こんな感じでRegisterCustomGestureでジェスチャーインスタンスを作成したら、そこのターゲットジェスチャーにグー、チョキ、パーをそれぞれ設定します。そのあと、それぞれのノンターゲットジェスチャーにターゲットジェスチャーで登録したもの以外のジェスチャーを登録していく、という手段をとるそうです。

例）ターゲット＝グー、ノンターゲット＝パー、チョキ　
ターゲット＝パー、ノンターゲット＝グー、チョキ　などなど

ただそれをそのまま実行すると、ターゲット×３、ノンターゲット×６で合計９回もジェスチャーを登録しなければならないということになるので、ゲームに組み込む際には少し考えなければいけないかもしれないですね…。

###registering
```C#
while(gesture.registering)
```

キャリブレーション中かどうかをbool型で返してくれます。

##最後に
コードが書けたらあと少しで終わりです。
もう一度UnityEditorに戻り、Hierarchy > Create > CreateEmptyを作ってください。名前は何でも大丈夫ですが、こちらではGestureManagerとでもしておきます。

![GestureManager.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/d1183e34-d4e6-a808-e80a-5878e7600410.png)
先ほど作ったSetGestureをアタッチします。

全て共通に言えることですが、配列になっているものの０番目の要素はグー、１番目はパーを設定してください。

CalibHandImg…色が付いた手のImage
CheckImg…一番下の手
CheckHandSp…手のSprite
SetGestureBtn…ボタン

を設定してください。

ここまで出来たら完成です。
ビルドして、ジェスチャー認識ができるかどうかを確かめましょう！
![AGDRec_20181029_1959422.gif](https://qiita-image-store.s3.amazonaws.com/0/307844/1a9f44e0-eb1f-c366-4376-64ff3d5db0fe.gif)
こんな感じにできていたら成功です。
お疲れさまでした！

##余談
一つ余談なのですが、上のままのプログラムだとジェスチャーを設定したときに一度反応するのに、すぐにキャリブレーションが終わるという現象がおきます。

```C#
/// The first time we set a target or non-target value, the round length and samples per second are ignored and the SVM takes only one value with dummy data then
/// the dummy data is replaced with real data. 
/// After the first round the FVRGesture.calibrated flag is set to true and you are ready to start calibrating with real data
```

SDKに入っているCalibTestCtrlをみると、コメントでこのようなことが書かれています。

一番最初にどちらをキャリブレーションしても、ダミーデータとして取得され、一番最初はキャリブレーションの時間なども無視される。ダミーデータは実データに置き換えられる。最初のラウンドの後、GVRGesture.calibratedフラグがtrueになるため、実データでキャリブレーションの値を取得する準備が整う。

エキサイト先生に聞いてみたところ、以上のようなことが書いてあるみたいです。
なんとなく意味はわかるようなわからないようなって感じですね…。

```C#
gesture.calibrated = true;
```
~~ひとまずは、Startの中にこう書いておけば、この現象はなくなります。
どうしても気になる、という方はこの１文を追加してみてください。~~

2018/11/10　追記

この方法でやると
![error.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/381d3ef9-c23f-67b1-a2f5-da3689f85e3e.png)
![error_2.PNG](https://qiita-image-store.s3.amazonaws.com/0/307844/f3e6aff9-7aa1-3553-8d51-13b269d6a2ab.png)

というようなエラーが出てしまいます。
FVRlib内のSVMで怒られているようなので、これは書かないようにしましょう。
一番最初のジェスチャー登録の際に、２回ジェスチャー登録をする、というような処理を書いてあげたほうが良いと思われます。