MRDesignLabを使いジェスチャー入力を取り扱う

投稿者: | 2017-06-16

先日Twitterの方に投稿しましたが、MicrosoftからWindows Mixed Realityアプリケーション用のUI/UXコンポーネントのサンプルを集めたリポジトリが公開されました。

プリインアプリであるHologramsのような操作UI、各種ボタン群が揃っており、その上Immersive Device等、HoloLens以外のデバイスでの利用も見越して様々な入力方法に対応できるように作られているようです。しかしながら、HoloToolkitの入力を取り扱うInputManagerとは異なる仕組みが用意されているため、今まで作ってきたアプリに適用するにはちょっと手間がかかります。

今回はHoloLensアプリ開発の際にMRDesignLabを使用する場合のジェスチャー入力の取り扱い方法をまとめていきたいと思います。

導入方法

アセットのインポート

リポジトリ内のサンプルを試したい場合は、展開後の「DesignLabs_Unity_Examples」をUnityで開けば試すことができます。サンプルの動作確認については@miyauraさんのQiita記事が参考になると思います。


ここではアプリ開発時に使用する場合を想定して説明します。

MRDesignLabでは一部のコードがHoloToolkitに依存しています。公式リポジトリではHoloToolkit 1.5.6.0が使用されていますが、ソースを一部修正すれば1.5.7.0も使うことが可能です。本記事では1.5.7.0を使います。

まずはHoloToolkit-Unity-v1.5.7.0.unitypackageをプロジェクトにインポートします。続いてリポジトリ内の\MRDesignLabs_Unity\DesignLabs_Unity\Assetsフォルダから「MRDesignLab」一式をプロジェクトにコピーします。そうするとHoloToolkit側で以下のようなエラーが発生します。

これはMRDesignLabでContextMenuというクラスが定義されているため、Unityで提供されているContextMenu属性と衝突してしまうために発生します。これを回避するために、MRDesignLab側のContextMenuクラスに名前空間を設定します。(バグといってもいいくらい酷いのでそのうち修正されると思います、というか後でissue投げます)

//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//
using UnityEngine;

namespace HUX    // 名前空間を追加
{
    public class ContextMenu : MonoBehaviour
    {
        public bool Visible = false;
    }
}

MRDesignLabのアセットを追加後、以下のようなウィンドウが表示されます。

MRDesignLabではボタンに表示するアイコンとして、テクスチャをアイコンとして使う方法と、「HoloLens MDL2 Symbols」というフォントに埋め込まれたアイコンを使う方法が提供されています。ただ、このアイコンフォントは別途ダウンロードする必要があり、このウィンドウはその案内をするためのものです。ウィンドウ内の「Click here to download font」ボタンを押すとダウンロードできるので、ダウンロード後に解凍して得られたフォントを適当なところ(例えばResources配下に新しくFontsフォルダを作るなど)に配置してください。次にウィンドウの「Click here to view button profile」ボタンを押してアイコンフォントの設定画面を開きます。(このあたりの詳しい解説は次回のButtonの解説時に説明します)

赤くなっている「Auto-assign ‘HoloLens MDL2 Symbols’ font」ボタンを押せば設定完了です。

シーン設定

HoloToolkit同様、デフォルトで設定されているカメラを削除します。その後、MRDesignLabで用意されているカメラや入力処理部分が一緒になっているHoloLens.prefabをシーンに配置します。
以上です。

仕組み

HoloLens.prefabの構成

入力周りの処理はすべてHoloLens.prefabにて行われます。このprefabの構成は以下の通りです。
なおInteractionManagerには同名のスクリプトがアタッチされていますが、Unity APIのInteractionManagerとは別物です。本記事ではInteractionManagerとはMRDesignLabのものを指すこととします。

MRDesignLabではハンドジェスチャー以外にも、マウスやXBOXコントローラなどのゲームパッドによる入力もサポートしています。これらのデバイスによるボタンやジョイスティックの状態を管理するのがInputMapping以下にある入力ソース管理部分です。

