e.blog

主にUnity/UE周りのことについてまとめていきます

XR Plug-in ManagementによるXR機能(サブシステム)の仕組みを追う

概要

普段の開発でクロスプラットフォームの対応をよくしている関係で、XR Plug-in Managementの仕組みに興味を持って調べてみました。今回はXR機能(以後、サブシステム)の仕組みについてまとめていきたいと思います。

ちなみに以下の画面で設定するプロバイダと実際にそれを使う仕組みのことです。

今回の調査にあたって以下のリポジトリの実装を参考にさせていただきました。

github.com



全体のフロー

大きく分けて3つの流れがあります。

  1. LoaderやSettingsなど、サブシステムを提供するにあたっての情報を定義する
  2. 指定されたLoaderからサブシステムを取得、実行時に利用できるようにセットアップする
  3. 実行時にサブシステムを取得、生成して利用する

(1)については概要に載せたウィンドウなどで設定を行えるようにするもので、(2)と(3)については実行時に処理されるものとなっています。

ということで、まずは最初のLoaderとSettings周りについて見ていきましょう。

サブシステムの設定

設定についてはさらに以下2つのパートに分けることができます。

  1. Project Settingsに項目を表示する
  2. パッケージのセットアップ(必要なパッケージのダウンロード設定など)

まずはシンプルな(1)から見ていきます。

Project Settingsに項目を表示する

XR Plug-in Management をインストールするとその下にさらに複数の項目が表示されます。(冒頭の図では ARCore が表示されています)

まずはここに表示するための処理を見ていきましょう。

XRConfigurationDataAttribute

実はこれ自体はとてもシンプルです。 XRConfigurationDataAttribute という属性が定義されており、以下のように定義するだけで自動的に認識され、 XR Plug-in Management の項に追加されます。

using UnityEngine;
using UnityEngine.XR.Management;

[System.Serializable]
[XRConfigurationData("TestXRMock", "com.edo.testxrmock.loader")]
public class TestXRMockLoaderSettings : ScriptableObject
{
    [SerializeField] private bool _hoge = true;
}

このスクリプトをプロジェクトに追加すると以下のように自動的に項目が表示されます。ちなみに属性の第一引数が項目名、第二引数がIDになっており、他とかぶらないユニークなIDを指定する必要があります。

パッケージのセットアップ

次に、 XR Plug-in Management から設定を行うと自動的にアセットなどのダウンロードを行ったり、利用するデータの設定を行うための実装を行います。

これを実装すると以下のように、Providerの一覧に表示されるようになります。

ここで重要になってくるインターフェースが以下です。

  • IXRPackage
  • IXRPackageMetadata
  • IXRLoaderMetadata

特に、 IXRPackage インターフェースを実装することで自動的にProviderのリストに表示されるようになります。残りのふたつのインターフェースはメタデータを表現し、例えばどのプラットフォーム向けに提供するProviderなのか、などの定義を行います。

今回のために作ったサンプルはコード量はそこまで多くないのでまずは全文を載せてしまいましょう。

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.XR.Management.Metadata;
using UnityEngine;

internal class TestXRMockPackage : IXRPackage
{
    private class TestXRMockLoaderMetadata : IXRLoaderMetadata
    {
        public string loaderName { get; set; }
        public string loaderType { get; set; }
        public List<BuildTargetGroup> supportedBuildTargets { get; set; }
    }

    private class TestXRMockPackageMetadata : IXRPackageMetadata
    {
        public string packageName { get; set; }
        public string packageId { get; set; }
        public string settingsType { get; set; }
        public List<IXRLoaderMetadata> loaderMetadata { get; set; }
    }

    private static IXRPackageMetadata s_metaData = new TestXRMockPackageMetadata()
    {
        packageName = "Test XR Mock",
        packageId = "com.edo.test-xr-mock",
        settingsType = typeof(TestXRMockLoaderSettings).FullName,
        loaderMetadata = new List<IXRLoaderMetadata>()
        {
            new TestXRMockLoaderMetadata()
            {
                loaderName = "Test XR Mock",
                loaderType = typeof(TestXRMockLoader).FullName,
                supportedBuildTargets = new List<BuildTargetGroup>
                {
                    BuildTargetGroup.Android,
                    BuildTargetGroup.Standalone,
                },
            }
        }
    };

