🤔

[Unity][SRP]自作レンダーパイプラインの設定をGraphicsSettingsに表示させるには

に公開

概要

URPやHDRPパッケージをダウンロードすると、このようにProjectSettings/Graphics(GraphiceSettingsとも呼ばれます)にPipeline Specific Settingsという項目として各レンダーパイプライン固有の設定タブが現れます。

この設定項目の正体は何なのか、また自作レンダーパイプラインでこれを表示させるにはどうすれば良いのか...といったことについて深堀りしていきます。
Unitバージョンは6000.2.0bです。

設定項目の本体

このタブ内の情報の本体はどこにあるのでしょうか?
普通GraphicsSettingsの情報はProjectSettings/GraphicsSettings.assetに保存されているのですが、タブ内の情報に関しては、URPやHDRPパッケージ導入時に生成される以下のScriptableObjectに保存されます。

  • URP:Assets/UniversalRenderPipelineGlobalSettings.asset
  • HDRP:Assets/HDRPDefaultResources/HDRenderPipelineGlobalSettings.asset

    このファイルのインスペクターにはプロパティが表示されず、代わりにProjectSettings/Graphicsから閲覧・編集せよというメッセージが表示されます。

    一応これは単なるエディタ拡張なので、UnityをDebugモードに切り替えれば普通にプロパティを読み書きできます。


    とはいえこのようなエディタ拡張が用意されているということは、このScriptableObjectは直接インスペクターから編集するのではなく、GraphicsSettingsウィンドウから編集する想定だということです。
    なお、このファイルは常に一つだけ存在することが想定されています。

RenderPipelineGlobalSettings

このScriptableObjectのクラス型はそれぞれ以下の通りで、いずれもRenderPipelineGlobalSettings型を継承しています。継承することで先ほどのエディタ拡張が適用されます。

  • UniversalRenderPipelineGlobalSettings
  • HDRenderPipelineGlobalSettings

自作レンダーパイプラインでの実装

自作レンダーパイプラインにおいてこのGlobalSettingsの実装は必ずしも必要なものではありませんが、Blitterクラスをはじめとしたいろいろなものがこの実装を前提としているため、SRPをフルに活用したいのならなるべく実装しておいたほうが良さそうです。(そのわりにドキュメント等がほぼないため、この記事を執筆することにしました。)
現に自作レンダーパイプラインではGlobalSettingsを正しく生成・セットアップしないとBlitterクラスを使うことができません。
GlobalSettingsの用途としては、レンダーパイプラインが用いるリソースの参照やオプションなどを保存・編集します。
例えば、描画において標準のBlitを行う際のシェーダーの参照、デフォルトのGlobalVolumeの参照、RenderGraphのCompatibilityModeのオプションなどがあります。

作り方

RenderPipelineGlobalSettings

まずRenderPipelineGlobalSettings型を継承したクラスを作りましょう。
なお、レンダーパイプライン名は仮でXRPとします。
また、レンダーパイプラインを作成する際にはRenderpipeline型、RenderPipelineAssets型を継承したクラスを作成することになりますが、それらのクラス名はそれぞれ次の通りとします。

  • Xrp
  • XrpAsset
[SupportedOnRenderPipeline(typeof(XrpAsset))]
[DisplayInfo(name = "XRP Global Settings Asset")]
[DisplayName("XRP")]
public class XrpGlobalSettings : RenderPipelineGlobalSettings<XrpGlobalSettings, Xrp>
{
	[SerializeField] RenderPipelineGraphicsSettingsContainer m_Settings = new();
	protected override List<IRenderPipelineGraphicsSettings> settingsList => m_Settings.settingsList;
}

[SupportedOnRenderPipeline(typeof(XrpAsset))]属性はどのレンダーパイプラインで用いられるものなのかを明示するもので、今回の場合は必ず付ける必要があります。
[DisplayInfo(name = "XRP Global Settings Asset")]属性はConsoleログ表示などで用いられます。
[DisplayName("XRP")]属性は、GraphicsSettingsのタブに表示される名前です。

非常に重要な点として、理由は後述しますが、フィールドのm_SettingssettingsListは必ずこの名前にする必要があります。別の命名にするとタブが表示されなくなります

RenderPipelineAsset

XrpAssetでGlobalSettingsを生成する処理も見ておきましょう。

