MRが楽しい

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

MRTKの手の検出イベントをUniRxのオブザーバで処理する その2(手のジェスチャーを判定する)

本日は MRTK と UniRx の小ネタ枠です。
MRTKの手の検出イベントをUniRxのオブザーバで処理する方法を記事にします。
今回は前回記事のコードに処理を追加して、手のジェスチャーを取得してみます。
f:id:bluebirdofoz:20210417225920j:plain

前提条件

前回記事の続きです。
bluebirdofoz.hatenablog.com

Buffer

Buffer は複数のメッセージを1つのメッセージに変換する UniRx のオペレータです。
今回はこのオペレータを使って、手のジェスチャーの判定結果の丸め込みを行います。

サンプルコード

以下のようなサンプルコードを作成しました。
3つの手の検出イベントそれぞれのオブザーバを作成し、ハンドジョイントの情報から手のジェスチャーを判定します。
更に細かな状態の変動を防ぐため、複数フレームの判定結果から最も多く判定されたジェスチャーの結果で丸め込みを行っています。
・TestHandStatusBuffer.cs

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

using System;

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

using UniRx;

namespace HMProject.Test
{
    public class TestHandStatusBuffer : MonoBehaviour, IMixedRealityHandJointHandler, IMixedRealitySourceStateHandler
    {
        /// <summary>
        /// ハンドタイプ(左右)
        /// </summary>
        [Serializable]
        private enum HandType
        {
            Right,
            Left,
        }

        /// <summary>
        /// 手の状態識別
        /// </summary>
        [Serializable]
        public enum HandStatus
        {
            Normal,           // 通常
            Gur,              // グー
            Choki,            // チョキ
            Par,              // パー
            Pistol,           // ピストル
            ThumbsUp,         // サムズアップ
        }

        [SerializeField, Tooltip("ハンドタイプ(左右)の設定")]
        private HandType p_SettingHandType;


        [SerializeField, Tooltip("親指の折れ曲がり判定角")]
        private float p_HoldThumbAngle = 30.0f;

        [SerializeField, Tooltip("四指の折れ曲がり判定角")]
        private float p_HoldFingerAngle = 60.0f;

        [SerializeField, Tooltip("手の状態を丸め込み判定するバッファサイズ")]
        private int p_HandStatusBufferCount = 10;


        [SerializeField, Tooltip("現在の手の状態")]
        private HandStatus p_CurrentHandStatus;

        [SerializeField, Tooltip("現在の手のソースID")]
        private uint p_CurrentHandSourceId;


        // 丸め込み用カウンタリスト
        private int[] p_RoundingCountList;


        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            // 丸め込み用カウンタリストを作成しておく
            p_RoundingCountList = new int[Enum.GetNames(typeof(HandStatus)).Length];


            // 手の検出時オブザーバを作成する
            IDisposable OnSourceDetectedObserver = Observable
                .FromEvent<SourceStateEventData>(
                    action => OnSourceDetectedAction += action,
                    action => OnSourceDetectedAction -= action
                )
                .Subscribe(eventData =>
                {
                    // 右手/左手判定が必要なため、検出処理はHandJointUpdatedで行う
                })
                .AddTo(this);

            // 手のロスト時オブザーバを作成する
            IDisposable OnSourceLostObserver = Observable
                .FromEvent<SourceStateEventData>(
                    action => OnSourceLostAction += action,
                    action => OnSourceLostAction -= action
                )
                .Subscribe(eventData =>
                {
                    // ロスト時
                    Debug.Log("Hand : Lost");
                })
                .AddTo(this);

            // 手の更新時オブザーバを作成する
            IDisposable OnHandJointsUpdatedObserver = Observable
                .FromEvent<InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>>>(
                    action => OnHandJointsUpdatedAction += action,
                    action => OnHandJointsUpdatedAction -= action
                )
                .Where(eventData => eventData.Handedness == GetCurrentHandednessType()) // チェック対象(右手/左手)である場合
                .Select(eventData => CheckCurrentHandStatus(eventData))                 // 手の状態を判定する
                .Buffer(p_HandStatusBufferCount)                                        // 判定結果をバッファする
                .Select(eventDataList => this.RoundingHandStatus(eventDataList))        // バッファした結果から最頻出の状態を取得する
                .Subscribe(status =>
                {
                    // 状態の判定結果
                    p_CurrentHandStatus = status;
                    Debug.Log("HandStatus : " + status.ToString());
                })
                .AddTo(this);
        }

