UnityのUniversal Render Pipeline (URP) において、ゲーム独自のポストエフェクト(モザイク、色収差、スキャンラインなど)や、特定のキャラクターのアウトライン描画、特定オブジェクトの最前面表示などを実現する強力な機能が「Scriptable Render Feature(および Scriptable Render Pass)」です。

しかし、Render Featureを追加したにもかかわらず「エフェクトが画面に表示されない」「半透明オブジェクト(パーティクルなど)の前後関係がおかしくなる」「UIの上にエフェクトが被ってしまう」「Unity 2022.3やUnity 6に移行したら cmd.Blit が機能しなくなったり画面が真っ黒(Black Screen)になった」といった不具合に直面する開発者が後を絶ちません。

本記事では、カスタム描画やポストエフェクトが意図通りに動作しない根本原因を、URPの描画パイプラインの仕組みとタイミング(RenderPassEvent)の観点から解説し、Unity 2022.3 / Unity 6に対応したモダンな RTHandleBlitter を用いた正しい実装手順を詳しく紹介します。

1. 根本原因:RenderPassEvent(描画タイミング)の不整合とバッファ状態

カスタム描画が機能しない、または描画順がおかしくなる最大の原因は、レンダーパスをパイプラインに挿入するタイミングである RenderPassEvent の設定ミスにあります。URPは、下図のように厳密な順序で不透明オブジェクト、デプステクスチャ、半透明オブジェクト、ポストプロセスなどを描画しています。

URPにおけるRenderPassEventのインジェクションポイントとバッファ状態の遷移図

図:URP描画パイプラインの流れとカスタムレンダーパスの挿入位置関係

不具合が発生する代表的なケースは以下の通りです:

  • まだバッファが準備されていない段階での読み込み: BeforeRenderingOpaques など、カメラのカラーバッファやデプスバッファの初期化・描画が完了していないタイミングで画面全体へのエフェクト(Blit処理など)を実行しようとすると、真っ黒な画面や前フレームの残像が描画されてしまいます。
  • ポストプロセスとの前後関係の矛盾: URP標準のポストプロセス(Bloom、Color Adjustmentsなど)は通常 BeforeRenderingPostProcessing から AfterRenderingPostProcessing の間で処理されます。もし独自のポストエフェクトを AfterRenderingPostProcessing より後に実行してしまうと、URPのカラーグレーディングやトーンマッピングがすでに適用し終わった後に処理が走るため、色調が極端に崩れたり、UI(Overlay Canvas)の上にエフェクトが重なって描画されてしまいます。
  • 半透明オブジェクト(Transparent)との干渉: 水面エフェクトや煙などの半透明オブジェクトは、不透明オブジェクト(Opaque)の描画が完了した後に描画されます。カスタムのアウトラインやマスク処理を AfterRenderingOpaques で行うと、不透明オブジェクトに対しては正しく描画されますが、その後から半透明オブジェクトが上に重ねて描画されるため、半透明オブジェクトの背後にあるはずのアウトラインが透けて見えなくなったり、逆に手前にある半透明オブジェクトを遮って描画されたりします。

2. RenderPassEvent の選定基準・マトリクス

実装したいエフェクトの種類に応じて、どの RenderPassEvent を指定すべきかをまとめた一覧です。

RenderPassEvent 主な用途・適した描画 利用可能なバッファと特徴
BeforeRenderingOpaques カスタムのデプスプリパス、地面のデカール投影など カラーバッファはクリア状態。3Dキャラ等は未描画。
AfterRenderingOpaques 不透明キャラのシルエット(壁越し表示)、アウトライン 不透明オブジェクトの描画が完了。半透明は未描画。
AfterRenderingTransparents 水面屈折、フォグへの干渉、ポストプロセス前の中間処理 3Dシーン全体のすべての不透明・半透明オブジェクトの描画が完了。
BeforeRenderingPostProcessing カスタムポストエフェクト(被写界深度やブラー、画面歪みなど) URP標準のポストプロセス(Bloomやトーンマップ)が適用される直前の状態。
AfterRenderingPostProcessing UI描画前の画面最終調整、ピクセルアート風モザイクなど すべての3D描画・ポストプロセスが完了。※UI(Screen Space - Overlay)の直前。

3. 解決策:Unity 2022.3 / Unity 6 対応のモダンな Scriptable Render Feature の実装例

従来の cmd.Blit(source, destination)GetTemporaryRT などのAPIは、解像度スケーリング(Render Scale)やVRマルチパス描画との相性が悪く、Unity 2022.3以降では非推奨(Deprecated)となっています。新しい RTHandleBlitter.BlitCameraTexture を用いた、毎フレームのGC Allocが発生しない正しい実装テンプレートコードを示します。

実装例:画面の色を反転させるカスタムポストエフェクト(Color Invert)

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

public class ColorInvertRenderFeature : ScriptableRenderFeature
{
    [System.Serializable]
    public class Settings
    {
        public Material invertMaterial;
        public RenderPassEvent renderEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    [SerializeField] private Settings settings = new Settings();
    private ColorInvertPass invertPass;

