おつかれさまです。皆様VR楽しんでますか?
今回は XR Interaction ToolKit を使ってみるという内容になります。
現在UnityではXR開発を効率化するための様々なツールがUnity XR Tech Stackという枠組みで用意されています。

その中には多岐に渡るXRデバイスを抽象化するXR Plugin Framework、ARデバイスの主要な機能を抽象化するARFoundation、ARアプリの確認を仮想空間で行うMARSとXR開発の難所を解決できるものが揃っています。
今回取り上げるXR Interaction ToolKitはUnity XR Tech Stackの中で移動・オブジェクト操作といった入力部分をサポートしています。
なぜXR Interaction ToolKitが作られたかという話なのですが、それには以下のような背景が考えられます。
たとえばVRの開発で、Oculus QuestとHTC Vive向けのアプリを作りたい場合があるとします。
従来であればその開発にはOculus Integration、Steam VR PluginといったSDKが選択される場合がほとんどであったと思います。しかし、これらはOculus・SteamVRデバイスにそれぞれに対応したものであり、別の環境では動作することができません。そのため、今回のような場合は何かしらの方法で2つのSDKをラップし、環境によって実際に使うSDKを切り替えるという手法を取る必要がありました。

XR Interaction ToolKitはそのような問題を解決するべく様々なXRデバイスの入力を抽象化し、開発者はビルドターゲットを変えるだけでアプリが動くという状況を用意します。ちなみにこのXRデバイスの抽象化をコンセプトにしたサードパーティ製のSDKは既にいくつか存在しており、例としてMicrosoftのMRTKやSysdiaのVRTKがあげられます。特にMRTKについては過去にギャップロで何度か取り扱っているのでぜひご覧ください。
Mixed Reality Toolkit v2
https://gaprot.jp/2020/02/06/mixed-reality-toolkit-v2/
最近のOculus Quest開発環境について
https://gaprot.jp/2020/11/12/oculus-quest-develop/
検証
シンプルなシーンを作ってXR Interaction ToolKitの使用感を試してみたいと思います。
前項の通り、主な役割は入力の抽象化になるので以下の要素を調査していきたいと思います。
- コントローラー入力
- インタラクション
- 移動
- 回転
- UI
ちなみにXR Interaction Toolkitの公式サンプルはこちらにあります。
Unity-Technologies/XR-Interaction-Toolkit-Examples
https://github.com/Unity-Technologies/XR-Interaction-Toolkit-Examples
コントローラー入力
XR Interaction ToolKitではXR Controllerというコンポーネントを介して、コントローラーの入力を専用のイベントに抽象化して使用します。
イベントには以下の種類があり、これらをコントローラーのボタンのどれかとマッピングして発火させることになります。
イベント | 内容 |
Select | オブジェクトを選択する |
Activate | オブジェクトの選択中に何かアクションを起こす |
UI | UIを選択する |
Haptics Device | デバイスを振動させる (※Action-Basedのみ設定可能) |
Rotate Anchor | 選択中のオブジェクトをY軸で回転させる |
Translate Anchor | 選択中のオブジェクトをY軸で移動させる |
マッピングにはデバイス入力と直に対応させる「Device-Based」とUnityのInput Systemを経由してアクションと対応させる「Action-Based」の2種類の方式があります。
Device-Based
Device-Basedでは入力の受け取りにInputDeviceが使われています。
InputDeviceはシンプルな入力管理クラスで、様々なXRデバイスのボタンと仮想ボタンのマッピングが既に決め打ちで組み込まれています。開発者はTryGetFeatureValueというAPIで使いたい仮想ボタンの状態をチェックしていれば入力を受け取れるという仕組みになっています。

