Unity 6 URP Render Graph 完全解説: なぜGraphで画面をレンダリングするようになったのか

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


Unity 6.3 にプロジェクトを更新してエディターを開いたとき、こんな警告が表示されて戸惑った方も多いかと思います。

“The project currently uses compatibility mode where the render graph API is disabled.”

Compatibility Mode(互換性モード)。すぐに問題はなさそうですが、この警告メッセージが示すことは明確です。Unity は Execute メソッドベースの ScriptableRenderPass 方式を段階的に廃止しようとしています。この警告を無視して進めることもできますが、一度立ち止まって考える価値はあります。なぜ Unity はわざわざレンダリング構造そのものを変える必要があったのでしょうか。

この記事はその問いに答えながら、Render Graph のコアコンセプトと最初のカスタムパス実装を取り上げます。コードを一行ずつ追うチュートリアルではなく、なぜこの構造でなければならないのかに焦点を当てます。

この記事のポイント
ScriptableRenderPass: メモリと依存関係を開発者が手動管理 — 規模が大きくなるほど複雑さが増大
RecordRenderGraph = 実行ではなく宣言。GPUコマンドはRender Graphが依存関係を分析した後に実行
TextureHandle = 即時メモリ割り当てではない。システムが必要なタイミングでのみ確保
– Tinting Effectの実装コードで上記3つの流れを実際に確認


目次

  1. 既存 ScriptableRenderPass の限界
  2. Render Graph とは何か
  3. コアクラス — RendererFeature と RenderPass
  4. RecordRenderGraph — 「実行ではなく意図の記述」
  5. Texture Handle — メモリの即時割り当てではない
  6. 最初のカスタムパス実装: Tinting Effect
  7. まとめ — 次回予告

既存 ScriptableRenderPass の限界

Unity の URP は長年にわたり ScriptableRenderPass を通じてレンダリングのカスタマイズをサポートしてきました。パスを作成し、順序を決めてパイプラインに追加し、Execute メソッド内で直接 GPU コマンドを積み上げる方式でした。

以下は、画面を単色でティンティングする簡単なエフェクトを実装した構造です。

// TintingFeature.cs — RendererFeature がパスの生成と登録を担当
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class TintingFeature : ScriptableRendererFeature
{
    private TintingPass m_Pass;

    public override void Create()
    {
        m_Pass = new TintingPass(RenderPassEvent.AfterRenderingOpaques);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_Pass);
    }
}
// TintingPass.cs — RenderPass が実際の GPU コマンドを Execute 内で即時実行
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class TintingPass : ScriptableRenderPass
{
    private RenderTargetIdentifier m_Source;
    private RenderTargetHandle m_TempTexture;
    private Material m_TintMaterial;

    public TintingPass(RenderPassEvent evt)
    {
        renderPassEvent = evt;
        m_TempTexture.Init("_TempTintTexture");
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        m_Source = renderingData.cameraData.renderer.cameraColorTarget; // レンダーターゲットを直接参照
        RenderTextureDescriptor desc = renderingData.cameraData.cameraTargetDescriptor;
        cmd.GetTemporaryRT(m_TempTexture.id, desc); // テクスチャを手動で割り当て
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        CommandBuffer cmd = CommandBufferPool.Get("TintingPass");

        // **↓ GPU コマンドを CommandBuffer に積む部分 (まだ実行されていない)**
        Blit(cmd, m_Source, m_TempTexture.Identifier());                // コマンド1: ソース → 一時テクスチャへコピー
        Blit(cmd, m_TempTexture.Identifier(), m_Source, m_TintMaterial);// コマンド2: マテリアル適用後ソースへコピー
        // **↑ ここまでは CPU でコマンドリストを作成しているだけ**

        context.ExecuteCommandBuffer(cmd); // 積まれたコマンドを GPU にまとめて送信 (この時点で実際に実行)

        CommandBufferPool.Release(cmd);
    }

    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        cmd.ReleaseTemporaryRT(m_TempTexture.id); // テクスチャを手動で解放
    }
}