    public bool PopulateNewSettingsInstance(ScriptableObject obj)
    {
        TestXRMockLoaderSettings loaderSettings = obj as TestXRMockLoaderSettings;
        if (loaderSettings == null)
        {
            return false;
        }

        return true;
    }

    public IXRPackageMetadata metadata => s_metaData;
}

特に重要な部分は以下の IXRPackageMetadata を生成している部分です。ここで、どう画面に表示されるのか、どのアセットを必要とするのかを定義します。

private static IXRPackageMetadata s_metaData = new TestXRMockPackageMetadata()
{
    packageName = "Test XR Mock",
    packageId = "com.edo.test-xr-mock",
    settingsType = typeof(TestXRMockLoaderSettings).FullName,
    loaderMetadata = new List<IXRLoaderMetadata>()
    {
        new TestXRMockLoaderMetadata()
        {
            loaderName = "Test XR Mock",
            loaderType = typeof(TestXRMockLoader).FullName,
            supportedBuildTargets = new List<BuildTargetGroup>
            {
                BuildTargetGroup.Android,
                BuildTargetGroup.Standalone,
            },
        }
    }
};

packageName は画面に表示される名前なので説明不要でしょう。

続く packageId は、参照する(実際に機能を提供する)アセットのパッケージIDを指定します。Providerのチェックを入れた際に、もしまだプロジェクトにパッケージがない場合は自動的にダウンロードされます。

例えばARCoreであれば、ARCore Pluginパッケージが自動的にダウンロードされる、という具合です。


ローカルパッケージを利用する

もしレジストリを利用しているわけではなく、機能をローカルで持っている場合はローカルのアセットとしてPackagesに追加すれば利用することができます。 package.json にローカルのパッケージを登録したあとに、そのIDを指定すればOKです。


settingsType は前述した XRConfigurationDataAttribute を付与したクラスの FullName を指定します。これはシステムが自動でアセットを生成するために必要となります。

上記までを実装し、XR Plug-in ManagementでProviderにチェックを入れると以下のようにアセットが生成されます。

※ まだローダについては説明していません。実際に表示するためにはローダの実装も必要です。ローダについては後述します。以下のキャプチャで Test XR Mock Loader となっている部分です。

サブシステムのセットアップ

前述の設定周りの実装により、XR Plug-in Managementの画面に表示されるようになり、さらにサブシステムを生成するための準備が整いました。ここからは、実際にモックのサブシステムの実装を見ながら、セットアップのフローを見ていこうと思います。

前述の設定内に TestXRMockLoader のクラス名の記述がありました。Loaderの名前が示す通り、このアセットが具体的なサブシステムのロード(つまり生成)を司ります。

詳細は後述しますが、実行時にはアクティブなLoaderを取り出し、そのLoaderの初期化処理を呼び出す仕組みになっています。つまり、これから説明するLoaderの実装はその初期化のタイミングで呼び出される処理となります。

Loaderの実装

興味深いことに、Loaderは ScriptableObject を継承したベースクラスがあり、設定などについてはファイルとして保存されています。前述の画像を再掲すると Assets/XR/Loaders に自動的に保存されるファイルがそれです。ARCoreやARKitを利用したことがある人は自動的にファイルが生成されているのを見たことがあると思います。ここではまさに、このLoaderの実装を行っていく、ということになります。

Loader実装の予備知識

実際の実装をしていく前に、いくつか予備知識を確認しておきましょう。

前述の通り、Loaderのベースクラスは ScriptableObject です。継承関係は以下のようになっています。

実は XRLoaderabstract クラスになっていて、これを継承した XRLoaderHelper というクラスが存在します。Helperの名前の通り、サブシステムの構築において便利なメソッドなどが定義されているクラスです。そのため、自作のLoaderを作成する場合はこの XRLoaderHelper を継承することになります。