        /// <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);
        }


        /// <summary>
        /// チェックすべき Handedness タイプを返す
        /// </summary>
        /// <returns></returns>
        private Handedness GetCurrentHandednessType()
        {
            Handedness handedness = Handedness.None;
            switch (p_SettingHandType)
            {
                case HandType.Right:
                    handedness = Handedness.Right;
                    break;
                case HandType.Left:
                    handedness = Handedness.Left;
                    break;
                default:
                    break;
            }
            return handedness;
        }


        /// <summary>
        /// 現在の手の状態を判定する
        /// </summary>
        /// <returns></returns>
        private HandStatus CheckCurrentHandStatus(InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>> eventData)
        {
            // ソースIDを取得する
            p_CurrentHandSourceId = eventData.SourceId;

            // 各指のジョイント位置を取得する
            IDictionary<TrackedHandJoint, MixedRealityPose> joint = eventData.InputData;
            Vector3 wrist_0 = joint[TrackedHandJoint.Wrist].Position;                // 手首
            Vector3 thumb_0 = joint[TrackedHandJoint.ThumbMetacarpalJoint].Position; // 親指手首
            Vector3 thumb_1 = joint[TrackedHandJoint.ThumbProximalJoint].Position;   // 親指第一関節
            Vector3 thumb_2 = joint[TrackedHandJoint.ThumbTip].Position;             // 親指先端
            Vector3 index_0 = joint[TrackedHandJoint.IndexMetacarpal].Position;      // 人指し指手首
            Vector3 index_1 = joint[TrackedHandJoint.IndexKnuckle].Position;         // 人指し指第一関節
            Vector3 index_2 = joint[TrackedHandJoint.IndexTip].Position;             // 人指し指先端
            Vector3 middle_0 = joint[TrackedHandJoint.MiddleMetacarpal].Position;    // 中指手首
            Vector3 middle_1 = joint[TrackedHandJoint.MiddleKnuckle].Position;       // 中指第一関節
            Vector3 middle_2 = joint[TrackedHandJoint.MiddleTip].Position;           // 中指先端
            Vector3 ring_0 = joint[TrackedHandJoint.RingMetacarpal].Position;        // 薬指手首
            Vector3 ring_1 = joint[TrackedHandJoint.RingKnuckle].Position;           // 薬指第一関節
            Vector3 ring_2 = joint[TrackedHandJoint.RingTip].Position;               // 薬指先端
            Vector3 pinky_0 = joint[TrackedHandJoint.PinkyMetacarpal].Position;      // 小指手首
            Vector3 pinky_1 = joint[TrackedHandJoint.PinkyKnuckle].Position;         // 小指第一関節
            Vector3 pinky_2 = joint[TrackedHandJoint.PinkyTip].Position;             // 小指先端


            // 各指の折れ曲がり角
            float angleThumb = Vector3.Angle((thumb_2 - thumb_1).normalized, (thumb_1 - thumb_0).normalized);
            float angleIndex = Vector3.Angle((index_2 - index_1).normalized, (index_1 - index_0).normalized);
            float angleMiddle = Vector3.Angle((middle_2 - middle_1).normalized, (middle_1 - middle_0).normalized);
            float angleRing = Vector3.Angle((ring_2 - ring_1).normalized, (ring_1 - ring_0).normalized);
            float anglePinky = Vector3.Angle((pinky_2 - pinky_1).normalized, (pinky_1 - pinky_0).normalized);


            // 各指の折れ曲がり状態の判定
            bool isHoldThumb = (angleThumb > p_HoldThumbAngle);
            bool isHoldIndex = (angleIndex > p_HoldFingerAngle);
            bool isHoldMiddle = (angleMiddle > p_HoldFingerAngle);
            bool isHoldRing = (angleRing > p_HoldFingerAngle);
            bool isHoldPinky = (anglePinky > p_HoldFingerAngle);


            // 開いている指の数を数える
            int openFingerCount = 0;
            if (!isHoldThumb) openFingerCount++;
            if (!isHoldIndex) openFingerCount++;
            if (!isHoldMiddle) openFingerCount++;
            if (!isHoldRing) openFingerCount++;
            if (!isHoldPinky) openFingerCount++;


            // 手の状態を判定する
            HandStatus status = HandStatus.Normal;

            // 開いている指が 0 本の場合
            if (openFingerCount == 0)
            {
                // グーと判定する
                status = HandStatus.Gur;
            }

            // 開いている指が 1 本の場合
            if (openFingerCount == 1)
            {
                // デフォルトではグーと判定する
                status = HandStatus.Gur;
                if (!isHoldThumb)
                {
                    // 親指が立っていればサムズアップと再判定する
                    status = HandStatus.ThumbsUp;
                }
                if (!isHoldIndex)
                {
                    // 人指し指が立っていればピストルと再判定する
                    status = HandStatus.Pistol;
                }
            }

            // 開いている指が 2 本の場合
            if (openFingerCount == 2)
            {
                // デフォルトではチョキと判定する
                status = HandStatus.Choki;
                if (!isHoldThumb && !isHoldIndex)
                {
                    // 親指と人指し指が立っていればピストルと再判定する
                    status = HandStatus.Pistol;
                }
            }

            // 開いている指が 3 本の場合
            if (openFingerCount == 3)
            {
                // デフォルトではパーと判定する
                status = HandStatus.Par;
                if (!isHoldIndex && !isHoldMiddle)
                {
                    // 人差し指と中指が立っていればチョキと再判定する
                    status = HandStatus.Choki;
                }
            }

            // 開いている指が 4 本の場合
            if (openFingerCount == 4)
            {
                // パーと判定する
                status = HandStatus.Par;
            }

            // 開いている指が 5 本の場合
            if (openFingerCount == 5)
            {
                // パーと判定する
                status = HandStatus.Par;
            }

            return status;
        }


        /// <summary>
        /// 手の状態を最も多区発生した状態で丸め込みを行う
        /// </summary>
        /// <param name="a_HandStatusList"></param>
        /// <returns></returns>
        private HandStatus RoundingHandStatus(IList<HandStatus> a_HandStatusList)
        {
            // 丸め込み結果
            HandStatus roundingResult = HandStatus.Normal;

            // 丸め込み用カウントリストを初期化する
            for (int index = 0; index < p_RoundingCountList.Length; index++)
            {
                p_RoundingCountList[index] = 0;
            }

            // ステータスの最大検出数
            int maxCount = 0;

            foreach (HandStatus checkHandStatus in a_HandStatusList)
            {
                // 各ステータスの判定回数をインクリメントする
                int statusCount = p_RoundingCountList[(int)checkHandStatus]++;

                // 最大検出数のステータスを記録する
                if (statusCount > maxCount)
                {
                    // 最大検出数であればステータスを保持しておく
                    roundingResult = checkHandStatus;
                    maxCount = statusCount;
                }
            }

            return roundingResult;
        }


        // 手の検出時に呼び出すアクション
        private Action<SourceStateEventData> OnSourceDetectedAction;

        // 手のロスト時に呼び出すアクション
        private Action<SourceStateEventData> OnSourceLostAction;

        // 手の更新時に呼び出すアクション
        private Action<InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>>> OnHandJointsUpdatedAction;

        /// <summary>
        /// 手の検出時に発生するイベント(IMixedRealitySourceStateHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnSourceDetected(SourceStateEventData eventData)
        {
            // アクション呼び出し
            if (OnSourceDetectedAction != null) OnSourceDetectedAction(eventData);
        }

        /// <summary>
        /// 手のロスト時に発生するイベント(IMixedRealitySourceStateHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnSourceLost(SourceStateEventData eventData)
        {
            // アクション呼び出し
            if (OnSourceLostAction != null) OnSourceLostAction(eventData);
        }

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

        {
            // アクション呼び出し
            if (OnHandJointsUpdatedAction != null) OnHandJointsUpdatedAction(eventData);
        }
    }
}