小規模プロジェクトではうまく動作していました。ただし、プロジェクトの規模が大きくなると以下のような問題が発生します。

  1. どこに何があるか把握しにくくなります。 レンダーテクスチャを誰が作り、誰が使い、いつ解放されるかを自分で追跡しなければなりません。グローバルテクスチャに依存するコードが積み重なると依存関係が複雑になります。あるパスの変更が別のパスにどう影響するか予測しにくくなります。

  2. メモリ管理が手動です。 不要なテクスチャが依然として GPU メモリを占有していてもシステムは気づきません。最適化は開発者の責任です。

  3. デバッグが苦痛です。 画面がおかしく表示されたとき、どのパスで問題が発生したか追跡する公式ツールがありませんでした。Frame Debugger を手探りで調べるしかありませんでした。

Unity はこの問題を根本的に解決するために Render Graph を導入しました。「開発者が直接管理していたものをシステムが代わりに行う」という方向性です。


Render Graph とは何か

Render Graph という名前はビジュアルエディターのように聞こえますが、実際にはプログラミングインターフェースです。名前の「Graph」はコンピューターサイエンスで言うノードとエッジのデータ構造を指します。

Old: 線形パスチェーン New: Render Graph 依存関係マップ Pass A Pass B Pass C Pass D 順序固定 最適化なし Pass A Pass B Pass C Pass D 並列実行可能 自動メモリ管理 パスマージ最適化

Render Graph の動作原理はこうです。開発者は各レンダーパスがどのリソースを読み取り(read)書き込む(write)かを宣言(declare)します。直接実行するのではなく、意図を宣言するのです。すると Render Graph システムがこれらの宣言を解析して依存関係マップ(Dependency Map)を作成し、3つの最適化を自動的に実行します。

最適化項目 内容
不要なパスの除去 最終結果に影響しないパスを自動的にスキップ
GPU メモリの再利用 重複しない Transient テクスチャが同じメモリを共有
パスマージ タイルベース GPU(モバイル)で隣接するパスを1つに統合してメモリ帯域幅を削減

これら3つを自前で実装するのは容易ではありません。Render Graph を使えば自動でついてきます。


コアクラス — RendererFeature と RenderPass

Render Graph でカスタムレンダリングを実装する際に核となる2つのクラスをまず理解する必要があります。

ScriptableRendererFeature マネージャークラス • Create(): パスインスタンスの生成 • AddRenderPasses(): パイプラインへ追加 • Inspector 設定値を保持 生成 ScriptableRenderPass ワーカークラス • RecordRenderGraph(): 意図の記述 • PassData: 実行時に渡すデータ • レンダー関数(static delegate)の定義

ScriptableRendererFeature はマネージャーです。Inspector で設定できるプロパティを持ち、パスインスタンスを生成し、パイプラインに追加する役割を担います。Unity Editor で Universal Renderer Data に追加するのがこのクラスです。

ScriptableRenderPass はワーカーです。実際のレンダリング処理がここに定義されます。以前の方式では Execute メソッドに直接 GPU コマンドを記述していましたが、Render Graph では RecordRenderGraph メソッドに意図を宣言します。

新しい Renderer Feature を作成するには、Project ビューで 右クリック → Create → Rendering → URP Renderer Feature Script を選択すると、この2つのクラスを含むボイラープレートコードが自動生成されます。


RecordRenderGraph — 「実行ではなく意図の記述」

Render Graph を初めて触ると RecordRenderGraph が混乱の元になります。毎フレーム呼ばれるのに、テクスチャを「生成」してパスを「追加」するコードが中にあります。毎フレームメモリを割り当てているのでしょうか?

違います。 これが Render Graph のコアです。

RecordRenderGraph は実行フェーズではなく記録フェーズです。このメソッド内で行うすべての処理は「このリソースを使って、このパスを実行したい」という宣言です。実際の GPU コマンド実行は Render Graph システムが解析を終えた後に別途行われます。

public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
    // このメソッドは「実行」ではなく「意図の記述」
    var resourceData = frameData.Get<UniversalResourceData>();
    var source = resourceData.activeColorTexture;

    // テクスチャハンドルの生成 — メモリ割り当てではない
    var descriptor = renderGraph.GetTextureDesc(source);
    descriptor.name = "CameraColor_TintingPass";
    descriptor.clearBuffer = false;
    TextureHandle destination = renderGraph.CreateTexture(descriptor);

    // AddBlitPass — パスの登録 (実行は後で)
    var blitParams = new RenderGraphUtils.BlitMaterialParameters(source, destination, material, 0);
    renderGraph.AddBlitPass(blitParams, "TintingPass");

    // 後続パスが修正済みテクスチャを参照するよう更新
    resourceData.cameraColor = destination;
}

