凹みTips

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

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 モジュールを作成してみます。

OMMF2014 で レゴ x プロジェクションマッピングなゲーム Mont Blanc Pj. を出展してきた

f:id:hecomi:20140910003725p:plain

はじめに

仕事が立て込んでいて書くのが遅れてしまいましたが、8/23、24 でソフトピアジャパンで行われた Ogaki Mini Maker Faire 2014 (OMMF2014) に、レゴで出来た自由に組み替えられるステージにプロジェクションしてインタラクティブに遊べるゲーム Mont Blanc Pj.(もんぶらんぷろじぇくと)を出展してきました。

Mont Blanc Pj. は以下の3つのコンセプトを主軸において、友人の id:jonki と趣味で開発しているゲームです。

  1. レゴで作成した立体ステージにプロジェクションしたゲーム
  2. レゴを自由に組み替えることでゲームの世界も連動して変化
  3. ゲーム中のオブジェクトハードウェアが連動

現実の世界とバーチャルな世界を良い感じに融合させることで、小さいお子さんでも直感的に楽しく遊べるゲームを目指して、昨年の Maker Faire Tokyo 2013 で出展し、その後昨年末の第5回ニコニコ学会で発表させて頂きました。大きく仕組みは変化していないので、詳細につきましては、以前の情報をご覧ください。

私はソフトウェア(認識周り・ゲーム)を担当しているので、本エントリでは、ソフトウェア側の改良点や発表しての感想、今後の展望などについてご紹介したいと思います。ハードウェア、本体の改良についてなどは後ほど id:jonki がエントリを書いてくれると思います(きっと)。

コード

利用しているコード一式は以下にあげています。

今回の改良点

ゲームの見た目

ニコニコ学会で発表した時のものがベースになっています。ロボット君(仮)が工場を駆け回ってるイメージです(世界観やストーリーは未設定です...)。

f:id:hecomi:20140909202814j:plain

f:id:hecomi:20140910003252p:plain

1日目の展示ではジャンプブロックが毒々しい赤色だった点など諸々実際に展示してみると見栄え的に辛いところが多かったので、懇親会後にドット打ちなおしたりして色々デザイン修正しました。

認識の高速化

以前は認識に2〜3秒かかっていたのですが、これは Qt で作成している認識プログラムにおいて QML 側(JavaScript 側) に多くのロジックを追いやっていたこと等が大きな原因でした。そこで今回は認識用のプログラムを全面的に書きなおして、ロジックはなるべく C++ 側に持たせ、QML 側では極力 UI 構築とデータの受け渡しのみにするよう注意しました。手が入っていないかを判断するために一定の判断時間を設けているのですが、感覚的には手をどけて直ぐに反応するようになりました。

キャリブ方法の改善

まだ自動化にまでは至っていないですが、レゴの位置合わせ(キャリブレーション)を大幅に改良しました。グリッドを微調整したりどうしても認識が暴れる部位はダブルクリックで除外(緑色のところ)するようにしたりなどの対応もしています。前回同様、突然の死(強制終了)に備えて、すべてのパラメタはリアルタイムに保存するようにしています(が、今回は1度も落ちませんでした)。

機材の削減(Xtion の利用)

以前は赤外線投光機 + 赤外線カメラを用いて赤外領域による認識を行っていましたが、赤外投光機がかなり大きく重いのと、散乱光を用いる(直接光だとムラができてしまう)関係上、展示員の立ち位置による影響が大きかったりと様々な問題が有りました。

そこで今回は Xtion を用いて投光機と赤外線カメラ両方の役割を果たしてもらうことにしました。Xtion は基本的にはデプスセンサなのですが、赤外画のみを利用しています。レーザーによる赤外パターン光は色々と試した結果、ティッシュ2枚が一番散乱したので、これを全面につけ投光機代わりに利用しています。

f:id:hecomi:20140823103936j:plain

正面から当てると中心部の明るい光が見えてしまいますが、側面からだとそれほどムラなく全体を照らすことが出来ました。結果として、USB ケーブル 1 本のみで電源供給&投光が可能になりました。

また、タブレットで行っていたブロック変更操作を物理ボタンにしたことから無線ルータも不要になりました。

ハードウェアの追加 / 改良

ゲーム上のキャラがブロックを叩くと実際の世界のブロックも壊れる(ように見える)ギミックを追加しました。場所は決め打ちで特殊なブロックが追加され、叩くとゲームの世界で壊れるエフェクトを出す & ソレノイドによって裏からついて実際のレゴブロックを吹っ飛ばす、というものです。

なるべくインタラクションはブロックの付け外しのみに拘りたかったのですが、それによって単にブロックが出たり消えたりでは単調になってしまうので、3つ目のコンセプトを活用して、こういった特殊なルールを設けるとちょっとした味付けになるかなと思って作成しました。何だ何だと見に来てくれたり、面白がって何度もつけてくれるお子さんもいて、作ってみてよかったなと思いました。大人は突然吹っ飛ぶとビクッとするのですが、お子さんはほとんどビクッとしないのが面白かったです(慣れてるのかな?)。

