WinMRで始めるMixed Reality Toolkit Unity - モーションコントローラの基本的な実装

# WinMR用に拡張されたMixed Reality Toolkit- Unity
Mixed Reality Toolkit-Unity(以下MTRK)はWinMR系の機能も強化されています。その１つとしてモーションコントローラの操作についても対応しているので今回はその使い方について紹介したいと思います。


## 開発環境などなど
今回は以下の環境を用意。なお、MTRKについてはモーションコントローラ用のサンプルがあります。値を一通り採取できるようになっていますのでかなり参考になると思います。今回はこのサンプルの中からモーションコントローラの検出と値取得部分の仕組みについてご紹介します。作成時点ではPreleaseのMRTKを利用していますが、今であれば正式版（V1.2017.2.0）の方がいいと思います。

* Visual Studio 2017 15.2
* Unity 2017.2.0f3
* [MixedRealityToolkit-Unity v1.Dev.2017.2.1](https://github.com/Microsoft/MixedRealityToolkit-Unity/releases/tag/v1.Dev.2017.2.1)

## モーションコントローラの値を利用した実装
では、そろそろモーションコントローラの取得値をアプリケーションで使うために一番わかりやすいMTRKのサンプル「MotionControllerTest」をベースに説明します。

### WinMR用のMRTKとサンプルの導入
MRTKとそのサンプルについては全て[github](https://github.com/Microsoft/MixedRealityToolkit-Unity/releases/tag/v1.2017.2.0)でUnityパッケージ化されています。ですので、以下の手順で導入は簡単にできます。

1. [MixedRealityToolkit-Unity](https://github.com/Microsoft/MixedRealityToolkit-Unity/releases/tag/v1.2017.2.0)から以下の2つのUnityパッケージをダウンロードする。<br/>・HoloToolkit-Unity-v1.Dev.2017.2.1.unitypackage <br/>・HoloToolkit-Unity-Examples-v1.Dev.2017.2.1.unitypackage 
2. 2. Unityの起動
3. 新規プロジェクトの作成
4. [Asset]-[Import Package]-[Custom Package]を選択し以下の順序でパッケージのインポートを行う（対象は全て）。<br/>・HoloToolkit-Unity-v1.Dev.2017.2.1.unitypackage <br/>・HoloToolkit-Unity-Examples-v1.Dev.2017.2.1.unitypackage 
5. [File]-[Open Scene]で「MotionControllerText」を開く

### MotionContollrtTestの構成
「MotionContollrtTest」を開くとHierarchyに以下のオブジェクトが構成されます。
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/fd9dc83b-6948-da58-7ec6-cb6e4c66e507.png)

このうち「MixedRealityCameraParent」あたりはMRTKで新たに使うことができるWinMR用の便利なコンポーネントがセット済みのPrefabになります。

|オブジェクト名|説明|
|:-:|:--|
|MixedRealityCameraParent |新MRTKで追加されたWinMR用のカメラPrefab。MixedRealityCameraとMotionContoller、Boundaryを要素を持ちます。またコンポーネントとしてモーションコントローラの親指スティックによるテレポートやカメラの向きを変更するコンポーネント「MixedRealityTeleport」が含まれています。|
|MixedRealityCamera|いわゆるカメラです。|
|MotionContollers|アプリケーション内で表示するモーションコントローラのモデル定義を行うためのコンポーネント「MotionContollerVisualizer」があります。|
|Boundary|BoundaryはWinMRの境界表示用のコンポーネントです。|

それ以外のものは旧HoloToolkit-Unityでもおなじみのよく使っていたコンポーネントになります。

|オブジェクト名|説明|
|:-:|:--|
|Directional Light|いつものライトです。|
|DefaultCursor|いつものカーソルです。|
|Managers|InputManagerやEventSystem等MRTKでイベントを拾うのに必要なコンポーネントが含まれます。|
|DebugEventLog|デバッグの情報を出力するオブジェクトです。|
|DebugInfoDisplayLeft|左手用のモーションコントローラの値を表示するためのオブジェクトを定義しています。|
|DebugInfoDisplayRight|右手用のモーションコントローラの値を表示するためのオブジェクトを定義しています。|
|NoticePlate|このサンプルの注意事項を表示するパネルです。|
|Cube|普通のCubeです。タップやモーションコントローラのボタン押下でサイズが徐々に大きくなります。|

### モーションコントローラに必要なコンポーネント
大きくは２つあります。１つは自身の移動を楽にするためのテレポート機能を実現するためのコンポーネント「MixedRealityTeleport」と、モーションコントローラのモデル定義を行うためのコンポーネント「MotionContollerVisualizer」です。

####  MixedRealityTeleportコンポーネント

![image.png](https://qiita-image-store.s3.amazonaws.com/0/152548/c988ccd7-60cc-f049-3e29-dd1926781e21.png)

WinMRの例の部屋と同じ操作方法で移動時の補助（テレポートと向きの変更）を可能にするコンポーネントです。作るアプリケーションによってはこの設定が不要になる場合はチェックを外すことで本来のスティック操作が可能な形でも値を利用することができます。

|プロパティ名|設定内容|設定値|
|:-:|:--|:--|
|Left Thumbstice X|プレーヤでのデバッグ時にモーションコントローラの左親指スティックのエミュレートを行うAxisの名前を設定する。デフォルト値のは現在のMRTKではは未設定です。|CONTROLLER_LEFT_STICK_HORIZONTAL|
|Left Thumbstice Y|プレーヤでのデバッグ時にモーションコントローラの左親指スティックのエミュレートを行うAxisの名前を設定する。デフォルト値のは現在のMRTKではは未設定です。|CONTROLLER_LEFT_STICK_VERTICAL|
|Right Thumbstice X|プレーヤでのデバッグ時にモーションコントローラの右親指スティックのエミュレートを行うAxisの名前を設定する。デフォルト値のは現在のMRTKではは未設定です。|CONTROLLER_RIGHT_STICK_HORIZONTAL|
|Right Thumbstice Y|プレーヤでのデバッグ時にモーションコントローラの右親指スティックのエミュレートを行うAxisの名前を設定する。デフォルト値のは現在のMRTKではは未設定です。|CONTROLLER_RIGHT_STICK_VERTICAL|
|Enable Teleport|親指スティックの操作によるテレポートの有効/無効を設定します。|<ul><li>True</li><li>False</li></ul> | 
|Enable Rotation|親指スティックの操作によるカメラ回転の有効/無効を設定します。| <ul><li>True</li><li>False</li></ul>|
|Enable Strafe|親指スティックの操作によるバック操作の有効/無効を設定します。 |<ul><li>True</li><li>False</li></ul> |
|Rotation Size| テレポートで一度の操作で回転する角度を設定します。| 45| 
|Strafe Amount| テレポートで一度にバックする移動量を設定します。|0.5 | 
|Teleport Marker| 移動先の位置を示すポインターとなるモデルを指定します。| こういうのです<br/>![image](https://qiita-image-store.s3.amazonaws.com/0/152548/a120364c-70d3-912c-33f8-98ec5afae3e6.png)|

####  MotionContollerVisualizerコンポーネント

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/383ea652-d7e5-662d-46db-b4d4666549ab.png)

アプリケーション内のモーションコントローラのモデルを設定します。例えば、これを豆腐とかほかのものに変更する場合はこのコンポーネントにPrefabをセットして上書きすれば好きなモデルに変更できます。設定がない場合は標準のモーションコントローラのモデルが使用されます。

|オブジェクト名|説明|
|:-:|:--|
|Animate Controller Model |モーションコントローラのモデルにアニメーションの有効/無効を設定します。|
|Left Controller Override|左手のモーションコントローラのモデルを設定します。設定がない場合は標準のものが利用されます。|
|Right Controller Override|右手のモーションコントローラのモデルを設定します。設定がない場合は標準んのものが利用されます。|
|Touchoad Touched Override|タッチパッドをタッチ中にでるマーカーのモデルを設定します。標準は白い小さな円がパッド上に表示されます。|
|GLTF Material|Touchoad Touched Overrideを使用しない場合、標準のタッチパッドが使用されます。このプロパティは標準マーカーのマテリアルを設定します。|

### モーションコントローラの操作値の取得方法

そろそろ本題、モーションコントローラの操作値に関する実装について説明します。
サンプルのコンポーネントとしては「DebugPanelControllerInfo」になります。このコンポーネントを理解すれば一通り取得方法がわかります。

#### Unity Editor上の開発注意点
基本的にモーションコントローラの利用はUWPアプリを前提にしています（当たり前ですが。。。）。このため、MTRKでのモーションコントローラ周りの制御は以下のifディレクティブが使用されます。

```csharp
#if UNITY_WSA && UNITY_2017_2_OR_NEWER
...
#endif
```
つまりそのままだと、Visual Studio上で文法チェック等がきかないので、一度Unityで「C# Project」にチェックを入れた状態でUWP用にビルドしたあと開発する形が正しいやり方になると思います。ただ、実際のところは面倒なので、UnityからエディタとしてVisual Studioを開いた後に以下の手順で強引に定数追加しました。

1.開いているプロジェクトをアンロードする
2.アンロードしたプロジェクトを選択し、右クリックメニューで[編集]を選択
3.DefineConstantsタグを探して、「UNITY_WSA;」を追加![image](https://qiita-image-store.s3.amazonaws.com/0/152548/710bb88f-1b24-e64f-b53c-544f775d2dc9.png)
4.プロジェクトを再読み込み

これでいちいちビルドしなくても開発は可能になります（VSプロジェクトがUnityによって更新されるともとに戻りますが。。。）。

#### モーションコントローラの情報取得
モーションコントローラについてはアプリケーション内で検出されていれば、InputSourceの１つとして情報を取り出すことが可能です。入力系のデバイス（マウスや、ゲームパッド、手など）を管理するクラスはUnityのXR系の機能で「InteractionManager」として定義されています。この管理クラスはいくつかのイベントを持っています。

```csharp
InteractionManager.InteractionSourceDetected += InteractionManager_InteractionSourceDetected;

InteractionManager.InteractionSourceLost += InteractionManager_InteractionSourceLost;
InteractionManager.InteractionSourceUpdated += InteractionManager_InteractionSourceUpdated;
```            

|イベント名|説明|
|:-:|:--|
|InteractionSourceDetected|入力系のソースが検出されたら発生する|
|InteractionSourceUpdated|入力系のソースの状態が更新されたら発生する|
|InteractionSourceLost|入力系のソースが消失（センサー検知外等）したら発生する|

基本的にはInteractionSourceUpdated内で値を取得します。このイベントは入力ソースの「何か」値が変更したら発生するのでほぼリアルタイムで数値取得できます。
MRTKでは、以下の手順で処理を行っています。

1.InteractionSourceDetectedイベントで入力ソースの種類を判定。モーションコントローラが検出された場合はIDを記録する。
2.InteractionSourceUpdatedイベントでは1.で検出したIDと照合しモーションコントローラの情報を取得する。
3.InteractionSourceLostイベントでモーションコントローラが消失した場合はIDを履歴から削除する。

##### InteractionSourceDetectedイベント
このイベントは入力ソースが検出された場合に発生します。主な処理としては入力ソースの判定等を行います。引数の「InteractionSourceDetectedEventArgs」クラスでその情報を取得が可能です。

```csharp
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
private void InteractionManager_InteractionSourceDetected(InteractionSourceDetectedEventArgs obj)
{
        //入力ソースの種類を取得
        InteractionSourceKind kind = obj.state.source.kind;
        //検出時の入力ソースのID（消失するまで同じID)
        var id = obj.state.source.id
}
```

入力ソースは以下の種類を判別可能です。

|列挙値|説明|
|:-:|:--|
|Other|その他何か入力ソース|
|Hand|ユーザの手（いわゆるジェスチャー)|
|Voice|音声|
|Controller|コントローラ（モーションコントローラはこれに属します）|

####  InteractionSourceUpdatedイベント
モーションコントローラの値を取得する部分になります。モーションコントローラは「[WinMRで始めるMixed Reality Toolkit Unity - モーションコントローラ使ったアプリ開発のTips](https://qiita.com/miyaura/items/7cd0f14b9b4baa20682f)」に説明した通り、色々値がとれるのですがそれぞれ取得方法が異なります。大きくは２つあり「1.モーションコントローラ本体に関する値」と「2.モーションコントローラ付属のボタンなどの入力値」です。それぞれ取り方が違うので紹介します。
まず、最初に入力ソースの取得については、このイベントの引数から取得可能です。取得した入力ソースがコントローラタイプの場合に各種値を取得できます。この際モーションコントローラ本体に関する以下の情報はプロパティではなくメソッドを呼び出して取得を行います。

* モーションコントローラの位置
* モーションコントローラの向き
* ポインタの位置
* ポインタの向き

```csharp
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
private void InteractionManager_InteractionSourceUpdated(InteractionSourceUpdatedEventArgs obj)
{
    var state = obj.state;
    if (state.source.kind == InteractionSourceKind.Controller)
    {

        Vector3 PointerPosition;
        Quaternion PointerRotation;
        Vector3 GripPosition;
        Quaternion GripRotation;
        //コントローラのポインタの情報を取得
        state.sourcePose.TryGetPosition(out PointerPosition, InteractionSourceNode.Pointer);
        state.sourcePose.TryGetRotation(out PointerRotation, InteractionSourceNode.Pointer);
        //コントローラ本体の情報を取得
        state.sourcePose.TryGetPosition(out GripPosition, InteractionSourceNode.Grip);
        state.sourcePose.TryGetRotation(out GripRotation, InteractionSourceNode.Grip);

        //グリップ上にあるボタンが押されているか
        bool Grasped = state.grasped;
        //メニューボタンが押下されているか
        bool MenuPressed = state.menuPressed;
        //Selectボタン（トリガー状のボタン）が押下されているか
        bool SelectPressed = state.selectPressed;
        //Selectボタンがどの程度押されているか(0～1)
        float SelectPressedAmount = state.selectPressedAmount;
        //親指スティックが押されているか
        bool ThumbstickPressed = state.thumbstickPressed;
        //親指スティックのXY座標(-1～1)
        Vector3 ThumbstickPosition = state.thumbstickPosition;
        //タッチパッドが押されているか
        bool TouchpadPressed = state.touchpadPressed;
        //タッチパッドに触れているか
        bool TouchpadTouched = state.touchpadTouched;
        //タッチパッドのXY座標(-1～1)
        Vector3 TouchpadPosition = state.touchpadPosition;
    }
}
```

####  InteractionSourceLostイベント
入力ソースが消失した際に発生するイベントです。主に後処理を実施する際に利用します。MRTKではDetectイベントで検出する際に入力ソースのIDをキャッシュするので後始末としてIDをキャッシュから削除する処理が実装されています。

### ボタン押下に対してどう対処するのか。
IInputClickHandlerインターフェースを実装する方法で各種ボタン押下時の処理を実行することができます。どのボタンが押下されているかはイベントの引数にある入力ソースから判別可能です。

## 最後に
上記の通り、比較的容易に情報を取得することが可能です。一応ボタン押下イベントも処理できます。一方でモーションコントローラの各ボタンや情報を制御するとなると意外と面倒です。現在この辺りを楽にするためのサンプルを構築中です。各ボタンでの操作で押下イベントが発火するような管理部品です。後日、Githubへの公開とそのあたりの説明も併せてしたいと思います。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">自分初WinMR版のモーションコントローラ使った豆腐アプリができた。。。せっかくなのでレーザーポインターも出せるようにして視認性をあげてみた。明日の用意はこれでおけー。<a href="https://twitter.com/hashtag/WinMR?src=hash&amp;ref_src=twsrc%5Etfw">#WinMR</a> <a href="https://twitter.com/hashtag/MTRK?src=hash&amp;ref_src=twsrc%5Etfw">#MTRK</a> <a href="https://t.co/XJrYIVFzJR">pic.twitter.com/XJrYIVFzJR</a></p>&mdash; takabrz (@takabrz1) <a href="https://twitter.com/takabrz1/status/926539037367603200?ref_src=twsrc%5Etfw">2017年11月3日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>