Unity 6 URP Render Graph パス種別完全ガイド:AddBlitPass vs AddRasterRenderPass

Unity 6 URP Render Graph 全5回シリーズ
第1回:基礎と実装原理
第2回:パス種別 Deep Dive
第3回:Render Graph Viewer 完全攻略
第4回:Migration 準備編
第5回:Migration 実践・運用編

TL;DR (Unity 6 URP 基準)
– パス選択の基準はひとつ ― Render Graph がリソースの流れを追跡できるか
– 基本の選択肢:AddRasterRenderPass(Safe、FBF・マージ可能)/ 単純コピー:AddCopyPass / マテリアル Blit:AddBlitPass / サイズ不一致 Blit:AddUnsafePass
– Frame Buffer Fetch:Vulkan・Metal・DX12 対応、TBDR GPU(モバイル・Apple Silicon)でVRAM往復なしにオンチップメモリから直接読み取り
– Unsafeパスが増えるほどマージ不可・最適化の余地が減少 → 設計段階からSafeパス中心で構成する


目次

  1. 第1回で残した疑問 — なぜマージされなかったのか
  2. パス6種を一目で比較
  3. Frame Buffer Fetch の仕組み — GPU チップ内部から読み取る
  4. Safe vs Unsafe パスとパスのマージ
  5. RequireIntermediateTexture — Back Buffer の制約を超える方法
  6. Frame Data と ContextItem — パス間のデータ共有
  7. Output Texture — Shader Graph から名前でアクセス
  8. MRT(Multiple Render Targets)— 1パスで複数バッファを出力
  9. Renderer List と Culling — レイヤーでオブジェクトをフィルタリング
  10. まとめ — 次回予告

1. 第1回で残した疑問 — なぜマージされなかったのか

第1回TintingRenderPass を実装した際に AddBlitPass を使いました。Render Graph Viewer を開くと、このパスは周囲のパスと青い接続線なしに単独で表示されています。マージされていないのです。
なぜでしょうか?
Render Graph ViewerでTintingPassが青い接続線なしに単独表示されている画面 — AddBlitPass使用によりパスのマージが適用されていない状態

理由はシンプルです。AddBlitPass は Unsafe パスです。Render Graph は Unsafe パスの内部リソースアクセスパターンを追跡できないため、自動最適化(パスのマージ・メモリ再利用)を諦めます。これが 「Render Graph が内部を追跡できない Unsafe パス(AddBlitPass など)」「リソースの流れを直接宣言する Safe パス(AddRasterRenderPass など)」 の違いです。

Render Graph が提供するパス追加メソッドは、AddCopyPass(Safe)、AddBlitPass(Unsafe)、AddRasterRenderPass(Safe)、AddUnsafePass(Unsafe)、AddComputePassAddRendererListPass の6種類です。AddComputePassAddRendererListPass は Safe/Unsafe の分類ではなく、別の実行モデル(Compute Shader、Renderer List)を持ちます。今回は各パスがどんなときに使われ、なぜその設計を選ぶべきなのかをコードとともに見ていきましょう。


2. パス6種を一目で比較

下の表は、Unity 公式 URP Render Graph Samples で各パスがどのような役割を担うかをまとめたものです。

API FBF サポート パスのマージ 主な用途
AddCopyPass ✅ 自動 ✅ 可能 テクスチャの単純コピー(FBF 自動適用)
AddBlitPass ❌ なし ❌ 不可 マテリアル適用 Blit(Unsafe)
AddRasterRenderPass ✅ 手動 ✅ 可能 カスタムレンダー・FBF の直接制御
AddUnsafePass ❌ なし ❌ 不可 サイズ異なる Blit、レガシー互換
AddComputePass Compute Shader
AddRendererListPass オブジェクトレイヤーフィルターレンダー

以下の意思決定ツリーで、パスを選ぶ際の判断基準を確認できます。

カスタムレンダーパスが必要か? RecordRenderGraph エントリーポイント GPU演算(Compute Shader)? バッファ並列処理、汎用演算 Yes AddComputePass No オブジェクトレイヤーフィルターレンダー? DrawRendererList、カスタムカリング Yes AddRenderer ListPass No SourceとTargetのサイズが異なるか? Blitサイズ不一致 → マージ自体が不可 Yes AddUnsafePass No マテリアル(シェーダー)適用 Blit? FBFなし、性能より利便性を優先 Yes AddBlitPass No 単純テクスチャコピー? FBF自動、マージ可能 Yes AddCopyPass No(FBF手動) AddRaster RenderPass Safe(マージ可) Unsafe(マージ不可)