f:id:bluebirdofoz:20210417230004j:plain

このスクリプトを適当なオブジェクトに追加します。
f:id:bluebirdofoz:20210417230017j:plain

動作確認

シーンを再生して動作を確認します。
ログを確認すると、手のジェスチャーを判定したログ結果が表示されています。
f:id:bluebirdofoz:20210417230034j:plain

MRTKの手の検出イベントをUniRxのオブザーバで処理する

本日は MRTK と UniRx の小ネタ枠です。
MRTKの手の検出イベントをUniRxのオブザーバで処理する方法を記事にします。
f:id:bluebirdofoz:20210416085616j:plain

FromEvent

FromEvent はイベントから Observable を作成する UniRx のファクトリメソッドです。
以下のように Action クラスから Observable を作成することができます。

Action OnEvent;

IObservable OnEventObservable = Observable.FromEvent(
    action => OnEvent += action,
    action => OnEvent -= action
    );

引数のあるイベントの場合は以下のように作成します。

Action<int> OnEvent;

IObservable<int> OnEventObservable = Observable.FromEvent<int>(
    action => OnEvent += action,
    action => OnEvent -= action
    );

IMixedRealitySourceStateHandlerとIMixedRealityHandJointHandler

IMixedRealitySourceStateHandler と IMixedRealityHandJointHandler は手の検出時にイベントを発生させるハンドラです。
OnSourceDetected と OnSourceLost で手の検出とロストをイベントとして取得できます。
OnHandJointsUpdated で詳細な手の関節(ハンドジョイント)情報を取得できます。
docs.microsoft.com

サンプルコード

以下のようなサンプルコードを作成しました。
3つの手の検出イベントそれぞれのオブザーバを作成し、情報をログに出力しています。
OnHandJointsUpdated のみ常に Handedness.None イベントが発生するため、Where オペレータで除外しています。
・TestFromEvent.cs

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

using System;

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

using UniRx;

namespace HMProject.Test
{
    public class TestFromEvent : MonoBehaviour, IMixedRealitySourceStateHandler, IMixedRealityHandJointHandler
    {
        // 手の検出時に呼び出すアクション
        private Action<SourceStateEventData> OnSourceDetectedAction;

        // 手のロスト時に呼び出すアクション
        private Action<SourceStateEventData> OnSourceLostAction;

