XR Interaction Toolkitをマウス入力に対応させる

XR Interaction ToolkitはUnity公式が提供するパッケージの一つです。その中には、XR向けのインタラクションを実装する為に必要なコンポーネントが一通り揃っています。

docs.unity3d.com

私が認識している限り、今までのUnityのインタラクション事情は、UIに対してはuGUIという強力な仕組みが用意されていたものの、3Dオブジェクトに対してのインタラクションは基本的な機能しか提供されていませんでした。それだけ各アプリケーションにおいての事情が異なるという事もあるのでしょうが、このためにそれぞれの開発者が独自にEventSystemを拡張したり、Raycastした結果を捏ね繰り回すコードを書いてやる必要がありました。(少なくとも私たちは書いていました)

そんな中登場したXR Interaction Toolkitは、触れ込みこそXR向けですが、内部に3Dオブジェクトに対するインタラクションを行うための一連の仕組みを持っています。これはXRデバイスに依存しないレベルで抽象化されており、非XRアプリでも使用することが十分可能です。しかも、以下のような素直に実装すると意外と面倒な機能も最初から持っています。

  • ワールド空間に存在するUIと競合しない
  • 複数個のInteractorが同時に触っても状態管理が破綻しない

また、XRアプリを非XRデバイスに対応したいとなった時も、インタラクションの仕組み自体を改修する必要がないというのは大きな強みになります(当然、別の操作体系で同じ体験を提供するための改修は必要ですが)。

ただし、現時点では非XRデバイスは残念ながら公式サポートされておらず、インポートしてすぐに非XRアプリに組み込む事はできません。パッケージ内にはInputSystemの入力をInteractorが使う形に変換するControllerというクラスが用意されているのですが、このクラスが現時点ではXRの各種トラッキングデバイス向けのものしか用意されていません。

しかし、目的に合わせたControllerを自作することは、実は非常に容易です。

そこで、今回はマウスを使ったControllerを例として実装してみました。

環境

  • Unity 2022.3.19f1
  • XR Interaction Toolkit 2.5.2

コードサンプル

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;

namespace MyApp.Interaction
{
    public class MouseController : XRBaseController
    {
        [SerializeField] private Camera controllerCamera;

        [SerializeField] private InputActionProperty mousePosition;
        [SerializeField] private InputActionProperty leftClick;

        private Transform controllerCameraTransform;
        private Transform parentTransform;

        private void Start()
        {
            if (controllerCamera == null)
            {
                Debug.LogWarning("CameraBaseController: No camera assigned. Disabling.");
                enabled = false;
            }

            controllerCameraTransform = controllerCamera.transform;
            parentTransform = transform.parent;
        }


        protected override void UpdateTrackingInput(XRControllerState controllerState)
        {
            base.UpdateTrackingInput(controllerState);

            if (controllerState == null)
            {
                return;
            }

            controllerState.isTracked = mousePosition.action.enabled;

            var screenPos = mousePosition.action.ReadValue<Vector2>();
            var origin =
                controllerCamera.ScreenToWorldPoint(new Vector3(
                    screenPos.x,
                    screenPos.y,
                    controllerCamera.nearClipPlane));
            var forward = (origin - controllerCameraTransform.position).normalized;
            var upward = controllerCameraTransform.up;
            
            ConvertWorldToLocal(ref origin, ref forward, ref upward);

            controllerState.position = origin;
            controllerState.rotation = Quaternion.LookRotation(forward, upward);
            controllerState.inputTrackingState = InputTrackingState.Position | InputTrackingState.Rotation;
        }

        private void ConvertWorldToLocal(ref Vector3 origin, ref Vector3 forward, ref Vector3 upward)
        {
            if (parentTransform == null)
            {
                return;
            }

            origin = parentTransform.InverseTransformPoint(origin);
            forward = parentTransform.InverseTransformDirection(forward);
            upward = parentTransform.InverseTransformDirection(upward);
        }

        protected override void UpdateInput(XRControllerState controllerState)
        {
            base.UpdateInput(controllerState);

            if (controllerState == null)
            {
                return;
            }

            controllerState.ResetFrameDependentStates();
            var clickActionValue = leftClick.action.ReadValue<float>();
            controllerState.selectInteractionState.SetFrameState(
                clickActionValue > 0.5f,
                clickActionValue);
        }
    }
}

オリジナルのControllerは、 XRBaseController を継承したクラスで UpdateTrackingInput(XRControllerState)UpdateInput(XRControllerState) をオーバーライド実装してやれば比較的簡単に作ることができます。

UpdateTrackingInput(XRControllerState) は、コントローラーの姿勢を更新する為のメソッドです。controllerState に対して、現在の座標と向きを入れてやります。ただし、姿勢データはローカル座標系で処理されるので、それを考慮して変換してやる必要があります。

UpdateInput(XRControllerState) は、コントローラーのSelect、Activate入力の状態を更新する為のメソッドです。controllerState.selectInteractionStateを更新してやれば、Select処理、いわゆる掴む処理を実装出来ます。

実装したコンポーネントの利用

上で実装したコンポーネントを、XR Interaction Toolkitに入っているInteractorと同じGameObjectにアタッチすることで動作させることが可能です。スクリプトで定義したカメラとマウス入力を指定してあげてください。マウス入力に関しては、Input Systemのパッケージにデフォルトで含まれているDefaultInputActionsに含まれているものを使えば問題ないかと思います。

(Input Action ManagerにDefaultInputActionsを忘れずに設定してください。)

最終的なセットアップの形は、XR Interaction Toolkitのサンプルに含まれる、XR Interaction Setupを参考にした場合、以下のようになるかと思います。各アプリの事情に合わせて柔軟に変更してください。

以下は動作している様子です。灰色のオブジェクトにXR Simple Interactableをアタッチし、受け取ったイベントをログ出力しています。