Unity 2022.3 LTS (URP 14) および Unity 6において、従来のC#スクリプトによる複雑な実装を行わずにカスタムポストプロセスを実装できる機能として「FullScreen Pass Renderer Feature」が導入されました。FullScreen Shader Graphと組み合わせることで、グリッチ効果、色収差、画面ブラー、レトロドット風モザイクなどの様々な画面エフェクトを容易に作成できます。しかし、実際に導入すると「画面が突然真っ黒になりフリーズする」「UIまでエフェクトが巻き込まれてボタンやテキストが読めなくなる」「VR環境で片目だけ機能しない」といったグラフィックス関連のバグに直面することが頻繁にあります。本記事では、これらFullScreen Pass特有のバグの根本原因と実務における正しい解決設定手順を詳細に解説します。

1. 根本原因①:テクスチャの同時読み書き(レンダリングフィードバックループ)

カスタムポストプロセスで「現在の画面カラーを取得し、加工して画面に出力する」処理を行う際、ゲーム画面全体が真っ黒、あるいは特定の一色に染まってしまう現象は、「レンダリングフィードバックループ」が原因です。

GPUは同じテクスチャ(レンダーターゲット)に対して、「同時に読み込みながら書き込みを行う」ことがアーキテクチャの都合上できません。FullScreen Passの実行中、描画ターゲットはカメラのカラーバッファ(Camera Color Target)に指定されています。この状態でシェーダー側から標準の Scene Color ノードなどを使用して現在のカラーバッファを読み出そうとすると、GPUは同じメモリ領域への読み書きの競合を検知し、安全のために描画をスキップするかメモリをクリアします。その結果、画面が真っ黒になるバグが発生します。

対策手順:_BlitTexture(URP専用バッファ)の利用

URPは、FullScreen Pass Renderer Featureを実行する直前に、現在のカメラカラーバッファのコピー(一時テクスチャ)を内部で確保します。このコピーされたテクスチャはシェーダー側に _BlitTexture というプロパティ名でバインドされます。したがって、シェーダー内では Scene Color ではなく、この _BlitTexture をサンプリングしなければなりません。

  1. Fullscreen Shader Graphを開きます。
  2. Blackboard(黒板)で新しい Texture2D プロパティを作成し、名前(Name)および参照名(Reference)を厳密に _BlitTexture に設定します(※先頭のアンダーバーを含めて完全に一致させる必要があります)。
  3. グラフ内で Sample Texture 2D ノード(または URP Sample Buffer の BlitSource)を追加し、入力テクスチャに _BlitTexture を接続します。
  4. サンプリングしたRGBAカラーを加工(グレースケール化や色変換など)し、最終出力(Fragment ShaderのColor)へ出力します。

この手順により、URPがコピーした安全なテクスチャからカラー情報を読み出しつつ、元のカメラカラーバッファへ上書き出力できるようになり、フィードバックループが解消されます。

2. 根本原因②:UI Canvasの描画タイミングと巻き込みバグ

作成した画面エフェクト(例:画面を揺らすグリッチや、色を反転させるネガポジ効果)が、手前に表示されているUI(スコア表示やボタンなど)にまで適用されてしまい、UIの文字がブレたり表示が崩れてしまうバグも非常によく見られます。

この原因は、UI Canvasのレンダリングモード(Render Mode)およびFullScreen Passを差し込む描画イベント(Injection Point / Event)のタイミングの不整合にあります。

デフォルト状態のUI Canvasは Screen Space - Overlay または Screen Space - Camera に設定されており、3Dカメラがすべてのシーンを描画した後に重ね合わせられます。FullScreen Passの「Event」が After Rendering Post ProcessingAfter Rendering Transparents に設定されている場合、UIが描画された直後に画面全体をポストエフェクトで再度書き換えるため、UIもエフェクトの影響を受けてしまいます。

解決策パターン 具体的な設定と動作 メリット・デメリット
A. UIを「Screen Space - Overlay」にする UIを3Dカメラのポストプロセス管理外に置き、最終画面にダイレクトに重畳描画します。 最も簡単で低負荷。ただしUIの手前に3Dエフェクトを重ねることはできなくなります。
B. Eventを「Before Rendering Post Processing」にする FullScreen Passの適用タイミングをUI描画の前に設定します。UI Canvasのレンダーモードは Screen Space - Camera(Baseカメラ指定)である必要があります。 3Dシーンのみにエフェクトがかかり、UIはクリーンに保たれます。最も推奨されるポストプロセスの差し込み位置です。
C. カメラスタッキングによるUI分離 Baseカメラ(3D専用)でポストプロセス付き描画を行い、Overlayカメラ(UI専用)をスタックして後からUIを乗せます。 UI要素の解像度やアンチエイリアスを完全に制御可能ですが、カメラスタックによるオーバーヘッドが若干発生します。

3. 根本原因③:VR環境での立体視ズレとテクスチャ配列未対応