上は実際のDevice-Basedのコンポーネントです。イベントと仮想ボタンの紐付けは赤枠のドロップダウンで選択して行います。
InputDeviceクラスの詳細は以下のドキュメントをご覧ください。仮想ボタンと各種デバイスのマッピング表もここに記載されています。
Unity の XR 入力
https://docs.unity3d.com/ja/2019.4/Manual/xr_input.html
Action-Based
Action-Basedでは入力の受け取りにInput Systemという比較的新しいUnityのシステムが使われています。Input Systemを簡単に説明するとデバイスの入力をアクションという概念に変換し、そのアクションをコードから参照するようにすることで柔軟な入力の切り替えを実現する仕組みです。変換に使われるテーブルは開発者が好きに変更できるため、コードの変更なしに入力を切り替えられる利点があります。

上は実際のAction-Basedのコンポーネントです。イベントとアクションの紐付けは別で用意したInputActionファイルを使って行います。InputActionファイルはInput Systemの仕組みを使って作成します。Input Systemの詳細については過去にギャップロで取り上げた記事があるのでぜひご覧ください。
Input SystemでFPSゲーム風サンプル
https://gaprot.jp/2020/09/23/input-system-fps/
XR Interaction ToolkitにはInputActionのサンプルが含まれているので、それを参考にするのも良いと思います。サンプルはPackage Manager経由で入手することができます。


また、ファイルを使わずにインスペクターにアクションの内容を直接記載する方法もあります。その場合、インスペクターは以下のような状態になります。

XR Interaction Manager
インタラクションを管理するマネージャーです。
シーンに1つだけ存在し、以下の2つのコンポーネントを結びつける役割を持っています。
- Interactor(影響を与える側 例:コントローラー)
- Interactable(影響を与えられる側 例:3Dオブジェクト・UI)
Interactor
レイキャストやコライダーなど衝突を検知できる仕組みとセットで使われ、検知したInteractableオブジェクトにイベントを通知するコンポーネントです。
デフォルトでは以下の3つのInteractorが用意されています。
XR Ray Interactor
レイを飛ばすタイプのInteractorです。
レイキャストを飛ばして、到達したInteractableに影響を与えます。

XR Direct Interactor
直接触れるタイプのInteractorです。
コライダーに接触したInteractableに影響を与えます。

XR Socket Interactor
空間に設置し、一定範囲内に入ったInteractableを吸着するInteractorです。

Interactable
Interactorからイベントを受け取り、登録されている処理を実行するコンポーネントです。
デフォルトでは以下の2つのInteractableが用意されています。
XR Grab Interactable
掴めるタイプのInteractableです。物を掴み、物を離して投げることができます。
上のInteractorの例ではすべてXRGrab Interactableが使われています。
XR Simple Interactable
名前の通りシンプルなInteractableです。
掴んで投げるなど特別な挙動は持たず、開発者に登録されたコールバックの発火のみを行います。
移動
テレポートと連続の2種類が用意されています。
移動はXR Plugin Frameworkで用意されているXRRigの座標を動かすことで移動を実現します。
XRRigは以下のようにカメラの親オブジェクトになっています。

テレポート
指定した座標に即時移動する方式です。InteractorとInteractableの仕組みを使って行われます。
Interactableは移動先に想定する床に設定し、そこにコントローラーなどのInteractorでイベントを起こすという流れです。
Interactableにはアンカーとエリアの2種類があり、アンカーは移動可能な局所的なポイント、エリアはPlaneなど広い空間に設定します。

連続
スティックを倒している間、連続的に移動する方式です。
Interactor/Interactableは必要なく、コントローラーの入力を検知して直接XRRigを動かします。

回転
回転はスナップと連続の2種類が用意されています。
移動と同様に、カメラの親オブジェクトのXRRigを回転させることで移動を実現します。
回転にはインタラクションな要素がないので、どちらの方式でもコントローラーの入力を直接検知してXRRigを動かす仕組みになっています。
スナップ
左右に指定角度分回転させる方式です。

連続
スティックを倒している間、連続的に回転する方式です。

