サイト内を検索

Unity、URP、RenderFeature を用いたマルチパスレンダリング【 解説編 】

こんにちは。

Cocone Engineeringでグラフィックエンジニア兼テクニカルアーティストのTechLeadを務めている高野正也たかのまさやです。

前回記事ではRenderFeatureを使用したマルチパスレンダリングの具体的な実装手順を説明しました。今回は実装内容について解説いたします。 (Unity 6000.2.2f1、URP 17.2.0、Forward Rendering Path で検証)

マルチパスレンダリング処理の流れ

このマルチパスレンダリング処理は以下の流れで実行されています

① カスタムRenderFeatureによりシェーダのWriteStencilパスを描画(1パス目)

② Unity標準の描画処理によりUniversalForwardパスを描画(2パス目)

上記流れで1つのシェーダにある複数パスを呼び出し、レンダリングを行っています

以下、実装ソースについて説明します

CustomRenderPass.cs

描画処理を行うCustomRenderPassクラスを実装しているソースです。

CustomRenderPass.cs 全ソース
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RendererUtils;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

namespace Example.Graphics
{
    public class CustomRenderPass : ScriptableRenderPass
    {
        private readonly string _profilerTag;
        private readonly ProfilingSampler _profilingSampler;

        private readonly Material _overrideMaterial;
        private readonly int _overrideMaterialPassIndex;
        private readonly LayerMask _layerMask;

        private readonly RenderStateBlock _renderStateBlock;
        private readonly List<ShaderTagId> _shaderTagIds = new();

        public CustomRenderPass(
                string tag,
                string shaderTagName,
                RenderPassEvent passEvent,
                LayerMask layerMask
        )
        {
            _profilerTag = tag;
            _profilingSampler = new ProfilingSampler(tag);
            renderPassEvent = passEvent;

            _layerMask = layerMask;
            _renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);

            if (!string.IsNullOrEmpty(shaderTagName))
            {
                _shaderTagIds.Add(new ShaderTagId(shaderTagName));
            }
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            var cameraData = frameData.Get<UniversalCameraData>();
            var renderingData = frameData.Get<UniversalRenderingData>();
            var resourceData = frameData.Get<UniversalResourceData>();

            foreach (var shaderTag in _shaderTagIds)
            {
                using var builder = renderGraph.AddRasterRenderPass<PassData>(
                        $"{_profilerTag}_{shaderTag.name}",
                        out var passData,
                        _profilingSampler
                );

                passData.ColorTarget = resourceData.activeColorTexture;

                builder.SetRenderAttachment(passData.ColorTarget, 0);
                builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);

                var sorting = SortingCriteria.CommonOpaque;

                var rendererListDesc = new RendererListDesc(
                        shaderTag,
                        renderingData.cullResults,
                        cameraData.camera
                )
                {
                    sortingCriteria = sorting,
                    renderQueueRange = RenderQueueRange.opaque,
                    layerMask = _layerMask,
                    overrideMaterial = _overrideMaterial,
                    overrideMaterialPassIndex = _overrideMaterialPassIndex,
                    stateBlock = _renderStateBlock,
                    rendererConfiguration = PerObjectData.None,
                };

                passData.RendererList = renderGraph.CreateRendererList(rendererListDesc);
                builder.UseRendererList(passData.RendererList);

                builder.AllowPassCulling(false);
                builder.SetRenderFunc((PassData data, RasterGraphContext ctx) => { ctx.cmd.DrawRendererList(data.RendererList); });
            }
        }

        private class PassData
        {
            public TextureHandle ColorTarget;
            public RendererListHandle RendererList;
        }
    }
}

CustomRenderPassコンストラクタ

public CustomRenderPass(string tag, string shaderTagName, RenderPassEvent passEvent, LayerMask layerMask)

tag: プロファイリングとRenderGraphのラベル用

shaderTagName: この名前をもとに、描画対象となるShaderPassを選別します

renderPassEvent: このパスがいつ実行されるか(例:BeforeRenderingOpaquesなど)

layerMask: 対象オブジェクトのレイヤー

RecordRenderGraph メソッド

public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)

このメソッドはURPのRenderGraphモードで有効になり、AddRasterRenderPassにより描画パスを登録します。RenderGraphはURP14以降でRenderFeatureに導入された仕組みで以下のメリットがあります。

・パス同士の依存関係の自動解決

・メモリ使用量の最適化(リソースのライフタイム管理)

・不要なパスのスキップ(AllowPassCullingなど)

(現行URPはRenderGraphが標準ですが、プロジェクトによっては「RenderGraphを無効化」している場合があります。また旧RenderFeatureベースの処理とは混在できませんのでご注意ください)