また、BoundingBoxと呼ばれるHologramsのような操作UIに関する処理もこのprefabに含まれています。入力ソースやBoundingBoxについての解説は次回以降とし、今回はハンドジェスチャーに絞って解説します。

ジェスチャー検出の仕組み

このprefabを利用してジェスチャー入力を取る方法として、主に以下の5つが挙げられます。

  1. InteractionManagerが実行するSendMessageをキャッチする
  2. InteractionManagerのイベントをサブスクライブする
  3. MRDesignLabのスクリプトInteractionReceiverを使う
  4. InputSourceHandsのイベントをサブスクライブする
  5. InputShellのイベントをサブスクライブする

このうち、4と5については入力ソースに関連する部分ですので解説は次回以降とし、今回は1~3について解説します。

本記事の説明範囲におけるクラス構成と役割のイメージは以下の図の通りです。

これらのクラスはジェスチャー認識時は以下のような流れで動きます。

このようにMRDesignLabではイベントをサブスクライバーに通知すると同時に、GazeでフォーカスをあてているGameObjectに対して直接SendMessageにてイベントを通知しています。
その一方、HoloToolkitのようなEventSystemのExecuteEventによるインタフェースを実装するだけでイベントに対するアクションが実装できるような仕組みは現時点では提供されていません。

AirTapジェスチャーに対するアクションを実装する

ではアクションの具体的な実装方法を見ていきましょう。前節の通り、3つのパターンを紹介します。

InteractionManagerからのSendMessageをキャッチする

まずはInteractionManagerがどのようにしてイベントを発火するのかを見ていきます。

public class InteractionManager : Singleton<InteractionManager> {
    // GestureRecognizerのプロパティ
    public GestureRecognizer ActiveRecognizer { get; private set; }
    // 検出対象とするジェスチャーの一覧
    public GestureSettings RecognizableGesures = 
        GestureSettings.Tap | 
        GestureSettings.DoubleTap | 
        GestureSettings.Hold | 
        GestureSettings.NavigationX | 
        GestureSettings.NavigationY;
    // Hold系ジェスチャー時に、フォーカスをロックするか
    public bool bLockFocus;

