凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

VR の世界に手を持ち込める Leap Motion VR の仕組みを調べてみた

はじめに

8月末に Leap Motion が公式でステレオの赤外画の取得の API の公開と、VR 用のソフトウェア群、および Oculus Rift DK2 マウンタを公開しました。

サンプルも幾つか用意されていて試すことが出来ます。マウンタは買わずとも両面テープやマジックテープでくっつければ問題なく動作しますし、3D プリンタをお持ちであればデータも公開されているので、自分で用意することも可能です。

ステレオの赤外カメラ画を両目に展開して、AR 的にその絵にぴったり合う形で手の 3D モデルが重畳されます。これは自分の手が VR 内のオブジェクトに簡単に干渉できることを意味し、とても衝撃的な体験でした。今回のアップデートによりトップダウンラッキングHMD ポジションからの手形状のトラッキング)が大幅に改良され、以前 Leap Motion マウンタを使ったそれとは全く異なる体験となっています。

今回はこの詳細な仕組みについて調べてみましたのでご紹介します。

Unity サンプル

以下に Leap Motion VR 用の Unity サンプルが上がっています。

LeapOculusPassthrough.unitypackage をダウンロードして Assets 下にある LeapOculusPassthrough シーンを開くと次のような画面が表示されます。

f:id:hecomi:20140922210736p:plain

Editor 拡張で Hand ControllerGizmoLeap Motion アイコンが表示しているのがわかりやすくて良いですね。実行すると以下のように歪み補正をしたカメラ画に、認識した手にぴったり合う形で手オブジェクトを重畳した、冒頭のサンプルのような画面が動作します。

f:id:hecomi:20140922215114p:plain

カメラ、カメラ画を重畳したプレーン、手オブジェクト、その他のオブジェクトの位置関係はこの順で次のようになっています。

f:id:hecomi:20140922215247p:plain

カメラ画を表示するプレーンや Leap Motion の手の位置の原点となる HandControllerOVRCameraController の子となっているので、Oculus Rift のヘッドトラッキングに追従してその位置も変わるため、顔の動きとそこから見える手の位置は常に一致してくれます。

それでは、まず画をどうやって取得しているのか見てみましょう。

Leap Motion からの画の取得

画の取得については以下のドキュメントが参考になります。

が、ドキュメントベースだと飽きるので実際に動いているスクリプトで見て行きましょう。Scenes ディレクトリを見てみると、以前(Leap Motion の Oculus Rift 用マウンタを使って VR の世界で積み木ゲームしてみた - 凹みTips)のものと比べてサンプルシーンが増えています。その中のひとつである RealtimeCameraViewer シーンを見てみます。

生画(1 押下時)

f:id:hecomi:20140922220527p:plain

歪み補正(2 押下時)

f:id:hecomi:20140922220817p:plain

背景抜き(3 押下時)

f:id:hecomi:20140922220836p:plain

こんな感じに表示されます。LeapOculusPassThrough シーンよりもシンプルなのでこのシーンで仕組みを見て行きましょう。画が適用されている Quad を見てみます。

f:id:hecomi:20140922222632p:plain

LeapImageRetriever.csImageRetrieverTypes.cs がアタッチされています。LeapImageRetriever.csLeapCSharp.NET3.5.dll 内内で定義されている Leap.Image を通じて Leap Motion から画を取ってくるスクリプトです。ImageRetrieverTypes.cs はこの LeapImageRetriever.cs のフラグをキー操作に応じてちょこっとだけ書き換える小さなスクリプトです。

LeapImageRetriever.cs 自体も 200 行程度の短いスクリプトなのでざっくり処理を見てみます。

LeapImageRetriever.cs(一部改変)

