💊

【Unity/URP】一部のオブジェクトをポストエフェクトの対象外にする➁(オブジェクトの再描画)

2025/01/16に公開

はじめに

この記事は前回の続きです。Transparentsが混在するシーンにおいて、指定したオブジェクトがポストエフェクトの影響を受けないようにする方法を提案しています。

方針

ざっくり下のような流れでレンダリングを行うことで実現を目指します。
前回の記事では➁➂について解説を行いました。この記事では、➄の方法を掲載しています。

➀ 通常どおり対象オブジェクトを描画
➁ RenderPassで対象オブジェクトの色をテクスチャに書き込む
➂ RenderPassで対象以外のオブジェクトの深度をテクスチャに書き込む
➃ 通常どおりポストエフェクトをかける
➄ ➁と➂のテクスチャを使って遮蔽物がない部分だけ対象オブジェクトを再描画

改めて実行結果を載せておきます。この例では、カプセル型のオブジェクトを対象にしています。

ポストエフェクトあり

ポストエフェクトなし

コード

動作環境
Unity 2022.3.17f1
URP 14.0.9

今回紹介するコードのみ載せています。全文はgithubで確認できます。

IgnorePostProcessingPass.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace IgnorePostProcessing
{
    public class IgnorePostProcessingPass : ScriptableRenderPass
    {
        static readonly int _colorTextureId = Shader.PropertyToID("_CustomColorTexture");

        ProfilingSampler _profilingSampler;
        List<ShaderTagId> _shaderTagIds;
        FilteringSettings _filteringSettings;
        Material _overrideMaterial = new Material(Shader.Find("Custom/IgnorePostProcessing"));
        CustomTexture _colorTexture;

        public IgnorePostProcessingPass(string samplerName, LayerMask layerMask, CustomTexture colorTexture)
        {
            // ポストエフェクト後に描画する. すべてのレンダーキューのオブジェクトが対象.
            renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
            _profilingSampler = new ProfilingSampler(samplerName);
            _filteringSettings = new FilteringSettings(RenderQueueRange.all, layerMask);

            // デフォルトのシェーダーパスを設定.
            _shaderTagIds = new List<ShaderTagId>()
            {
                new ShaderTagId("SRPDefaultUnlit"),
                new ShaderTagId("UniversalForward"),
                new ShaderTagId("UniversalForwardOnly")
            };

            _colorTexture = colorTexture;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var cmd = CommandBufferPool.Get();
            using (new ProfilingScope(cmd, _profilingSampler))
            {
                context.ExecuteCommandBuffer(cmd);
                cmd.Clear();

                // 描画方法の設定.
                var drawingSettings = CreateDrawingSettings(_shaderTagIds, ref renderingData, SortingCriteria.BackToFront);

                // シェーダープロパティにレンダーテクスチャを渡し、マテリアルをオーバーライド.
                _overrideMaterial.SetTexture(_colorTextureId, _colorTexture?.Pass.destination);
                drawingSettings.overrideMaterial = _overrideMaterial;

                // 画面にレンダリング.
                context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
            }
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }
}
IgnorePostProcessing.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;

namespace IgnorePostProcessing
{
    public class IgnorePostProcessing : ScriptableRendererFeature
    {
        [SerializeField] LayerMask _layerMask;
        [SerializeField] CustomTexture _colorTexture;

        IgnorePostProcessingPass _pass;

        public override void Create()
        {
            _pass = new IgnorePostProcessingPass(name, _layerMask, _colorTexture);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            renderer.EnqueuePass(_pass);
        }
    }
}
IgnorePostProcessing.shader
Shader "Custom/IgnorePostProcessing"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

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

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

            TEXTURE2D(_CustomColorTexture);
            TEXTURE2D(_CustomDepthTexture);
            SAMPLER(sampler_CustomColorTexture);
            SAMPLER(sampler_CustomDepthTexture);
            
            Varyings vert (Attributes IN)
            {
                Varyings OUT;
                OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = IN.uv;
                return OUT;
            }

            half4 frag (Varyings IN) : SV_Target
            {
                // レンダーテクスチャから深度をサンプリング.
                float2 uv = IN.positionCS.xy / _ScaledScreenParams.xy;
                float sceneDepth = SAMPLE_TEXTURE2D(_CustomDepthTexture, sampler_CustomDepthTexture, uv).r;

                // サンプリングした深度とオブジェクトの深度を線形化して比較. 遮蔽物があれば描画しない.
                sceneDepth = LinearEyeDepth(sceneDepth, _ZBufferParams);
                float depth = LinearEyeDepth(IN.positionCS.z, _ZBufferParams);
                clip(sceneDepth - depth);

                // レンダーテクスチャからオブジェクトの色をサンプリング.
                half4 color = SAMPLE_TEXTURE2D(_CustomColorTexture, sampler_CustomColorTexture, uv);
                return color;
            }
            ENDHLSL
        }
    }
}