    protected void Start() {
        // GestureRecognizerのインスタンスを生成し、検出対象ジェスチャーを設定
        ActiveRecognizer = new GestureRecognizer();

        ActiveRecognizer.SetRecognizableGestures(RecognizableGesures);
        // GestureRecognizerのイベントをサブスクライブする
        ActiveRecognizer.TappedEvent += TappedCallback;

        ActiveRecognizer.NavigationStartedEvent += NavigationStartedCallback;
        ActiveRecognizer.NavigationUpdatedEvent += NavigationUpdatedCallback;
        ActiveRecognizer.NavigationCompletedEvent += NavigationCompletedCallback;
        ActiveRecognizer.NavigationCanceledEvent += NavigationCanceledCallback;

        ActiveRecognizer.HoldStartedEvent += HoldStartedCallback;
        ActiveRecognizer.HoldCompletedEvent += HoldCompletedCallback;
        ActiveRecognizer.HoldCanceledEvent += HoldCanceledCallback;

        ActiveRecognizer.ManipulationStartedEvent += ManipulationStartedCallback;
        ActiveRecognizer.ManipulationUpdatedEvent += ManipulationUpdatedCallback;
        ActiveRecognizer.ManipulationCompletedEvent += ManipulationCompletedCallback;
        ActiveRecognizer.ManipulationCanceledEvent += ManipulationCanceledCallback;
        // GestureRecognizerの起動
        ActiveRecognizer.StartCapturingGestures();

        // 入力ソース関連のセットアップ
        SetupEvents(true);

        // Unity APIのInteractionManagerのイベントをサブスクライブ
        UnityEngine.VR.WSA.Input.InteractionManager.SourcePressed += InteractionManager_SourcePressedCallback;
        UnityEngine.VR.WSA.Input.InteractionManager.SourceReleased += InteractionManager_SourceReleasedCallback;

        bLockFocus = true;
    }

~以下、省略~

過去の記事でも説明しましたが、GestureRecognizerはタップやホールドといったある規定の手の動きを検出するUnity APIです。このクラスのインスタンスを生成し、認識対象のジェスチャーを設定し、StartCapturingGestures()でアクティベートすれば、検出対象としたジェスチャーを認識したタイミングで各イベント(TappedEvet, HoldStartedEvent等)を発火します。認識対象とするジェスチャーはInteractionManagerのインスペクタから設定できます。

一方、Unity APIのInteractionManagerは指がセンサ内に入った、倒された、持ち上げられたといったジェスチャーよりも低レベルな動きを検出します。こちらはイベントがstaticなメンバとして定義されていますので、インスタンスを生成せずに使うことができます。

続いて、InteractionManagerがUnity APIから受け取ったイベント通知をどのようにハンドリングするかを見ていきます。

/// <summary>
/// GestureRecognizerが発火するイベントのコールバック
/// タップ回数からシングルタップかダブルタップかを判別し、イベントを発行
/// </summary>
/// <param name="source">ジェスチャーのトリガーとなった入力、ここでは手</param>
/// <param name="tapCount">タップされた回数</param>
/// <param name="ray">イベントが発火したタイミングでのGazeに使ってたRay</param>
private void TappedCallback(InteractionSourceKind source, int tapCount, Ray ray) {
    // FocusManagerを経由して、現在アプリが使用しているFocuserを取得
    AFocuser focuser = GetFocuserForSource(source);
    if (focuser != null) {
        if (tapCount >= 2) {
            DoubleTappedEvent(focuser, ray);
        } else {
            TappedEvent(focuser, ray);
        }
    }
}

/// <summary>
/// フォーカスしているGameObject、およびサブスクライバーに対してイベントを発行
/// </summary>
/// <param name="focuser"></param>
/// <param name="ray"></param>
private void TappedEvent(AFocuser focuser, Ray ray) {
    if (focuser != null) {
        // フォーカスしている、すなわちユーザがGazeで選択しているGameObjectを取得
        GameObject focusObject = focuser.PrimeFocus;
        // イベントデータを作成
        InteractionEventArgs eventArgs = new InteractionEventArgs(focuser, Vector3.zero, true, ray);
        // フォーカスしているGameObjectに対してSendMessage
        if (focusObject != null) {
            focusObject.SendMessage("Tapped", eventArgs, SendMessageOptions.DontRequireReceiver);
        }
        // 後ほど説明
        SendEvent(OnTapped, focusObject, eventArgs);

        ~中略~
    }
}

GestureRecognizerのイベントをサブスクライブすると、イベントデータとしてInteractionSourceKind型のデータが渡されます。これはジェスチャの入力元が手なのかコントローラなのか、はたまた声だったのか、といったジェスチャのソースが格納されます。今回はハンドジェスチャーを想定していますので、値としてはHandが設定されています。その後、このソースをもとにアプリ内で使用されているAFocuserの具象クラスを取得するのですが、この具象クラスがどのようなものなのかは入力ソースの解説時に詳しく説明しようと思います。ここではとりあえずGazeしているGameObjectを参照するためにFocuserを持ってきていると思っておけば大丈夫です。

その後、Focuserから取得したフォーカス中のGameObjectに対してSendMessageを送りメソッドをコールします。開発者側はこのメソッドを実装することによってジェスチャー入力をハンドリングすることができます。なお、SendMessageを実行するときに新しく作ったイベントデータを一緒に送るのですが、そのデータの構造は以下の通りとなっています。

/// InteractionManager.cs

public struct InteractionEventArgs {
    // フォーカスに使用しているFocuser
    public readonly AFocuser Focuser;
    