void Update()
{
    // フラグに応じ生画 / 歪み補正用にシェーダを切り替え
    if (undistortImage) {
        renderer.material = new Material(Shader.Find(UNDISTORT_SHADER));
    } else {
        renderer.material = new Material(Shader.Find(NORMAL_SHADER));
    }

    // Leap Motion からの情報が色々つまった Frame を取得
    Frame frame = leap_controller_.Frame();

    // ...(略)

    // 画および関連した情報が詰まった Image を取得
    Image image = frame.Images[imageIndex];
    int image_width = image.Width;
    int image_height = image.Height;

    // ...(略)

    // 生画を取得
    image_data_ = image.Data;
    distortion_data_ = image.Distortion;

    // Leap Motion から取得した値をアルファ値としてセット
    // (画は使わずにアルファのみを利用している)
    int num_pixels = main_texture_.width * main_texture_.height;
    for (int i = 0; i < num_pixels; ++i) {
        image_pixels_[i].a = image_data_[i];
    }

    // ...(略)

    // テクスチャに image_pixels_ をセット
    ApplyDataToTextures();

    // ...(略)
}

Leap.Controller.Frame を通じて Image(画のデータや情報が入ったクラス)を取得しています。取得した画は単色グレースケールで 8 bit の明度が入っているのみです。上記コードではこれをアルファとしてテクスチャにセットし、フラグメントシェーダ内で以下のように利用しています。

LeapDistorted.shader(一部改変)

float4 frag(fragment_input input) : COLOR
{
    // ...(略)

    // 先ほどアルファをセットしたテクスチャ
    float4 textureColor = tex2D(_MainTex, position);

    // そのアルファを取り出す
    float a = textureColor.a;

    // GUI から設定した明るい場所の色
    float4 color = _Color;

    // 透明モード(3 キー押下時)だったらアルファをそのまま透明度として反映
    if (_BlackIsTransparent == 1) {
        color.a *= a;
    // そうでない時は背景は黒にしてアルファを明るさとして使用し RGB 値の方を暗くする
    } else {
        color = a * color;
        color.a = 1.0;
    }
}

とてもシンプルです。

歪み補正について

Leap Motion の生画は画角 150°の広角レンズを通じて得られる画であり、レンズの特性により樽型の歪みが生じます。Leap Motion ではこの補正をシェーダ内で面白いやり方で補正しています。具体的には、予め Image.Distortion を利用してキャリブレーションマップを画像として用意しておき、これを利用して歪み前から歪み後の座標へと変換しています。

LeapImageRetriever.cs

void Update()
{
    // ...(略)

    // Leap Motion SDK からキャリブレーションマップを貰う
    distortion_data_ = image.Distortion;

    // ...(略)

    // キャリブレーションマップをシェーダ内で扱える形に変換
    if (undistortImage) {
        EncodeDistortion();
    }

    // ...(略)
    if (undistortImage) {
        // そのキャリブレーションマップをシェーダにデータをセット
        renderer.material.SetTexture("_DistortX", distortionX_);
        renderer.material.SetTexture("_DistortY", distortionY_);
        renderer.material.SetFloat("_RayOffsetX", image.RayOffsetX);
        renderer.material.SetFloat("_RayOffsetY", image.RayOffsetY);
        renderer.material.SetFloat("_RayScaleX", image.RayScaleX);
        renderer.material.SetFloat("_RayScaleY", image.RayScaleY);
    }
}

Image.Distortion は 64x64x2(2 は x と y 両方詰まっているので) の float のデータなのですが、これはある位置 {(x, y)}{(x', y')} に変換するマップになっています。つまり、{x' = f(x, y)}{y' = g(x, y)} となる関数を作ってるわけです。より具体的にはこの float を RGBA にエンコードし、シェーダ内でこの RGBA を float にデコードしています。補間は内部的にテクスチャで Bilinear で行われていると思います。ちなみにエンコードされたテクスチャ(distortionX_distortionY_)を見てみるとこんな感じです(絵柄そのものに意味はありません、モアレになっているのは x, y, z, w で各オーダーを扱っているからだと思います)。

f:id:hecomi:20140923175004p:plain

CPU パワーを借りるのであれば Image.Warp()Image.Rectify() を使えば良いようですが、処理が遅くなると思うので GPU パワーを借りるためにこの方式を採用しているものと思われます。法線マップも (x, y, z) を (r, g, b) なテクスチャに置き換えて処理していますが、こういった形で予め用意したテクスチャを元に画像処理を行うのは色々出来そうで面白いです。

手のトラッキングと重畳

次に手のトラッキングを見るには PassthroughWithTracking シーンがシンプルで分かりやすいです。トラッキングには以前と同じく HandController.cs が担当していますので詳しくは以前の説明に譲ります。