カメラ・レンダリング・リソースデータの取得

var cameraData = frameData.Get<UniversalCameraData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var resourceData = frameData.Get<UniversalResourceData>();

URPから現在のフレームのコンテキストを取得し、カメラなど様々なデータを利用できます。

描画パスの登録とRendererListの設定

RendererListDesc は、「どのオブジェクトをどんな条件で描画対象とするか」 をまとめた構造体です。ここで指定した条件に基づいて、UnityがRendererListを作り、そのリストをまとめて描画します。

var rendererListDesc = new RendererListDesc(
    shaderTag,
    renderingData.cullResults,
    cameraData.camera
)

shaderTag: Shaderのパス名で対象をフィルタリングします

cullResults: カリング済みのオブジェクト一覧です

camera: 現在のカメラを指定しています

{
    sortingCriteria = SortingCriteria.CommonOpaque,
    renderQueueRange = RenderQueueRange.opaque,
    layerMask = _layerMask,
    overrideMaterial = _overrideMaterial,
    overrideMaterialPassIndex = _overrideMaterialPassIndex,
    stateBlock = _renderStateBlock,
    rendererConfiguration = PerObjectData.None,
}

sortingCriteria: 描画順序の基準を指定します。SortingCriteria.CommonOpaque は「不透明オブジェクト用の標準的なソート」で、カメラからの距離が近いものから順に描画されます

renderQueueRange: 対象となるレンダリングキューの範囲を指定します。RenderQueueRange.opaqueはRenderQueue <= 2500 の範囲が対象です

layerMask: UnityのLayerを使って描画対象を絞り込んでいます

overrideMaterial: ここで指定したマテリアルで 対象オブジェクトを強制的に描画します。nullだとアサインされているマテリアルが使用されます。(今回はnullのまま使用)

overrideMaterialPassIndex: 指定マテリアルの場合、どのパスを使うかも選べます

stateBlock: 描画時に適用するレンダーステートを上書きします。RenderStateMask.Nothing 設定で無効となっていますが、深度テストの有効/無効切り替え、ブレンド設定を強制変更、ステンシル操作の上書きなどを設定できます

rendererConfiguration: オブジェクトごとにGPUへ渡す追加データを指定します。None指定で追加情報を持たずにシンプルに描画しています。 PerObjectData.LightProbe(ライトプローブ情報使用)、PerObjectData.ReflectionProbes (リフレクションプローブ情報使用)、PerObjectData.Lightmaps(ライトマップ情報使用)等を指定できます

描画の実行

builder.SetRenderFunc((PassData data, RasterGraphContext ctx) => {
    ctx.cmd.DrawRendererList(data.RendererList);
});

「このRenderPassで実際にどのような描画コマンドを発行するか」を定義している部分です。 RenderGraphは 「どのリソースを使うか」 と 「どう処理するか」 を分けて管理します。 SetRenderFunc はその「どう処理するか」を具体的にGPUへ伝える 関数ポインタ的な登録です

PassData data: このパスが必要とする情報が纏まっている値です

RasterGraphContext ctx: RenderGraphが提供する実行時コンテキストです

ctx.cmd.DrawRendererList(data.RendererList): RendererListに登録されたオブジェクトをまとめて描画する部分です

CustomRenderFeature.cs

設定用GUI、描画処理を行うCustomRenderPassを呼び出す為に使用するCustomRenderFeatureクラスを実装しているソースです

CustomRenderFeature.cs 全ソース
using System;
using UnityEngine;
using UnityEngine.Rendering.Universal;

namespace Example.Graphics
{
    public class CustomRenderFeature : ScriptableRendererFeature
    {
        [Serializable]
        public class Settings
        {
            public string passTag = "CustomPass";
            public string drawTagName = "WriteStencil";
            public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
            public LayerMask layerMask = ~0;
        }

        public Settings settings = new();
        private CustomRenderPass renderPass;

        public override void Create()
        {
            renderPass = new CustomRenderPass(
                    settings.passTag,
                    settings.drawTagName,
                    settings.renderPassEvent,
                    settings.layerMask
            );
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            // プレビュー用カメラではスキップ
            if (renderingData.cameraData.cameraType == CameraType.Preview)
            {
                return;
            }

            renderer.EnqueuePass(renderPass);
        }
    }
}

Settings クラス

[Serializable]
public class Settings
{
    public string passTag = "CustomPass";
    public string drawTagName = "WriteStencil";
    public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
    public LayerMask layerMask = ~0
}

passTag: RenderPassの名前。Profilerでの識別に使えます

drawTagName: ShaderTagId に渡す文字列。”WriteStencil” とすれば、Shader の Pass { Name “WriteStencil” } だけが対象になります