3. Frame Buffer Fetch の仕組み — GPU チップ内部から読み取る

まず、一般的な Blit の流れを考えてみましょう。Fragment Shader がソーステクスチャを読み取るには、GPU のテクスチャサンプラーが VRAM(Video RAM)上のデータをバス経由で取得しなければなりません。このプロセスでメモリ帯域幅が消費され、特にモバイル GPU ではバッテリーと発熱に直結します。

Frame Buffer Fetch(FBF) はこれとは異なる経路を取ります。FBF は Vulkan、Metal、DirectX 12 API でサポートされており(DirectX 11 は非対応)、実質的な性能向上は TBDR(タイルベースドレンダリング)GPU でのみ 得られます。TBDR GPU ― モバイル(Mali、Adreno)と Apple Silicon ― は現在のタイルのピクセルデータを GPU のオンチップメモリに保持するため、FBF を使うことで VRAM を経由せず Fragment Shader から直接読み取れます。デスクトップ向けの NVIDIA(Maxwell 以降)や最近の AMD アーキテクチャもタイルベースラスタライザーを部分的に採用しており、DX12 Native RenderPass 環境では FBF を利用できます。ただし、完全なオンチップタイルメモリ構造ではないため、モバイルと比べた性能向上は限定的です。

通常の Blit(AddBlitPass) GPU Fragment Shader Texture Sampler VRAM (Video RAM) 帯域幅消費 ↑ 発熱・バッテリー ↑ バス往復 ↓書込 ↑読込 Frame Buffer Fetch(AddRasterRenderPass) GPU Fragment Shader On-chip Tile Buffer 直接 VRAM(未使用) 帯域幅消費なし → 性能 ↑ モバイル:GPU帯域幅 ↑ 発熱 ↑ モバイル:GPU帯域幅 ↓ 性能 ↑

FBF をコードで有効にするには、2つのステップが必要です。まず、AddRasterRenderPass の Builder でソースを Input Attachment として登録します。次に、Fragment Shader 側で通常の tex2D の代わりに FRAMEBUFFER_INPUT_HALF マクロを使います。

// AddRasterRenderPassでFBFパスを追加する例
void FBFetchPass(RenderGraph renderGraph, ContextContainer frameData,
    TextureHandle source, TextureHandle destination)
{
    using var builder = renderGraph.AddRasterRenderPass(
        "FBFetch Pass", out var passData);

    // ソースをInput Attachmentとして設定 → FBF有効化
    passData.source = builder.SetInputAttachment(source, 0, AccessFlags.Read);
    passData.destination = builder.SetRenderAttachment(destination, 0, AccessFlags.Write);

    builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
    {
        // DrawProceduralでFBFシェーダーを実行
        ctx.cmd.DrawProcedural(Matrix4x4.identity, fbfMaterial, 0,
            MeshTopology.Triangles, 3);
    });
}

シェーダー側では、URP の FRAMEBUFFER_INPUT_HALF_DECLARE マクロを使います。tex2D のように UV 座標を指定するのではなく、現在のピクセルのデータをオンチップメモリからそのまま読み取るのが特徴です。

// FBFシェーダーのコアコード
FRAMEBUFFER_INPUT_HALF(0);   // Input Attachment 0 の宣言

half4 frag(Varyings input) : SV_Target
{
    half4 color = LOAD_FRAMEBUFFER_INPUT(0, input.positionHCS.xy);
    // ピクセルデータの操作(例:青いティント)
    return color * half4(0, 0, 1, 1);
}

4. Safe vs Unsafe パスとパスのマージ

Render Graph が「Safe」と呼ぶパスとは、リソースアクセス情報を Render Graph に完全に宣言するパスのことです。Render Graph はこの情報をもとに、2つの最適化を行います。

パスのマージ(Pass Merge): 互いに依存しない連続したパスを、単一の GPU レンダーパスにまとめます。Render Graph Viewer では青い接続線として表示されます(下の画像の赤枠を参照)。パスのマージで得られる利点は3つあります。第一に、API コールのオーバーヘッド削減 ― BeginRenderPass/EndRenderPass の呼び出し回数が減ります。第二に、VRAM 帯域幅の節約 ― マージされたパス間の中間レンダーテクスチャを VRAM に書き出して再度読み返す往復が省略されます(TBDR GPU ではオンチップメモリにそのまま保持)。第三に、GPU 同期バリアの削減 ― パスが分離されていた場合に挿入されていたパイプラインバリアが減り、GPU ストールを抑制できます。