差分としてはツール(鉛筆などのような物体)が使えるようになっていることで、これは TooDarkToSee シーンなどを参考にすると良いと思います。カメラ画とオブジェクトの重畳に関しては、そもそもカメラ画をベースに手の位置を推定しているため、カメラ画 - 手オブジェクト - カメラが適切な距離で並びさえすれば Unity のカメラの画角によらず一致するようになります。

f:id:hecomi:20140923184237p:plain

インタラクションの作成

@koukiwf さんが先駆けてやられています。

この表現を LeapOculusPassthrough をベースに作成してみます。

手の大きさを変えるには色々な場所を修正する必要があるので、今回は割愛して、周りのオブジェクトを触りやすい大きさにします。以下はプレーン3枚のコライダの Is Trigger をチェックして簡単なスクリプトをくっつけたサンプルになります。

using UnityEngine;
using System.Collections;

public class LeapTouchablePanel : MonoBehaviour 
{
    public Color normalColor  = new Color(0, 0, 255);
    public Color touchedColor = new Color(0, 255, 0);
    private int touchedColliderNum_ = 0;

    void Start()
    {
        renderer.material.color = normalColor;
    }

    void OnTriggerEnter(Collider other)
    {
        if (touchedColliderNum_ == 0) {
            renderer.material.color = touchedColor;
        }
        ++touchedColliderNum_;
    }

    void OnTriggerExit(Collider other)
    {
        --touchedColliderNum_;
        if (touchedColliderNum_ == 0) {
            renderer.material.color = normalColor;
        }
    }
}

そのままでは距離感が取りづらいので、例えば影を落としてあげるといった前後関係の提示の工夫は必要だと思います。

Unity 4.6 UI との組み合わせ

公式のフォーラムに実現された方の動画が上がっています。

前回の記事(Oculus Rift で頭の動き + タップで簡単に Unity 4.6 UI を選択できるやつを作ってみた - 凹みTips)を元に同じように作成してみようと思います(別記事で公開予定)。

Leap Motion VR の Tips

以下のエントリに 12 の Q&A が公開されています。重要なポイントだけピックアップしてみます。

HMD モード

ヘッドマウントディスプレイに最適化したモード(上下反対にして配置した時の手の検出を改良したモード)があるのですが、これは個々のアプリケーションからポリシーフラグに 'POLICY_OPTIMIZE_HMD' をセットすることで有効かどうかを切り替えます。サンプル中では LeapImageRetriever.csStart() 中で以下のようにポリシーフラグをセットしています。

leap_controller_.SetPolicyFlags(leap_controller_.PolicyFlags | Controller.PolicyFlag.POLICY_IMAGES);

Oculus Rift DK2 のポジショントラッキングの妨げにならないか?

Leap Motion を前面に配置することから、DK2 本体に配置されているポジショントラッキング用の赤外線 LED を覆ってしまうことで精度が落ちてしまう懸念がありますが、DK2 にはロバスト性確保のために複数の赤外線 LED が配置されていることから問題とならないようです。

DK2 の USB ポートを利用できないか?

DK2 には本体に拡張用の USB ポートがついているのですが、残念ながら現在のところ転送速度に問題があり認識のフレームレートが落ちてしまうことから、PC 本体の USB ポートを使うことが推奨されています。

認識範囲について

Leap Motion の FOV は 150° x 120° もあり、Oculus Rift DK2 の対角 100° を十分にカバーする範囲を扱うことが出来ます。つまり見える範囲で手が届く範囲のオブジェクトは操作できるということになります。

https://blog.leapmotion.com/wp-content/uploads/2014/08/mount-fov.png (http://blog.leapmotion.com/12-faqs-vr-developer-mount/ より)

今後の展開

Dragonfly

公式のエントリによると、Dragonfly というコードネームで、HD よりも高解像度な画を扱い、近赤外画と共にカラー画も取得でき、かつ FOV も大きな次世代バージョンを開発しているとのことです。とても楽しみですね。

おわりに

他にもカメラ画を加工してあげたりして面白い表現を作成したりできそうなので色々と試していきたいです。

Oculus Rift で頭の動き + タップで簡単に Unity 4.6 UI を選択できるやつを作ってみた