    // 何かしらの位置情報
    // たとえばManipulation系のイベントの場合、指の座標がセットされる
    public readonly Vector3 Position;
    // 位置情報が相対値かどうか
    public readonly bool IsPosRelative;
    // gazeに使用しているRay
    public readonly Ray GazeRay;
    // コンストラクタ
    public InteractionEventArgs(AFocuser focuser, Vector3 pos, bool isRelative, Ray gazeRay) {
        this.Focuser = focuser;
        this.Position = pos;
        this.IsPosRelative = isRelative;
        this.GazeRay = gazeRay;
    }
}

それではジェスチャー入力に対するアクションを実装してみましょう。以下を参考し適当なPrimitiveをシーン配置し、新規作成したスクリプトをアタッチします。

アタッチしたスクリプトの内容は以下の通りです。(載せる必要がないくらい簡単…!!)

using UnityEngine;
using HUX.Interaction;

public class TapMessageReceiver : MonoBehaviour {
    public void Tapped(InteractionManager.InteractionEventArgs e) {
        Debug.Log("Tapped");
        // 以下にAirTapされたときのアクションを書く
    }
}

注意点としてはSendMessageを使用している都合上、メソッド名をタイポすると動作しません。しかもコンパイル時、実行時にもエラーを吐かないので気づきにくいです。

InteractionManagerから送られるメッセージの種類は後ろに掲載しているので参照してください。

InteractionManagerのイベントをサブスクライブする

InteractionManagerではGestureRecognizerに紐づく形で各種イベントが定義されています。

public class InteractionManager : Singleton<InteractionManager> {
    // ナビゲーション系
    public static event Action<GameObject, InteractionEventArgs> OnNavigationStarted;
    public static event Action<GameObject, InteractionEventArgs> OnNavigationUpdated;
    public static event Action<GameObject, InteractionEventArgs> OnNavigationCompleted;
    public static event Action<GameObject, InteractionEventArgs> OnNavigationCanceled;
    // タップ系
    public static event Action<GameObject, InteractionEventArgs> OnTapped;
    public static event Action<GameObject, InteractionEventArgs> OnDoubleTapped;
    // ホールド系
    public static event Action<GameObject, InteractionEventArgs> OnHoldStarted;
    public static event Action<GameObject, InteractionEventArgs> OnHoldCompleted;
    public static event Action<GameObject, InteractionEventArgs> OnHoldCanceled;
    // マニピュレーション系
    public static event Action<GameObject, InteractionEventArgs> OnManipulationStarted;
    public static event Action<GameObject, InteractionEventArgs> OnManipulationUpdated;
    public static event Action<GameObject, InteractionEventArgs> OnManipulationCompleted;
    public static event Action<GameObject, InteractionEventArgs> OnManipulationCanceled;
    // Unity APIのInteractonManagerのイベント関連
    // 指が倒されたとき
    public static event Action<GameObject, InteractionEventArgs> OnPressed;
    // 指が持ち上げられたとき
    public static event Action<GameObject, InteractionEventArgs> OnReleased;

これらのイベントは前節で説明したコード上で発火します。

/// InteractionManager.cs

/// <summary>
/// フォーカスしているGameObject、およびサブスクライバーに対してイベントを発行
/// </summary>
/// <param name="focuser"></param>
/// <param name="ray"></param>
private void TappedEvent(AFocuser focuser, Ray ray) {
    if (focuser != null) {
        // フォーカスしている、すなわちユーザがGazeで選択しているGameObjectを取得
        GameObject focusObject = focuser.PrimeFocus;
        // イベントデータを作成
        InteractionEventArgs eventArgs = new InteractionEventArgs(focuser, Vector3.zero, true, ray);
        // フォーカスしているGameObjectに対してSendMessage
        if (focusObject != null) {
            focusObject.SendMessage("Tapped", eventArgs, SendMessageOptions.DontRequireReceiver);
        }
        /// ここで発火する!!
        SendEvent(OnTapped, focusObject, eventArgs);

        ~中略~
    }
}

SendEventメソッドは以下のように定義されています。

/// InteractionManager.cs

private void SendEvent(
    Action<GameObject, InteractionEventArgs> sendEvent,
     GameObject gameObject, 
     InteractionEventArgs eventArgs) 
{
    if (sendEvent != null) {
        // フォーカスしているGameObjectとイベントデータを引数として
        // 第一引数で渡されたデリゲート(イベント)をコールする
        sendEvent(gameObject, eventArgs);
    }
}

なぜこのような設計になっているかというと、GestureRecognizerはジェスチャーの認識はできますが、何に対する/どのオブジェクトに対するジェスチャーなのかは考慮していません。InteractionManagerはジェスチャー対象を考慮してイベントを発火しますので、GestureRecognizerを拡張しジェスチャー対象まで考慮できるようになったクラス、と思えば捉えやすいかもしれません。

では、InteractionManagerのイベントをサブスクライブする方法でジェスチャーに対するアクションを実装しましょう。前節のテスト用スクリプトを以下のように修正します。

using UnityEngine;
using HUX.Interaction;

public class TapMessageReceiver : MonoBehaviour {

