[MR Dev Tips #14] UnityEngine.XR InputDevices を使って HoloLens 2 のタップを検出する

今回は UnityEngine.XR の InputDevices クラスを使って、HoloLens 2 の Tap を検出してみたいと思います。

# 検証環境
- Unity 2020.3
- MRTK v2.8.3

# UnityEngine.XR InputDevices を使って HoloLens 2 のタップを検出する

``InputDevices`` クラスは、XR入力サブシステム (XR Input Subsystem) 内のデバイスにアクセスするためのインターフェースです。``InputDevices`` クラスでは、以下3つの方法で入力デバイスへアクセスが可能です。

- デバイス特性 (Characteristics)
- ロール (Role)
- XRノード (XRNode)

今回は XRNode を用いた入力デバイスへのアクセスを行います。XRNode を用いた入力デバイスへのアクセスは ``InputDevices.GetDeviceAtXRNode`` メソッドを使用します。

```
public static XR.InputDevice GetDeviceAtXRNode (XR.XRNode node);
```

XRNode は 列挙型 (enum) で定義されています。

| XRNode | 説明 |
| --- | --- |
| CenterEye | ユーザーの目の瞳孔の中間地点 |
| GameController | コンソールスタイルのゲームコントローラーアプリケーションは複数のゲームコントローラーデバイスを持つことができます。|
| HardwareTracker | ハードウェアトラッキングデバイス。通常、ユーザーや物理的なアイテムに取り付けられます。複数のハードウェアトラッカーノードが可能です。|
| Head | XRシステムによって計算された、ユーザーの頭の中心点 |
| LeftEye | ユーザーの左目 |
| LeftHand | ユーザーの左手 |
| RightEye | ユーザーの右目 |
| RightHand | ユーザーの右手 |
| TrackingReference | Oculusカメラなどのトラッキング基準点。複数のトラッキング参照ノードが存在できます。|


```cs
// 右手のインスタンスを取得する場合
InputDevice device = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
// 左手のインスタンスを取得する場合
InputDevice device = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
```

デバイスが取得出来たら、次にデバイスの状態を取得します。デバイスの状態を取得する際は、``InputDevice.TryGetFeatureValue`` メソッドを使用します。``TryGetFeatureValue()`` は、機能の現在の値へのアクセスを試行して、以下の結果を返却します。

- 引数で指定された機能の値を正常に取得した場合は ``true``
- 現在のデバイスが指定された機能をサポートしていない場合、または、デバイスが無効な場合は ``false``

また、デバイスの機能の指定は、``CommonUsage`` クラスを用いて行います。調査をしていると、HoloLens 2 の タップ は ``CommonUsage.primaryButton`` にマッピングされているみたいです。タップ状態の有無は ``isTapping`` 変数に格納します。``isTapping`` が ``true`` の場合はタップしている状態、``false`` の場合はタップしていない状態を表します。

```cs
device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping);
```

これらを組み合わせて、(ショート)タップを検出するコードを作成することができます。

```Sample.cs
using UnityEngine;
using UnityEngine.XR;

public class Sample : MonoBehaviour
{
  /// <summary>
  /// タップ時間を記録するタップタイマー (右手/左手)
  /// </summary>
  private float[] _tappingTimer = { 0, 0 };

  void Update()
  {
    for (int i = 0; i < 2; i++)
    {
      // 右手/左手のデバイスインスタンスを取得する
      InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
      // PrimaryButton の現在の状況を取得して、tapping に格納する
      if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
      {
        // タップをしていない場合
        if (!isTapping)
        {
          // タップが1秒以内であるかどうかを判定
          if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
          {
            // タップを検出
            if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
            {
              // タップされた位置をログ出力
              Debug.Log(handPosition);
            }
          }
            // タップタイマーのリセット
            _tappingTimer[i] = 0;
        }
        // タップをしている場合
        else
        {
          // タップタイマーを更新
          _tappingTimer[i] += Time.deltaTime;
        }
      }
    }
  }
}
```

以下 GitHub に MS 公式のサンプルコードが用意されているので、興味のある方はこちらを試されるのが良いと思います。

https://github.com/microsoft/OpenXR-Unity-MixedReality-Samples

# Refs
https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.mixedreality.openxr?view=mixedreality-openxr-plugin-1.4

https://learn.microsoft.com/ja-jp/azure/spatial-anchors/tutorials/tutorial-new-unity-hololens-app?tabs=azure-portal

https://github.com/microsoft/OpenXR-Unity-MixedReality-Samples