UnityのInputSystemからQuest2のデバイスの取り扱い方を考える

はじめに

エンジニアの松原です。最近社内ではXRのハイスペックデバイスを使ったビジネス展開について営業や開発活動が活発になっております。以前に紹介された記事のように、Varjo XR-3には様々な可能性があり、社内では日々様々なエンジニアがこのデバイスに新たなユースケースの開拓にチャレンジしています。

Varjo XR-3以外にも、様々なデバイスを用いたビジネス検討は毎週のようにディスカッションされており、VRのみならず、AR/MR方面での技術検証やビジネス活用についての取り組みも活発化しています。

 

さて、これらのデバイスを用いてアプリケーションを作る際には、デバイスを動作させるためのソフトウェアライブラリであったり、サポートアプリケーション(SteamVRやOculus、上記のVarjo XR-3では別途Varjo Baseなどのアプリ)などが必要です。

ソフトウェアライブラリについては、VRの各プラットフォームで言えば統合パッケージであるOculus IntegrationSteamVR Pluginがイメージしやすく、2019年以前からUnityを使ってVRアプリケーションを制作したことがある方であれば、どちらかは使われたことがあると思います。

最近は各プラットフォームでOpenXR対応が進んでおり、統合パッケージを入れなくてもUnity公式が提供するXR Plugin ManagementをPackageManager経由でインストールし、セットアップすることでHMDとVRコントローラーの機能が利用できるようになっています。(ただし、パススルーやハンドトラッキングなどは2021年10月現在ではまだ対応に至っていない印象です。)

過去のOpenXRの動向などは以下の記事をお読みいただけるとイメージしやすいです。


一方、UnityではInputSystemという新しい外部入力デバイスを扱うライブラリが登場してしばらく経ちましたが、このInputSystemはただマウスやキーボード、ゲームパッドの入力操作の置き換わりというわけではなく、VRデバイスも扱えるように設計されており、前のシステムから根本的に異なっているようです。

本記事はそこに注目し、InputSystemを通してのVRデバイスの取り扱いについて触れ、手を動かす部分ではOculus Quest2を使って検証してみたいと思います。

 

VRデバイスとInputSystemとの関係について

前述のXR Plugin ManagementはXRデバイスとUnityの低レイヤーAPIとの橋渡しを行うためのライブラリとして存在しているため、実際にUnity上で使えるようにするにはコントローラーとして処理を実装する必要があります。
ある程度便利に組み込まれた状態で扱いたい場合はXR Interaction Toolkitのような、コントローラーとGUI間の処理を扱いやすく実装しているパッケージを導入して使うことが多いと思います。(XR Interaction Toolkitそのものの説明は今回割愛させていただきます。)

XR Interaction Toolkitのコードを追っていくと、Action-based~関連のクラスの実装にInputSystemの機能を使っており、HMDやVRコントローラーの特定デバイス向けの低レイヤーAPIを意識しないように設計されています。

XR Interaction Toolkitは今後Action-based(InputSystemのInputActionMap)の仕組みを利用することを掲げているので、InputSystemの理解を深めることは今後必要になってきそうです。

 

InputSystemからOculus Quest2のHMDの情報を取り出してみる

ここからは実際にInputSystemの機能を利用して、直接Quest2のHMDのデバイス検出やポジショントラッキングを行ったことについて書いています。

本記事で利用したUnityの環境などは以下の通りです。

  • Unity 2021.1.20f1
  • PackageManagerに追加したもの
    • XR Plugin Management 4.1.0
    • Oculus XR Plugin 1.10.0
    • Input System 1.1.1
  • Project SettingsのXR Plugin-ManagementでOculusを有効化

f:id:fb8r5jymw6fd:20211023174802p:plain

 

以下のコードが書かれたスクリプトを追加し、適当なGameObjectにこのスクリプトを追加します。

using UnityEngine;
using UnityEngine.InputSystem;