    // イベントのサブスクライブ設定
    private void OnEnable() {
        InteractionManager.OnTapped += ReceivedTapped;
    }

    private void OnDisable() {
        InteractionManager.OnTapped -= ReceivedTapped;
    }

    private void ReceivedTapped(GameObject go, InteractionManager.InteractionEventArgs e) {
        // イベントはサブスクライバー全てにブロードキャストされる
        // そのため引数をチェックして、このイベントが自分宛であるかチェックする必要がある
        if(go == this.gameObject) {
            Debug.Log("Received Tapped Event!!");
        }
    }
}

前節のSendMessageによる方法との違いとしては、概要図に書いた通りSendMessageがフォーカスをあてているオブジェクトだけに通知されるのに対し、この方法はサブスクライバー全体に通知されます。ですのでコードのコメントにも書いた通り、イベントの宛先が自分のものであるかをレシーバ側でチェックする必要があります。(意図的にすべてのAirTapイベントを取得したいという場合は必要ありません)

InteractionReceiverを使う

InteractionReceiverは前節で説明したInteractionManagerのイベントをすべてサブスクライブするよう、予め定義されたabstractなクラスです。ですので基本的にはこのクラスを継承してアクションを実装していくのですが、ちょっとした手順を踏まなくてはいけません。

先ほどで説明したとおりInteractionManagerのイベント通知ではフォーカスしているGameObjectが一緒に通知されます。先ほどはその宛先チェックを独自に実装していましたが、InteractionReceiverではそのチェック機能がインスタンスメソッドとして用意されています。そもそもInteractionReceiverでは、定義したアクションの動作元、トリガーとなるGameObjectをListで保持しています。InteractionManagerからのイベント通知を受けたときに、イベントデータとして通知されるフォーカスしているGameObjectがこのリスト内に存在しているかを確認し、存在していればアクションを起こします。

実装は以下のようになっています。

/// InteractionReceiver.cs

/// <summary>
/// InteractionManagerが発火したイベントのコールバック
/// </summary>
/// <param name="obj">フォーカスしているGameObject</param>
/// <param name="eventArgs">イベントデータ</param>
private void OnTapInternal(GameObject obj, InteractionManager.InteractionEventArgs eventArgs) {
    CheckAndSendEvent(OnTapped, obj, eventArgs); 
}

/// <summary>
/// 通知されたイベントの対象であるかを確認した上でアクションを実行する
/// </summary>
/// <param name="sendEvent">ジェスチャーに対するアクションを表すデリゲート</param>
/// <param name="obj">フォーカスしているGameObject</param>
/// <param name="eventArgs">イベントデータ</param>
private void CheckAndSendEvent(System.Action<GameObject, InteractionManager.InteractionEventArgs> sendEvent, GameObject obj, InteractionManager.InteractionEventArgs eventArgs) {
    if (IsInteractible(obj)) {
        sendEvent(obj, eventArgs);
    }
}

[Tooltip("Target Interactible Object to receive events for")]
public List<GameObject> Interactibles = new List<GameObject>()

/// <summary>
/// フォーカスしているGameObjectが
/// </summary>
/// <param name="interactible"></param>
/// <returns></returns>
protected bool IsInteractible(GameObject interactible) {
    return (Interactibles != null && Interactibles.Contains(interactible));
}

リストへのGameObjectの追加はインスペクタから設定することができます。

インスタンスメソッドからも可能です。

/// InteractionReceiver.cs

/// <summary>
/// Register an interactible with this receiver.
/// </summary>
/// <param name="interactible">takes a GameObject as the interactible to register.</param>
public virtual void RegisterInteractible(GameObject interactible) {
    if (interactible == null || Interactibles.Contains(interactible))
        return;

    Interactibles.Add(interactible);
}

それではこちらもジェスチャーに対するアクションを実装してみましょう。
まずはReceiverとして空のGameObjectをシーンに配置し、新規作成したスクリプトをアタッチします。

アタッチしたスクリプトの内容は以下の通りです。

using UnityEngine;
using HUX.Interaction;
using HUX.Receivers;

public class TapMessageReceiver : InteractionReceiver  {
    