CreateTexture が実際の GPU メモリを割り当てないという点は、初見では戸惑うかもしれません。返されるのはテクスチャハンドル、つまり「後でこのようなテクスチャが必要になる」という予約です。Render Graph がすべてのパスの依存関係を解析した後、実際に必要なタイミングにのみメモリを確保します。


Texture Handle — メモリの即時割り当てではない

TextureHandle が重要な理由をもう少し掘り下げてみましょう。

従来の方式では RenderTexture.GetTemporary()RTHandle などを直接管理する必要がありました。パスが終わったら解放しなければならず、忘れるとメモリリークが発生しました。Render Graph ではこのプロセスを自動管理します。

TextureHandle は内部的に RT Handle を含みますが、この RT Handle は最初は未割り当て状態(null)です。Render Graph システムが実際にそのテクスチャが必要だと判断したときにメモリを確保します。不要であれば一切割り当てません。

実際に使用されないテクスチャは Render Graph のコンパイル時に刈り取られます。「念のため確保しておこう」といった保守的なコードがパフォーマンスに影響しないということです。従来の方式では不可能だったアプローチです。

Render Graph Viewer(Window → Analysis → Render Graph Viewer)を開くと、各パスがどのテクスチャを読み取り(緑)書き込む(赤)かを視覚的に確認できます。このツールの詳細な説明は第3回で扱います。
URP Render Graph Viewer — パスごとにテクスチャのread(緑)·write(赤)アクセスをカラーコードで表示した画面


最初のカスタムパス実装: Tinting Effect

画面全体に色調を乗せる Tinting Effect を実際に実装しながら流れを掴んでいきましょう。シンプルな例ですが、Render Graph のデータフローを体感するには十分です。

1. Renderer Feature の骨格

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

// ScriptableRendererFeature: Inspector に公開されるマネージャー役割
// URP Renderer Asset に追加するとカメラレンダリングごとに動作する
public class TintingRendererFeature : ScriptableRendererFeature
{
    // Inspector に公開する設定値を別クラスにまとめる慣習的パターン
    [System.Serializable]
    public class Settings
    {
        public Material material;
        public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
    }

    public Settings settings = new Settings();
    private TintingRenderPass _pass;

    // Create(): RendererFeature 初期化時に1回呼ばれる — パスインスタンスの生成
    public override void Create()
    {
        _pass = new TintingRenderPass();
        _pass.renderPassEvent = settings.renderPassEvent; // パス実行タイミングの指定
    }

    // AddRenderPasses(): 毎フレームカメラごとに呼ばれる — パイプラインへパスを登録
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (settings.material == null) return; // マテリアル未設定時は登録をスキップ
        _pass.Setup(settings.material);
        renderer.EnqueuePass(_pass); // レンダラーキューにパスを追加
    }
}

2. Render Pass — RecordRenderGraph の実装

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;      // RenderGraph, TextureHandle
using UnityEngine.Rendering.RenderGraphModule.Util; // BlitMaterialParameters, AddBlitPass

public class TintingRenderPass : ScriptableRenderPass
{
    private Material _material;

    // このパスは現在のカラーバッファにアクセスする必要があるため中間テクスチャが必要
    public TintingRenderPass()
    {
        requiresIntermediateTexture = true;
    }

    public void Setup(Material material) => _material = material;

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

        // バックバッファでは実行不可 (AfterRendering より早いタイミングでのみ中間テクスチャ使用可能)
        if (resourceData.isActiveTargetBackBuffer)
        {
            Debug.LogWarning("TintingPass: バックバッファでは実行できません。");
            return;
        }

        var source = resourceData.activeColorTexture;
        var descriptor = renderGraph.GetTextureDesc(source);
        descriptor.name = "CameraColor_TintingPass";
        descriptor.clearBuffer = false;

        TextureHandle destination = renderGraph.CreateTexture(descriptor);