public class InputSystemDeviceDetector : MonoBehaviour
{
private void Awake()
{
InputSystem.onDeviceChange += OnDeviceChange;
}

private void OnApplicationQuit()
{
InputSystem.onDeviceChange -= OnDeviceChange;
}

private void OnDeviceChange(InputDevice inputDevice, InputDeviceChange inputDeviceChange)
{
switch (inputDeviceChange)
{
case InputDeviceChange.Added:
OnDeviceAdded(inputDevice);
break;
case InputDeviceChange.Removed:
OnDeviceRemoved(inputDevice);
break;
}
}

private void OnDeviceAdded(InputDevice inputDevice)
{
Debug.Log($"Device Added: {inputDevice.displayName}");
}

private void OnDeviceRemoved(InputDevice inputDevice)
{
Debug.Log($"Device Removed: {inputDevice.displayName}");
}
}

 

スクリプトを追加後、OculusQuest2をOculusLinkでPCに接続した状態で実行します。

実行時にいくつかログが返ってきます。Oculus Quest2となっているものがQuest2のHMDデバイスに相当します。Controllerとつくものは左右のコントローラになります。

f:id:fb8r5jymw6fd:20211023133924p:plain

 

さらにOnDeviceAdded()のメソッドに以下のように1行追加し、再度実行してみます。

...
private void
OnDeviceAdded(InputDevice inputDevice)
{
Debug.Log($"Device Added: {inputDevice.displayName}");
Debug.Log(inputDevice.description.ToJson()); // <== 追加する箇所
}
...

 

実行するとJson形式でいくつかログが返ってきます。

f:id:fb8r5jymw6fd:20211023142242p:plain

このJsonにはデバイスの概要が含まれており、capabilitiesの箇所にはデバイスの詳細が入っています。さらにcapabilitiesをJsonとして整理したものは以下の図になります。全部で300行ぐらいあるので、以降の内容は省略します。

f:id:fb8r5jymw6fd:20211023144047p:plain

 

HMD(デバイス名の上ではOculus Quest2)として認識されているデバイスのcapabilitiesの中身になります。characteristicsはInputDeviceCharacteristicsの値と一致しています(HeadMounted = 1, TrackedDevice = 32)

さらにinputFeaturesに含まれている各項目の一部はCommonUsagesと一致しており、デバイスの入力の機能で何が利用できるか記述されています。

この中にCenterEyePositionとCenterEyeRotationが含まれているので、ほぼHMDとして認識しているのは間違いないかと思います。

 

余談ですが、非VRデバイスの場合、capabilitiesが空文字になっていたり、以下のように上記のものとは別の構造になっています。(下の画像はPS4のコントローラを接続したいに取得したcapabilitiesになります)

f:id:fb8r5jymw6fd:20211023152006p:plain

 

Oculus Quest2のHMDデバイス検出時にヘッドトラッキングを行う処理を追加する

HMDデバイスを認識できるようになったので、デバイス検出時にヘッドトラッキングできるようにします。

以下のスクリプトを新しく追加します。

using UnityEngine;
using UnityEngine.InputSystem;
using CallbackContext = UnityEngine.InputSystem.InputAction.CallbackContext;

public class HMDController : MonoBehaviour
{
private readonly InputAction positionAction = new InputAction(binding: "<XRHMD>/centereyeposition");
private readonly InputAction rotationAction = new InputAction(binding: "<XRHMD>/centereyerotation");

public void Activate()
{
positionAction.Enable();
rotationAction.Enable();
positionAction.performed += OnPositionUpdate;
rotationAction.performed += OnRotationUpdate;
}

public void Deactivate()
{
positionAction.Disable();
rotationAction.Disable();
positionAction.performed -= OnPositionUpdate;
rotationAction.performed -= OnRotationUpdate;
}

private void OnPositionUpdate(CallbackContext context)
{
transform.position = context.action.ReadValue<Vector3>();
}

private void OnRotationUpdate(CallbackContext context)
{
transform.rotation = context.action.ReadValue<Quaternion>();
}
}

 

このスクリプトをカメラのコンポーネントを持っているGameObjectに追加します。(Main Cameraなどが良さそうです)

f:id:fb8r5jymw6fd:20211023172423p:plain

 

また、InputSystemDeviceDetector.csの中身も少し書き換えます。先ほどのスクリプトを参照できるよう、SerializeFieldアトリビュートを付けたローカル変数をコードの最初の方に書き加えます。

using UnityEngine;
using UnityEngine.InputSystem;