はじめに

HMD での UI 操作は色々と議論がなされていると思いますが、中でも頭の向きを利用した UI は結構安定して操作できると個人的に感じています。頭は思ったよりも低カロリーで正確に動かすことが可能で、実際ブラウザ操作(リンククリック)もできるくらいです。

他にも Leap Motion を使った指の空中タッチ等も色々試したり、色んな人が利用できるようにしてフィードバックを頂いて色々考察できればと思い、まずは手始めに Unity 4.6 UI頭の向きで選択 + HMD タップで決定できる VR 用 Input Module およびカスタム Raycaster を作ってみました。

デモ

頭の向きで UI を選択、コツンと HMD を叩くと決定します。

ダウンロード

Unity 4.6 新 UI システムのイベントシステム

前回、Unity 4.6 GUI の EventSystem を拡張する方法について書きました。

が、文字ばっかりでわかりにくかったので、もう少しざっくりとした概念を絵を交えて書きます(ただしドキュメントやコードからの推測が混ざっているため正しい情報とは限らないのでご留意頂ければと思います)。

Unity 4.6 の新 GUI でのイベントの取り扱いは、Event System を基点として、どのオブジェクトが現在選択されているかを判断する Raycaster とマウスやキーボードのイベントを解釈・ディスパッチする Input Module からなります。色々なルールを複数持てるように Raycaster は複数存在可能です。具体的には CanvasGUI の当たり判定を担当する GraphicRaycaster やコライダのついたオブジェクトの当たり判定を担当する PhysicsRaycaster 等です。一方、イベントの解釈を行う Input Module はただ 1 つしか持てません。

f:id:hecomi:20140930223856p:plain

具体的な流れとしては、EventSystem コンポーネントを基点として、選択された Input Module の Process() が毎フレーム呼ばれ、その中で EventSystem.RaycastAll() を通じて当たり判定を行い、該当のオブジェクトに対して適切なイベント(ホバーやドラッグ、クリック等)を ExecuteEvents.Execute() で送信してあげます。

f:id:hecomi:20140930224959p:plain

これらをよしなに自作してあげれば、既存の Event System の上に乗っかってイベントハンドリングが出来るので、GUI をよしなに利用してあげたり出来るわけです。ひな形としては、Input Module は UnityEngine.EventSystems.BaseInputModule を、Raycaster は UnityEngine.EventSystems.BaseRaycaster を継承する形になります。

CustomInputModule

using UnityEngine;
using UnityEngine.EventSystems;

public class MyInputModule : BaseInputModule
{
    /*!
     * 毎フレーム呼ばれるイベント解釈処理
     */
    public override void Process()
    {
        // モリモリする
    }
}

CustomRaycaster

using UnityEngine;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;

public class CustomRaycaster : BaseRaycaster
{
    /*!
     * Raycast に使用するカメラ
     *
     * eventCamera は override 必須
     * Raycast でしか使われないと思われるので別に実装しなくても良い(はず)
     * ここではメインカメラを割り当てている
     */
    public override Camera eventCamera
    {
        get { return Camera.main; }
    }

    /*!
     * 当たり判定を行う処理
     * Raycast は現在の InputModule から呼ばれる
     *
     * @param eventData
     *     マウス座標が入っている
     *     このモジュールでは無視
     * @param resultAppendList
     *     他の Raycaster も含めた全ヒットデータが入っている
     *     この Raycaster によって追加される要素を詰めていく
     */
    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        // resultAppendList.Add( hogehoge );
    }
}

あとは、このあたりのコードを参考にモリモリする形です。

タップ決定の仕組み

ザバイオーネさんのコードを利用させていただきました。

タップ決定の瞬間、叩くとカーソルがずれるので、そのズレを吸収するために照準の動きはフィルタを掛けて滑らかにしています。

設計の反省点

  • GraphicRaycaster はそのままに、Input Module 側だけで Raycast() する際に引き渡すマウスの座標情報を照準の位置に差し替えてやれば、Input Module の変更だけで済んだかもしれません...。

考察