具体的にどんな内容があるか簡単に見ておくと、以下のようなメソッドが定義されています。

  • T GetLoadedSubsystem<T>();
  • void StartSubsystem<T>();
  • void CreateSubsystem<TDescriptor, TSubsystem>(List<TDescriptor> descriptors, string id);

なんとなくどういうことをやってくれるクラスかが見えてくるかと思います。
ちなみに XRLoaderHelperabstract クラスになっています。


さて、では実際に実装を見ていきましょう。

初期化処理

まず最初に見るのは初期化処理です。いくつかの処理を経てLoaderの Initialize() メソッドが呼び出されるようになっています。(メソッドが呼び出されるまでのフローは後述)

Loaderの名前が示す通り、 Initialize() メソッド内で各機能を提供するサブクラスの生成を行います。

ごくシンプルな初期化処理の実装コードを載せます。以下では XRSessionSubsystemXRCameraSubsystem のみを生成するサンプルコードとなっています。実際には、提供する機能のサブシステム分の実装が必要となります。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Management;
using UnityEngine.XR.ARSubsystems;

public class TestXRMockLoader : XRLoaderHelper
{
    private static List<XRSessionSubsystemDescriptor> s_sessionSubsystemDescriptors = new List<XRSessionSubsystemDescriptor>();
    private static List<XRCameraSubsystemDescriptor> s_cameraSubsystemDescriptors = new List<XRCameraSubsystemDescriptor>();

    // ↑必要なサブシステムの数分、これらのListを定義する必要がある。

    public override bool Initialize()
    {
        XRSessionSubsystem sessionSubsystem = GetLoadedSubsystem<XRSessionSubsystem>();

        if (sessionSubsystem != null)
        {
            return true;
        }

        Debug.unityLogger.Log("xr-mock", $"Initializing {nameof(TestXRMockLoader)}");

        CreateSubsystem<XRSessionSubsystemDescriptor, XRSessionSubsystem>(s_sessionSubsystemDescriptors, typeof(TestXRMockSessionSubsystem).FullName);
        CreateSubsystem<XRCameraSubsystemDescriptor, XRCameraSubsystem>(s_cameraSubsystemDescriptors, typeof(TestXRMockCameraSubsystem).FullName);
        
        sessionSubsystem = GetLoadedSubsystem<XRSessionSubsystem>();
        if (sessionSubsystem == null)
        {
            Debug.unityLogger.LogError("xr-mock", "Failed to load session subsystem.");
        }

        return sessionSubsystem != null;
    }
}

上記の初期化処理はランタイム時にLoaderが決定され(*1)、選択されたLoaderの Initialize() メソッドが呼び出されます。ここで行っている処理は CreateSubsystem<T1, T2>() メソッドを利用してサブシステムを生成することです。このメソッドはヘルパーである XRLoaderHelper クラスで実装されており、後述するDescriptorとIDを指定することでインスタンスを生成しています。

*1 ... 複数のLoaderが設定できるため、有効なひとつのLoaderを選択する仕組みになっている。

ちなみに生成に関しては Activator.CreateInstance(System.Type type); メソッドを利用しています。生成している箇所を抜粋してみましょう。

// SubsystemDescriptorWithProvider`2.cs

public TSubsystem Create()
{
    if (SubsystemManager.FindStandaloneSubsystemByDescriptor((SubsystemDescriptorWithProvider) this) is TSubsystem subsystemByDescriptor)
    return subsystemByDescriptor;
    TProvider provider = this.CreateProvider();
    if ((object) provider == null)
    return default (TSubsystem);
    TSubsystem subsystem = this.subsystemTypeOverride != null ? (TSubsystem) Activator.CreateInstance(this.subsystemTypeOverride) : new TSubsystem();
    subsystem.Initialize((SubsystemDescriptorWithProvider) this, (SubsystemProvider) provider);
    SubsystemManager.AddStandaloneSubsystem((SubsystemWithProvider) subsystem);
    return subsystem;
}

