MRが楽しい

MRやVRについて学習したことを書き残す

MRTK で両手から伸びるハンドレイのポインターの座標を取得する

本日は MRTK の小ネタ枠です。
MRTK で両手から伸びるハンドレイのポインターの座標を取得する方法を記事にします。
f:id:bluebirdofoz:20211113235013j:plain

MRTKのポインタ

MRTK のポインターは様々なポインタークラスで構成されています。
これらは InputSystem からアクセスすることができ、ハンドレイのポインターもここに含まれます。
docs.microsoft.com
docs.microsoft.com

本記事では右手と左手の両方のハンドレイからポインターの座標を取得して表示してみます。

サンプルプロジェクトの作成

MRTK をインポートしたサンプルプロジェクトを作成します。

MRTK のインポートと基本設定

MRTK のインポートと HoloLens 向けプロジェクトの基本設定を行い、サンプルプロジェクトを作成します。
手順の詳細は以下の記事を参照してください。
bluebirdofoz.hatenablog.com

サンプルシーンの作成

Unity エディター上で動作確認を行うため、Plane オブジェクトで簡単に壁を作成したシーンを準備しました。
f:id:bluebirdofoz:20211113235054j:plain

サンプルコード

ハンドレイのポインター座標を取得する以下のサンプルスクリプトを作成しました。
左右または両手が表示されている間、ポインター座標に指定されたプレハブオブジェクトを表示します。
・HandRayTracking.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using Microsoft.MixedReality.Toolkit;
using Microsoft.MixedReality.Toolkit.Utilities;
using Microsoft.MixedReality.Toolkit.Input;

public class HandRayTracking : MonoBehaviour, IMixedRealityHandJointHandler, IMixedRealitySourceStateHandler
{
    [SerializeField, Tooltip("ポインターとして表示するプレハブ")]
    private GameObject p_PointerPrefab;

    [SerializeField, Tooltip("参照するハンドタイプの指定")]
    private Handedness p_HandednessType;

    /// <summary>
    /// スポーンオブジェクトの参照
    /// </summary>
    private GameObject p_Pointer;

    /// <summary>
    /// 現在スクリプトが参照中のハンドレイ
    /// </summary>
    private ShellHandRayPointer p_ShellHandRayPointer;


    /// <summary>
    /// 手の検出時に発生するイベント(IMixedRealitySourceStateHandler)
    /// </summary>
    /// <param name="eventData"></param>
    public void OnSourceDetected(SourceStateEventData eventData)
    {
        // 既に対象を検出済みの場合は処理しない
        if (p_ShellHandRayPointer != null) return;

        // 現在監視対象のポインターが存在するか
        ShellHandRayPointer handRayPointer = DetectionTargetHandRay();
        if (handRayPointer == null) return;

        // 対象が見つかった場合参照を取得しておく
        p_ShellHandRayPointer = handRayPointer;

        // ポインターオブジェクトを生成する
        p_Pointer = Instantiate(p_PointerPrefab);
    }

    /// <summary>
    /// 手のロスト時に発生するイベント(IMixedRealitySourceStateHandler)
    /// </summary>
    /// <param name="eventData"></param>
    public void OnSourceLost(SourceStateEventData eventData)
    {
        // 既に対象を削除済みの場合は処理しない
        if (p_ShellHandRayPointer == null) return;

        // 現在監視対象のポインターが存在するか
        ShellHandRayPointer handRayPointer = DetectionTargetHandRay();
        if (handRayPointer != null) return;

        // 対象が見つからなくなっている場合ロスト処理を行う
        p_ShellHandRayPointer = null;

        // ポインターオブジェクトを破棄する
        Destroy(p_Pointer);
    }

    /// <summary>
    /// 手の更新時に発生するイベント(IMixedRealityHandJointHandler)
    /// </summary>
    /// <param name="eventData"></param>
    public void OnHandJointsUpdated(InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>> eventData)

    {
        // 監視対象のポインターが取得済みか否か
        if (p_ShellHandRayPointer == null) return;

        // ポインターのレイキャスト座標にポインターオブジェクトを移動する
        Vector3 handRayPosition = p_ShellHandRayPointer?.BaseCursor?.Position ?? new Vector3();
        p_Pointer.transform.position = handRayPosition;
    }

    /// <summary>
    /// 有効時処理
    /// </summary>
    private void OnEnable()
    {
        // ハンドラ登録
        CoreServices.InputSystem?.RegisterHandler<IMixedRealityHandJointHandler>(this);
        CoreServices.InputSystem?.RegisterHandler<IMixedRealitySourceStateHandler>(this);
    }

    /// <summary>
    /// 無効時処理
    /// </summary>
    private void OnDisable()
    {
        // ハンドラ解除
        CoreServices.InputSystem?.UnregisterHandler<IMixedRealityHandJointHandler>(this);
        CoreServices.InputSystem?.UnregisterHandler<IMixedRealitySourceStateHandler>(this);
    }

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }

    private ShellHandRayPointer DetectionTargetHandRay()
    {
        ShellHandRayPointer handRayPointer = null;

        // 現在の InputSystem に対象が存在するかチェックする
        foreach (IMixedRealityInputSource inputSource in CoreServices.InputSystem.DetectedInputSources)
        {
            foreach (IMixedRealityPointer pointer in inputSource.Pointers)
            {
                // ハンドレイでなければ対象外
                if (pointer.GetType() != typeof(ShellHandRayPointer)) continue;
                // 指定のハンドタイプでなければ対象外
                if (((ShellHandRayPointer)pointer).Handedness != p_HandednessType) continue;

                handRayPointer = (ShellHandRayPointer)pointer;
            }
        }

        return handRayPointer;
    }
}

f:id:bluebirdofoz:20211113235110j:plain

作成したスクリプトをシーン内の適当なオブジェクトに設定します。
今回は右手と左手のポインターを別々に追跡したいので[HandednessType]にそれぞれ[Right]と[Left]を設定した2つのコンポーネントを設定しておきます。
f:id:bluebirdofoz:20211113235118j:plain

ポインタープレハブの作成

ポインターの座標を示すためのポインタープレハブを作成します。
今回は以下のような簡単な Sphere オブジェクトとしました。ポインター再帰的にヒットしないように Collider コンポーネントを外しておく必要がある点に注意してください。
f:id:bluebirdofoz:20211113235127j:plain
f:id:bluebirdofoz:20211113235136j:plain

作成したポインタープレハブを右手と左手それぞれのスクリプトに設定して準備完了です。
f:id:bluebirdofoz:20211113235144j:plain

動作確認

シーンを再生して動作を確認します。
f:id:bluebirdofoz:20211113235154j:plain

手を出すと同時にそのハンドレイが示すポインター座標にポインタープレハブがスポーンして追跡します。
f:id:bluebirdofoz:20211113235203j:plain

両手を出すとそれぞれのポインター座標にポインタープレハブがスポーンして追跡している状態になります。
f:id:bluebirdofoz:20211113235212j:plain

手を隠すとポインタープレハブは破棄されて消えます。
f:id:bluebirdofoz:20211113235220j:plain

因みにハンドレイのポインターは壁などの障害物がない場合、デフォルトでは5m遠方の場所がポインター座標になるようです。
f:id:bluebirdofoz:20211113235230j:plain