protected override RenderPipeline CreatePipeline()
{
#if UNITY_EDITOR
    XrpGlobalSettings globalSettings = GraphicsSettings.GetSettingsForRenderPipeline<Xrp>() as XrpGlobalSettings;
    if(RenderPipelineGlobalSettingsUtils.TryEnsure<XrpGlobalSettings, Xrp>(ref globalSettings, "Assets/XrpGlobalSettings.asset", true))
    {
        AssetDatabase.SaveAssetIfDirty(globalSettings);
    }
}
#endif
    ...

このTryEnsureメソッドが重要で、GlobalSettingsのファイルの生成を行ってくれるとともに、いろいろと内部的なセットアップも行ってくれます。
これでいったんGraphicsSettingsにPipeline Specific Settingsが表示されるようになります。

IRenderPipelineSettings、IRenderPipelineResources

GraphicsSettingsに独自の項目を追加するには、IRenderPipelineSettingsか、IRenderPipelineResourcesを継承したクラスを作成します。

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Categorization;

[Serializable]
[SupportedOnRenderPipeline(typeof(XrpAsset))]
[CategoryInfo(Name = "XRP Resources")]
public class XrpResources : IRenderPipelineResources
{
    int IRenderPipelineGraphicsSettings.version => 0;
    bool IRenderPipelineGraphicsSettings.isAvailableInPlayerBuild => true;