ん? Descriptor? と思っていた人もいるかもしれません。XR Plug-in Managementシステムの特徴として、Descriptorによってサブシステムを生成し、Providerによって実機能を提供するという構造になっています。なので CreateSubsystem メソッドでも、型引数に指定しているのは Descriptor でした。

別の言い方をするとサブシステムは、

  • Descriptorによって定義され、
  • Providerによって機能を提供し、
  • ISubsystem インターフェースによって機能を公開する

となります。

サブシステムは ISubsystem インターフェースを実装したものを期待されており、ARFoundationなどの高APIから低レイヤーの機能を呼び出すためのインターフェースになっているわけです。

Descriptorによるサブシステムの登録

前段ではDescriptorという名前が出てきました。Descriptorはサブシステムごとに用意することになっておりサブシステムの生成処理を担います。

なぜDescriptorが生成処理を担うかというと、システムの裏には SubsystemDescriptorStore というクラスが存在しており、 Store の名前の通りDescriptorを複数保持する形になっています。実はLoaderの初期化処理よりも前にDescriptor郡がすでに多数登録されており、初期化のタイミングで対象のDescriptorを取り出して生成を依頼する、という形になっているのです。リストにまとめるとフローは以下の通り。

  1. 各サブシステムのDescriptorを SubsystemDescriptorStore へ登録する
  2. システムが適切なLoaderを決定する
  3. 選択されたLoaderが各サブシステムを、Descriptorを通じて生成する
  4. 各サブシステムを利用するクラス(*2)にDIする

*2 ... SubsystemLifecycleManager クラスを継承した ARCameraManager などがあります。

ちなみに(4)のDI部分ですが、ジェネリクスによってそれを実現しています。詳細については後述します。

登録処理はRuntimeInitializeOnLoadMethodAttributeを使用

前述のDescriptorの登録処理は、各サブクラスが利用される前に登録が完了していないとならないため、かなり早いタイミングで行われています。これを実現するために [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] をstaticメソッドに付与し、シーンのロードなどよりも早い段階で処理されるようになっています。

今回実装した独自クラスの実装部分を掲載します。

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
internal static void Register()
{
    XRSessionSubsystemDescriptor.RegisterDescriptor(new XRSessionSubsystemDescriptor.Cinfo
    {
        id = typeof(TestXRMockSessionSubsystem).FullName,
        providerType = typeof(MockProvider),
        subsystemTypeOverride = typeof(TestXRMockSessionSubsystem),
        supportsInstall = false,
        supportsMatchFrameRate = false,
    });
}

RuntimeInitializeOnLoadMethod はその名前の通り、ランタイム時の初期化処理を実装するために指定する属性です。さらに引数にはいくつかのタイプがあり、ここで指定している RuntimeInitializeLoadType.SubsystemRegistration は、この属性の中でも一番早く実行されます。これによって、シーンロードの前、サブシステムが実際に必要になるタイミングよりも早い段階でDescriptorの登録が完了している、というわけなんですね。

Descriptorの登録にはそのDescriptorの中で定義されている Cinfo 構造体を用いて登録しています。フィールドは主に、そのサブシステムが期待されている機能リストで、作成しているサブシステムがどの機能をサポートするか、などの情報を指定するようになっています。また一番大事な部分として idproviderTypesubsystemTypeOverride の指定があります。

id はLoaderが名前解決に利用するIDとなっていて、同じIDのものが選択されインスタンス化されます。またインスタンス化されるサブシステムとProviderはそれぞれ subsystemTypeOverrideproviderType で指定したクラスです。そのためこのみっつは、独自実装したクラスと紐づける必要があります。

XRSessionSubsystemDescriptor.RegisterDescriptor という名前から推測できる通り、ARFoundationなどが期待する各XR関連のサブクラスがそれぞれ用意されており、さらにそれぞれにDescriptorが存在しています。登録処理はそのベースクラスとなる XR****SubsystemDescriptor クラスが担当してくれるため、基本的にはそれらを利用するだけで済むでしょう。