UI
uGUIの設定をいくつか変更することで、XR Interaction Toolkitに対応させたUIを作ることができます。
- CanvasにTracked Device Graphic Raycasterを付ける。
- EventSystemにXRUI Input Moduleを付ける。

利用例
利用例として前回Oculus Integrationで作った雪合戦ゲームをXR Integraction Toolkitに移植してみました。
このゲームの詳細は前回の記事をご覧ください。
OculusQuestでのVRゲーム開発について
https://gaprot.jp/2021/03/11/unity-cloud-build/
開発環境
今回はUnityのバージョンを2020.2に上げています。
Oculus QuestとHTC Viveに両対応したアプリを作りたいと考えているため、2020.2から使用できるOpenXR Pluginを使いたいためです。これはOpenXR対応デバイスとXR Plugin Frameworkを結びつけるプラグインになります。

先日(2021/02/25)にSteamVRが1.16にバージョンアップされてOpenXRに対応するようになったため、OpenXR Plugin を使うことでSteamVR対応デバイスを動かせるようになりました。

SteamVR Update Brings Full Support for OpenXR 1.0, A Huge Step for the Open Standard
https://www.roadtovr.com/steamvr-update-brings-full-support-openxr-1-0/
今回はUnity公式のツールだけで完結するので、PackageManagerで「Unity Registry」から必要なものをインストールしていきます。
- XR Plugin Management 4.0.1
- Oculus XR Plugin 1.6.1
- OpenXR Plugin 1.0.2
- XR Interaction Toolkit 1.0.0-pre.2
すべてインストールできたら準備完了です。
見つからない場合はPackageManagerのPreview表示が有効になっているか確認してみてください。
コントローラー設定
コントローラーはAction-Basedで以下のように設定しました。今回は分かりやすいようにインスペクターに直接アクションの内容を書く方式にしています。また、設定内容はゲーム内の以下の操作を想定したものになっています。
左手
・スティックを倒して離したとき、レイが当たっている地点にテレポート移動する。
右手
・トリガーを引いたとき、レイが当たっているUIを操作する。

結果
移植したアプリをOculus QuestとHTC Vive向けにそれぞれビルドして動かしてみました。
見比べてみると挙動が同じになっていることが分かると思います。
Oculus Quest
HTC Vive
躓いたポイント
このゲームはVRの基本的な操作ばかりで作られているので、上で紹介したXR Interaction Toolkitをそのまま使えば作ることができます。しかし、その中でも躓いたポイントがいくつかあったのでまとめました。
Viveコントローラーの入力が受け取れない
最初OpenXR PluginからViveコントローラーの入力が受け取れず困りました。
「XR Plugin Management > OpenXR > Features」でViveコントローラーにチェックが入っていなかったのが原因でした。

Interactableを動的に生成して掴む
このゲームでは積もった雪に触れてトリガーを引いたとき、雪玉を生成して投げることができる要素があります。

雪玉はInteractableな要素でありますが、トリガーを引くまでシーンには存在しません。このケースはデフォルトの仕組みでは実現できず、自前でコードを書く必要がありました。
この挙動のためにやらなくてはならないことは以下の3つです。
- コントローラー(Interactor)のトリガーを引いたとき、雪玉(Interactable)を生成する。
- コントローラーと生成直後の雪玉を紐付けて選択状態にする。
- トリガーを離したときに選択状態を解除する。
これを実現するために以下のようにコントローラーと雪玉に変更を加えました。