public class InputSystemDeviceDetector : MonoBehaviour
{
[SerializeField] // <== 追加する箇所
private HMDController hmdController; //

private void Awake()
...

 

OnDeviceAdded()やOnDeviceRemoved()のメソッドの中身を書き換え、ContainsControl()メソッドを追加します。

...
private void OnDeviceAdded(InputDevice inputDevice)
{
Debug.Log($"Device Added: {inputDevice.displayName}");
if (ContainsControl(inputDevice, "centereyeposition") && // <== 追加する箇所
ContainsControl(inputDevice, "centereyerotation")) //
{ //
hmdController.Activate(); //
} //
}

private void OnDeviceRemoved(InputDevice inputDevice)
{
Debug.Log($"Device Removed: {inputDevice.displayName}");
if (ContainsControl(inputDevice, "centereyeposition") && // <== 追加する箇所
ContainsControl(inputDevice, "centereyerotation")) //
{ //
hmdController.Deactivate(); //
} //
}

private bool ContainsControl(InputDevice device, string controlName) // <== 追加する箇所
{ //
return device.TryGetChildControl(controlName) != null; //
} //
}

 

書き換え終わったらHMDController.csのスクリプトを追加しているGameObjectをInspectorからアタッチします。

f:id:fb8r5jymw6fd:20211023173910p:plain

 

あとは適当にシーンを整えて実行するとヘッドトラッキングが行えるようになっていると思います。

f:id:fb8r5jymw6fd:20211023174138p:plain

 

なぜこのように実装できるのか

細かいコードの解説までは省きますが、Oculus XR Plugin(1.10.0)ではInputSystemのLayoutsの設計に従ってデバイスを利用できるように作られているようです。分かりやすく言うと、Oculusのデバイスはプラグインの設計によりゲームコントローラ等と同様にInputSystemから直接扱えるようになっています。

さらに、InputSystemを使った各入力の取得についてになりますが、認識したデバイスは複数のInputControlの集まりとしてみなされており、それぞれはボタンやジョイスティックなどの部品単位で定義されており、InputControlPathという概念で識別できるようになっています。

例えば、Quest2のCenterEyePositionのInputControlPathは以下のようになっています。

/OculusQuest2/centereyeposition

CenterEyePositionはVector3型の値を持つので、さらに下に3つのパスを持ちます。

/OculusQuest2/centereyeposition/x

/OculusQuest2/centereyeposition/y

/OculusQuest2/centereyeposition/z

Quaternion型の値を持つCenterEyeRotationはxyzに加え、wのパスを持つようです。

/OculusQuest2/centereyerotation/x

/OculusQuest2/centereyerotation/y

/OculusQuest2/centereyerotation/z

/OculusQuest2/centereyerotation/w

これらのパスの概念はOpen Sound Controlのアドレスがイメージに近いかもしれません。

認識したデバイスが有効化されている場合、デバイスは指定されたパスに対して値を送り続けるようです。読み取り側はこの情報を利用し、InputActionのbindingに取得したい入力部品のパスを指定します。そしてEnable()を呼び出すことにより、指定したパスの値変更の監視を行い、センサーやジョイスティックの値を取得します。

このように、InputSystemではデバイスを個々の部品に分けて定義することができ、VR機器でもInputSystemの仕組みに則ることでゲームコントローラーと同じように扱うことができるようになっています。

VRデバイスがInputSystemで扱えるようになった理由

ここからは憶測になりますが、Unityが当初掲げていたXRプラグインフレームワークの内部設計を整理している最中に、非VRデバイスとの入力を共通化する設計が追加されたのかもしれません。

その共通化のためにInputSystemが候補に挙がり、VRデバイスもInputSystemを経由して利用できるように変更があったのではないかと考えています。

以下は2021年10月現在のOculus XR PluginとInputSystemベースのシステムのフレームワーク間のイメージになります。

f:id:fb8r5jymw6fd:20211023185134p:plain

Unity XR Tech Stackの図からだいぶ離れてきているイメージがあるので、情報のアップデートを期待したいところです。 

最後に

2021年10月現在のUnityを使ったVR/XR関連の開発はかなり流動的で、以前のVRアプリの開発手法とはだいぶ変わってきています。その時点での流行り廃りもありますので、定期的に知識のアップデートは今後も必要になってきそうです。