ギミック作成を頑張ってくれた相方に感謝です。その他の改良もあるので id:jonki の執筆に期待。

今回の反省・今後の改良について

認識の高速化

小さいお子さんだとブロックを付けた後になかなか手を引っ込めてくれなかったりしました。そこで次は領域分割して手が画面に入っていてもある程度認識をさせようかなと考えています。現在は全体が安定したら差分でブロック追加/削除を判断、としているのですが、ここを全体でなくて複数領域に分割するイメージです。

これに加えて、手を引っ込めたら面白いことが起こりそうだという表現で自然に促せたら良いなと思うので、なにか考えたいです。

1ポチ認識

今は 2x2 のブロックを認識して計 24 x 24 ステージを構成しているのですが、奇数位置に 2x2 をつけてしまうと誤認識(2x4 や 1 ポチズレた 2x2 等)してしまい、これに惑わされる方々が多かったです。そこで、次回は 1x1 を認識するようにして奇数位置も認識できるようにしたいです。

おそらく Xtion 化で解像度が高くなったので問題なく認識はできると思うのですが、問題はゲームの方で色々と整合取れなくなるところが多いので、そこをうまく何とかしたいです。

レゴのみによるブロックの出し分け

今回一番失敗したな、というのがスイッチによるブロック種別選択です。追加できるブロックには普通のブロックと接地状帯で触れるとジャンプするブロックがあるのですが、以前はタブレットでブロックを切り替えできるようにしていたところを、物理的なスイッチで切り替えるようにしてみました。

f:id:hecomi:20140909214224p:plain

が、そもそもブロックの切り替えは説明しないと絶対に分からない点、ボタンを押すと新たに取り付けたレゴブロックがゲームに反映されると勘違いされてしまった点など、色々な反省点が有りました。

ただこれはレゴブロックの識別が難しいために直前でえいやで作った苦肉の策でもあったので、淡色レゴやレゴの凸部にパターンを描くなど色々と試して、次回こそはなんとかこの点を修正したいと思います。

ハードウェアのモジュール化

ハードウェア連動についてはもっと色々と出来るのではと考えていて、その中で最もやりたいのがハードウェアを搭載したレゴブロックです。現在1ブロックしかない光るブロックを任意の位置につけれたり、スイッチを押すとアイテムが出てくるブロックがあったり、スライダでゲーム内のギミックを動かせるブロックがあったりなどです。

レゴの付け外しのインタラクションからルールは増えてしまいますが、何が出来るか(スイッチを押す、スライダを動かすなど)直感的に分かる範囲内では色々挑戦してみたいです。

1段以上の段差の取得

複数段の取得については以前 Kinect で試して失敗した(うまく取れなかった)経緯あったのですが、近距離ならいけるかなと思い、今回 Intel Createive Camera でも試してみました。が、精度は出ていると思うのですが綺麗に認識できなかったので保留にしています。

f:id:hecomi:20140909205020p:plain

取り敢えず Kinect v2 も手に入れたので試してみます。

画面のズレ

Unity のゲーム画面とレゴブロックの位置調整のために、カメラの位置移動、回転を用意していたのですが、いざやってみると微妙に位置が合わなかったりして残念だったので、次回は Image Effect で簡単に射影変換出来るようにしておこうと思います。

教えていただいた先行事例

ぶろっくぴーぽー

Eric さんに SEGA が 2009 年の CEDEC にて発表した資料を教えて頂きました。プロジェクションと液晶の違いはあれど、実際のブロックの赤外線による認識、そのゲームへの反映などかなり共通点があるなと感じました。アーケードゲームにまで仕上げるのはさすがプロですね...。

よく頂いた質問

売らないの?

今のところ販売やビジネス化は考えていません。2点理由があり、1点目は機材がまだ高過ぎること(特にプロジェクタ)で、2点目はビジネス化しようとすると楽しくならなくなりそうだな...、と思ったからです。特に2点目について、まだまだ掘り甲斐のある分野だと思うので、体験を縮小させずに純粋に楽しさの部分を色々改良していきたいと思っています。あと単純に我々が作ってるのが好きなので、あまり別のことに時間を割きたくない...、というのも大きいです。

ただ、ご要望も頂いたので、色々な方に体験してもらうためにソフト / ハード両面から設置の簡単化や耐久性の向上をして、我々以外でもいろんな場所で展示してもらえるようにする、といった改良につきましては、可能な限り進めていきたいと思います。

次回出展予定

Maker Faire Tokyo 2014 に出展したいと思っています。

おわりに

今回は展示のお手伝いに id:AMANE に参加してもらったお陰で、去年の MFT と比べてかなり楽させてもらうことが出来ました。ありがとうございます。また、色々な方と交流できる素敵な場を提供して頂いた OMMF2014 の運営の皆様、ありがとうございました!