1. サイトトップ
  2. ブログ
  3. Unity
  4. 【Unity】ScriptableRendererでURPを一から

【Unity】ScriptableRendererでURPを一から

こんにちは!
情熱開発部 プログラム2課の柄本です。

今年の振り返りには少し早いですが、CEDECで講演するという貴重な体験をできたのでそちらのお話しから。

今年はCEDECでUnityのScriptableRenderPipeline(以下SRP)のお話をさせていただきました。
こちらでは一からSRPを組んで、実際の業務に役立てた際の話をしました。
まだ見ていない方や、興味のある方はCEDECのYoutubeチャンネルから見ることができますので、ぜひご覧ください。

さて、講演のまとめの際に、UniversalRenderPipeline(以下URP)を利用して描画処理を一から組む方法としてRendererを作成することを紹介しました。

講演では時間の都合上あまり詳しく触れることはできませんでしたので、今回のブログで説明させていただきます。

なお、今回の説明に際してURPやSRPに関する基本的な説明は省略させていただくことをあらかじめご承知おきください。

ScriptableRendererとは

Rendererは正しくはScriptableRendererと言いまして、URPで定義された描画の基本クラスの1つになります。

URPを拡張する際に用いられる、RendererFeatureやScriptableRenderPassも上記のScriptableRendererで組んだ描画処理をベースとして、そこに乗っかる形で処理を追加しております。

ではそんなScriptableRendererのメリット、デメリットについてお話する前に実際に組んでいきます。

URPへの組み込み手順

今回は以下の環境でURPへの組み込みを行っていきます。

必要なクラス

URPの組み込みにあたり必要なクラスは2つです。

  • ScriptableRenderer
    • ScriptableRenderPassを積んでいくクラス。
      RendererFeature以外のメイン描画フローを組む。
  • ScriptableRendererData
    • ScriptableRendererクラスをサポートするクラス。
      Editor拡張と合わせてデフォルトで保持しておく設定やリソースを宣言する。

どちらもURPで追加される拡張用のため、SRPだけでは使用することができません。

基本的にScriptableRendererクラスで実際の描画処理を積んでいき、ScriptableRendererDataクラスでは基本的な処理で使用するシェーダーを宣言したり、画像のように細かく設定を分けたいものを宣言したりすることになります。(画像はURPのもの)

ソース例

以下に示すのは上記2つのクラスをほぼ最小構成で組んだものになります。

まずはScriptableRendererクラスです。

using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class LogicalbeatRenderer : ScriptableRenderer
{
    private NonePass nonePass;

    public LogicalbeatRenderer(LogicalbeatRendererData data) : base(data)
    {
        nonePass = new NonePass();
    }

    public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        EnqueuePass(nonePass);
    }
}

1つもScriptableRenderPassが積まれていないとエラーが出てしまうため、何もしないNonePassを積んであります。
実際にはここにメインの描画処理を積んでいくことになります。

続いてScriptableRendererDataクラスです。

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.ProjectWindowCallback;
#endif
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class LogicalbeatRendererData : ScriptableRendererData, ISerializationCallbackReceiver
{
#if UNITY_EDITOR
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812")]
    internal class CreateLogicalbeatRendererAsset : EndNameEditAction
    {
        public override void Action(int instanceId, string pathName, string resourceFile)
        {
            var instance = CreateInstance<LogicalbeatRendererData>();
            string dataPath = pathName;
            AssetDatabase.CreateAsset(instance, dataPath);
            ResourceReloader.ReloadAllNullIn(instance, UniversalRenderPipelineAsset.packagePath);
            Selection.activeObject = instance;
        }
    }

    [MenuItem("Assets/Create/Rendering/URP Logicalbeat Renderer", priority = CoreUtils.Sections.section1 + CoreUtils.Priorities.assetsCreateRenderingMenuPriority)]
    private static void CreateLogicalbeatRendererData()
    {
        ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, CreateInstance<CreateLogicalbeatRendererAsset>(), "New Logicalbeat Renderer Data.asset", null, null);
    }