リソースの再利用: パスが終了すると、その TextureHandle のメモリを即座に別のパスへ再割り当てします。すべてのレンダーテクスチャをゲーム実行中ずっと保持し続ける従来の方式とは対照的です。これにより、フレーム内の最大 GPU メモリ使用量が減り、メモリの断片化も抑制されます。

AddBlitPassAddUnsafePassUnsafe パスです。これらのパスは内部で CommandBuffer API(cmd.Blitcmd.SetRenderTarget など)を使用しており、Render Graph はそうした低レベル命令内のリソースアクセスを追跡できません。その結果、パスのマージが適用されなくなります。

Render Graph Viewer で Unsafe パスを確認するには、Pass Filter → Unsafe Pass トグルを有効にする必要があります(下の画像のオレンジ色の枠を参照)。

unsasfe-filter-and-merged-passes]

5. RequireIntermediateTexture — Back Buffer の制約を超える方法

URP で画面に最終出力されるバッファを Back Buffer と呼びます。カスタムパスが Back Buffer をソースとして読み取りながら、同時に Back Buffer へ書き込もうとすると、GPU で未定義の動作が発生します。

これを防ぐため、URP は自動的に中間テクスチャ(Intermediate Texture)を作成し、Back Buffer の代わりに使用します。ただし、この動作はカスタムパス側から明示的にリクエストしなければなりません。

// ScriptableRenderPassのコンストラクターで
public TintingRenderPass()
{
    // Back Bufferをソースとして読むパスなら必ずtrue
    requiresIntermediateTexture = true;
}

requiresIntermediateTexture = true を設定すると、URP は現在の Active Color Texture を Back Buffer ではなく別の RenderTexture として用意します。第1回の TintingRenderPass でこの設定が必要だったのも、Back Buffer をソーステクスチャとして読み取るためでした。

false のままでよいケースは? パスがソースを読まずに書き込みだけを行う場合、または Injection Point が After Rendering のように Back Buffer より後になる場合です。中間テクスチャの生成にはメモリコストが伴うため、不要であれば false(デフォルト)のままにしましょう。


6. Frame Data と ContextItem — パス間のデータ共有

複数のパスが同じテクスチャを共有しなければならない場面では、かつては Global TextureShader.SetGlobalTexture)がよく使われていました。しかし Render Graph 環境で Global Texture を使うと2つの問題が生じます。Render Graph がそのテクスチャへのアクセスを追跡できず最適化の機会を失うこと、そしてすべてのシェーダーが公開されたテクスチャを意図せず参照してしまうリスクです。

Unity が推奨する代替手段が Frame Data です。ContextContainer(以下 frameData)は現在のフレームでのみ有効な辞書型コンテナで、パス間データ共有の公式チャンネルとして機能します。

// 1) ContextItemを継承したカスタムデータクラス
public class BlitFrameData : ContextItem
{
    public TextureHandle savedTexture;

    public override void Reset() => savedTexture = TextureHandle.nullHandle;
}

// 2) 最初のパスで保存
public override void RecordRenderGraph(RenderGraph renderGraph,
    ContextContainer frameData)
{
    var blitData = frameData.Create();

    using var builder = renderGraph.AddRasterRenderPass(
        "Save Texture", out var passData);

    var colorTexture = frameData.Get().activeColorTexture;
    blitData.savedTexture = colorTexture;
    // ...
}

// 3) 後続パスで読み取り
public override void RecordRenderGraph(RenderGraph renderGraph,
    ContextContainer frameData)
{
    var blitData = frameData.Get();
    var savedTex = blitData.savedTexture;
    // savedTexをソースとして使うパスの構成...
}

ContextItem.Reset() はフレーム終了時に自動的に呼び出されます。TextureHandle を null で初期化しておくことで、Render Graph がメモリを正しく回収できます。


7. Output Texture — Shader Graph から名前でアクセス

Output Texture サンプルは、レンダーパスが生成したテクスチャを名前(文字列)ベースで Shader Graph や他のマテリアルからアクセス可能にするパターンを紹介しています。

// AddBlitPassにテクスチャ名をProperty IDとして指定
var blitParams = new BlitMaterialParameters(
    source: sourceTexture,
    destination: activeColorTexture,
    material: blitMaterial,
    passIndex: 0
);
// シェーダープロパティ "_InputTexture" の名前でソースをバインド
blitParams.sourceTexturePropertyID = Shader.PropertyToID("_InputTexture");
renderGraph.AddBlitPass(blitParams, "Output Texture Pass");

