本日はMetaQuest3の技術調査枠です。
MRTKv2.xを使ってMetaQuest3向けのUnityプロジェクト作成を行う手順を記事にします。
本記事はコントローラに追従するメニューを作る方法です。
前提条件
以下の記事で作成した Unity プロジェクトを基に設定を行います。
記事その1~その4までの作業を実施済みであることが前提になります。
bluebirdofoz.hatenablog.com
コントローラに追従するメニューを作る
MRTKでは対象に追従する仕組みとしてSolverを利用できます。
ただし追従対象は Head, ControllerRay, HandJoint, CusstomOverride からの選択になるため、直接コントローラを指定できません。
learn.microsoft.com
MRTK入力システムから呼び出される入力イベントを参照することでコントローラのポーズや検出、ロストを受け取ることができます。
learn.microsoft.com
今回はMRTKの入力イベントを使ってコントローラを直接追従する以下のサンプルスクリプトを作成しました。
・MRTKMotionControllerSolverHandler.cs
using System; using System.Collections; using System.Collections.Generic; using Microsoft.MixedReality.Toolkit; using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Utilities; using UnityEngine; using UnityEngine.Serialization; public class MRTKMotionControllerSolverHandler : MonoBehaviour, IMixedRealityInputHandler<MixedRealityPose>, IMixedRealitySourceStateHandler { /// <summary> /// 追従の有効化 /// </summary> public bool UpdateSolver = true; /// <summary> /// 追従するコントローラの種類 /// </summary> [SerializeField] private TrackedTargetHandednessType targetTrackedHandedness = TrackedTargetHandednessType.None; /// <summary> /// 向きの振る舞い /// </summary> [SerializeField] private RotationBehaviorType rotationBehavior = RotationBehaviorType.LookAtMainCamera; /// <summary> /// 位置のオフセット /// </summary> [SerializeField] private float saveZoneOffset = 0.1f; /// <summary> /// エディター上ではハンドオブジェクトをコントローラ替わりに追従する /// </summary> /// <returns></returns> [SerializeField] private bool handTrackingInEditor = true; /// <summary> /// 現在追従中のソースID(追従していないとき:0) /// </summary> [SerializeField] private uint currentTrackingSourceId = 0; private void OnEnable() { // コントローラの位置と回転を追従するためにInputSystemに登録する CoreServices.InputSystem?.RegisterHandler<IMixedRealityInputHandler<MixedRealityPose>>(this); CoreServices.InputSystem?.RegisterHandler<IMixedRealitySourceStateHandler>(this); } private void OnDisable() { // InputSystemから登録を解除する CoreServices.InputSystem?.UnregisterHandler<IMixedRealityInputHandler<MixedRealityPose>>(this); CoreServices.InputSystem?.UnregisterHandler<IMixedRealitySourceStateHandler>(this); } /// <summary> /// 最後の更新時間 /// </summary> private float lastUpdateTime = 0; public void OnSourceDetected(SourceStateEventData eventData) { // 追従が無効なら何もしない if(UpdateSolver == false) return; // OnSourceDetectedではHandednessが取得できないので追跡対象の判定はOnInputChangedで行う } public void OnSourceLost(SourceStateEventData eventData) { // 追従が無効なら何もしない if(UpdateSolver == false) return; // 現在追従中のソースがロストすれば追跡対象を解除する if (currentTrackingSourceId == eventData.SourceId) { currentTrackingSourceId = 0; lastUpdateTime = 0; } } public void OnInputChanged(InputEventData<MixedRealityPose> eventData) { // 追従が無効なら何もしない if(UpdateSolver == false) return; // 入力ソースの種類、ソースID、ハンドタイプを取得する var trackedSourceId = eventData.SourceId; var trackedTargetType = eventData.InputSource.SourceType; var trackedHandedness = eventData.Handedness; // 現在追従中のソースがなければ追跡対象か判定を行う if (currentTrackingSourceId == 0) { // 追従対象ならソースIDを保持する if (IsTrackingTargetData(eventData)) currentTrackingSourceId = trackedSourceId; } // 現在追従中のソースと異なるソースの場合は何もしない if (currentTrackingSourceId != trackedSourceId) return; // 追従対象の場合は現在のオブジェクトの向きを設定に応じて更新する switch (rotationBehavior) { case RotationBehaviorType.None: this.transform.rotation = Quaternion.Lerp(this.transform.rotation, eventData.InputData.Rotation, 0.01f); break; case RotationBehaviorType.LookAtMainCamera: this.transform.LookAt(CameraCache.Main.transform); break; default: throw new ArgumentOutOfRangeException(); } // 追従対象の場合は現在のオブジェクトの位置をコントローラの位置と現在の回転で更新する var controllerPosition = eventData.InputData.Position; var selfRotation = this.transform.rotation; var direction = trackedHandedness switch { Handedness.Left => selfRotation * Vector3.left, Handedness.Right => selfRotation * Vector3.right, _ => Vector3.zero }; var targetPosition = controllerPosition + direction * saveZoneOffset; // 更新間隔を基にlerpの係数を決定する var lerpFactor = Mathf.Clamp01((Time.time - lastUpdateTime) * 10.0f); this.transform.position = Vector3.Lerp(this.transform.position, targetPosition, lerpFactor); lastUpdateTime = Time.time; } /// <summary> /// イベントデータが追従対象のコントローラか判定する /// </summary> /// <param name="eventData"></param> /// <returns></returns> private bool IsTrackingTargetData(InputEventData<MixedRealityPose> eventData) { // 入力ソースの種類、ソースID、ハンドタイプを取得する var trackedSourceId = eventData.SourceId; var trackedTargetType = eventData.InputSource.SourceType; var trackedHandedness = eventData.Handedness; // エディター上かつhandTrackingInEditorが有効ならハンドオブジェクトをコントローラ替わりに追従する if (Application.isEditor && handTrackingInEditor && trackedTargetType == InputSourceType.Hand) { trackedTargetType = InputSourceType.Controller; } // コントローラでない場合は追従しない if (trackedTargetType != InputSourceType.Controller) return false; // 追従するコントローラの種類と異なる場合は追従しない switch (this.targetTrackedHandedness) { case TrackedTargetHandednessType.None: // 種類が指定されていない場合は全ての種類を追従する break; case TrackedTargetHandednessType.Left: if (trackedHandedness != Handedness.Left) return false; break; case TrackedTargetHandednessType.Right: if (trackedHandedness != Handedness.Right) return false; break; case TrackedTargetHandednessType.Both: if (trackedHandedness != Handedness.Left && trackedHandedness != Handedness.Right) return false; break; } // 追従対象と判定する return true; } private enum TrackedTargetHandednessType { None, Left, Right, Both, } private enum RotationBehaviorType { None, LookAtMainCamera, } }
本スクリプトを適当なオブジェクトにアタッチし、子オブジェクトにメニュー替わりのボタンを配置しました。
動作確認のため、エディター上ではシミュレータのハンドポインターに追従するようにしています。
エディター上で動作確認すると、以下のようにボタンがハンドポインターに追跡します。
ビルドと動作確認
以下の記事を参考にプロジェクトのビルドとQuest3へのデプロイを実行してください。
bluebirdofoz.hatenablog.com
MetaQuest3でデプロイしたアプリを起動し、目の前でコントローラを動かしてみます。
以下の通り、ボタンがコントローラに追従します。