        // 手の更新時に呼び出すアクション
        private Action<InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>>> OnHandJointsUpdatedAction;


        /// <summary>
        /// 手の検出時に発生するイベント(IMixedRealitySourceStateHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnSourceDetected(SourceStateEventData eventData)
        {
            // アクション呼び出し
            OnSourceDetectedAction(eventData);
        }

        /// <summary>
        /// 手のロスト時に発生するイベント(IMixedRealitySourceStateHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnSourceLost(SourceStateEventData eventData)
        {
            // アクション呼び出し
            OnSourceLostAction(eventData);
        }

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

        {
            // アクション呼び出し
            OnHandJointsUpdatedAction(eventData);
        }


        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            // 手の検出時オブザーバを作成する
            IDisposable OnSourceDetectedObserver = Observable
                .FromEvent<SourceStateEventData>(
                    action => OnSourceDetectedAction += action,
                    action => OnSourceDetectedAction -= action
                )
                .Subscribe(eventData =>
                {
                    Debug.Log("!!! OnSourceDetected !!! - SourceId : " + eventData.SourceId.ToString());
                })
                .AddTo(this);

            // 手のロスト時オブザーバを作成する
            IDisposable OnSourceLostObserver = Observable
                .FromEvent<SourceStateEventData>(
                    action => OnSourceLostAction += action,
                    action => OnSourceLostAction -= action
                )
                .Subscribe(eventData =>
                {
                    Debug.Log("!!! OnSourceLost !!! - SourceId : " + eventData.SourceId.ToString());
                })
                .AddTo(this);

            // 手の更新時オブザーバを作成する
            IDisposable OnHandJointsUpdatedObserver = Observable
                .FromEvent<InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>>>(
                    action => OnHandJointsUpdatedAction += action,
                    action => OnHandJointsUpdatedAction -= action
                )
                .Where(eventData => eventData.Handedness != Handedness.None)   // Noneイベントは処理しない
                .Subscribe(eventData =>
                {
                    Debug.Log("!!! OnHandJointsUpdated !!! " +
                        "- SourceId : " + eventData.SourceId.ToString() +
                        ", Handedness : " + eventData.Handedness.ToString());
                })
                .AddTo(this);

        }


        /// <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);
        }
    }
}

f:id:bluebirdofoz:20210416090005j:plain

スクリプトを適当なオブジェクトに追加します。
f:id:bluebirdofoz:20210416090020j:plain

動作確認

シーンを再生して動作を確認します。
ログを確認すると、手の検出イベントをオブザーバが受け取っていることが分かります。
これで MRTK で発生するイベントを UniRx の仕組みを使ってフィルタリングすることができます。
f:id:bluebirdofoz:20210416090035j:plain

UniRxを使って一定フレーム毎に同一フレームで発生したUnityのイベントをまとめて処理する

本日は UniRx の小ネタ枠です。
UniRxを使って一定フレーム毎に同一フレームで発生したUnityのイベントをまとめて処理する方法を記事にします。
f:id:bluebirdofoz:20210415161420j:plain

OnTriggerStayAsObservable

今回利用する OnTriggerStayAsObservable は Collider の OnTriggerStay イベントを Observable に変換します。
UniRx の ObservableTriggers を利用すると、他にも様々な Unity イベントを Observable に変換することができます。

サンプルコード

以下のようなサンプルコードを作成しました。
100 フレームに一度、同一フレームで検出した collider をリストにまとめて通知します。
・TestOnTriggerStayAsObservable.cs

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

using UniRx;
using UniRx.Triggers;

namespace HMProject.Test
{
    public class TestOnTriggerStayAsObservable : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start()
        {
            this.OnTriggerStayAsObservable()   // OnTriggerStayイベント
                .BatchFrame()                  // フレーム毎に値をまとめる
                .ThrottleFirstFrame(100)       // 100フレーム毎に結果を取得する
                .Subscribe(colliderlist =>
                {
                    Debug.Log("collider count : " + colliderlist.Count);
                    foreach (Collider collider in colliderlist)
                    {
                        int frameCount = Time.frameCount;
                        Debug.Log("Stay GameObject : " + collider.gameObject.name + ", FrameCount : " + frameCount.ToString());
                    }
                })
                .AddTo(this);
        }
    }
}

f:id:bluebirdofoz:20210415161332j:plain

以下のオペレータを利用してメッセージをフィルタリングしています。

BatchFrame

指定したフレーム数の間に発行されたメッセージを1つにまとめるオペレータです。
フレーム数のカウントはメッセージを受け取ったタイミングで開始されます。
引数を指定しない場合、同一フレームのメッセージをまとめます。フレーム数は 0 オリジンです。

ThrottleFirstFrame

メッセージを受け取った後、指定フレームの間、次のメッセージを通さないオペレータです。
時間で指定する ThrottleFirst というオペレータもあります。