Loaderの決定

今まではDescriptorの登録やLoaderの初期化処理について書いてきました。ここでは、そもそもLoaderはどう決定されるのかについて見ていこうと思います。

ちなみにLoaderの決定がなにを意味しているかというと、XR Plug-in Managementは複数のLoaderを登録することができるようになっています。つまり、「どのLoaderを採用すべきか」を決定しないとならないということです。

XR Plug-in ManagementウィンドウのPlug-in Providersの数だけLoaderがあると考えるといいでしょう。そして以下の画像を見てわかる通り、チェックボックスのため複数設定することができるようになっています。この中から、どのLoaderを使うべきか、を判定する必要があるというわけです。

Loaderの処理フロー

Loaderの処理フローを見てみましょう。

※ ちなみに、Loaderの処理フローはEditorとビルド後で挙動が異なります。ここで解説するのはあくまでビルドされたあとの話となります。

  1. XRGeneralSettingsAwake (*3)で s_RuntimeSettingsInstancethis を設定し、どこからでも参照できるようにする(シングルトン)
  2. XRGeneralSettings.AttemptInitializeXRSDKOnLoad()[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] にて呼び出し
  3. (2)を経て、XRGenralSettingsInitXRSDK() インスタンス・メソッドの呼び出し
  4. XRGeneralSettings が保持している XRManagerSettingsInitializeLoaderSync() インスタンス・メソッドの呼び出し

*3 ... XRGeneralSettings は以下のようにアセット化されているものです。 ScriptableObject なので Awake はかなり早いタイミングで実行されます。

という流れを経て、最後の InitializeLoaderSync() により、アクティブなLoaderが決定されます。実際に処理を行っている部分を見てみると以下のようになっています。

public void InitializeLoaderSync()
{
    if (activeLoader != null)
    {
        Debug.LogWarning(
            "XR Management has already initialized an active loader in this scene." +
            " Please make sure to stop all subsystems and deinitialize the active loader before initializing a new one.");
        return;
    }

    foreach (var loader in currentLoaders)
    {
        if (loader != null)
        {
            if (CheckGraphicsAPICompatibility(loader) && loader.Initialize())
            {
                activeLoader = loader;
                m_InitializationComplete = true;
                return;
            }
        }
    }

    activeLoader = null;
}

登録されているLoaderの中からひとつを取り出して設定、初期化処理を実行しているのが確認できます。 loader.Initialize() の部分が、前述のLoaderの初期化処理部分ですね。こうしてサブシステム郡が生成される、というわけです。

ちなみに、 currentLoadersm_LoaderManagerInstance はともに SerializeField になっており、Editor側でそれを設定したものをビルド時に含めていると思われます。そのため、ビルド後は設定処理がされていません。

サブシステムの実装

ここからは、実際に各サブシステムをどう実装すればいいのかについて見ていきます。

まず把握するべき点として、サブシステムを構成するクラス図があります。例として XRSessionSubsystem のクラス図を図にすると以下のようになります。

図中の SubsystemWithProvider_3 は実際にはジェネリクス版のクラスとなっていますが、PlantUMLでそれが描けなかったので _3 で代用しています。実際のクラス定義は以下となっています。

public abstract class SubsystemWithProvider<TSubsystem, TSubsystemDescriptor, TProvider> : 
  SubsystemWithProvider
  where TSubsystem : SubsystemWithProvider, new()
  where TSubsystemDescriptor : SubsystemDescriptorWithProvider
  where TProvider : SubsystemProvider<TSubsystem>
{
    // 略
}

サブシステムクラスの構造

自分ははじめ、なぜ WithProvider という名前がついているのだろうと疑問に思っていました。しかし理解するとなんのことはない、サブクラス内で一緒に Provider を定義することを明示していただけなのでした。

図に取り上げた XRSessionSubsystem の実装を見てみると以下のようになっています。