renderPassEvent: いつこのパスを実行するか。BeforeRenderingOpaquesは不透明オブジェクトを描画する前に実施します

layerMask: どのレイヤーのオブジェクトを対象にするか。~0 は「全レイヤー」を指定しています

Create メソッド

public override void Create()
{
    renderPass = new CustomRenderPass(
            settings.passTag,
            settings.drawTagName,
            settings.renderPassEvent,
            settings.layerMask
    );
}

URPがRendererFeatureを初期化するときに呼ばれます。ここで CustomRenderPass のインスタンスを生成し、Settings の内容を渡しています。この時点では まだパスは追加されておらず用意しているだけになります。

AddRenderPasses メソッド

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    // プレビュー用カメラではスキップ
    if (renderingData.cameraData.cameraType == CameraType.Preview)
    {
        return;
    }
    renderer.EnqueuePass(renderPass);
}

毎フレーム呼ばれ、どのパスを追加するかを決めています。またCameraType.Preview のときはスキップしプレビュー用カメラで処理しないようにしています。 renderer.EnqueuePass(renderPass) で、作成したCustomRenderPassをキューに登録。 レンダリングパイプラインに組み込まれ、毎フレーム実行されるようになります

CustomMultipass.shader

1つのシェーダに複数パスを定義したシェーダファイルです。1パス目にステンシル値を描画。2パス目でステンシルテストを行いピクセルのカラー、深度値を描画します

CustomMultipass.shader 全ソース
Shader "Example/CustomMultipass"
{
    Properties
    {
        _MainTex("Main Texture", 2D) = "white" {}
        _Color("Color Tint", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }

        // === パス 1: ステンシルを書き込む ===
        Pass
        {
            Name "WriteStencil"
            Tags { "LightMode" = "WriteStencil" }

            Stencil
            {
                Ref 1
                Comp always
                Pass replace
            }

            ZWrite Off
            ColorMask 0

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }

        // === パス 2: ステンシルにマスクされた範囲だけ描画 ===
        Pass
        {
            Name "RenderMasked"
            Tags { "LightMode" = "UniversalForward" }

            Stencil
            {
                Ref 1
                Comp equal
                Pass keep
            }

            ZWrite On
            ColorMask RGBA

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float4 _Color;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS);
                OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
                return texColor * _Color;
            }
            ENDHLSL
        }
    }

    FallBack "Hidden/Shader Graph/FallbackError"
}

パス 1: ステンシル書き込み

Pass
{
    Name "WriteStencil"
    Tags { "LightMode" = "WriteStencil" }

    Stencil
    {
        Ref 1
        Comp always
        Pass replace
    }

    ZWrite Off
    ColorMask 0
    ...
}    

ステンシル設定

Ref 1: 参照値に1を指定

Comp always: 常に通過

Pass replace: 通過したらステンシル値を1に「置換」

描画設定

ZWrite Off: 深度バッファに書き込みしません

ColorMask 0: カラーバッファには何も描かない

このパスは「対象の形状に基づいてステンシルバッファを更新するだけ」で、深度バッファ、カラーバッファ共に書き込みは行いません

パス 2: ステンシルにマスクされた範囲だけ描画

Pass
{
    Name "RenderMasked"
    Tags { "LightMode" = "UniversalForward" }

    Stencil
    {
        Ref 1
        Comp equal
        Pass keep
    }

    ZWrite On
    ColorMask RGBA
    ...
}

ステンシル設定

Ref 1: 判定用の参照値に「1」を指定

Comp equal: ステンシルが「1」と等しいなら通過

Pass keep: ステンシル値は変更しない

描画設定

ZWrite On: 深度バッファに書き込む(通常の描画と同じ)

ColorMask RGBA: 全カラーを書き込む(通常の描画と同じ)

Varyings vert(Attributes IN)
{
    Varyings OUT;
    OUT.positionHCS = TransformObjectToHClip(IN.positionOS);
    OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
    return OUT;
}

half4 frag(Varyings IN) : SV_Target
{
    half4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
    return texColor * _Color;
}

頂点シェーダは、オブジェクト座標のクリップ座標変換、UV値の処理を実施。 フラグメントシェーダーはテクスチャをサンプルし、カラー値と乗算して出力しているだけの処理です。 パス1でステンシル値が1に設定されたピクセルにのみ、カラーと深度を書き込みます

まとめ

レンダリング処理の基礎と言えるマルチパスレンダリングのURPでの実装について説明させて頂きました。今回の例は最小構成ですが、応用すればポータル/マスク表現やアウトラインなど様々な表現が可能になると思います。

この記事が皆様のお役に立てましたら幸いです。

Tag

Category

Tag