Meta QuestなどのVR向けビルドで Single Pass Instanced(または Multiview) を有効にした場合、画面全体にかけるポストエフェクトが右目側(または左目側)だけ適用されなかったり、左右の目のテクスチャが混ざり合って強烈なブレが発生することがあります。

これは、VRレンダリングにおいて左右の目が「テクスチャ配列(Texture2DArray)」の各スライス(インデックス0と1)に対して別々に描画されるためです。標準のシェーダーコードや簡易的なShader Graphノードは通常の Texture2D(単一画像)を想定してコンパイルされるため、配列インデックスが右目描画時に正しく切り替わらず、左目のバッファを使い回してしまい描画がバグを起こします。

対策手順:ステレオ対応サンプリングの設定

  1. Fullscreen Shader Graphの _BlitTexture プロパティ設定で、黒板の該当項目を選択します。
  2. インスペクターで Use Texture Array オプションがあれば有効にし、シェーダーが自動的に Texture2DArray として宣言されるようにします。
  3. UV座標の計算において、直接 Screen Position ノードをサンプリングする場合、出力ピンのタイプがVRステレオ対応になっているかを確認し、Unity内部のマクロ(UNITY_DECLARE_SCREENSPACE_TEXTURE 等)が機能するようにマテリアル設定のコンパイルターゲットにVRプラットフォームを指定します。

4. C#スクリプトによるポストプロセス値の動的制御・アニメーション

FullScreen Pass Renderer Featureは標準のVolumeシステムとは異なり、単にマテリアルを割り当てて画面をBlitする機能であるため、ゲームの状況に応じてC#から「画面揺れ強度」や「ノイズの混入率」といったパラメータをリアルタイムに変更するには、Renderer Featureが保持するマテリアルに対してスクリプト経由で直接値を設定する必要があります。

以下は、FullScreen Passのマテリアルインスタンスを安全に取得・制御し、ゲームプレイ中にエフェクト強度をC#から滑らかに変更・アニメーションさせるための実用的なコントローラースクリプトです。

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

public class FullscreenEffectController : MonoBehaviour
{
    [Header("対象のURPデータアセット")]
    public UniversalRenderPipelineAsset urpAsset;
    
    [Header("エフェクト名 (Renderer FeatureのNameと一致させる)")]
    public string featureName = "GlitchFullscreenPass";

    [Header("リアルタイム変更パラメータ")]
    [Range(0f, 1f)] public float glitchIntensity = 0f;
    public Color tintColor = Color.white;

    private Material targetMaterial;
    private int intensityPropId;
    private int colorPropId;

    void Start()
    {
        intensityPropId = Shader.PropertyToID("_GlitchIntensity");
        colorPropId = Shader.PropertyToID("_TintColor");
        
        ExtractMaterialFromRenderer();
    }

    void ExtractMaterialFromRenderer()
    {
        if (urpAsset == null) return;

        // URPのRenderer Dataから指定のFeatureを探し出し、設定されているマテリアルへの参照を取得する
        var rendererDataList = typeof(UniversalRenderPipelineAsset)
            .GetField("m_RendererDataList", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        
        if (rendererDataList != null)
        {
            var dataArray = rendererDataList.GetValue(urpAsset) as ScriptableRendererData[];
            if (dataArray != null && dataArray.Length > 0)
            {
                var activeData = dataArray[0]; // デフォルトのRenderer Data
                foreach (var feature in activeData.rendererFeatures)
                {
                    if (feature != null && feature.name == featureName)
                    {
                        // フルスクリーンパスの内部フィールド「m_Settings」からマテリアルを取得
                        var settingsField = feature.GetType().GetField("m_Settings", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                        if (settingsField != null)
                        {
                            var settings = settingsField.GetValue(feature);
                            var materialField = settings.GetType().GetField("passMaterial", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
                            if (materialField != null)
                            {
                                targetMaterial = materialField.GetValue(settings) as Material;
                            }
                        }
                        break;
                    }
                }
            }
        }

        if (targetMaterial == null)
        {
            Debug.LogWarning($"FullScreen Pass Feature '{featureName}' またはそのマテリアルが見つかりませんでした。");
        }
    }

    void Update()
    {
        if (targetMaterial == null) return;

        // C#からシェーダーマテリアルのパラメータをリアルタイム更新
        targetMaterial.SetFloat(intensityPropId, glitchIntensity);
        targetMaterial.SetColor(colorPropId, tintColor);
    }
}

※このスクリプトは、インスペクターや他のアニメーションシステム(DOTweenやAnimator)から glitchIntensity などの変数を操作することで、ゲームの被ダメージ時やメニュー展開時に画面をノイズだらけにする演出などを安全に実現することができます。実行時にマテリアルが複製(インスタンス化)されないよう、参照先の共有マテリアルを書き換えるため、エディタ実行を停止した際はアセット側の初期値にリセットするコードを適宜追加してください。