_InputTexture は予約語ではありません。PropertyToID に渡した文字列と Shader Graph の Texture2D プロパティ名が完全に一致して初めて、レンダーパスが注入したテクスチャが接続されます。_MyBuffer など別の名前に変えても、両側を揃えれば問題なく動作します。ポストエフェクトとワールドオブジェクトのマテリアルが同一の中間バッファを共有したい場面で役立つパターンです。


8. MRT(Multiple Render Targets)— 1パスで複数バッファを出力

ディファードレンダリングの G-Buffer では、単一のパスが Color・Normal・Depth など複数のバッファへ同時に書き出します。Render Graph でも AddRasterRenderPass の Builder に複数の Render Attachment を登録することで MRT を実装できます。

Single RenderPass AddRasterRenderPass Color Buffer Attachment Index 0 Normal Buffer Attachment Index 1 Depth Buffer Attachment Index 2 RGBA カラー 法線ベクトル 深度値 単一パス → 3バッファ同時出力(Draw Call 1回)
using var builder = renderGraph.AddRasterRenderPass("MRT Pass", out var passData);

// 出力テクスチャ3つをそれぞれ別のインデックスに登録
builder.SetRenderAttachment(colorHandle,  0, AccessFlags.Write);
builder.SetRenderAttachment(normalHandle, 1, AccessFlags.Write);
builder.SetRenderAttachment(depthHandle,  2, AccessFlags.Write);

builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
{
    // DrawProcedural または DrawRendererList でMRT出力
    ctx.cmd.DrawProcedural(Matrix4x4.identity, mrtMaterial, 0,
        MeshTopology.Triangles, 3);
});

シェーダー内では SV_Target0SV_Target1SV_Target2 で各出力を区別します。1回のドローコールで複数のバッファを書き込めるため、G-Buffer を個別パスで埋める方式よりドローコール数を削減できます。


9. Renderer List と Culling — レイヤーでオブジェクトをフィルタリング

AddRendererListPass(または Builder の UseRendererList)系のパスは、レイヤーマスクでフィルタリングしたオブジェクトリストをレンダリングします。カスタムアウトライン、選択ハイライト、ミニマップなど、特定のレイヤーだけを描画したい場面で活躍します。

// RendererListの初期化(RecordRenderGraph内で呼び出し)
RendererListHandle InitRendererList(RenderGraph renderGraph, ContextContainer frameData)
{
    var renderingData = frameData.Get();
    var cameraData   = frameData.Get();
    var lightData    = frameData.Get();

    var filterSettings = new FilteringSettings(RenderQueueRange.opaque, layerMask);
    var shaderTagIds   = new ShaderTagId[] { new ShaderTagId("UniversalForwardOnly") };
    var drawSettings   = RenderingUtils.CreateDrawingSettings(
        shaderTagIds, ref renderingData, ref cameraData, ref lightData,
        SortingCriteria.CommonOpaque);

    var rlParams = new RendererListParams(
        renderingData.cullResults, drawSettings, filterSettings);

    return renderGraph.CreateRendererList(rlParams);
}

Culling サンプルはさらに一歩踏み込み、カメラのカリング結果を直接操作します。CullContextData.Cull(cullingParams) を呼び出してカメラの視野角やシャドウレイヤーをカスタマイズしてから、レンダーリストを構成します。マルチプレイヤーでチームごとに見えるオブジェクトを変えたい場合や、RTSのように戦場の霧(Fog of War)をレイヤー単位で制御したい場合には、このパターンが最もストレートな解法となります。


10. まとめ — 次回予告

パス選択のポイントは結局ひとつです。Render Graph がリソースの流れを把握できるか。 AddRasterRenderPass が基本の選択肢で、単純コピーは AddCopyPass、マテリアル Blit は AddBlitPass、サイズが異なる Blit には AddUnsafePass を使います。Unsafe パスが増えるほど最適化の余地が狭まるため、最初の設計段階から Safe パス中心で構成する習慣が重要です。

パスを正しく選べたら、次の疑問は自然と浮かびます。「実際にマージされているのか?」第3回では Render Graph Viewer でこの疑問に直接答えます。カラーコードやアイコンの意味、Frame Debugger との連携まで ― 青い接続線がどこで途切れているかを目で確認する方法を解説します。


前回: 第1回 — Render Graph 基礎と実装原理
次回: 第3回 — Render Graph Viewer 完全攻略


合わせて読みたい記事


さらに深く学ぶために

この記事を書くにあたって直接確認した資料です。

コメントする