シェーダー

前の記事のRenderPassで生成されるようにした2つのテクスチャを使って、ポストエフェクトの処理後にオブジェクトをレンダリングするようにします。まずは、使用するシェーダーから見ていきます。

テクスチャを宣言

IgnorePostProcessing.shader
TEXTURE2D(_CustomColorTexture);
TEXTURE2D(_CustomDepthTexture);
SAMPLER(sampler_CustomColorTexture);
SAMPLER(sampler_CustomDepthTexture);

シーン内のオブジェクトの色を格納しているものと、深度を格納しているものとで2種類のテクスチャとサンプラーを宣言しています。前回の記事で作成したCustomTexturePassのレンダリング結果がこの2つに渡されます。

フラグメントシェーダー

IgnorePostProcessing.shader
// レンダーテクスチャから深度をサンプリング.
float2 uv = IN.positionCS.xy / _ScaledScreenParams.xy;
float sceneDepth = SAMPLE_TEXTURE2D(_CustomDepthTexture, sampler_CustomDepthTexture, uv).r;

// サンプリングした深度とオブジェクトの深度を線形化して比較. 遮蔽物があれば描画しない.
sceneDepth = LinearEyeDepth(sceneDepth, _ZBufferParams);
float depth = LinearEyeDepth(IN.positionCS.z, _ZBufferParams);
clip(sceneDepth - depth);

// レンダーテクスチャからオブジェクトの色をサンプリング.
half4 color = SAMPLE_TEXTURE2D(_CustomColorTexture, sampler_CustomColorTexture, uv);
return color;

結構シンプルだと思います。テクスチャから深度をサンプリング⇒描画中のオブジェクトの深度と比較⇒手前にオブジェクトがあればスキップ⇒なければテクスチャからサンプリングした色を出力、といった流れですね。

IgnorePostProcessingPass(RenderPass)

上で作成したシェーダーを使ってカメラのレンダーターゲット(画面)に描画を行うRenderPassを作成します。

変数

ちょっと大事なところがあるので、変数についても取り上げておきます。

IgnorePostProcessing.cs
static readonly int _colorTextureId = Shader.PropertyToID("_CustomColorTexture");

ProfilingSampler _profilingSampler;
List<ShaderTagId> _shaderTagIds;
FilteringSettings _filteringSettings;
Material _overrideMaterial = new Material(Shader.Find("Custom/IgnorePostProcessing"));
CustomTexture _colorTexture;

はじめの行で_CustomColorTextureというシェーダープロパティを指定しています。これはシェーダーで宣言したカラー用のテクスチャを指しています。
また、下から2番目の行では、先ほどのシェーダーでマテリアルを作成しています。

コンストラクタ

IgnorePostProcessing.cs
public IgnorePostProcessingPass(string samplerName, LayerMask layerMask, CustomTexturePass colorTexture)
{
    // ポストエフェクト後に描画する. すべてのレンダーキューのオブジェクトが対象.
    renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
    _profilingSampler = new ProfilingSampler(samplerName);
    _filteringSettings = new FilteringSettings(RenderQueueRange.all, layerMask);

    // デフォルトのシェーダーパスを設定.
    _shaderTagIds = new List<ShaderTagId>()
    {
        new ShaderTagId("SRPDefaultUnlit"),
        new ShaderTagId("UniversalForward"),
        new ShaderTagId("UniversalForwardOnly")
    };

    _colorTexture = colorTexture;
}

今回は renderPassEventRenderPassEvent.AfterRenderingPostProcessing とすることで、ポストエフェクトをかけた後にPassを実行します。
最後の行ではCustomTextureを受け取っていますが、これは後でCustomTexturePassのレンダーターゲットを動的に参照するためです。

Execute()

