凹みTips

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

Oculus Rift と Leap Motion で空中お絵描きアプリを作ってみた

f:id:hecomi:20141031012737p:plain

はじめに

Leap Motion VR の登場で簡単に VR 空間に干渉出来る自分の手を持ち込むことが可能になりました。

そこでふと思いついてやってみようと思ったのが、自分の指で空中にお絵描きできるアプリです。本エントリでは作ってみたアプリの配布と、この作り方(なんとコード0行!)についての紹介をしたいと思います。

デモ

ダウンロード

下で解説を行っているものになります(上のデモの線をカラフルにしたものです)。↑キーと↓キーで明るさの調節ができます。

簡単な仕組み解説

Leap Motion VR の世界はおおよそ実空間に則したスケールで作られています(Unity 上のスケール 1 = 実空間の 1m)。これについては以下のエントリで詳しく解説されています。

また、Oculus のトラッキングも同じく実空間に則したスケールで作られているため、なんと特に何もしなくてもおおよその位置が合う形になります(微妙にはずれています)。

環境

作り方

基本的には、指先に Trail Renderer をつけるだけです。

まず、Leap Motion VR のページから Unity 用の Asset を落としてきてインポートします。

LeapMotion+OVR > LeapOculusPassthrough シーンを開いて実行してみると手で崩せるブロックがたくさんある画面になると思います。今回はブロックは必要ないので Walls オブジェクトを削除するか、Inspector からチェックを外して inactive にして非表示にしておきましょう。

次に、指から線が出るように手のモデルである FadingHandbone3(人差し指の先)に Trail Renderer をアタッチします(FadingHandLeap Motion で認識した手に乗っかるコピー元のオブジェクトとして扱われるため、普段は見えないように inactive な状態になってますが、ここでは分かりやすいように一度 active な状態にしてスクリーンショットを撮っています)。

f:id:hecomi:20141031002839p:plain

デフォルトだと 1 の太さ(= 幅 1 メートルの線)になっているので、適当に 1 cm の太さ、線の頂点の間隔を 5 mm などとしてみます。

f:id:hecomi:20141031004056p:plain

最後に、Durovis Dive 用のアセットがビルドの妨げになるので、DiveLeapMotion+Dive は Project から消去した後にビルドします。

f:id:hecomi:20141031004250p:plain

これで完成です!色をつけたい場合は FadingHand にアタッチされている HmdFading スクリプト(手の信頼度に応じて手の透過率を変更するスクリプト)のチェックを外して disabled にしておき、Trail Renderer のパラメタを適当にいじります。

f:id:hecomi:20141031005046p:plain

これで完成です!

f:id:hecomi:20141031010057p:plain

f:id:hecomi:20141031010104p:plain

また、LeapPassthroughOVRPlayerController > OVRCameraController > CameraLeft > HandController にアタッチされている HandController.cs の Tool に適当なオブジェクトを指定すると棒状のものもトラッキングできるようになるので、同様にトレイルを引いてくれる棒オブジェクトを作ってあげると空中ペンができます。

f:id:hecomi:20141031020022p:plain

あとはパーティクルをモリモリしたりすればきれいな感じになると思います。また標準の Trail Renderer はカクカクしがちなのでスプラインにしてくれるアセットを代わりに適用しても良いと思います(次回エントリで紹介する Magic VR ではこちらを利用しています)。

Leap Motion VR の無駄な処理削減

さて、Leap Motion VR で画を取ってくる処理ですが、結構重いです。そこで Deep Profile で見てみると、EncodeDistortion() が結構な時間を食っています。

f:id:hecomi:20141031010352p:plain

これは LeapImageRetriever.cs の中で行われている Leap Motion の歪み補正を行うための処理(前回の記事参照)なのですが、テクスチャのサイズが変更されない限り初回のみで良いはずです。そこでこれを最初のみ行われるようにちょっとだけ書き換えたものが以下になります。

これを代わりに使うことで軽量化することが出来ます。

f:id:hecomi:20141031014252p:plain

先行事例紹介

AR の分野では MIT Media Lab の Second Surface が有名かと思います。

Second SurfaceiPad を使ったものですが、自作の透明パッドによる Gravity というプロジェクトもあり、こちらでは Oculus Rift と組み合わたデモも実現されています。

昨今の VR 関連の技術進歩によって、これらのコンセプトを実現することが容易になったのは本当に素晴らしいと思います。

おわりに

ちょっとした工夫でもっと色々なことが出来ると思います。例えば犬を描くとそれを認識して動き出したり、うごメモのように 3D 漫画が作れたり、他人の描いた絵を 3D 空間上でそのまま再現して見れる、などなどたくさん思いつきます。Leap Motion の次期バージョン Dragonfly が出るとスルー画もカラーに出来てもっと楽しくなりそうですね。

次回は、このコンセプトを発展させて現在開発中の、図形を描いて魔法を発射できる Magic VR の紹介と解説エントリを書きたいと思います。

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 用モジュールを作って頭の向きによる操作と比較してみたいと思います。