4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

vlcsnap-2019-04-22-16h28m17s785.png

はじめに

前回 は、マウスやキーボードが使えなくて、入力装置がLeapMotionしかない状況で、 Main Camera を平行移動する方法を考えました。

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

デモ動画

実際に作ったのはこんな感じです。Looking Glass ミートアップ(るきみと) に展示させていただきました。

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

  • 拡縮
  • 回転
  • 平行移動

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

サンプルコード

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

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 で判定しています。前回 は片手グーの判定をしましたが、今回は両手グーの判定をします。

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

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

カメラ拡縮について

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

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

// カメラ拡縮
beginDoubleRockGripStream
    // 両手の距離を計算
    .Select(hands => hands[0].PalmPosition.DistanceTo(hands[1].PalmPosition))
    // 距離が正の値なら(0割防止)
    .Where(distance => distance > 0.0f)
    // バッファに前回と今回の2つの値を詰める
    .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] の距離を算出しています。

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

カメラ回転について

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

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

// カメラ回転
beginDoubleRockGripStream
    // 両手の差ベクトルを計算
    .Select(hands => ToVector3(hands[1].PalmPosition - hands[0].PalmPosition))
    // 距離が正の値なら(0割防止)
    .Where(diff => diff.magnitude > 0.0f)
    // バッファに前回と今回の2つの値を詰める
    .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 に乗算します。

カメラ移動について

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

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

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

// カメラ移動
beginDoubleRockGripStream
    // 両手のひらの中点の位置を取得
    .Select(hands => ToVector3((hands[0].PalmPosition + hands[1].PalmPosition) * 0.5f))
    // バッファに前回と今回の2つの値を詰める
    .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 ミートアップ(るきみと) に展示したところ、ほとんどの人が一瞬で操作を理解してくれて、「るきみとすげえ!」って思いました。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?