public class XRSessionSubsystem
    : SubsystemWithProvider<XRSessionSubsystem, XRSessionSubsystemDescriptor, XRSessionSubsystem.Provider>
{
    // ... 中略 ...

    /// <summary>
    /// The API this subsystem uses to interop with
    /// different provider implementations.
    /// </summary>
    public class Provider : SubsystemProvider<XRSessionSubsystem>
    {
        // ... 中略 ...
    }
}

XRSessionSubsystem の内部クラスとして Provider が定義されています。このようにして、サブクラスの内部で Provider を定義することを期待しているために WithProvider という名前が与えられてるのだと思います。

サブクラスの実装詳細

次は実際にサブクラスの実装の詳細を見ていきましょう。ここでは、冒頭で紹介したリポジトリのものを引用させていただきました。ここから、どうやって自作のサブシステムを作っていけばいいかが見えてきます。まずはコード全文を見てみましょう。

ちなみに以下に公開されています。

github.com

using System;
using UnityEngine.Scripting;
using UnityEngine.XR.ARSubsystems;

namespace UnityEngine.XR.Mock
{
    [Preserve]
    public sealed class UnityXRMockSessionSubsystem : XRSessionSubsystem
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        internal static void Register()
        {
            XRSessionSubsystemDescriptor.RegisterDescriptor(new XRSessionSubsystemDescriptor.Cinfo
            {
                id = typeof(UnityXRMockSessionSubsystem).FullName,
                providerType = typeof(MockProvider),
                subsystemTypeOverride = typeof(UnityXRMockSessionSubsystem),
                supportsInstall = false,
                supportsMatchFrameRate = false
            });
        }

        private class MockProvider : Provider
        {
            private TrackingState? prevTrackingState;
            private Guid m_sessionId;

            [Preserve]
            public MockProvider()
            {
                this.m_sessionId = Guid.NewGuid();
            }

            public override Guid sessionId => this.m_sessionId;

            public override Feature currentTrackingMode => Feature.AnyTrackingMode;

            public override int frameRate => Mathf.RoundToInt(1.0f / Time.deltaTime);

            public override IntPtr nativePtr => IntPtr.Zero;

            public override Feature requestedFeatures
                => Feature.AnyTrackingMode
                | Feature.AnyCamera
                | Feature.AnyLightEstimation
                | Feature.EnvironmentDepth
                | Feature.EnvironmentProbes
                | Feature.MeshClassification
                | Feature.PlaneTracking
                | Feature.PointCloud;

            public override Feature requestedTrackingMode
            {
                get => Feature.AnyTrackingMode;
                set { }
            }

            public override TrackingState trackingState => SessionApi.trackingState;

            public override NotTrackingReason notTrackingReason => NotTrackingReason.None;

            public override Promise<SessionInstallationStatus> InstallAsync() => new SessionInstallationPromise();

            public override Promise<SessionAvailability> GetAvailabilityAsync() => new SessionAvailabilityPromise();

            public override void Start()
            {
                SessionApi.Start();
                base.Start();
            }

            public override void Stop()
            {
                SessionApi.Stop();
                base.Stop();
            }

            public override void Destroy()
            {
                SessionApi.Reset();
                base.Destroy();
            }

            public override void OnApplicationPause()
            {
                prevTrackingState = SessionApi.trackingState;
                SessionApi.trackingState = TrackingState.None;
                base.OnApplicationPause();
            }

            public override void OnApplicationResume()
            {
                SessionApi.trackingState = prevTrackingState ?? TrackingState.Tracking;
                base.OnApplicationResume();
            }
        }

        private class SessionInstallationPromise : Promise<SessionInstallationStatus>
        {
            public SessionInstallationPromise()
            {
                this.Resolve(SessionInstallationStatus.Success);
            }

            public override bool keepWaiting => false;

            protected override void OnKeepWaiting() { }
        }

        private class SessionAvailabilityPromise : Promise<SessionAvailability>
        {
            public SessionAvailabilityPromise()
            {
                this.Resolve(SessionAvailability.Supported | SessionAvailability.Installed);
            }