    [SerializeField][ResourcePath("Shaders/Utilities/CoreBlit.shader")] private Shader _coreBlitShader;
    [SerializeField][ResourcePath("Shaders/Utilities/CoreBlitColorAndDepth.shader")] private Shader _coreBlitColorAndDepthShader;
    public Shader CoreBlitShader => _coreBlitShader;
    public Shader CoreBlitColorAndDepthShader => _coreBlitColorAndDepthShader;

IRenderPipelineResourcesはIRenderPipelineSettingsを継承しているという関係にあります。

public interface IRenderPipelineResources : IRenderPipelineGraphicsSettings
{
}

ただ継承しているだけで何も違いが無さそうに見えますが、IRenderPipelineResourcesを使うと、[ResourcePath(パス)]属性を使うことができるようになります。この属性を付けることで初期値としてリソースの参照を指定することができます。毎回プロジェクトを作成するたびにGraphicsSettingsにアタッチするのは面倒なので、ぜひ利用しましょう。なおこのパスはレンダーパイプラインがあるディレクトリからの相対パスです。
[SupportedOnRenderPipeline]属性は必ず付けましょう。
[CategoryInfo]属性はこのクラス全体につくヘッダーの名称となります。

これでGraphicsSettingsのタブ内にプロパティが表示されるようになります。
つまりこのように特定のインターフェイスと属性を使ったクラスを作成するだけでGraphicsSettingsのタブ内に項目を追加していけるということです。

また、追加された設定は、GraphicsSettings.GetRenderPipelineSettings<型名>()メソッドを通じてどこからでも取得することができます。

_resources = GraphicsSettings.GetRenderPipelineSettings<XrpResources>();

内部的な仕組みの話

ここからは内部的な仕組みの話なので、興味のある方のみご覧ください。

TryEnsureメソッドを起点に

TryEnsureメソッドを起点に処理の流れを辿りつつ、このGlobalSettingsに関する全体的な仕組みについてみていきましょう。
TryEnsureメソッドは、GlobalSettingsのインスタンスの有無を調べ、なければ作成して諸々のセットアップまで行ってくれるメソッドです。

RenderPipelineGlobalSettingsUtils
internal static bool TryEnsure<TGlobalSetting, TRenderPipeline>(ref TGlobalSetting instance, string defaultPath, bool canCreateNewAsset, out Exception error)
    where TGlobalSetting : RenderPipelineGlobalSettings<TGlobalSetting, TRenderPipeline>
    where TRenderPipeline : RenderPipeline
{
...
    instance = Create<TGlobalSetting>(defaultPath);
...
    EditorGraphicsSettings.SetRenderPipelineGlobalSettingsAsset<TRenderPipeline>(instance);

作成の部分はRenderPipelineGlobalSettingsUtils.Createメソッドの呼び出しとなっています。
また、GraphicsSettingsへの登録はEditorGraphicsSettings.SetRenderPipelineGlobalSettingsAssetメソッドの呼び出しにより行われます。

EditorGraphicsSettings
public static void SetRenderPipelineGlobalSettingsAsset(Type renderPipelineType, RenderPipelineGlobalSettings newSettings)
{
...
    RenderPipelineGraphicsSettingsManager.PopulateRenderPipelineGraphicsSettings(newSettings);
    flag = Internal_TryRegisterRenderPipeline(renderPipelineType.FullName, newSettings);
...

肝心のGraphicsSettingsへの登録処理はUnity内部のコードになっており見られません。
RenderPipelineGraphicsSettingsManager.PopulateRenderPipelineGraphicsSettingsメソッドでは、特定のインターフェイス(つまりIRenderPipelineSettings)を継承しているクラスをFetchRenderPipelineGraphicsSettingInfosメソッドにより集めています。

RenderPipelineGraphicsSettingsmanager

internal static void PopulateRenderPipelineGraphicsSettings(RenderPipelineGlobalSettings settings)
{
    if (!GraphicsSettingsInspectorUtility.TryExtractSupportedOnRenderPipelineAttribute(settings.GetType(), out var supportedOnRenderPipelineAttribute, out var message))
    {
        throw new InvalidOperationException(message);
    }
    Type globalSettingsRenderPipelineAssetType = supportedOnRenderPipelineAttribute.renderPipelineTypes[0];
...
    List<IRenderPipelineGraphicsSettings> list = new List<IRenderPipelineGraphicsSettings>();
    foreach (RenderPipelineGraphicsSettingsInfo item in FetchRenderPipelineGraphicsSettingInfos(globalSettingsRenderPipelineAssetType, includeUnsupported: true))
    {
        UpdateRenderPipelineGlobalSettings(item, settings, out var assetModified, out var createdSetting);
...

UpdateRenderPipelineGlobalSettingsメソッドにより、GlobalSettingsインスタンスのsettingsListプロパティに、IRenderPipelineSettingsを継承しているクラスのインスタンスがAddされていきます。
FetchRenderPipelineGraphicsSettingInfosメソッドにはIRenderPipelineSettingsを継承しているすべてのクラスを集める処理(TypeCache.GetTypesDerivedFromメソッド)があります。

RenderPipelineGraphicsSettingsmanager
internal static IEnumerable<RenderPipelineGraphicsSettingsInfo> FetchRenderPipelineGraphicsSettingInfos(Type globalSettingsRenderPipelineAssetType, bool includeUnsupported = false)
{
    foreach (Type renderPipelineGraphicsSettingsType in TypeCache.GetTypesDerivedFrom(typeof(IRenderPipelineGraphicsSettings)))
    {
...

この仕組みにより、先に見たように、ただIRenderPipelineSettings(と属性)を使ったクラスを作るだけで、GraphicsSettingsに項目を追加することができるというわけですね。
ちなみにTypeCacheはreflectionでなくUnityの機能で、特定の型を継承している型にアクセスすることなどができ、またreflectionよりも速いそうです。
https://www.hanachiru-blog.com/entry/2023/12/08/120000

GraphicsSettingsInspector

肝心のGraphicsSettingsへの登録処理はUnity内部の処理になっており見られませんが、GraphicsSettingsにおけるレンダーパイプラインのタブの描画部分は以下から見ることができます。
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/Inspector/GraphicsSettingsInspector.cs
UIElementを用いてGUIを描画しているようですね。
また、先ほど少し登場したGraphicsSettingsInspectorUtilityのメソッドが多く呼ばれていることがわかります。

GraphicsSettingsInspectorUtility.TryGetSettingsListFromRenderPipelineGlobalSettings

GraphicsSettingsInspectorUtility.TryGetSettingsListFromRenderPipelineGlobalSettingsメソッドでは、GlobalSettingsのm_settingsListプロパティの取得が行われていますが、この取得はプロパティ名を文字列で指定するという形になっています。

GraphicsSettingsInspectorUtility
private static bool TryGetSettingsListFromRenderPipelineGlobalSettings(RenderPipelineGlobalSettings globalSettings, out SerializedObject globalSettingsSO, out SerializedProperty settingsContainer, out SerializedProperty settingsListInContainer)
{
    globalSettingsSO = new SerializedObject(globalSettings);
    settingsContainer = globalSettingsSO.FindProperty("m_Settings");
...
    settingsListInContainer = globalSettingsSO.FindProperty("m_Settings.m_SettingsList.m_List");
    return settingsListInContainer != null;
}

そのため、GlobalSettingsの設定リストのプロパティ名は必ず上記と一致していなければなりません。
一致していないと、GraphicsSettingsにタブが描画されなくなります。

最後に

自作SRPの道は険しく、GraphicsSettingsにタブメニューを出すだけでも、GlobalSettingsのプロパティ名が特定の文字列と一致していなければ表示されないという信じられないようなトラップがあります。困ったときは落ち着いてURP/SRPのコードを追っていく必要があります。

Discussion