このスクリプトをオブジェクトを検出する Collider を設定したオブジェクトに追加します。
f:id:bluebirdofoz:20210415161344j:plain

動作確認

シーンを再生して動作を確認します。
ログを確認すると、一定のフレーム間隔で、同一フレームに検出したコライダーの情報が取得できていることがわかります。
f:id:bluebirdofoz:20210415161355j:plain

UniRxでObserveEveryValueChangedを使って特定の値の変化をフレームごとにチェックする

本日は UniRx の技術調査枠です。
UniRxでObserveEveryValueChangedを使って特定の値の変化をフレームごとにチェックする方法を記事にします。
f:id:bluebirdofoz:20210414123355j:plain

ObserveEveryValueChanged

UniRxで定義されている全クラスの拡張メソッドです。
この関数を利用することで、様々なクラスのフレーム間での値変化の監視が行えるようになります。

サンプルコード

以下のようなサンプルコードを作成しました。
2つの異なる参照変数を Subscribe することで、以下の2種類の値変化の通知を受け取ることができます

  • ReactiveProperty によるベクトル情報全体の変化通知
  • ObserveEveryValueChanged によるベクトルのX情報の変化通知

・EveryValueVectorReactiveProperty.cs

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

using System;

using UniRx;

namespace HMProject.Test
{
    /// <summary>
    /// テスト用ベクトル情報
    /// </summary>
    [Serializable]
    public class EveryValueVector
    {
        public int X;
        public int Y;
        public int Z;

        public EveryValueVector(int x, int y, int z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }

    /// <summary>
    /// ベクトル情報を表すReactiveProperty
    /// </summary>
    [Serializable]
    public class EveryValueVectorReactiveProperty : ReactiveProperty<EveryValueVector>
    {
        public EveryValueVectorReactiveProperty()
        {
        }
        public EveryValueVectorReactiveProperty(EveryValueVector a_EveryValueVector) : base(a_EveryValueVector)
        {
        }
    }

    public class TestEveryValueChanged : MonoBehaviour
    {
        /// <summary>
        /// ベクトル情報
        /// </summary>
        [SerializeField, Tooltip("ベクトル情報")]
        private EveryValueVectorReactiveProperty p_EveryValueVector = new EveryValueVectorReactiveProperty();

        /// <summary>
        /// ベクトル情報のReadOnlyReactivePropertyの保持変数
        /// </summary>
        private IReadOnlyReactiveProperty<EveryValueVector> p_IReadOnlyReactivePropertyEveryValueVector;

        /// <summary>
        /// ベクトル情報のReadOnlyReactivePropertyの参照変数
        /// </summary>
        public IReadOnlyReactiveProperty<EveryValueVector> IReadOnlyReactivePropertyEveryValueVector
            => p_IReadOnlyReactivePropertyEveryValueVector
            ?? (p_IReadOnlyReactivePropertyEveryValueVector = p_EveryValueVector.ToSequentialReadOnlyReactiveProperty());


        // EveryValueChanged を使ってベクトル情報の特定の値に変化があった際に通知する
        // EveryValueChanged を使うとフレーム間での変化のみを検出する
        /// <summary>
        /// ベクトル情報の EveryValueChanged オブザーバ保持変数
        /// </summary>
        private IObservable<int> p_IObservableEveryValueVectorEveryValueChanged;

        /// <summary>
        /// ベクトル情報の EveryValueChanged オブザーバ参照変数
        /// </summary>
        public IObservable<int> IObservableEveryValueVectorEveryValueChanged
            => p_IObservableEveryValueVectorEveryValueChanged
            ?? (p_IObservableEveryValueVectorEveryValueChanged =
            p_EveryValueVector.ObserveEveryValueChanged(property => property.Value.X)); // X の値の変化を検知する


        /// <summary>
        /// ベクトル情報の設定
        /// </summary>
        private void ReceptionVector(int x, int y, int z)
        {
            p_EveryValueVector.SetValueAndForceNotify(new EveryValueVector(x, y, z));
        }

        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            // 初期値を設定する
            VectorReset();
        }

        /// <summary>
        /// 定期処理
        /// </summary>
        void Update()
        {
        }

        /// <summary>
        /// 初期値を設定する
        /// </summary>
        public void VectorReset()
        {
            ReceptionVector(0, 0, 0);
        }

        /// <summary>
        /// Xの値変更テスト
        /// 同一フレーム内で値を2回変更する
        /// </summary>
        public void XChangeTest()
        {
            // 値を変更する
            ReceptionVector(1, 0, 0);

            // 値を変更する
            ReceptionVector(2, 0, 0);
        }

