はじめに
前回 は、マウスやキーボードが使えなくて、入力装置がLeapMotionしかない状況で、 Main Camera
を平行移動する方法を考えました。
今回は、カメラの回転やズームも制御します。
デモ動画
実際に作ったのはこんな感じです。Looking Glass ミートアップ(るきみと) に展示させていただきました。
両手グーでズーム・回転等をできるようにしました。地図アプリのピンチアウトとかの操作の3D版という感じでしょうか。ちょっと癖はあるけど、慣れると便利!デバッグでマウス触らなくてよくなるのが地味に嬉しい。#LeapMotion #LookingGlass pic.twitter.com/NhizrDS7Tj
— せぎゅ (@segur_vita) March 24, 2019
両手がグーの時に、カメラを動かします。両手の動きに合わせて、
- 拡縮
- 回転
- 平行移動
の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 ミートアップ(るきみと) に展示したところ、ほとんどの人が一瞬で操作を理解してくれて、「るきみとすげえ!」って思いました。