        var blitParams = new RenderGraphUtils.BlitMaterialParameters(source, destination, _material, 0);
        renderGraph.AddBlitPass(blitParams, "TintingPass");

        // 後続パスが修正済みテクスチャを参照するよう更新
        resourceData.cameraColor = destination;
    }
}

3. Shader Graph でマテリアルを作成する

Create → Shader Graph → URP → Fullscreen Shader Graph を選択してフルスクリーンシェーダーグラフを作成します。URP Sample Buffer ノードの Source Buffer を Blit Source に設定し、Multiply ノードで任意の色調を乗算して Fragment 出力に接続すれば完成です。
Unity Fullscreen Shader Graphエディター — URP Sample BufferノードをBlit Sourceに設定した構成

Tinting Effect Shader Graph完成画面 — Blit SourceにTintカラーをMultiplyしFragmentへ出力する構成
シェーダーグラフを保存した後、マテリアルを作成して接続します。

  1. Project ウィンドウで Create → Material から新しいマテリアルを作成します。
  2. Inspector 上部の Shader ドロップダウンをクリックし、先ほど作成した Shader Graph の名前を検索して選択します。
  3. または Shader Graph アセットを右クリックして Create → Material を選択すると、自動的に接続済みのマテリアルが生成されます。

4. RendererFeature の登録とマテリアルの割り当て

  1. Project ウィンドウで URP Renderer Asset を選択します(UniversalRenderPipelineAsset_Renderer など)。
  2. Inspector の Renderer Features 項目で Add Renderer Feature → TintingRendererFeature を選択します。
  3. 追加した Feature の Settings → Material フィールドに、手順3で作成した Shader Graph マテリアルを割り当てます。
  4. Render Pass Event はデフォルト値 After Rendering Post Processing のままにします。
    URP Renderer Asset Inspector — TintingRendererFeatureを追加しShader Graphマテリアルを割り当てた画面

5. Render Graph Viewer で確認する

  1. Play Mode を開始します。
  2. Window → Analysis → Render Graph Viewer を開くと、現在のフレームのパス一覧が表示されます。
  3. 列の一覧から TintingPass を探します。RenderPassEvent.AfterRenderingPostProcessing に設定しているため、Blit Post ProcessingBlit Final To Back Buffer の間に位置します。
  4. TintingPass の列をクリックすると、このパスが読み書きするテクスチャリソースがハイライト表示されます。
    Render Graph Viewer — TintingPassがBlit Post ProcessingとBlit Final To Back Bufferの間に配置された画面

このようにして作成した Tinting Effect を Render Graph Viewer で確認すると、Blit Post ProcessingBlit Final To Back Buffer の間に TintingPass が表示されているのがわかります。AfterRenderingPostProcessing のタイミングに設定しているため、ポストプロセッシングが終わった直後に実行されます。

そして隣接するパスとはマージされませんAddBlitPass が内部的に Unsafe Pass を使用しているためです。どのパスがマージ可能でどのパスはできないのか、それがパフォーマンスにどう影響するかは次回で取り上げます。


まとめ

Render Graph のコアは結局制御権の移動です。従来の方式ではレンダーテクスチャの割り当て・解放・依存順序を開発者がすべて自分で管理しなければなりませんでした。Render Graph はその役割をシステムが引き受けます。代わりに開発者は「何を読み、何を書くか」を宣言するだけで済みます。最初は宣言的な思考方式に馴染みにくいですが、一度慣れると元のやり方に戻りたくなくなります。

RecordRenderGraph が実行ではなく記録であること、CreateTexture が実際のメモリ割り当てではなく約束であること、この2つを体得するだけでこのシステムの半分は理解できたも同然です。残りの半分は、どのパスタイプをどの状況で選ぶかという話です。

次回は AddBlitPassAddCopyPassAddRasterRenderPass など6種類の Add メソッドがそれぞれいつ使われるのか、マージ可否がなぜ異なるのかを取り上げます。Tinting Effect がなぜマージされなかったのか、その理由もそこで明らかになります。


さらに学ぶには

この記事を書きながら直接確認したリソースです。


← 前回: なし(第1回)     次回 → 第2回: パスの種類 Deep Dive — AddBlitPass vs AddRasterRenderPass

合わせて読みたい記事

コメントする