        /// <summary>
        /// Yの値変更テスト
        /// 同一フレーム内で値を2回変更する
        /// </summary>
        public void YChangeTest()
        {
            // 値を変更する
            ReceptionVector(0, 1, 0);

            // 値を変更する
            ReceptionVector(0, 2, 0);
        }
    }

#if UNITY_EDITOR
    // エディター定義
    // 拡張するクラスを指定する
    [CustomEditor(typeof(TestEveryValueChanged))]
    // 継承クラスは Editor を設定する
    public class TestEveryValueChangedEditor : Editor
    {
        // GUIの表示関数をオーバーライドする
        public override void OnInspectorGUI()
        {
            // 元のインスペクター部分を表示
            base.OnInspectorGUI();

            // targetを変換して対象スクリプトの参照を取得する
            TestEveryValueChanged editorTarget = target as TestEveryValueChanged;

            // public関数を実行するボタンの作成
            if (GUILayout.Button("VectorResetの実行"))
            {
                editorTarget.VectorReset();
            }

            // public関数を実行するボタンの作成
            if (GUILayout.Button("XChangeTestの実行"))
            {
                editorTarget.XChangeTest();
            }

            // public関数を実行するボタンの作成
            if (GUILayout.Button("YChangeTestの実行"))
            {
                editorTarget.YChangeTest();
            }
        }
    }
# endif
}

f:id:bluebirdofoz:20210414123530j:plain

合わせて参照変数に Subscribe を行い、結果を表示する以下のスクリプトを作成しました。
・TestEveryValueChangedSubscribe.cs

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

using UniRx;

namespace HMProject.Test
{
    public class TestEveryValueChangedSubscribe : MonoBehaviour
    {
        [SerializeField, Tooltip("スクリプト参照")]
        private TestEveryValueChanged p_TestEveryValueChanged;

        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            p_TestEveryValueChanged.IReadOnlyReactivePropertyEveryValueVector
                .ObserveOnMainThread()
                .Subscribe(vector =>
                {
                    Debug.Log("ForceNotify - X : " + vector.X.ToString() +
                        ", Y : " + vector.Y.ToString() +
                        ", Z : " + vector.Z.ToString());
                })
                .AddTo(this);

            p_TestEveryValueChanged.IObservableEveryValueVectorEveryValueChanged
                .ObserveOnMainThread()
                .Subscribe(value =>
                {
                    Debug.Log("EveryValueChanged : " + value.ToString());
                })
                .AddTo(this);
        }

        /// <summary>
        /// 定期処理
        /// </summary>
        void Update()
        {

        }
    }
}

f:id:bluebirdofoz:20210414123547j:plain

作成したスクリプトを以下の通り、設定して準備は完了です。
f:id:bluebirdofoz:20210414123559j:plain

動作確認

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

Y の値を変化させる関数を実行すると、ReactiveProperty の通知のみが発生します。
f:id:bluebirdofoz:20210414123630j:plain

X の値を変化させる関数を実行すると、ReactiveProperty の通知と ObserveEveryValueChanged による通知が発生します。
また、ReactiveProperty の通知は2回の変更ごとに通知が発生していますが、ObserveEveryValueChanged の通知はフレーム間の1回の変化のみを通知していることがわかります。
f:id:bluebirdofoz:20210414123712j:plain

参考ページ

qiita.com

UnityのスクリプトにVisualStudioを使ってブレークポイントを設定する

本日は Unity の小ネタ枠です。
UnityのスクリプトにVisualStudioを使ってブレークポイントを設定する手順を記事にします。
f:id:bluebirdofoz:20210413231343j:plain

前提条件

VisualStudio に、Visual Studio Tools for Unity プラグインをインストールしておく必要があります。
通常、Unity と VisualStudio のインストール時に、自動でインストールされます。
後から追加でインストールを行う場合は以下の記事を参照ください。
bluebirdofoz.hatenablog.com

Unityの設定

メニューから[Edit -> Preferences]を選択して Preference ダイアログを開きます。
f:id:bluebirdofoz:20210413231417j:plain

[External Tools]タグを開き、以下の通り、設定します。
・[External Script Editor]に Visual Studio を選択する。
・[Editor Attaching]にチェックを入れる。
f:id:bluebirdofoz:20210413231431j:plain

ブレークポイントの設定

ブレークポイントを設定したいスクリプトを開きます。
f:id:bluebirdofoz:20210413231450j:plain

処理を停止させたい行の左端をクリックして、ブレークポイントを設定します。
f:id:bluebirdofoz:20210413231500j:plain

デバッグの実行

VisualStudio の設定が[Unity にアタッチ]担っていることを確認して、クリックして実行します。
f:id:bluebirdofoz:20210413231515j:plain

VisualStudio の準備が完了したら、Unity のシーンを再生します。
f:id:bluebirdofoz:20210413231528j:plain

スクリプトブレークポイント行が実行されるタイミングで処理が停止するので、変数の確認などを行います。
再びシーンを再生したい場合は[続行]をクリックします。
f:id:bluebirdofoz:20210413231538j:plain

HoloLens2でホロモンアプリを作る その30(ホロモンの視野から障害物に遮られたオブジェクトを除外する)