思ったより小突く操作は高カロリーだし、操作してる感がないなぁ、という感じがしました。以前試してみた時間が経つと決定する操作は楽だったのですが、勝手にどこか決定されてしまってうーん、という感じでした。が、その時の UI がブラウザだったのでコンテンツを見るのが主になり、それで誤決定されてしまったので相性が悪かったのかもしれません。今回のようなメニューを表示して選択、閉じる、みたいな流れだと、誤操作も少なくなりそうな気がするので、楽な方、という意味では以前の方式のほうが良い気もします。ただ、もう少し複雑な UI や誤操作をなるべく避けたい場合は、今回のような明示的に決定を行う方式のほうが優れていると思います。

明示的に、という意味では USB ポートを利用して側面にスイットを設けてクリック、とかのほうが良さそうです。が、今回の方式だとスイッチなくても Oculus Rift さえあればできるのでその分利点はあると思います。

操作してる感、という点では横に叩いてるのに UI は押し込まれているので、そこら辺の見せ方を工夫すると良くなる気もします。

あと、メニューの表示に関しては、ダブルタップもハンドル出来るようにして、これに割り当てると良いかな、とか思いました。

おわりに

次は Leap Motion 用モジュールを作って頭の向きによる操作と比較してみたいと思います。

Unity 4.6 から利用できる UI での EventSystem をカスタムする方法について調べてみた

はじめに

Unity 4.6 から導入された新 UI システムでは、マウスやキーボード、キーパッドの入力を担当する Standalone Input Module と、タッチを担当する Touch Input Module によって、入力イベントが扱われます。何かしらの UI 要素を追加すると、自動でこれらのモジュール及び EventSystem のアタッチされたゲームオブジェクトが自動で生成されます。

f:id:hecomi:20140925204359p:plain

そして新 UI システムでは、CanvasRender ModeWorld Space にすることで、VR のシーンでも簡単に扱うことが出来ます。が、そのままではマウスやキーボードの操作になってしまい、色んなガジェットと組み合わせたりしながら VR 内で望ましい UI を色々と試行錯誤するにはカスタムしてあげる必要があります。

そこで本エントリでは、独自のイベントハンドリングを新 UI システムで行う方法について調べてみた内容を共有したいと思います。

参考

既に VR 用に実装されている方がいらっしゃいますので、これが何をしているか理解するところまでやります。

あとはドキュメント、及び Gist に上がった各モジュールのコードを参考に追っていきます。

イベント送信の仕組み概要

イベントを送信するのは各 Input Module の基底クラス BaseInputModule が担当します。これは MonoBehaviour を継承した UIBehaviour を継承したクラスのため、GameObject にアタッチすることができます。カスタムしたモジュールのコードをドキュメントを参考にして書いてみます。

using UnityEngine;
using UnityEngine.EventSystems;

public class CustomInputModule : BaseInputModule
{
    public GameObject[] targetObjects;

    public override void Process()
    {
        if (targetObjects == null || targetObjects.Length == 0) {
            return;
        }

        foreach (var target in targetObjects) {
            ExecuteEvents.Execute(target, new BaseEventData(eventSystem), ExecuteEvents.submitHandler);
        }
    }
}

Process()Update() のように毎フレーム呼ばれる関数です(BaseInputModule 内で呼ばれます)。ここで対象となる GUI の GameObject を判断し、ExecuteEvents.Execute() を通じて対象の GameObject に登録されたハンドラを呼び出します。引数は順番に、対象の GameObejct、引数、ハンドラとなっています。Inspector から適当なボタンを targetObjects に登録しておくと、毎フレームクリックされる形になりますので試してみてください。

おまけ

更に進んでカスタムイベントを作成するには以下の Gist がとても参考になります。

StandaloneInputModule を見てみる

概要が掴めたので StandaloneInputModule を見てみると何となく概要が掴めると思います。

namespace UnityEngine.EventSystems
{
    [AddComponentMenu("Event/Standalone Input Module")]
    public class StandaloneInputModule : PointerInputModule
    {
        // ...(略)
        
        public override void Process()
        {
            bool usedEvent = SendUpdateEventToSelectedObject ();

            if (!usedEvent)
                usedEvent |= SendMoveEventToSelectedObject ();

            if (!usedEvent)
                SendSubmitEventToSelectedObject ();

            ProcessMouseEvent ();
        }

        // ...(略)