    // アクション用のメソッドは親クラスで定義されているので、オーバライドして内容を実装
    // 親クラスのメソッドには何の処理も実装されていないので呼び出す必要はない
    protected override void OnTapped(GameObject obj, InteractionManager.InteractionEventArgs eventArgs) {
        Debug.Log("Tapped Receiver!");
    }
}

InteractionReceicer.csにはアクションの内容を定義するためのprotectedなメソッドがイベントの分だけ用意されているので、そちらをオーバライドしてアクションを実装します。GameObjectが対象であるかの判定は基底クラスであるInteractionReceiverで行われるのでここで気にする必要はありません。

最後にアクションの対象となるGameObjectをインスペクタから設定します。


以上です。

InteractionReceiverはあるジェスチャーで同じ動作をするオブジェクトがシーン内に複数登場させる必要がある場合に有用です。これまで紹介した方法では対象となるオブジェクト一つ一つに対してスクリプトをアタッチする必要がありました。InteractionReceiverを使用する場合は動作毎に一元管理することができ、対象となるオブジェクトもエディタ上から追加/削除ができるので見通しが良く、漏れも発見しやすくなるかと思います。

リファレンス

InteractionManagerから通知されるメッセージとイベントのリファレンスです。

メッセージ SendMessage()にて送られるコール対象のメソッド名
アクション側はこの名前でメソッドを実装する
イベント アクション側がサブスクライブするInteractionManagerのイベント名
説明 どのようなタイミングでジェスチャーイベントが発生する
チェック項目 このジェスチャーイベントを検出するためにチェックを入れる必要がある項目
InteractionManagerのインスペクタ上で設定できる

低レイヤー系

メッセージ Pressed
イベント OnPressed
説明 指が倒されたとき
チェック項目 なし
メッセージ Released
イベント OnReleased
説明 倒された指が持ち上げられたとき
チェック項目 なし

タップ、ホールド系

メッセージ Tapped
イベント OnTapped
説明 シングルタップを検出したとき
チェック項目 Tap
メッセージ DoubleTapped
イベント OnDoubleTapped
説明 ダブルタップを検出したとき
チェック項目 DoubleTap
メッセージ HoldStarted
イベント OnHoldStarted
説明 ホールド開始時、すなわち指を一定時間倒したとき
チェック項目 Hold
メッセージ HoldCompleted
イベント OnHoldCompleted
説明 ホールド完了時、すなわちホールド中の指を持ち上げたとき
チェック項目 Hold
メッセージ HoldCanceled
イベント OnHoldCanceled
説明 ホールド中、指の検知をロストしたとき
チェック項目 Hold

マニピュレーション系

マニピュレーションはマウスでいうところのドラッグ操作に相当します。マニピュレーション開始時の座標を原点とした指の位置座標がイベントデータにセットされて通知されます。たとえばイベントデータとして(0.5, 0.1, -0.1)というデータが渡されたとすると、これはマニピュレーション開始位置から右に0.5m、上に0.1m、手前に0.1mの位置に指が移動したことを表しています。なお、マニピュレーション系イベントで通知される位置情報は1m=1単位となっています。

メッセージ ManipulationStarted
イベント OnManipulationStarted
説明 ホールド後、指の位置が変化したとき(マニピュレーション開始)
チェック項目 ManipulationTranslate
メッセージ ManipulationUpdated
イベント OnManipulationUpdated
説明 マニピュレーション中、指の位置が変化したとき
チェック項目 ManipulationTranslat
メッセージ ManipulationCompleted
イベント OnManipulationCompleted
説明 マニピュレーション完了時、すなわちマニピュレーション中に指を持ちあげたとき
チェック項目 ManipulationTranslat
メッセージ ManipulationCanceled
イベント OnManipulationCanceled
説明 マニピュレーション中、指の検知をロストしたとき
チェック項目 ManipulationTranslat

ナビゲーション系

ナビゲーションはジェスチャー方法はマニピュレーションと同じですが、大きく異なる点はイベントデータで渡される指の位置情報の軸を指定できる点です。たとえばNavigationXジェスチャーだけを許可して指を動かすと、イベントデータには(0.3, 0.0, 0.0)として位置情報が渡されます。このとき指を上下前後に動かしてもY成分とZ成分は変化しません。このようにナビゲーションでは取得できる指の位置情報に制限を加えることができます。
ちなみにNavigationXとYを一緒に許可すると(0.2, 0.1, 0.0)といったように位置情報のX成分とY成分の変化量を同時に取得できるようになります。

なお、ジェスチャーの許可設定においてマニピュレーションとナビゲーションを同時に設定すると競合してしまうのでどちらか一方を設定するようにしてください。
また、マニピュレーションとナビゲーションでは位置情報の単位が異なります。マニピュレーションが1m=1単位であったのに対し、ナビゲーションでは10cm=1単位ぐらいのようです。値域もマニピュレーションは制限がない(と思われる)のに対し、ナビゲーションでは-1~1の範囲に限定されます。これはマニピュレーションがその言葉の通り、物体を操作するときに使うことを期待されているのに対し、ナビゲーションはスクロールなどに使われることを期待されているからだと思われます。
【公式サイト】

メッセージ NavigationStarted
イベント OnNavigationStarted
説明 ホールド後、指の位置が変化したとき(ナビゲーション開始)
チェック項目 NavigationX/Y/Z, NavigationRailsX/Y/Z
メッセージ NavigationUpdated
イベント OnNavigationUpdated
説明 ナビゲーション中、指の位置が変化したとき
チェック項目 NavigationX/Y/Z, NavigationRailsX/Y/Z
メッセージ NavigationCompleted
イベント OnNavigationCompleted
説明 ナビゲーション完了時、すなわちナビゲーション中に指を持ちあげたとき
チェック項目 NavigationX/Y/Z, NavigationRailsX/Y/Z
メッセージ NavigationCanceled
イベント OnNavigationCanceled
説明 ナビゲーション中、指の検知をロストしたとき
チェック項目 NavigationX/Y/Z, NavigationRailsX/Y/Z

NavigationとNavigationRailsの違いは、複数軸のナビゲーションを許可していた場合、ジェスチャ中に取得できる取得できる位置情報の軸を、単一に固定するかどうかです。たとえばNavigationXとYを有効化して右下に指を運ぶようなジェスチャーを行うと、イベントデータで渡されるPositionは(0.5, -0.2, 0.0)といった2次元のデータが渡されます。一方、NavigationRailsXとYを有効化して同じ動作をした場合、右方向に最初動かして下方向に持っていた場合は(0.5, 0.0, 0.0)が、下方向を先に動かし右方向を後から動かした場合は(0.0, -0.2, 0.0)といった2次元のデータが渡されます。すなわちNavigationRailsはジェスチャーの初期段階に動かした方向へ軸を固定して移動量を通知してきます。

その他のTips

なにもないところをAirtap

過去のブログでもちょっとしたTipsとして書きましたが、何もない空間、すなわちフォーカスを当てていない状態でのAirTapジャスチャーをハンドリングするスクリプトです。

public class TestReceiver : MonoBehaviour {