using UnityEngine.XR.Interaction.Toolkit;
public class MyXRGrabInteractable : XRGrabInteractable
{
/// <summary>
/// Interactableを強制的に掴む
/// </summary>
/// <param name="interactor">Interactor</param>
public void ForceGrab(XRBaseInteractor interactor)
{
var args = new SelectEnterEventArgs {interactable = this, interactor = interactor};
base.OnSelectEntering(args);
}
/// <summary>
/// Interactableを強制的に離す
/// </summary>
/// <param name="interactor">Interactor</param>
public void ForceRelease(XRBaseInteractor interactor)
{
var args = new SelectExitEventArgs {interactable = this, interactor = interactor};
base.OnSelectExiting(args);
}
}
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.Interaction.Toolkit;
/// <summary>
/// 雪を掴んで離す処理
/// </summary>
public class MyHand : MonoBehaviour
{
/// <summary>
/// Interactor
/// </summary>
private XRBaseInteractor _interactor;
/// <summary>
/// Interactable
/// </summary>
private MyXRGrabInteractable _interactable;
/// <summary>
/// Selectイベントに入ったときのアクション
/// </summary>
[SerializeField]
private InputAction _selectEnterAct;
/// <summary>
/// Selectイベントから抜けるときのアクション
/// </summary>
[SerializeField]
private InputAction _selectExitAct;
/// <summary>
/// 雪玉Prefab
/// </summary>
[SerializeField]
private GameObject _snowBallPrefab;
/// <summary>
/// Awake
/// </summary>
private void Awake()
{
_interactor = GetComponent<XRBaseInteractor>();
}
/// <summary>
/// OnEnable
/// </summary>
private void OnEnable()
{
// アクションの有効化+コールバックの登録
_selectEnterAct.Enable();
_selectExitAct.Enable();
_selectEnterAct.performed += OnSelectEntered;
_selectExitAct.performed += OnSelectExited;
}
/// <summary>
/// OnDisable
/// </summary>
private void OnDisable()
{
// アクションの無効化+コールバックの解除
_selectEnterAct.Disable();
_selectExitAct.Disable();
_selectEnterAct.performed -= OnSelectEntered;
_selectExitAct.performed -= OnSelectExited;
}
/// <summary>
/// ボタン押し込み
/// </summary>
private void OnSelectEntered(InputAction.CallbackContext context)
{
// 雪玉の生成
var go = Instantiate(_snowBallPrefab, transform.position, Quaternion.identity);
_interactable = go.GetComponent<MyXRGrabInteractable>();
// 雪玉へ掴むことを通知
_interactable.ForceGrab(_interactor);
}
/// <summary>
/// ボタン離す
/// </summary>
private void OnSelectExited(InputAction.CallbackContext context)
{
if (_interactable != null)
{
// 雪玉へ離すことを通知
_interactable.ForceRelease(_interactor);
}
}
}
Oculus Quest (OpenXR)
UnityにはOculus用のOculus XR Pluginがあるので、それを使えばOculus Questでアプリを動かすことができます。しかしその一方でOculus QuestはOpenXRに対応しているので、OpenXR Pluginを使っても同じようにアプリを動かすことができます。
Oculus OpenXR Mobile SDK
https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/
上のリンクの通り、Oculus QuestでOpenXR対応のアプリを動かすには以下の手順が必要です。
- AndroidManifestに以下のインテントフィルターを追加
<intent-filter>
<action android:name="android.intent.action.MAIN" /> <category android:name="com.oculus.intent.category.VR" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
- Oculus OpenXR Mobile SDKからlibopenxr_loaderを抜き出して以下のように配置する。

- Plugin Managementの設定を切り替える


まとめ
今回はVRに着目し、XR Interaction Toolkitを試してみました。
見てきたようにVRを作る上で必要な機能が揃っており、それに加えてXRプラグインに対応しているデバイスではコードの変更なく動かすことができるので非常に生産性の高いツールだと感じました。
OpenXR Pluginと合わせて使えば既に出ている主要デバイスのほとんどに対応でき、今後出てくる未知のデバイスであってもそれがOpenXRに対応さえしていれば動くようになっている仕組みも面白いです。
XRはそもそも基本的な挙動を動かすのが大変なので、こうしたツールを使うことでコンテンツの内容に注力できる時間が増えるのは本当に嬉しいです。