【LeapMotion+UniRx】手の動きでカメラを動かす：両手編

![vlcsnap-2019-04-22-16h28m17s785.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/186a8042-61be-21dd-82db-20c22b558c3c.png)


# はじめに

[前回](https://qiita.com/segur/items/13d22727c913f8159990)  は、マウスやキーボードが使えなくて、入力装置がLeapMotionしかない状況で、 `Main Camera` を平行移動する方法を考えました。

今回は、カメラの回転やズームも制御します。



# デモ動画
実際に作ったのはこんな感じです。[Looking Glass ミートアップ（るきみと）](https://connpass.com/event/124916/) に展示させていただきました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">両手グーでズーム・回転等をできるようにしました。地図アプリのピンチアウトとかの操作の3D版という感じでしょうか。ちょっと癖はあるけど、慣れると便利！デバッグでマウス触らなくてよくなるのが地味に嬉しい。<a href="https://twitter.com/hashtag/LeapMotion?src=hash&amp;ref_src=twsrc%5Etfw">#LeapMotion</a> <a href="https://twitter.com/hashtag/LookingGlass?src=hash&amp;ref_src=twsrc%5Etfw">#LookingGlass</a> <a href="https://t.co/NhizrDS7Tj">pic.twitter.com/NhizrDS7Tj</a></p>&mdash; せぎゅ (@segur_vita) <a href="https://twitter.com/segur_vita/status/1109754332092624896?ref_src=twsrc%5Etfw">March 24, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


両手がグーの時に、カメラを動かします。両手の動きに合わせて、

- 拡縮
- 回転
- 平行移動

の３つの操作を実装しました。



# サンプルコード

こんな感じのコードです。 `Main Camera` にアタッチすることを想定しています。

```C#
using Leap;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

/// <summary>
/// カメラ制御
/// </summary>
public class CameraController : MonoBehaviour
{
    /** カメラの移動スピード */
    private float speed = 0.025f;

    /** LeapMotionのコントローラー */
    private Controller controller;
    
    /** エントリポイント */
    void Start()
    {
        // LeapMotionのコントローラー
        controller = new Controller();

        // LeapMotionから手の情報を取得
        var handsStream = this.UpdateAsObservable()
            .Select(_ => controller.Frame().Hands);

        // 両手グー開始判定ストリーム
        var beginDoubleRockGripStream = handsStream
            .Where(hands => IsDoubleRockGrip(hands));

        // 両手グー終了判定ストリーム
        var endDoubleRockGripStream = handsStream
            .Where(hands => !IsDoubleRockGrip(hands));

        // カメラ拡縮
        beginDoubleRockGripStream
            .Select(hands => hands[0].PalmPosition.DistanceTo(hands[1].PalmPosition))
            .Where(distance => distance > 0.0f)
            .Buffer(2, 1)
            .Select(distances => distances[1] / distances[0])
            .TakeUntil(endDoubleRockGripStream).RepeatUntilDestroy(this)
            .Where(distanceRate => distanceRate > 0.0f)
            .Subscribe(distanceRate => transform.localScale /= distanceRate);

        // カメラ回転
        beginDoubleRockGripStream
            .Select(hands => ToVector3(hands[1].PalmPosition - hands[0].PalmPosition))
            .Where(diff => diff.magnitude > 0.0f)
            .Buffer(2, 1)
            .Select(diffs => Quaternion.AngleAxis(Vector3.Angle(diffs[0], diffs[1]), Vector3.Cross(diffs[1], diffs[0])))
            .TakeUntil(endDoubleRockGripStream).RepeatUntilDestroy(this)
            .Subscribe(quaternion => transform.rotation *= quaternion);

        // カメラ移動
        beginDoubleRockGripStream
            .Select(hands => ToVector3((hands[0].PalmPosition + hands[1].PalmPosition) * 0.5f))
            .Buffer(2, 1)
            .Select(positions => positions[1] - positions[0])
            .TakeUntil(endDoubleRockGripStream).RepeatUntilDestroy(this)
            .Subscribe(movement => transform.Translate(-speed * movement));
    }

    /** 両手グーかどうか */
    public bool IsDoubleRockGrip(List<Hand> hands)
    {
        return
            hands.Count == 2 &&
            hands[0].Fingers.ToArray().Count(x => x.IsExtended) == 0 &&
            hands[1].Fingers.ToArray().Count(x => x.IsExtended) == 0;
    }

    /** LeapのVectorからUnityのVector3に変換 */
    Vector3 ToVector3(Vector v)
    {
        return new Vector3(v.x, v.y, -v.z);
    }
}
```




# 両手グーの判定について

`IsDoubleRockGrip`  で判定しています。[前回](https://qiita.com/segur/items/13d22727c913f8159990) は片手グーの判定をしましたが、今回は両手グーの判定をします。

```C#
/** 両手グーかどうか */
public bool IsDoubleRockGrip(List<Hand> hands)
{
    return
        // 両手なら
        hands.Count == 2 &&
        // １つ目の手の指の内、開いている数が0個なら
        hands[0].Fingers.ToArray().Count(x => x.IsExtended) == 0 &&
        // ２つ目の手の指の内、開いている数が0個なら
        hands[1].Fingers.ToArray().Count(x => x.IsExtended) == 0;
}
```

まず、 `hands.Count` で、LeapMotionが検出している手の数が２つかどうかを確認しています。
そして、 `hands[0].Fingers.ToArray().Count(x => x.IsExtended)` で、開いている指の数を数えています。この数が0ならじゃんけんの **グー** だと判断します。




# カメラ拡縮について

- 両手をグーにしてから、右手と左手の距離が離れたらズームイン
- 両手をグーにしてから、右手と左手の距離が近づいたらズームアウト

という仕様にしてみました。コードを抜粋して、コメントをつけるとこんな感じです。

```C#
// カメラ拡縮
beginDoubleRockGripStream
    // 両手の距離を計算
    .Select(hands => hands[0].PalmPosition.DistanceTo(hands[1].PalmPosition))
    // 距離が正の値なら（0割防止）
    .Where(distance => distance > 0.0f)
    // バッファに前回と今回の２つの値を詰める
    .Buffer(2, 1)
    // 今回と前回の商から距離の変化量を計算
    .Select(distances => distances[1] / distances[0])
    // 両手グーが終了したらバッファをクリアにする
    .TakeUntil(endDoubleRockGripStream).RepeatUntilDestroy(this)
    // 距離変化割合が正の値なら（0割防止）
    .Where(distanceRate => distanceRate > 0.0f)
    // カメラを拡縮
    .Subscribe(distanceRate => transform.localScale /= distanceRate);
```

`PalmPosition` が手のひらの位置です。

`hands[0].PalmPosition.DistanceTo(hands[1].PalmPosition)` で `hand[0]` と `hand[1]` の距離を算出しています。

この距離の変化量でカメラのスケールを制御します。



# カメラ回転について

これは文章での説明が難しいですが、両手をグーにしてから、車のハンドルを回すように両手を動かすと、その方向にカメラも回転します。

コードはこんな感じです。

```C#
// カメラ回転
beginDoubleRockGripStream
    // 両手の差ベクトルを計算
    .Select(hands => ToVector3(hands[1].PalmPosition - hands[0].PalmPosition))
    // 距離が正の値なら（0割防止）
    .Where(diff => diff.magnitude > 0.0f)
    // バッファに前回と今回の２つの値を詰める
    .Buffer(2, 1)
    // 方向の変化量（クォータニオン）を内積と外積から計算
    .Select(diffs => Quaternion.AngleAxis(Vector3.Angle(diffs[0], diffs[1]), Vector3.Cross(diffs[1], diffs[0])))
    // 両手グーが終了したらバッファをクリアにする
    .TakeUntil(endDoubleRockGripStream).RepeatUntilDestroy(this)
    // カメラを回転
    .Subscribe(quaternion => transform.rotation *= quaternion);
```

両手のベクトルの差の回転量を、そのままカメラの回転に適用します。具体的には以下の通りです。

- `hands[1].PalmPosition - hands[0].PalmPosition` で、 `hand[0]` と `hand[1]` のベクトルの差を求めます。
- このベクトルを `Buffer` で保持します。
- 前回と今回のベクトルの内積と外積を計算します。
- 内積と外積からベクトルの方向の変化量をクォータニオンとして算出します。
- そのクォータニオンをカメラの姿勢 `rotation` に乗算します。



# カメラ移動について

右手と左手の中点の変化量を、カメラの移動に適用することにしました。これにより、

- 両手をグーにしてから、右手と左手を同じ方向に動かせば、その方向にカメラは平行移動する
- 右手と左手が逆方向に動いた場合、カメラは平行移動しない（ズームはする）

というのを要件を満たせました。コードはこんな感じです。

```C#
// カメラ移動
beginDoubleRockGripStream
    // 両手のひらの中点の位置を取得
    .Select(hands => ToVector3((hands[0].PalmPosition + hands[1].PalmPosition) * 0.5f))
    // バッファに前回と今回の２つの値を詰める
    .Buffer(2, 1)
    // 今回と前回の差から中点の移動ベクトルを計算
    .Select(positions => positions[1] - positions[0])
    // 両手グーが終了したらバッファをクリアにする
    .TakeUntil(endDoubleRockGripStream).RepeatUntilDestroy(this)
    // カメラを移動
    .Subscribe(movement => transform.Translate(-speed * movement));
```

`(hands[0].PalmPosition + hands[1].PalmPosition) * 0.5f` で、 `hand[0]` と `hand[1]` の中点を求めています。



# さいごに

LeapMotionに慣れていない人にこのUIをデモしたところ、すぐには理解してくれなくて、説明が必要だったのですが、 [Looking Glass ミートアップ（るきみと）](https://connpass.com/event/124916/) に展示したところ、ほとんどの人が一瞬で操作を理解してくれて、「るきみとすげえ！」って思いました。