    void Start() {
        InteractionManager.OnTapped += TappedCallBack;
    }

    private void TappedCallBack(GameObject go, InteractionManager.InteractionEventArgs e) {
        if (go == null) {
            Debug.Log("Received Tapped Event!!");
        }
    }
}

このスクリプトを空のGameObjectにアタッチしてシーンに配置すれば完成です。もちろんGazeで何かにフォーカスをあてているときはアクションしません。

SpatialMappingで取得したメッシュに対してAirtap

続いてSpatialMappingで取得した空間メッシュをAirTapしたときに反応するスクリプトです。

public class TestReceiver : MonoBehaviour {

    void Start() {
        InteractionManager.OnTapped += TappedCallBack;
    }

    private void TappedCallBack(GameObject go, InteractionManager.InteractionEventArgs e) {
        if (go != null && go.transform.parent.gameObject == SpatialMappingManager.Instance.gameObject) {
            Debug.Log("Mesh tapped");
        }
    }
}

このスクリプトをSpatialMapping.prefabにアタッチしてシーンに配置すれば完成です。

フォーカスロック

MRDesignLabではHold、Manipulation、Navigation中にGazeをオブジェクトから外してしまっても、フォーカスが外れないようにするフォーカスロックの仕組みが盛り込まれています。HoloToolkitを使う場合は、たとえばマニピュレーション中に操作中のGameObjectからGazeを外してしまうとマニピュレーションが終了しています。これを回避するためにはInputManager.Instance.PushModalInputHandler(gameObject)といったメソッドを使って操作対象を固定するよう実装する必要があったのですが、MRDesignLabを使う場合はそういったことを気にせずに自然な動作になるよう配慮されています。

フォーカス系イベント

ジェスチャーイベントより使用する頻度は低いかもしれませんが、FocusManagerにはColliderを持つGameObjectにフォーカスしたタイミング、およびフォーカスを外したタイミングでイベントが発火するようになっています。

public class FocusManager : Singleton<FocusManager> {

    public static event Action<GameObject, FocusArgs> OnFocusEnter;
    public static event Action<GameObject, FocusArgs> OnFocusExit

    ~以下略~

なお、フォーカスイベントはハンドラが見つかるまでGameObjectのヒエラルキーを親方向に上っていくイベントバブリングが実装されています。ジェスチャーはバブリングしません。

まとめ

MRDesignLabにおけるジェスチャー入力の取り扱いについてまとめました。HoloToolkitのInputManagerとは大きく実装が異なっているので、既存アプリにMRDesignLabのコンポーネントを適用する場合は必ず把握しておかなければいけない部分だと思います。

これからもMRDesignLabの調査結果をまとめていく予定で、今後はButton系コンポーネント→BoundingBox→入力ソース関連→ObjectCollectionといった順番でまとめていこうと思います。