    public override void Create()
    {
        // パスをインスタンス化
        invertPass = new ColorInvertPass(settings.invertMaterial);
        invertPass.renderPassEvent = settings.renderEvent;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (settings.invertMaterial == null) return;
        
        // カメラのカラーターゲットをパスに設定
        invertPass.Setup(renderer.cameraColorTargetHandle);
        renderer.EnqueuePass(invertPass);
    }

    protected override void Dispose(bool disposing)
    {
        invertPass?.Dispose();
    }
}

public class ColorInvertPass : ScriptableRenderPass
{
    private Material material;
    private RTHandle cameraColorTarget;
    private RTHandle tempTexture; // 一時バッファ用RTHandle

    public ColorInvertPass(Material material)
    {
        this.material = material;
    }

    public void Setup(RTHandle colorTarget)
    {
        this.cameraColorTarget = colorTarget;
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        // カメラのテクスチャ詳細(解像度やフォーマットなど)を取得
        var desc = renderingData.cameraData.cameraTargetDescriptor;
        desc.depthBufferBits = 0; // カラー処理なのでデプスバッファは不要

        // テンポラリテクスチャを安全に確保(解像度変更などに自動追従)
        RenderingUtils.ReAllocateIfNeeded(ref tempTexture, desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_TempColorInvertTexture");
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (material == null || cameraColorTarget == null) return;

        // CommandBufferをPoolから取得
        CommandBuffer cmd = CommandBufferPool.Get("ColorInvertPass");

        using (new ProfilingScope(cmd, new ProfilingSampler("Color Invert Execution")))
        {
            // 1. カメラカラーバッファの内容を、一時バッファにBlit(マテリアル適用なし)
            Blitter.BlitCameraTexture(cmd, cameraColorTarget, tempTexture);

            // 2. 一時バッファの画像を、マテリアルを適用しながらカメラカラーバッファへ書き戻す
            // マテリアル側では "_BlitTexture" という名前でBlit元テクスチャを受け取る必要があります
            Blitter.BlitCameraTexture(cmd, tempTexture, cameraColorTarget, material, 0);
        }

        // コマンドの実行と解放
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        // OnCameraSetupで確保したバッファをフレーム終了時にリセットすることはしない(ReAllocateIfNeededに任せる)
    }

    public void Dispose()
    {
        // メモリリークを防ぐため、オブジェクト破棄時に明示的にリリース
        tempTexture?.Release();
    }
}

対応するマテリアル用シェーダー(HLSL / URP Blit Shader)

Blitterによって流し込まれる画像テクスチャは、シェーダー側で _BlitTexture という名前でサンプリングする必要があります。以下は色を反転するHLSLの最小実装です。

Shader "Hidden/URPInvertBlit"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZWrite Off ZTest Always Cull Off

        Pass
        {
            Name "InvertColorPass"

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            // URPの標準Blit用HLSLインクルード
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

            half4 Frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                
                // _BlitTextureからピクセルカラーをサンプリング(スクリーンスペースUVを使用)
                float2 uv = input.texcoord;
                half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
                
                // 色を反転
                return half4(1.0 - color.rgb, color.a);
            }
            ENDHLSL
        }
    }
}

4. ありがちなバッドプラクティスと回避策

カスタムレンダーフィーチャーの実装でよくあるミスと、その解決方法は以下の通りです。

1. Execute の中で毎回 new CommandBuffer や RTHandle のアロケートを行う

毎フレーム実行される Execute メソッド内でメモリ確保を行うと、劇的なGC Alloc(ゴミメモリの発生)を引き起こし、ゲームが一瞬カクつくGCスパイクの原因になります。
対策: CommandBufferは必ず CommandBufferPool.Get() から取得し、RTHandleのメモリ確保は OnCameraSetupRenderingUtils.ReAllocateIfNeeded を使用してください。また、作成した一時バッファは Dispose() メソッド内で Release() を呼び、VRAMのリークを防ぎます。

2. cmd.Blit(cameraColorTarget, cameraColorTarget, material) を行う

同じテクスチャに対して読み込みと書き込みを同時に行うと、グラフィックスドライバやGPUの仕様上「未定義の動作(Undefined Behavior)」となり、画面が激しくちらつく、表示が壊れる、あるいは完全にブラックアウトする原因になります。
対策: 必ず一時バッファ(テンポラリテクスチャ)を1枚挟み、「カメラバッファ → 一時バッファ」へコピーした後に「一時バッファ → マテリアル適用 → カメラバッファ」という二段階のBlitフロー(Ping-pongパターン)を踏んでください。

3. 半透明の奥に隠すべきオブジェクトが手前に透けて見える

アウトライン等のカスタム描画を行う際、深度テスト(ZTest)を無効化(ZTest Always)して描画を上書きしてしまうと、手前にある半透明オブジェクトや他の遮蔽物を無視して描画されてしまいます。
対策: 独自の描画用マテリアル側で ZTest LEqual (または ZTest Keep) を指定して通常の深度判定を行わせるか、RenderPassEventAfterRenderingTransparents に設定してすべての半透明の描画が終わった後に適用するように調整してください。

これらのルールとタイミング関係を理解して選定することで、URPの持つパワフルなグラフィックスパイプラインを最大限に活かし、思い通りの画面エフェクトをバグなしで安定動作させることが可能になります。