            public override bool keepWaiting => false;

            protected override void OnKeepWaiting() { }
        }
    }
}

コードはとてもシンプルですね。概観するとおおよそ以下のような感じでしょう。

  • XRSessionSubsystem を継承する
  • RuntimeInitializeOnLoadMethodAttribute を利用してサブシステムの登録を行う
  • 内部クラスで Provider クラスを定義する
  • Provider クラスのベースクラスで期待されている機能を実装する
  • 実際の機能の提供はさらに別クラスの SessionApi クラスが担当している

という感じでしょうか。

ここから分かるのは、機能提供のインターフェースとしての Provider をサブシステムが提供し、その具体的な実装をAPIという形で利用しているという点です。参考にしたものはモック用のものなのですべてC#で実装されていますが、これがARCoreやARKitの場合はネイティブ実装を呼び出す、名前通りAPIとして振る舞うクラスが後ろにいると考えるとイメージしやすいでしょう。

SessionApi実装はごくごくシンプルです。コードも短いので見てみましょう。

using UnityEngine.XR.ARSubsystems;

public static class SessionApi
{
    public static TrackingState trackingState { get; set; } = TrackingState.None;

    public static void Start()
    {
        trackingState = TrackingState.Tracking;
    }

    public static void Stop()
    {
        trackingState = TrackingState.None;
    }

    public static void Reset()
    {
        trackingState = TrackingState.None;
    }
}

たんに enum の値を変更しているのみですね。実際にデバイス依存の機能を実装する場合は、こうした形でネイティブ呼び出しをすればいいでしょう。あるいは、例えば、開発効率を爆上げしてくれる ARFoundation Remote というアセットがありますが、こうした機能を実装したい場合は、デバイスから送られてくるデータを流す機能を提供すれば似た機能が実装できるでしょう。

assetstore.unity.com

なんとなく、サブシステムがどう実装されているか見えてきたと思います。

これ以外のサブクラス(例えば XRCameraSubsystem など)もほぼ同様の構成になっています。ひとつサブクラスの仕組みを知ってしまえば他のサブクラスの実装は応用で実装することができると思います。

ということで実装については以上です。最後に、これらをどう使っているかを確認して全体像把握を完成させましょう。

サブシステムの利用

前段までで、サブシステムがどう定義され、どう登録され、どうLoadされるのかについて見てきました。最後に見ていくのは、実際にこれらを利用する部分についてです。興味深いことに、登録したサブクラスの利用はジェネリクスによって実現されています。

利用するためのベースクラス SubsystemLifecycleManager

ここではよく見る ARSession クラスを題材にして見ていきましょう。ARFoundationを利用しているとシーンに配置するあれです。 ARSessionSubsystemLifecycleManager を継承して作られています。そしてこの SubsystemLifecycleManager がまさにサブシステムを利用するための機能を提供してくれているクラスとなります。

サブクラスはいわゆるDIされる形で利用されています。DIの仕組みとしてジェネリクスを利用しているのが興味深いところでしょう。 SubsystemLifecycleManager のクラス宣言部分を見てみると以下のようになっています。

public class SubsystemLifecycleManager<TSubsystem, TSubsystemDescriptor, TProvider> : MonoBehaviour
    where TSubsystem : SubsystemWithProvider<TSubsystem, TSubsystemDescriptor, TProvider>, new()
    where TSubsystemDescriptor : SubsystemDescriptorWithProvider<TSubsystem, TProvider>
    where TProvider : SubsystemProvider<TSubsystem>
{
    // ... 略 ...
}

今まで見てきたクラスが期待されているのが分かりますね。具体的には SubsystemWithProvider, SubsystemDescriptorWithProvider, SubsystemProvider の3つです。

続いて ARSession の宣言部分を見てみましょう。

public sealed class ARSession :
    SubsystemLifecycleManager<XRSessionSubsystem, XRSessionSubsystemDescriptor, XRSessionSubsystem.Provider>
{
    // ... 略 ...
}

