Hiromuブログ

最近はこちら(https://zenn.dev/hiromu)が本体

MRTKのFocusProvider実装確認

FocusProvider

以前の記事でポインターが FocusProvider と関係していることが分かったので、今回は FocusProvider について見ていくことにしました(確認したバージョンはMRTK v2.4.0)。

かなり長くなってしまったので最初に FocusProvider とはどのようなものかを簡単にまとめると

  • 入力ソースを検出したときにソースに紐づいたポインターを FocusProvider 内に登録する

  • 各ポインターがフォーカスしている(レイがヒットしている)オブジェクトの詳細を取得する

  • フォーカスしているオブジェクトに対して必要に応じてイベントを発生させる(OnFocusEnter / OnFocusExit など)

というものです。

詳細について特に興味がなければここまでで大丈夫だと思います。

以降はほとんど調べたことのメモという感じなので興味のある方だけご覧ください。

では、まず継承関係を見てみます。

継承関係

f:id:HiromuKato:20200721032008p:plain

入り組んだ印象がありますが、順を追って見ていきます。

IDisposable

リソースを解放するためのインタフェース。Dispoaseメソッドを持つのみ。

IMixedRealityService

すべての Mixed Reality Services の汎用インターフェース。

以下は、コードからコメントを削除したもの。割とシンプルです。が、内容としてはオブジェクトのライフサイクルにおける処理を担っているので重要なものと言えます。Monobehaviour を継承せずにライフサイクルを管理するためのインタフェースと考えれば分かりやすいと思います。

public interface IMixedRealityService : IDisposable
{
    string Name { get; }
    uint Priority { get; }

    BaseMixedRealityProfile ConfigurationProfile { get; }

    void Initialize();
    void Reset();
    void Enable();
    void Update();
    void LateUpdate();
    void Disable();
    void Destroy();
}

IMixedRealityEventSystem

Mixed Reality Toolkit と互換性のあるイベントシステムを実装するために使用されるインターフェイス。

Obsolete な物を除いたコードは以下。3つのメソッドを持つのみですが、ちょっと分かりにくいと感じるかもしれません。ただ具体的な処理はこのインタフェースを実装したクラス次第なので、こういったメソッドがあるんだなとだけ理解しておけば良いと思います。

public interface IMixedRealityEventSystem : IMixedRealityService
{
    // すべてのイベントを処理して目的の受信者に転送するための主な機能
    void HandleEvent<T>(BaseEventData eventData, ExecuteEvents.EventFunction<T> eventHandler)
        where T : IEventSystemHandler;

    // 引数に渡されたハンドラを、T インターフェースを介して処理されるすべてのイベントのグローバルリスナーとして登録する
     void RegisterHandler<T>(IEventSystemHandler handler) where T : IEventSystemHandler;

    // 登録されたハンドラーを解除する
     void UnregisterHandler<T>(IEventSystemHandler handler) where T : IEventSystemHandler;
}

なお、3つのメソッドすべてにおいて、型パラメータの T として IEventSystemHandler(もしくはその派生型)であることが指定されています。そのためこれを理解するには IEventSystemHandler については理解しておく必要があります。

tsubakit1.hateblo.jp

BaseService

IMixedRealityService を実装し、すべてのサービスのデフォルトプロパティを提供する。

抽象クラスであまり中身はありません。気になるところは以下ぐらいです。

  • Priority のデフォルト値が 10 に設定されている
  • Dispose メソッドの実装

BaseEventSystem

このクラスを継承することで、他のシステムの機能にイベント機能を持たせることができる基本イベントシステム。

継承元にある IMixedRealityService の実装は Destroy メソッド以外は含まれていないので、主な内容としては IMixedRealityEventSystem のデフォルト実装をしたものであるということでしょうか。 IMixedRealityEventSystem で Obsolete とされている EventListeners / Register メソッド / Unregister メソッドに関連する部分については省略しつつ、実装を見てみます。

// (このイベントシステムに)登録された型によってグループ化されたすべてのイベントハンドラーのディクショナリ
public Dictionary<Type, List<EventHandlerEntry>> EventHandlersByType

// RegisterHandler API を介して登録されたすべてのハンドラーにイベントを送信する
public virtual void HandleEvent<T>(BaseEventData eventData, ExecuteEvents.EventFunction<T> eventHandler) where T : IEventSystemHandler
{
    (略...)

    List<EventHandlerEntry> handlers;
    if (EventHandlersByType.TryGetValue(typeof(T), out handlers))
    {
        for (int i = handlers.Count - 1; i >= 0; i--)
        {
            var handlerEntry = handlers[i];
            
            (略...)
            
            // ★このメソッド内で一番重要な部分
            // EventHandlersByTypeから一致する型のインタフェースのハンドラーを取り出しイベントを実行する
            // つまり、
            // T 型のインタフェースを継承した handler インスタンスの eventHandler メソッドに
            // eventData パラメータを渡して実行する
            eventHandler.Invoke((T)handlerEntry.handler, eventData);
        }
    }

    (略...)
}

今の段階では、具体的にどのようなデータやハンドラーが渡ってくるのかは分かりませんが、何かしらのインタフェース(T)を実装したインスタンスのメソッドを実行する部分になります。 (なお、呼び出し元は MixedRealityInputSystem クラスの DispatchEventToGlobalListeners メソッドです)

このイベントのディスパッチ中に EventHandlersByType ディクショナリへの追加・削除を行うことは安全ではないため、eventExecutionDepth という変数と postponedActions というリストを用いて制御するような処理も入っています。

// 引数に渡されたハンドラー(IEventSystemHandlerを継承したインスタンス)を、T インターフェースを介して処理されるすべてのイベントのグローバルリスナーとして登録する
public virtual void RegisterHandler<T>(IEventSystemHandler handler) where T : IEventSystemHandler
{
    (略...)

    // IEventSystemHandler を除いた handler の親インターフェースも登録する
    TraverseEventSystemHandlerHierarchy<T>(handler, RegisterHandler);
}

// 登録されたハンドラーを解除する
public virtual void UnregisterHandler<T>(IEventSystemHandler handler) where T : IEventSystemHandler
{
    (略...)

    // IEventSystemHandler を除いた handler の親インターフェースも登録を解除する
    TraverseEventSystemHandlerHierarchy<T>(handler, UnregisterHandler);
}

EventHandlersByType ディクショナリおよびその内部のリストに型とハンドラーを登録・解除しています。 グローバルリスナー(オブジェクトにフォーカスが当たっていなくても入力を受け付けるもの)として登録・解除するところがポイントかと思います。

上記以外の部分については private メソッドなので、上記処理を行うための内部処理になります。

BaseCoreSystem

コンストラクタで プロファイルの設定Priorityを 5 に設定している(コアシステムのデフォルトの優先度は他のサービスよりも高い)だけで、他はObsolete。

IEventSystemHandler

Unity で準備されたインタフェース、中身は空。

IMixedRealityBaseInputHandler

すべての入力ハンドラーの基本インターフェース。

これにより、ExecuteEvents.ExecuteHierarchy<IIMixedRealityBaseInputHandler> を使用して、すべての入力ハンドリングインターフェイスにイベントを送信できる。

コードは以下の通りで中身は空。

public interface IMixedRealityBaseInputHandler : IEventSystemHandler {}

IMixedRealitySpeechHandler

音声認識に反応するために実装するインターフェース。

メソッドを 1 つだけ持つ。

public interface IMixedRealitySpeechHandler : IMixedRealityBaseInputHandler
{
    void OnSpeechKeywordRecognized(SpeechEventData eventData);
}

IMixedRealitySourceStatehandler

入力ソースが検出または失われたときなど、ソースの状態変化に対応するために実装するインターフェイス。

メソッドは以下の 2 つを持つ。

public interface IMixedRealitySourceStateHandler : IEventSystemHandler
{
    // ソースが検出されると発生する
    void OnSourceDetected(SourceStateEventData eventData);

    // ソースが失われたときに発生する
    void OnSourceLost(SourceStateEventData eventData);
}

IMixedRealityFocusProvider

ポインターのフォーカスを処理するフォーカスプロバイダーを実装するためのインタフェース。

以下は、日本語コメントを付けたもの。どのようなものがあるかだけ目を通しておき、あとは、継承先の FocusProvider を見ていくのが良いと思います。

public interface IMixedRealityFocusProvider : IMixedRealityService, IMixedRealitySourceStateHandler, IMixedRealitySpeechHandler
{
    // オーバーライド範囲がない限り、すべてのポインターがGameObjectと衝突できる最大距離
    float GlobalPointingExtent { get; }

    // レイキャストの対象となるフォーカスポインターのレイヤーマスク
    LayerMask[] FocusLayerMasks { get; }

    // EventSystem がレイキャストに使用するカメラ
    Camera UIRaycastCamera { get; }

    // 現在のプライマリポインタ。 使用中のプライマリポインターセレクターによって決定される(MixedRealityPointerProfile.PrimaryPointerSelectorを参照)
    IMixedRealityPointer PrimaryPointer { get; }

    // ポインティングソースの現在フォーカスされているオブジェクトを取得する
    // ポインティングソースが登録されていない場合、GazeのFocused GameObjectが返される
    GameObject GetFocusedObject(IMixedRealityPointer pointingSource);

    // ポインティングソースの現在フォーカスされているオブジェクトの詳細情報を取得する
    bool TryGetFocusDetails(IMixedRealityPointer pointer, out FocusDetails focusDetails);

    // 指定されたポインターのFocusDetailsを設定し、現在設定されているフォーカスポイントをオーバーライドする
    // これは、フォーカスがロックされている場合でも、特定のポインターのFocusDetailsを変更するために使用できる
    bool TryOverrideFocusDetails(IMixedRealityPointer pointer, FocusDetails focusDetails);

    // 新しい一意のポインターIDを生成する
    uint GenerateNewPointerId();

    // ポインタがフォーカスプロバイダーに登録されているかどうかを確認する
    bool IsPointerRegistered(IMixedRealityPointer pointer);

    // ポインタをフォーカスプロバイダーに登録する
    bool RegisterPointer(IMixedRealityPointer pointer);

    // フォーカスプロバイダーに登録されているポインタの登録を解除する
    bool UnregisterPointer(IMixedRealityPointer pointer);

    // 指定されたタイプのすべての登録済みポインターへのアクセスを提供する
    IEnumerable<T> GetPointers<T>() where T : class, IMixedRealityPointer;

    // プライマリポインターの変更をサブスクライブする
    void SubscribeToPrimaryPointerChanged(PrimaryPointerChangedHandler handler, bool invokeHandlerWithCurrentPointer);

    // プライマリポインタの変更のサブスクライブを解除する
    void UnsubscribeFromPrimaryPointerChanged(PrimaryPointerChangedHandler handler);
}

IPointerPreferences

入力システムのポインターの動作や、その他の設定を取得・設定するためのインターフェースを提供する。

動作は、ポインターごとではなく、ポインタータイプと入力タイプに基づいて記述される。

これは、表示される新しいポインターが一貫した動作を維持するようにするため。

public interface IPointerPreferences
{
    // 指定されたポインタの PointerBehavior を取得する
    PointerBehavior GetPointerBehavior(IMixedRealityPointer pointer);

    // 指定されたポインタータイプ、利き手、および入力タイプの PointerBehavior を取得する
    PointerBehavior GetPointerBehavior<T>(
        Handedness handedness,
        InputSourceType sourceType) where T : class, IMixedRealityPointer;

    // 指定されたポインタータイプ、利き手、および入力タイプの PointerBehavior を設定する
    void SetPointerBehavior<T>(Handedness handedness, InputSourceType inputType, PointerBehavior pointerBehavior) where T : class, IMixedRealityPointer;

    // 視線ポインターのポインター動作
    // 内部クラスであるため、内部視線ポインターは実際にはここから参照できないため、視線ポインターを一意にする
    PointerBehavior GazePointerBehavior { get; set; }
}

なお、PointerBehavior とは以下のようなポインターの動作を示す enum です。

public enum PointerBehavior
{
    // ポインターのアクティブ状態はMRTK入力システムによって管理される
    // ニアポインター(グラブ、ポーク)の場合、常に有効になる
    // ニアポインターでない場合、同じ手のニアポインターがアクティブな場合は無効になる
    // これは、手がグラブ可能なものの近くにあるときにレイをオフにすることを可能にするもの
    Default = 0,
    // 他のどのポインターがアクティブであるかに関係なく、ポインターは常にオン
    AlwaysOn,
    // 他のどのポインターがアクティブであるかに関係なく、ポインターは常にオフ
    AlwaysOff
};

FocusProvider

フォーカスプロバイダーは、入力ソースごとにフォーカスされたオブジェクトを処理する。

さてついに本題の FocusProvider です。

言うまでもなく、上で書いてきたすべてのインタフェースの実装をしたもので、確認のために継承関係の図を再掲します。

f:id:HiromuKato:20200721032008p:plain

また、FocusProvider は内部に PointerHitResult / PointerData / PointerPreferences クラスを持っています。PointerData クラスについては継承元があるので、継承関係を掲載します。

f:id:HiromuKato:20200721032046p:plain

PointerHitResult

中間ヒット結果を格納するためのヘルパークラス。ポインターの可能なすべてのヒットが処理されたら、PointerDataに適用する必要がある。

IEquatable

インスタンスの等価性を判断するためのインタフェース。

IPointerResult

ポインタの結果を定義するインターフェイス。

public interface IPointerResult
{
    // ポインターレイステップの開始点。
    Vector3 StartPoint { get; }

    // 現在フォーカスされているゲームオブジェクトの詳細。
    FocusDetails Details { get; }

    // 現在のポインターのターゲットGameObject
    GameObject CurrentPointerTarget { get; }

    // 前のポインターターゲット。
    GameObject PreviousPointerTarget { get; }

    // 最後のレイキャストヒットを生成したステップのインデックス。レイキャストヒットがない場合は0。
    int RayStepIndex { get; }
}

PointerData

ポインターに関する情報を持つ。

コンストラクタでポインターを渡され生成される。

ヒットしているオブジェクトの詳細情報などのポインターに関連した付随情報を持つ。

PointerPreferences

ポインターの動作を取得・設定するためのクラス(入力ソースのタイプ、ポインターのタイプ、手に応じたポインタービヘイビアーを持つ)


以降は、FocusProvider の実装にコメントを日本語で付けたものです。重要そうなメソッドなどは中身を簡略化しつつコメントを入れました。

/// フォーカスプロバイダーは、入力ソースごとにフォーカスされたオブジェクトを処理します。
/// 必要に応じてGaze Pointerのみを取得するための便利なプロパティがあります。
public class FocusProvider : BaseCoreSystem,
    IMixedRealityFocusProvider,
    IPointerPreferences
{
    // コンストラクタ
    public FocusProvider(MixedRealityInputSystemProfile profile) : base(profile) { ...}

    // このディクショナリにポインター情報を格納する
    private readonly Dictionary<uint, PointerData> pointers = new Dictionary<uint, PointerData>();

    private readonly HashSet<GameObject> pendingOverallFocusEnterSet = new HashSet<GameObject>();
    private readonly Dictionary<GameObject, int> pendingOverallFocusExitSet = new Dictionary<GameObject, int>();
    private readonly List<PointerData> pendingPointerSpecificFocusChange = new List<PointerData>();

    private readonly Dictionary<uint, IMixedRealityPointerMediator> pointerMediators =
        new Dictionary<uint, IMixedRealityPointerMediator>();

    private readonly PointerHitResult hitResult3d = new PointerHitResult();
    private readonly PointerHitResult hitResultUi = new PointerHitResult();

    // シーンクエリタイプが SphereOverlap の場合に対象とするコライダーの最大数
    private readonly int maxQuerySceneResults = 128;
    // 複合コライダーの場合、個々のコライダーはフォーカスを受け取るかどうか
    private bool focusIndividualCompoundCollider = false;

    public IReadOnlyDictionary<uint, IMixedRealityPointerMediator> PointerMediators => pointerMediators;

    // アクティブな IMixedRealityNearPointers の数
    public int NumNearPointersActive { get; private set; }

    // ゲイズカーソルを除く、アクティブなファーインタラクション(モーションコントローラーレイ、ハンドレイなど)をサポートするポインターの数
    public int NumFarPointersActive { get; private set; }

    // プライマリーポインターを取得・設定する(変更時は変更イベント発火)
    public IMixedRealityPointer PrimaryPointer { ... }
    private IMixedRealityPointer primaryPointer;

    #region IMixedRealityService Properties

    // 名前
    public override string Name { get; protected set; } = "Focus Provider";

    // プライオリティー(Default値 10, コアシステム 5 なので重要であることがわかる)
    public override uint Priority => 2;

    // ポインターの届く距離(プロファイルに設定されている値を取得する、デフォルト値は10)
    float IMixedRealityFocusProvider.GlobalPointingExtent { ... }

    // フォーカスするレイヤーマスク(PointerProfileから取得する)
    private LayerMask[] focusLayerMasks = null;
    public LayerMask[] FocusLayerMasks { ... }

    // uiRaycastCamera用のレンダリングテクスチャ(デバイスディスプレイの解像度に関係なくドラッグの閾値が同じように扱われるようするために利用)
    private RenderTexture uiRaycastCameraTargetTexture = null;

    // UIコンポーネントへのヒット判定するためのUIレイキャストカメラ
    private Camera uiRaycastCamera = null;
    public Camera UIRaycastCamera => uiRaycastCamera;

    #endregion IMixedRealityService Properties

    // このサービスを開始するために MixedRealityToolkit が正しくセットアップされているかどうかを確認する
    private bool IsSetupValid { ... }

    // GazeProvider は特殊なため、登録されたポインタでなくても追跡するようにする
    //   StabilizationPlaneModifier およびユーザーがどこを見ているかを気にする可能性のある他のコンポーネントのために、
    //   フォーカスにゲイズが使用されていない場合でも、ゲイズのレイキャストを行う必要がある
    private PointerData gazeProviderPointingData;
    private PointerHitResult gazeHitResult;

    // 新しいレイキャスト位置のキャッシュ(UIレイキャスト結果の更新にのみ使用される)
    private Vector3 newUiRaycastPosition = Vector3.zero;

    // 中間ヒット結果を格納するためのヘルパークラス
    // ポインターの可能なすべてのヒットが処理されたら、PointerDataに適用する必要がある
    private class PointerHitResult
    {
        public MixedRealityRaycastHit raycastHit;
        public RaycastResult graphicsRaycastResult;

        public GameObject hitObject;
        public Vector3 hitPointOnObject = Vector3.zero;
        public Vector3 hitNormalOnObject = Vector3.zero;

        public RayStep ray;
        public int rayStepIndex = -1;
        public float rayDistance;

        // 略...
    }

    // ポインターに関する情報を持つクラス
    [Serializable]
    private class PointerData : IPointerResult, IEquatable<PointerData>
    {
        // ポインター(コンストラクタで設定される)
        public readonly IMixedRealityPointer Pointer;

        // フォーカスしているオブジェクトの詳細情報
        public FocusDetails Details { ... }
        private FocusDetails focusDetails = new FocusDetails();

        // 略...

        // ポインターのヒット情報を更新する
        // FocusProvider の Update > UpdatePointers > UpdatePointer メソッドから呼ばれる
        public void UpdateHit(PointerHitResult hitResult)
        {
            // hitResult をもとに focusDetails を更新する
        }

        // フォーカスがロックされているときにフォーカス情報を更新する
        // (ロックされている場合は、UpdateHitではなくこっちが呼ばれる)
        // オブジェクトが動いている場合、ヒットポイントが新しいワールドトランスフォームに更新されます。
        public void UpdateFocusLockedHit()
        {
            // 
        }

        // 略...
    }

    // ゲイズポインターの有効・無効を切り替えるステートマシン(手が表示されたらゲイズカーソルを非表示にしたりなど)
    private readonly GazePointerVisibilityStateMachine gazePointerStateMachine = new GazePointerVisibilityStateMachine();

    // プライマリポインターを選択するために使用されるインターフェイス
    private IMixedRealityPrimaryPointerSelector primaryPointerSelector;

    // プライマリポインターの変更時に発生するイベント
    private event PrimaryPointerChangedHandler PrimaryPointerChanged;

    #region IMixedRealityService Implementation

    // FocusProvider の初期化処理
    public override void Initialize() { ... }

    // FocusProvider の終了処理
    public override void Destroy() { ... }

    // 毎フレーム行う処理
    public override void Update()
    {
        if (!IsSetupValid)
        {
            return;
        }

        UpdatePointers();      // ポインターの更新
        UpdateGazeProvider();  // ゲイズは特別扱い
        UpdateFocusedObjects();// フォーカスオブジェクトの更新

        PrimaryPointer = primaryPointerSelector?.Update();
    }

    // ゲイズがフォーカスに使用されていないシナリオでも、ゲイズレイキャストプロバイダーを更新する
    private void UpdateGazeProvider() { ... }

    #endregion IMixedRealityService Implementation

    #region Focus Details by IMixedRealityPointer

    // ポインターがフォーカスしているオブジェクトを取得する
    public GameObject GetFocusedObject(IMixedRealityPointer pointingSource) { ... }

    // ポインターがフォーカスしているオブジェクトの詳細を取得する
    public bool TryGetFocusDetails(IMixedRealityPointer pointer, out FocusDetails focusDetails) { ... }

    // フォーカス情報を上書きする
    public bool TryOverrideFocusDetails(IMixedRealityPointer pointer, FocusDetails focusDetails) { ... }

    #endregion Focus Details by IMixedRealityPointer

    #region Utilities

    // 新しいポインターIDを生成する
    public uint GenerateNewPointerId() { ... }

    // UIRaycastCamera を作成するためのユーティリティ
    private void FindOrCreateUiRaycastCamera() { ... }

    // UIRaycastCamera を廃棄する
    private void CleanUpUiRaycastCamera() { ... }

    // ポインターが pointers ディクショナリに登録されているかどうかを返す
    public bool IsPointerRegistered(IMixedRealityPointer pointer) { ... }

    // pointers ディクショナリにポインターを登録する
    public bool RegisterPointer(IMixedRealityPointer pointer) { ... }

    // すべてのポインターをメディエイター・ pointers ディクショナリに登録する
    // 初期化時および入力ソースが検出されたときに呼ばれる
    private void RegisterPointers(IMixedRealityInputSource inputSource) { ... }

    // ポインターの登録を解除する
    public bool UnregisterPointer(IMixedRealityPointer pointer) { ... }

    // T型のポインターのコレクションを取得する
    public IEnumerable<T> GetPointers<T>() where T : class, IMixedRealityPointer { ... }

    // プライマリーポインターが変更したときに通知をするようサブスクライブする
    public void SubscribeToPrimaryPointerChanged(PrimaryPointerChangedHandler handler,
        bool invokeHandlerWithCurrentPointer) { ... }

    // プライマリーポインター変更通知のサブスクライブを解除する
    public void UnsubscribeFromPrimaryPointerChanged(PrimaryPointerChangedHandler handler) { ... }

    // 指定されたポインティング入力ソースの登録済み PointerData を返す
    private bool TryGetPointerData(IMixedRealityPointer pointer, out PointerData data) { ... }

    // すべてのポインターを更新する
    private void UpdatePointers() { ... }

    // ポインターを更新する
    private void UpdatePointer(PointerData pointerData) 
    {
        // ポインターのOnPreSceneQuery関数を呼び出す。これにより、レイキャストの準備をする機会が与えられる
        pointerData.Pointer.OnPreSceneQuery();

        // 略...

        // ポインターがロックされている場合は、フォーカスされているオブジェクトをそのままにする
        // これにより、ポインターがオブジェクトを指していない場合でも、これらのオブジェクトでイベントを確実に実行できる
        if (pointerData.Pointer.IsFocusLocked && pointerData.Pointer.IsTargetPositionLockedOnFocusLock)
        {
            pointerData.UpdateFocusLockedHit();

            // 略...
        }
        else
        {
            // 略...

            // フォーカスされているオブジェクトを特定する
            QueryScene(pointerData.Pointer, raycastProvider, prioritizedLayerMasks, hitResult3d, maxQuerySceneResults, focusIndividualCompoundCollider);

            // 略...

            // ここでのみヒット結果を適用して、現在のターゲットの変更がフレームごとに1回だけ検出されるようにする
            pointerData.UpdateHit(hit);

            // 略...
        }
        // 略...

        // ポインターのOnPostSceneQuery関数を呼び出す。これにより、レイキャストの結果に応答する機会が与えられる
        pointerData.Pointer.OnPostSceneQuery();
    }

    // terminus をオブジェクトがヒットした位置に書き換えてRayStepを更新している
    // (名称からも長く伸びているレイをオブジェクトとヒットした位置まで切り詰めるというイメージだと思う)
    private void TruncatePointerRayToHit(IMixedRealityPointer pointer, PointerHitResult hit) { ... }

    // 優先度の高いヒット結果を取得する
    private PointerHitResult GetPrioritizedHitResult(PointerHitResult hit1, PointerHitResult hit2,
        LayerMask[] prioritizedLayerMasks) { ... }

    // アクティブなポインターを整頓するために非アクティブなポインターを無効にする
    private void ReconcilePointers()
    {
        // 略...

        // ゲイズポインターのステートマシンを更新する(ゲイズポインターを有効にするか無効にするかを設定する)
        gazePointerStateMachine.UpdateState() { ... }

        // 略...
    }

    #region Physics Raycasting

    // 球のオーバーラップ結果を保存するために使用されるコライダー
    private static Collider[] colliders = null;

    // シーンクエリを実行して、コライダーを備えたどのシーンオブジェクトが現在注視されているかを特定する
    private static void QueryScene(IMixedRealityPointer pointer, IMixedRealityRaycastProvider raycastProvider,
        LayerMask[] prioritizedLayerMasks, PointerHitResult hit, int maxQuerySceneResults,
        bool focusIndividualCompoundCollider)
    {
        // ポインターがヒットしているオブジェクトを見つける(フォーカスしているオブジェクトを特定する)処理
    }

    #endregion Physics Raycasting

    #region uGUI Graphics Raycasting

    // Unity グラフィックレイキャストを実行して、現在どの uGUI 要素がポイントされているかを特定する
    private void RaycastGraphics(IMixedRealityPointer pointer, PointerEventData graphicEventData,
        LayerMask[] prioritizedLayerMasks, PointerHitResult hit)
    {
        // ポインターがヒットしているuGUI要素を見つける(フォーカスしているUIオブジェクトを特定する)処理
    }

    // 単一のグラフィック RayStep をレイキャストする
    private bool RaycastGraphicsStep(PointerEventData graphicEventData, RayStep step, LayerMask[] prioritizedLayerMasks,
        out RaycastResult uiRaycastResult) { ... }

    #endregion uGUI Graphics Raycasting

    // 必要に応じて、フォーカスイベントを入力マネージャーに発生させる
    private void UpdateFocusedObjects()
    {
        // MixedRealityInputSystem の RaiseFocusEnter や RaiseFocusExit メソッド等が呼ばれ、
        // そして ExecuteEvents.ExecuteHierarchy(eventTarget, focusEventData, eventHandler) がよればイベントを実行
        // 最終的にターゲットオブジェクト(フォーカスしているオブジェクト)の OnFocusEnter / OnFocusExit メソッド等が呼ばれる

        // ★つまりフォーカスイベントの発生源がここ★
    }

    #endregion Utilities

    #region ISourceState Implementation

    // ソースが検出されたときにポインターを登録する
    public void OnSourceDetected(SourceStateEventData eventData) { ... }

    // ソースの検出が失われたときにポインターの登録を解除する
    public void OnSourceLost(SourceStateEventData eventData) { ... }

    #endregion ISourceState Implementation

    #region IMixedRealitySpeechHandler Implementation

    // 「select」の音声が認識されると、目または頭に基づくゲイズカーソルを再アクティブ化するフラグを立てる
    public void OnSpeechKeywordRecognized(SpeechEventData eventData) { ... }

    #endregion

    #region IPointerPreferences Implementation

    private List<PointerPreferences> customPointerBehaviors = new List<PointerPreferences>();

    // ポインターの動作(ビヘイビアー)を取得する
    public PointerBehavior GetPointerBehavior(IMixedRealityPointer pointer) { ... }
    // 指定されたポインタ型の動作(ビヘイビアー)を取得する
    public PointerBehavior GetPointerBehavior<T>(Handedness handedness, InputSourceType sourceType)
        where T : class, IMixedRealityPointer { ... }
    private PointerBehavior GetPointerBehavior(Type type, Handedness handedness, InputSourceType sourceType) { ... }

    // ポインターの動作(ビヘイビアー)のプロパティ
    public PointerBehavior GazePointerBehavior { get; set; } = PointerBehavior.Default;

    // ポインターの動作(ビヘイビアー)を設定する(PointerUtils から呼ばれる)
    // PointerUtilsから呼ばれてポインターの有効・無効の切り替えを行っている
    public void SetPointerBehavior<T>(Handedness handedness, InputSourceType inputType, PointerBehavior pointerBehavior)
        where T : class, IMixedRealityPointer { ... }

    // ポインター設定クラス(ソースのタイプ、ポインターのタイプ、手に応じたポインタービヘイビアーを持つ)
    private class PointerPreferences { ... }

    #endregion
}

分かりにくいと感じた部分についての補足

  • フォーカスがロックされている状態とは?

    • ざっくりとは、何かしらのオブジェクトにフォーカスが当たった状態でクリック(エアタップ)していればロックするもの

    もう少し詳しく書くと

    • RaisePointerDown で IMixedRealityPointerHandler インタフェースを実装したコンポーネントを付けたオブジェクトにフォーカスが当たっている場合、IsFocusLocked = true に設定される
    • RaisePointerUp で IsFocusLocked = false に設定される

    • GGVPointer は OnInputDown 時にオブジェクトにフォーカスが当たっていれば IsFocusLocked = true そうでなければ false に設定される

    • GGVPointer は OnInputUp 時に IsFocusLocked = false に設定される

    ロックされている間は、ポインターがオブジェクトを指していない場合でもフォーカスされているものとして扱うためのもので、ポインターが当たっていないオブジェクトでもフォーカスイベントを確実に実行するための仕組み。

  • プライマリーポインターとは?

  • RayStep とは?

    • CurvePointerを生成するときに以下のように生成されています。

      protected int LineCastResolution = 10;

      Rays = new RayStep[LineCastResolution];

    • CurvePointer 以外の LinePointer や PokePointer や SpherePointer では 要素は1つしか持っていません。

      Rays = new RayStep[1];

    • GenericPointer も以下の通り要素は1つです。

      public RayStep[] Rays { get; protected set; } = { new RayStep(Vector3.zero, Vector3.forward) };

    つまり「レイを何分割するか」というもので、カーブを描いていなければ基本的に1直線なので1つのレイと考えればよさそうです。

  • TruncatePointerRayToHit メソッドは何をしているか?

    一番優先度が高いレイヤーのオブジェクトとヒットしている場合は(それ以上先にレイが伸びている必要がないので)レイの終点をこのヒット位置まで切り詰める(ということだと思う)。

その他ポイント

  • MixedRealityInputSystemProfile で Focus Provider Type として FocusProvider が設定されている

  • GazeProviderは特殊扱いする(登録されたポインタでなくても追跡する)

    • StabilizationPlaneModifierおよびユーザーがどこを見ているかを気にする可能性のある他のコンポーネントのために、フォーカスに視線が使用されていない場合でも、視線レイキャストを行う必要がある

まとめ

FocusProvider の実装について確認しました。

概要については冒頭に書いた通りです。

今後も入力周りを中心に、(自分のメモとして)徐々に内部の実装について確認していきたいと思います。

なお、FocusProvider は以下の通り MRTK の中ではライン数がベスト 10 に入るヘビー級のクラスです。