        private void ProcessMouseEvent()
        {
            bool pressed = Input.GetMouseButtonDown (0);
            bool released = Input.GetMouseButtonUp (0);

            var pointerData = GetMousePointerEventData ();

            // Take care of the scroll wheel
            float scroll = Input.GetAxis ("Mouse ScrollWheel");
            pointerData.scrollDelta.x = 0f;
            pointerData.scrollDelta.y = scroll;

            if (!UseMouse (pressed, released, pointerData))
                return;

            // Process the first mouse button fully
            ProcessMousePress (pointerData, pressed, released);
            ProcessMove (pointerData);

            if (!Mathf.Approximately (scroll, 0.0f))
            {
                var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler> (pointerData.pointerCurrentRaycast.go);
                ExecuteEvents.ExecuteHierarchy (scrollHandler, pointerData, ExecuteEvents.scrollHandler);
            }
        }

        // ...(略)
    }
}

Process() を見てみると ProcessMouseEvent()Input.GetMouse*() をしているのが分かります。ここで得られたデータを元に色々なイベントに変換して、対象の GameObject に対して ExecuteEvents.Execute() してイベントハンドラを呼び出しています。では対象の GameObject はどうやって取得しているか見てみます。

ProcessMouseEvent() の中で GetMousePointerEventData() を呼び出しているのが見えると思います。これは基底クラスの PointerInputModule で定義されている関数で中を見てみしょう。

using System.Collections.Generic;
using System.Text;

namespace UnityEngine.EventSystems
{
    public abstract class PointerInputModule : BaseInputModule
    {
        // ...(略)
        
        protected virtual PointerEventData GetMousePointerEventData()
        {
            PointerEventData pointerData;
            var created = GetPointerData (kMouseId, out pointerData, true);

            pointerData.Reset ();

            if (created)
                pointerData.position = Input.mousePosition;

            Vector2 pos = Input.mousePosition;
            pointerData.delta = pos - pointerData.position;
            pointerData.position = pos;
            pointerData.scrollDelta = Input.mouseScrollDelta;

            eventSystem.RaycastAll (pointerData, m_RaycastResultCache);
            var raycast = FindFirstRaycast (m_RaycastResultCache);
            pointerData.pointerCurrentRaycast = raycast;
            m_RaycastResultCache.Clear ();
            return pointerData;
        }

        // ...(略)
    }
}

中を見てみると、なにやら eventSystem.RaycastAll() なるものが見えます。実は Canvas オブジェクトについていた Graphic Raycaster が、この Raycast を行っているコンポーネントで、Graphic Raycaster はコライダのついていない GUI のオブジェクトを判定するためのものです。

f:id:hecomi:20140925231630p:plain

つまりこいつが、マウスカーソルがどの GUI の上に乗っているか判断してくれているものになります。他にもコライダのついたオブジェクトに対して Physics Raycaster があったり、独自に Raycaster を作成することも出来ます。

まとめると、Raycaster を通じて得られたマウスカーソルが乗った(or タッチした等)オブジェクトに対して、Input Module が解釈したイベントを、イベントハンドラを通じて呼び出している形になっています。つまり、イベントを送信する対象をマウスカーソルでなく別の方法で決定したかったらカスタム Raycaster を、独自のルールでクリックやホバーなどを行いたかったらカスタム Input Module を作成する、ということを行えばよい形になります。

VRInputModule を見てみる

概要が掴めたところで、先ほどの VRInputModule.cs を見てみます。

ここでは、カスタム Raycaster を作らずに、SetTargetObject() という static なメンバを通じてどのオブジェクトにイベントを送信するか、というのを決定しています。別途 InputManager というクラスで独自のボタン押下を検出し、SetTargetObject() を通じて指定された GameObject に対して Submit イベントを通知しています。また SetTargetObject() 内で、以前指定されていたオブジェクトは PointerExit イベントを、新しく指定されたオブジェクトに対しては PointerEnter イベントを送信することにより、フォーカス表現を再現しています。

元エントリにもある通りちょっと dirty な感じがしますが、実装はとても簡単なので良いですね。真面目にやるのであればカスタム Raycaster を作成すれば良いと思います。

おわりに

次回は、これを元に VR 用の GUI モジュールを作成してみます。