まさに、今まで解説してきたクラス、つまり XRSessionSubsystem, XRSessionSubsystemDescriptor, XRSessionSubsystem.Provider が指定されていますね。この指定によって、利用したいサブクラスおよびプロバイダを呼び出すことができるようになっているわけです。そしてインスタンス化されている実際のクラスはDescriptorによって生成されているため、ベースクラス型を指定するだけで実クラスの詳細を気にすることなく利用することができるわけです。

では肝心の、サブクラスのインスタンスを取得している部分を見てみましょう。

protected TSubsystem GetActiveSubsystemInstance()
{
    TSubsystem activeSubsystem = null;

    // Query the currently active loader for the created subsystem, if one exists.
    if (XRGeneralSettings.Instance != null && XRGeneralSettings.Instance.Manager != null)
    {
        XRLoader loader = XRGeneralSettings.Instance.Manager.activeLoader;
        if (loader != null)
            activeSubsystem = loader.GetLoadedSubsystem<TSubsystem>();
    }

    if (activeSubsystem == null)
        Debug.LogWarningFormat($"No active {typeof(TSubsystem).FullName} is available. Please ensure that a " +
                               "valid loader configuration exists in the XR project settings.");

    return activeSubsystem;
}

先に説明したLoaderの GetLoadedSubsystem メソッドからインスタンスを取得しているのが分かります。このメソッドはヘルパークラスである XRLoaderHelper に実装されています。合わせてそれも確認してみましょう。

public override T GetLoadedSubsystem<T>()
{
    Type subsystemType = typeof(T);
    ISubsystem subsystem;
    m_SubsystemInstanceMap.TryGetValue(subsystemType, out subsystem);
    return subsystem as T;
}

前述の CreateSubsystem で生成されたインスタンスを取り出して返しているのが分かります。該当メソッドの実装は以下のようになっています。

protected void CreateSubsystem<TDescriptor, TSubsystem>(List<TDescriptor> descriptors, string id)
    where TDescriptor : ISubsystemDescriptor
    where TSubsystem : ISubsystem
{
    if (descriptors == null)
        throw new ArgumentNullException("descriptors");

    SubsystemManager.GetSubsystemDescriptors<TDescriptor>(descriptors);

    if (descriptors.Count > 0)
    {
        foreach (var descriptor in descriptors)
        {
            ISubsystem subsys = null;
            if (String.Compare(descriptor.id, id, true) == 0)
            {
                subsys = descriptor.Create();
            }
            if (subsys != null)
            {
                m_SubsystemInstanceMap[typeof(TSubsystem)] = subsys;
                break;
            }
        }
    }
}

Descriptorによってサブクラスが生成されているのが分かります。ちなみに生成箇所をもう一度見てみると以下のようになっています。

CreateSubsystem<XRSessionSubsystemDescriptor, XRSessionSubsystem>(s_SessionSubsystemDescriptors, typeof(UnityXRMockSessionSubsystem).FullName);

ジェネリクスとして指定しているのはベースクラスである XRSessionSubsystem ですが、実際に生成されるのは UnityXRMockSessionSubsystem です。Descriptorはどこからくるか? そう、 UnityXRMockSessionSubsystem の Registerメソッドによって自身を生成するDescriptorが指定されているのでしたね。こうしてすべての機能がつながりました。

最後に

イチから理解するにはやや規模の大きいシステムとなっていますが、理解してしまえばあまりむずかしいところはありません。ジェネリクスの使い方なども面白く、とても学びのあるコードリーディングとなりました。

特に、シーンロード前などにシステムがセットアップされているのは、独自でシステムを構築する際にはとても参考になりそうな実装になっていました。普段なにげなく使っているARFoundationですが、裏では結構色々とがんばってくれていたのですね。裏を知ると、ちょっとしたカスタマイズやモックのような新機能を作って開発を加速することもできるようになるので、やはりブラックボックスをなくすことはとても重要だと思います。

XR Plug-in Managementの記事はあまり見かけないので、なにかの参考になれば幸いです。