#endif

    private const int LatestAssetVersion = 1;
    [SerializeField] int assetVersion = 0;

    protected override ScriptableRenderer Create()
    {
        if (!Application.isPlaying)
        {
            ReloadAllNullProperties();
        }
        return new LogicalbeatRenderer(this);
    }

    private void ReloadAllNullProperties()
    {
#if UNITY_EDITOR
        ResourceReloader.TryReloadAllNullIn(this, UniversalRenderPipelineAsset.packagePath);
#endif
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
        assetVersion = LatestAssetVersion;
    }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        assetVersion = LatestAssetVersion;
    }
}

先に作成したLogicalbeatRendererクラスを作成する処理がメインとなりますが、AssetとしてScriptableRendererDataを作成する処理も一緒に記載してあります。

そしてこちらが組んだScriptableRendererクラスで描画した結果になります。

当然ではあるんですが、描画処理を何も積んでいないのでオブジェクトが何も描画されていません。

メリット・デメリット

メリット・デメリットをあげるにあたって、同じくカスタマイズ性の高いCustomSRPと比較していきたいと思います。

メリット

GameViewの描画に専念できる

弊社堂前が以前行った下記の講演をご覧になった方や、自身でSRPを組んでみたことのある方なら気づいたかもしれません。

描画処理を何も積んでいないにも関わらず、Sceneビューにグリッド線とGizmosが描画されています。
これはScriptableRenderer基底クラス内でそれらの処理が実装済みであることが要因です。

そのため一からSRP作成する場合と違い、ほぼGameの描画だけを実装していけば処理が完成するようになっています。

URPの設定をそのまま使用できる

URPがベースですので、URPで使用されている設定を大体そのまま使用することができます。

特にライティング周りに関してはどのシェーダー変数が上書きできて、どの変数が新規で宣言しなければならないのか等の調査が不要になるため、SRPを一から組むことに比べてかなり手間が削減されています。

URPとの併用が可能

URPではUniversalRenderPipelineAssetにどのScriptableRendererを使用するかを設定して描画方法を決定します。
そして、この時設定できるScriptableRendererは複数設定することが可能で、カメラ毎にどのScriptableRendererを使用するかを決定していくことになります。

なので、基本の描画はURPに任せて、URPだけでは難しいといった時に自前で組んだ描画フローを通すことが可能になります。(画像はURPをデフォルト描画処理、自前の描画処理を補助として設定したもの)

デメリット

CustomSRPより拡張性が低い

これは全てを組むことができるCustomSRPと比較しているので、当然ではありますが、何をするにしてもURPの基底処理に左右されます。

かなり凝ったことをしたくて、一から組むより他ないという場合には使用が難しい場面もあるかと思います。

保守性が少々低い

他のRendererFeatureなどのURP拡張機能と比べると変更点が多い分、バージョンアップの際に不具合が発生するリスクが高くなってしまいます。

CustomSRPと比べるとまだマシではありますが、バージョンアップの際に注意を払わなければならないのは間違いないでしょう。

実用例

URPそのままだと困るというところでいうと、URP以外のLightModeをメインで描画したいということが考えられます。

例えばマルチパス描画を主軸に考えているや、映り込み描画などで負荷を下げるためにざっくりとした描画Passを通したいなどです。

特にざっくりとした描画だけ行いたいという場合には、既存のURPシェーダーに別のPassを追加することで独自の描画フローを通すことができます。

映り込み時は頂点シェーダー走らせないようにしたもの

まとめ

今回はURPのScriptableRendererについてお話させていただきました。

SRPを一から組むのは自由に描画フローを組める一方で描画に関するかなりの知識と作業コストが要求されます。

ScriptableRendererの場合は多少自由度が下がるものの、ある程度の描画フローを自分で組むことができます。
何より、URPをベースとしているのでCustomSRPよりも保守が楽になります。

この記事が皆様のご助力となれば幸いです。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。