本日はアプリ作成枠です。
HoloLens2でホロモンアプリを作る進捗を書き留めていきます。
f:id:bluebirdofoz:20210412233356j:plain

今回はホロモンの視野から障害物に遮られたオブジェクトを除外するメモです。

障害物の有無を判定する

今回、ホロモンの視野における障害物を判定するため、以下のようなコードを作成しました。
ホロモンの頭の位置と、検出オブジェクトの位置を算出し、その直線上に別のオブジェクトが存在するかチェックします。

// 視界原点とオブジェクト間のベクトルを算出し、距離と方向ベクトルを取得する
Vector3 betweenVector = collider.transform.position - p_VisionRoot.position;
float betweenDistance = betweenVector.magnitude;
Vector3 betweenDirection = betweenVector.normalized;

// レイキャストの結果
RaycastHit[] raycastHits = new RaycastHit[3];

// レイキャストでその方向の衝突オブジェクトを検知する
int hitCount = Physics.RaycastNonAlloc(p_VisionRoot.position, betweenDirection, raycastHits, betweenDistance);

if (hitCount > 1)
{
    // ヒット数が 1 以上なら障害物(対象以外)があるとみなして無視する
    return;
}

ホロモンの視野から障害物に遮られたオブジェクトを除外する

前回作成したスクリプトにこの障害物検知の機能を追加します。
bluebirdofoz.hatenablog.com

サンプルスクリプトとして以下のスクリプトを作成しました。
コライダーの範囲で検出されたオブジェクトが視野角に収まるか、視界原点との間に障害物がないかを判定します。
・VisionTest.cs

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

// CoreSystemへのアクセスのため
using Microsoft.MixedReality.Toolkit;
// 空間認識情報の取得のため
using Microsoft.MixedReality.Toolkit.SpatialAwareness;

namespace HMProject.Test
{
    [RequireComponent(typeof(Collider))]
    public class VisionTest : MonoBehaviour
    {
        [SerializeField, Tooltip("空間認識レイヤー番号")]
        int p_SpatialAwarenessLayer;

        [SerializeField, Tooltip("視界原点トランスフォーム")]
        private Transform p_VisionRoot;

        [SerializeField, Tooltip("視野角")]
        private float p_ViewingAngle = 60;


        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            // 空間認識のオブザーバを取得する
            IMixedRealitySpatialAwarenessMeshObserver SpatialAwarenessMeshObserver =
                CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

            // オブザーバからレイヤー番号を取得する
            p_SpatialAwarenessLayer = SpatialAwarenessMeshObserver.MeshPhysicsLayer;
        }


        private void OnTriggerStay(Collider collider)
        {
            // オブジェクトのレイヤー番号を取得する
            int layernumber = collider.gameObject.layer;

            if (layernumber == p_SpatialAwarenessLayer)
            {
                // 空間認識レイヤーの場合は無視する
                Debug.Log("OnTriggerStay : " + collider.name + " is Spatial Awareness Layer.");
                return;
            }

            // 視界原点とオブジェクト間のベクトルを算出し、距離と方向ベクトルを取得する
            Vector3 betweenVector = collider.transform.position - p_VisionRoot.position;
            float betweenDistance = betweenVector.magnitude;
            Vector3 betweenDirection = betweenVector.normalized;

            // 視界原点の方向ベクトルを取得する
            Vector3 headDirection = p_VisionRoot.rotation * Vector3.forward;

            // 2つの方向ベクトルの角度差(0°~360°)を取得する
            float diffAngle = Vector3.Angle(headDirection, betweenDirection);
            Debug.Log("OnTriggerStay : " + collider.name + ", Angle : " + diffAngle.ToString());

            if (diffAngle > p_ViewingAngle)
            {
                // 視野角外の場合は無視する
                Debug.Log("OnTriggerStay : " + collider.name + " is Out of ViewingAngle.");
                return;
            }

            // レイキャストの結果
            RaycastHit[] raycastHits = new RaycastHit[3];

            // レイキャストでその方向の衝突オブジェクトを検知する
            int hitCount = Physics.RaycastNonAlloc(p_VisionRoot.position, betweenDirection, raycastHits, betweenDistance);

            if (hitCount > 1)
            {
                // ヒット数が 1 以上なら障害物(対象以外)があるとみなして無視する
                Debug.Log("OnTriggerStay : " + collider.name + " is invisible to obstacles.");
                return;
            }

            // 視野角に収まる場合は見えていると判定する
            Debug.Log("OnTriggerStay : " + collider.name + " can be seen.");
        }
    }
}

f:id:bluebirdofoz:20210412233427j:plain

前回と同様に、このスクリプトをオブジェクトを検出する Collider を設定したオブジェクトに設定します。
f:id:bluebirdofoz:20210412233440j:plain

シーンを再生して動作を確認します。
ホロモンの正面にいるときは障害物がないため、見えていると判定されます。
f:id:bluebirdofoz:20210412233453j:plain