IgnorePostProcessing.cs
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    var cmd = CommandBufferPool.Get();
    using (new ProfilingScope(cmd, _profilingSampler))
    {
        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // 描画方法の設定.
        var drawingSettings = CreateDrawingSettings(_shaderTagIds, ref renderingData, SortingCriteria.BackToFront);

        // シェーダープロパティにレンダーテクスチャを渡し、マテリアルをオーバーライド.
        _overrideMaterial.SetTexture(_colorTextureId, _colorTexture?.Pass.destination);
        drawingSettings.overrideMaterial = _overrideMaterial;

        // 画面にレンダリング.
        context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
    }
    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

基本的には設定済みの内容でレンダリングしているだけですが、冒頭に作成したマテリアルのプロパティに対し、CustomTexturePassのレンダーターゲットを設定しています。
また、DrawingSetteingsoverrideMaterial にマテリアルを代入し、このマテリアルで描画を行うように設定しています。
わざわざマテリアルを作成してレンダーターゲットを渡していることには理由があるのですが、それについては後述します。

IgnorePostProcessing(RenderFeature)

先述のPassを実行するためのRendererFeatureを用意します。特に解説することはないですが、強いて挙げるとInspectorから紐づけしたいCustomTextureを選べるようにしました。

IgnorePostProcessing.cs
public class IgnorePostProcessing : ScriptableRendererFeature
{
    [SerializeField] LayerMask _layerMask;
    [SerializeField] CustomTexture _colorTexture;

    IgnorePostProcessingPass _pass;

    public override void Create()
    {
        _pass = new IgnorePostProcessingPass(name, _layerMask, _colorTexture);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(_pass);
    }
}

Rendererに追加

作成したRenderFeatureをRendererが使えるようにします。使用中のUniverisal Renderer Dataで【Add Renderer Feature > IgnorePostProcessing】を選択します。

IgnorePostProcessingを設定

追加したIgnorePostProcessingを下のように設定しました。LayerMaskは対象としたいレイヤー、ColorTextureにはそのレイヤーにあるオブジェクトの色を書き込むCustonTexutreを指定します。


ここまで完了したら、一旦完成です。

対象同士が重なる場合

上の画像のような状況では、対象とするオブジェクトを1つのレイヤーにまとめておいても問題にはなりません。ここで、カメラを手前にして次のような配置を考えてみます。


この画像では2つのカプセルをRenderPassの対象としており、壁を隔てて手前に半透明なカプセルがあります。この状況で、先ほどと同じ設定のまま出力結果を見ると...

このように、本来見えないはずの壁の向こうのカプセルが描画されてしまいました。これは、対象オブジェクトの色を決めているテクスチャで、壁の存在が考慮されていないためです。

色を格納しているレンダーテクスチャ

このような場合には、重なり合うオブジェクトを別々のレイヤーに分けることで一応対処ができます。次の例では、【IgnorePostProcessing1】と【IgnorePostProcessing2】にレイヤーを分けています。

レイヤーごとにカラー用のCustomTextureを作成

レイヤーごとにIgnorePostProcessingを作成

深度用のCustomTextureはどちらのレイヤーも対象外に

先程の2つのカプセルに異なるレイヤーを割り当てれば、下のような結果が得られます。

少しカプセルをずらしてみると、どちらもしっかりポストエフェクトを回避できていることがわかります。

Passの中であえてマテリアルを作り、個別にレンダーターゲットを渡していたのはこの仕組みを作るためでした。

ただし、この場合は奥のオブジェクトのレイヤーを描画⇒手前のオブジェクトのレイヤーを描画という順序でPassを実行しないといけません。当然Passが増えるとパフォーマンスが落ちていくので、基本的にはレイヤーの数(Passの数)を抑えるための画面設計が重要になると思います。
また、ポストエフェクトと一括りに言っても、DOF(被写界深度)のようなオブジェクトの周囲のピクセルにも影響を及ぼすエフェクトでは限界があるので注意が必要です。

最後に

以上、一部のオブジェクトをポストエフェクトの対象外にする方法でした。パイプラインの拡張などを行う際に今回の記事が参考になれば嬉しいです。
ここまで読んでいただきありがとうございました!

リンク

https://github.com/kr405/UnityIgnorePostProcessing
https://zenn.dev/kr405/articles/eee1afc71df14b

Discussion