そのまま下がり、空間認識レイヤーの裏側に回り込みました。
すると間に障害物となる壁があるため、見えていないと判定されました。
f:id:bluebirdofoz:20210412233505j:plain

HoloLens2でホロモンアプリを作る その29(ホロモンの視野内のオブジェクトを検出する)

本日はアプリ作成枠です。
HoloLens2でホロモンアプリを作る進捗を書き留めていきます。
f:id:bluebirdofoz:20210411223748j:plain

今回はホロモンの視野内のオブジェクトを検出するメモです。

視野角を判定する

今回、ホロモンの視野角を判定するため、以下のようなコードを作成しました。
ホロモンの頭の方向と、検出オブジェクトの方向の角度を算出し、視野角に収まる範囲にオブジェクトが存在するか判定します。

// 視界原点とオブジェクト間のベクトルを算出し、距離と方向ベクトルを取得する
Vector3 betweenVector = collider.transform.position - p_VisionRoot.position;
float betweenDistance = betweenVector.magnitude;
Vector3 betweenDirection = betweenVector.normalized;

// 視界原点の方向ベクトルを取得する
Vector3 headDirection = p_VisionRoot.rotation * Vector3.forward;

// 2つの方向ベクトルの角度差(0°~360°)を取得する
float diffAngle = Vector3.Angle(headDirection, betweenDirection);

if (diffAngle > p_ViewingAngle)
{
    // 視野角外の場合は無視する
    Debug.Log("OnTriggerStay : " + collider.name + " is Out of ViewingAngle.");
    return;
}

ホロモンの視野内のオブジェクトを検出する

サンプルスクリプトとして以下のスクリプトを作成しました。
コライダーの範囲で検出されたオブジェクトが視野角に収まるか否かを判定します。
・VisionTest.cs

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

// CoreSystemへのアクセスのため
using Microsoft.MixedReality.Toolkit;
// 空間認識情報の取得のため
using Microsoft.MixedReality.Toolkit.SpatialAwareness;

namespace HMProject.Test
{
    [RequireComponent(typeof(Collider))]
    public class VisionTest : MonoBehaviour
    {
        [SerializeField, Tooltip("空間認識レイヤー番号")]
        int p_SpatialAwarenessLayer;

        [SerializeField, Tooltip("視界原点トランスフォーム")]
        private Transform p_VisionRoot;

        [SerializeField, Tooltip("視野角")]
        private float p_ViewingAngle = 60;


        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            // 空間認識のオブザーバを取得する
            IMixedRealitySpatialAwarenessMeshObserver SpatialAwarenessMeshObserver =
                CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

            // オブザーバからレイヤー番号を取得する
            p_SpatialAwarenessLayer = SpatialAwarenessMeshObserver.MeshPhysicsLayer;
        }


        private void OnTriggerStay(Collider collider)
        {
            // オブジェクトのレイヤー番号を取得する
            int layernumber = collider.gameObject.layer;

            if (layernumber == p_SpatialAwarenessLayer)
            {
                // 空間認識レイヤーの場合は無視する
                Debug.Log("OnTriggerStay : " + collider.name + " is Spatial Awareness Layer.");
                return;
            }

            // 視界原点とオブジェクト間のベクトルを算出し、距離と方向ベクトルを取得する
            Vector3 betweenVector = collider.transform.position - p_VisionRoot.position;
            float betweenDistance = betweenVector.magnitude;
            Vector3 betweenDirection = betweenVector.normalized;

            // 視界原点の方向ベクトルを取得する
            Vector3 headDirection = p_VisionRoot.rotation * Vector3.forward;

            // 2つの方向ベクトルの角度差(0°~360°)を取得する
            float diffAngle = Vector3.Angle(headDirection, betweenDirection);
            Debug.Log("OnTriggerStay : " + collider.name + ", Angle : " + diffAngle.ToString());

            if (diffAngle > p_ViewingAngle)
            {
                // 視野角外の場合は無視する
                Debug.Log("OnTriggerStay : " + collider.name + " is Out of ViewingAngle.");
                return;
            }

            // 視野角に収まる場合は見えていると判定する
            Debug.Log("OnTriggerStay : " + collider.name + " can be seen.");
        }
    }
}

f:id:bluebirdofoz:20210411223809j:plain

このスクリプトをオブジェクトを検出する Collider を設定したオブジェクトに追加します。
f:id:bluebirdofoz:20210411223824j:plain

更にホロモンからの視野を判定するため、頭部トランスフォームを指定して準備は完了です。
f:id:bluebirdofoz:20210411223836j:plain

シーンを再生して動作を確認します。
ホロモンの正面にいるときは視野角に収まるため、見えていると判定されます。
f:id:bluebirdofoz:20210411223847j:plain

そのままホロモンの側面に回り込みます。
すると視野角から外れ、見えていないと判定されました。
f:id:bluebirdofoz:20210411223859j:plain