Elytra
Blog图形

Render Graph:从 Render Pass 到资源依赖与调度

从 Why 出发理解 Render Graph:它如何把 Render Pass、纹理与缓冲区组织成依赖图,如何管理资源生命周期、同步和调度,以及 Unity Render Graph、Unreal RDG 与 Bevy 渲染架构的异同。

如果只是把场景画到屏幕上,一条渲染流程似乎并不复杂:

Shadow
→ Depth
→ Opaque
→ Transparent
→ Post Process
→ Final

但现代实时渲染很快会膨胀成几十甚至上百个阶段。阴影、Depth Prepass、GBuffer、SSAO、SSR、SSGI、体积雾、透明、Bloom、TAA、Upscaling……每一步不仅要执行 Draw 或 Compute,还会读取和产生大量 Texture / Buffer。

真正困难的事情于是从:

“这一道 Pass 怎么画?”

变成:

“整帧里这么多 Pass 和 GPU 资源之间到底是什么关系?”

这就是 Render Graph 试图解决的问题。

Render Graph 的核心价值不是“把 Render Pass 画成一张图”,而是把原本隐藏在程序执行顺序里的 资源依赖 显式描述出来,让渲染器获得整帧视角。

本文先从 Render Graph 为什么存在讲起,再进入 Unity 6 的具体 API,最后与 Unreal RDG 和 Bevy 的渲染调度思路进行比较。

本文不会完整展开 SRP 本身,也不会重复介绍 Renderer Feature / HDRP Custom Pass 的全部用法。它们分别属于另外两个问题:

  • SRP:一条 Render Pipeline 本身如何建立;
  • 自定义渲染扩展:如何往现有 URP / HDRP 中插入自己的工作;
  • Render Graph:这些工作与资源如何形成依赖,并被组织、调度和优化。

1为什么现代渲染器需要 Render Graph?

1.1从一条简单的渲染流水线开始

假设我们正在实现一个很普通的延迟渲染器:

Loading diagram…

如果只看“执行顺序”,这似乎只是一串 Pass。

但真正运行时,每个 Pass 都会涉及资源:

Depth Prepass
    Write: Depth

GBuffer
    Read:  Depth
    Write: Albedo, Normal, Material

SSAO
    Read:  Depth, Normal
    Write: AO

Lighting
    Read:  GBuffer, AO, Shadow
    Write: HDR Color

Bloom
    Read:  HDR Color
    Write: Bloom Pyramid

于是渲染器还必须回答:

  • AO 纹理到底什么时候创建?
  • 它最后一次被谁使用?
  • 用完以后什么时候可以释放?
  • BloomTempA 和 SSAOTemp 生命周期不重叠,能不能复用同一块 GPU Memory?
  • 某个 Debug Pass 的输出最后没人读,还需要执行吗?
  • Compute Pass 和 Graphics Pass 之间何时需要 Fence?
  • Texture 当前处于 Render Target、Shader Read 还是 UAV 状态?
  • 哪些 Pass 可以合并?
  • 哪些工作可以并行记录?

这些问题都不是单独一个 Pass 能回答的。

1.2局部视角为什么难以优化整帧?

传统的命令式写法往往像这样:

SetRenderTarget(tempA);
DrawSomething();

SetRenderTarget(tempB);
Blit(tempA, tempB);

Release(tempA);

每段代码只知道“现在我要做什么”。

但很多优化要求知道未来:

tempA 后面还会不会有人读?

这个 Pass 的输出是否最终影响屏幕?

两份资源的生命周期是否重叠?

单个 Pass 没有这些全局信息。

因此,现代渲染器越来越倾向于先收集一帧中各个任务对资源的需求,再统一分析和执行。Unity 的 Render Graph 位于 SRP Core 这套公共渲染基础设施中,URP 与 HDRP 都支持 Render Graph;在当前 URP 中,Unity 也明确表示新的图形功能应使用 Render Graph 路径,而非继续依赖 Compatibility Mode。1 2

1.3Render Graph 要解决的核心问题

可以把它概括为:

Pass
+
Resource
+
Dependency
+
Lifetime
+
Scheduling

Render Graph 并不是凭空制造新的 GPU 能力。

它只是让渲染器拥有足够的信息,去更正确、更集中地利用这些能力。


2Render Graph 到底是什么?

最简单的定义是:

Render Graph 是一帧渲染任务及其资源依赖关系的高层表示。

例如:

SSAO
Read:  Depth, Normal
Write: AO

Lighting
Read:  GBuffer, AO
Write: HDRColor

Tonemap
Read:  HDRColor
Write: FinalColor

可以得到:

Loading diagram…

这里真正重要的不是图长什么样,而是:

边不再只是“程序员先写 A 再写 B”,而是“B 需要 A 产生的资源”。

如果 Pass A 写入资源 RR,Pass B 读取 RR,那么存在一个生产者到消费者的依赖:

A→RBA \xrightarrow{R} B

于是系统可以知道:

A≺BA \prec B

也就是 A 必须先于 B 完成相关工作。

2.1Node、Edge 与 Resource

从抽象角度可以把 Render Graph 看成:

  • Node:Render Pass;
  • Resource:Texture、Buffer、Acceleration Structure 等;
  • Edge:由资源读写产生的依赖。

不过具体引擎不一定真的要求程序员手动创建 Node 和 Edge。

Unity 和 Unreal 更倾向于让你声明“这个 Pass 读什么、写什么”,然后系统根据资源访问自动推导依赖。

2.2Render Graph 是 DAG 吗?

在单帧调度层面,可以把典型 Render Graph 理解为一张 有向无环依赖图(DAG)。

如果同一帧的执行关系形成不可解的环:

A 等 B
B 等 C
C 又等 A

就不存在合法的拓扑执行顺序。

需要跨帧反馈的算法——例如 TAA History、上一帧曝光、ReSTIR Reservoir History——通常不会通过“同一帧内部形成环”实现,而是把历史资源作为跨帧保存的外部状态引入当前帧。

2.3Render Graph 不是什么?

2.3.1它不是 Shader Graph

Shader Graph 描述的是:

一个 Shader 内部如何计算顶点或像素结果。

Render Graph 描述的是:

一帧中多个 GPU 工作如何读取、产生和传递资源。

两者完全不是一个层级。

2.3.2它不是 Scene Graph

Scene Graph 主要描述场景对象之间的空间或层级关系,例如:

World
└─ Car
   ├─ Body
   └─ Wheel

Render Graph 描述的是渲染任务依赖。

2.3.3它也不只是“Render Pass 列表”

如果只有:

Pass A
Pass B
Pass C

那只是执行序列。

Render Graph 真正多出来的是:

谁读什么?
谁写什么?
哪个结果被谁消费?
资源活多久?

3从立即执行到“先记录,再执行”

这是理解 Render Graph 最关键的转变。

3.1没有 Render Graph:执行顺序就是依赖

命令式渲染通常类似:

DrawDepth();
DrawGBuffer();
ComputeSSAO();
DrawLighting();
DrawPostProcess();

代码顺序同时承担两个职责:

  1. 表达算法流程;
  2. 隐式表达依赖。

问题是系统很难知道:

ComputeSSAO() 为什么必须在 DrawLighting() 前面?

它只能看到“你就是这么调用的”。

3.2有 Render Graph:先描述整帧工作

Render Graph 更接近:

SSAO:
    Read  Depth
    Read  Normal
    Write AO

Lighting:
    Read  GBuffer
    Read  AO
    Write HDRColor

于是:

Loading diagram…

执行关系从资源关系中被推导出来。

3.3Record / Build 阶段

Unity 在 URP 的文档中明确区分了“记录阶段”和“执行阶段”:RecordRenderGraph() 主要描述 Pass 所需的资源和要加入的工作;实际 GPU 命令在后续执行 Render Function 时记录。3

也就是说:

RecordRenderGraph()
≠ GPU 现在立即执行

它更像:

“请把这一项工作加入本帧计划。”

3.4Compile / Analyze 阶段

不同引擎实现细节不同,但概念上 Graph 在拥有整帧信息后可以进行:

  • 依赖分析;
  • Pass Culling;
  • Resource Lifetime Analysis;
  • Transient Allocation;
  • Barrier / Synchronization 规划;
  • Pass Merge;
  • Queue Scheduling。

Unreal 的 RDG 把这个过程说得非常直接:先向 FRDGBuilder 加资源和 Pass,再调用 Execute() 编译并执行图。4

3.5Execute 阶段

最终仍然需要产生 GPU 能理解的工作:

Draw
Dispatch
Copy
Barrier
Render Pass Begin / End
Command Buffer

所以 Render Graph 不是 GPU API 的替代品。

它是在更高层组织这些 GPU 命令。

Render Graph 最重要的思想变化是:从“告诉 GPU 一步一步怎么做”,转向“先描述这一帧有哪些任务,它们需要什么、产生什么,再统一决定如何执行”。


4Render Graph 能替渲染器做什么?

4.1自动推导 Pass 依赖

假设:

Pass A → 写 Texture X
Pass B → 读 Texture X

那么系统自然知道:

A → B

如果:

Pass C → 写 Texture Y

并且 C 与 X 完全无关,那么 C 不一定需要被硬编码在 A 或 B 的前后。

这给并行和异步调度留下了空间。

4.2管理资源生命周期

假设资源 A 只在 Pass 1 到 Pass 3 之间使用:

LA=[1,3]L_A = [1, 3]

资源 B 只在 Pass 5 到 Pass 7 之间使用:

LB=[5,7]L_B = [5, 7]

如果两个生命周期不重叠:

LA∩LB=∅L_A \cap L_B = \varnothing

那么底层有机会让它们复用同一块物理显存。

注意:

逻辑上仍然是两张 Texture,但物理内存不一定需要同时存在两份。

4.3Transient Resource 与内存复用

这种“只在本帧某个区间存在”的资源通常被称为 Transient Resource。

它特别适合:

  • Blur 临时纹理;
  • Bloom Pyramid;
  • SSAO 中间结果;
  • Lighting 临时 Buffer;
  • 一次性 Object Mask。

Render Graph 知道资源的 first use / last use,才能比“每个系统自己维护一个长期 RenderTexture”做出更好的整体决策。

4.4自动剔除无用 Pass

假设:

Loading diagram…

如果 Temp 最终没人读取,并且 C 没有其他副作用,那么 C 对最终输出没有贡献。

Graph 可以把它 Cull 掉。

Unity 的 Render Graph Viewer 甚至专门提供 Culled Pass 过滤器,用于观察因为不影响最终结果而被移除的 Pass。5

4.5Resource State 与 Barrier

现代显式图形 API 中,同一资源在不同用途之间可能需要状态转换,例如:

Render Target
→ Shader Resource
→ UAV
→ Copy Source

这些状态转换和同步如果完全手工维护,非常容易出错。

Render Graph 已经知道:

A 写入
B 读取
C 再写入

因此它拥有生成或辅助生成 Barrier 的必要信息。

Unreal RDG 就明确将资源 Transition、Barrier 和异步 Compute Fence 作为依赖图负责的核心工作。4

4.6Graphics / Compute Queue 同步

如果一个 Compute Pass 依赖 Graphics Pass:

Loading diagram…

系统必须在合适的位置建立同步:

Graphics
   ↓ Fence
Compute
   ↓ Fence / Join
Graphics

Unity 官方也把 Graphics / Compute Queue 的自动同步列为 URP Render Graph 的优化能力之一。3

4.7Render Pass Merge

有些 Raster Pass 在底层其实可以合并成更大的 Native Render Pass,减少中间结果落到外部显存的次数。

这对 Tile-Based GPU 尤其重要,因为中间结果如果可以继续留在 Tile Memory,就能减少带宽。

不过:

“Render Graph 有机会合并”不等于“所有 Pass 都会自动合并”。

资源访问方式、Attachment、平台能力和 Pass 类型都会限制它。


5Render Graph 在整个渲染系统中的位置

可以把大致关系理解为:

Loading diagram…

在 Unity 中,再具体一点:

Loading diagram…

SRP Core 是 URP 与 HDRP 共用的基础库,Render Graph 也属于这层公共渲染基础设施,而不是“URP 独有功能”。Unity 当前功能对比表也把 Render Graph 同时列为 URP 和 HDRP 支持的能力。1 6

5.1Render Graph 和 Render Pass 是什么关系?

可以这样记:

Render Pass
= 一道工作

Render Graph
= 组织许多工作以及它们使用的资源

例如一个 Outline Feature:

Mask Pass
Blur Pass
Composite Pass

Render Graph 关心的是:

Loading diagram…

5.2Render Graph 和 CommandBuffer 是什么关系?

它们也不是替代关系。

CommandBuffer 描述更低层的 GPU 命令:

Draw
Dispatch
Copy
SetViewport

Render Graph 描述:

哪一个 Pass 什么时候拥有资格去记录这些命令,以及它允许访问哪些资源。

最终执行函数里依然会调用命令 API。

5.3Render Graph 会“产出”什么?

它不是生成一张 Graph 图片。

概念上的产出更接近:

Pass 描述
+
资源描述
+
依赖
        ↓
Graph Analysis / Compilation
        ↓
执行顺序
资源分配计划
资源生命周期
同步与 Barrier
可能的 Pass Culling / Merge
        ↓
GPU Command Recording
        ↓
GPU

因此可以把 Render Graph 看成一种:

用于组织一帧 GPU 工作的中间表示与调度层。


6Unity 的 Render Graph 架构

6.1Render Graph 属于 SRP Core,而不只是 URP

Unity 的 SRP Core 提供 URP 与 HDRP 共用的渲染基础能力。Render Graph 是其中的一部分。

URP 则在 Unity 6 中把 Render Graph 深度整合进自己的帧渲染流程;自定义 ScriptableRenderPass 通过 RecordRenderGraph() 把额外工作加入这张图。7

6.2URP 为什么在 Unity 6 转向 Render Graph?

过去的 ScriptableRenderPass.Execute() 风格更接近:

进入 Pass
→ 手动取得 CommandBuffer
→ 手动管理临时 RT
→ 设置 Render Target
→ 发命令

它让每个 Pass 有很大的自由,但系统难以知道 Pass 内部到底做了什么。

Unity 6 的 Render Graph 路线则要求开发者显式告诉系统:

我读什么
我写什么
我的 Attachment 是谁
我的执行函数是什么

这给自动优化、验证与资源管理创造了条件。

当前 URP 设置中仍然存在 Compatibility Mode (Render Graph Disabled),但 Unity 文档已经明确说明:新的图形功能应使用 Render Graph 路径。2

6.3HDRP 与 Render Graph 是什么关系?

HDRP 同样使用 Render Graph。

但这里要区分两个问题:

  1. Render Pipeline 内部是否使用 Render Graph;
  2. 用户扩展 API 是否直接暴露 Render Graph。

URP 的自定义 ScriptableRenderPass 可以重写:

RecordRenderGraph(...)

而 HDRP 的公开 CustomPass 工作流目前仍以:

Setup(...)
Execute(CustomPassContext ctx)
Cleanup()

为主要入口。HDRP 的 Custom Pass 文档仍然如此定义脚本模板。8

所以:

URP 用户扩展
→ RecordRenderGraph()
→ 直接描述 Graph

HDRP 用户扩展
→ CustomPass.Execute()
→ HDRP 内部再接入自己的渲染流程 / Render Graph

“内部用了 Render Graph”与“把 Render Graph API 直接交给扩展开发者”并不是一回事。


7Unity Render Graph 的核心 API

在 URP 自定义渲染中,最值得先掌握的不是几十个 API,而是下面几组概念。

7.1RenderGraph:整张图的入口

RenderGraph 是加入 Pass、创建 Graph Resource 的入口。

常见操作包括:

renderGraph.AddRasterRenderPass<T>()
renderGraph.AddComputePass<T>()
renderGraph.AddUnsafePass<T>()
renderGraph.CreateTexture(...)
renderGraph.ImportTexture(...)

7.2RecordRenderGraph():记录这一帧的工作

在 URP 中:

public override void RecordRenderGraph(
    RenderGraph renderGraph,
    ContextContainer frameData)
{
    // 描述 Pass 和资源
}

这里不要把它理解成传统 Update():

它的职责不是“现在把效果执行掉”,而是把工作登记进 Graph。

7.3PassData:把 Record 阶段的数据带到 Execute 阶段

常见结构:

private sealed class PassData
{
    public TextureHandle source;
}

然后:

passData.source = source;

到了真正的 Render Function:

builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
{
    // 使用 data.source
});

这样 Record 阶段与 Execute 阶段的数据边界非常明确。

7.4Raster Pass

Raster Pass 适合:

  • 绘制 Mesh;
  • Fullscreen Triangle;
  • 写 Color / Depth Attachment;
  • 大部分传统图形管线操作。
using var builder =
    renderGraph.AddRasterRenderPass<PassData>(
        "My Raster Pass",
        out var passData);

7.5Compute Pass

Compute Pass 适合:

  • Compute Shader;
  • 粒子模拟;
  • GPU Culling;
  • Prefix Sum;
  • 某些 Blur / Lighting;
  • 生成 Buffer。
using var builder =
    renderGraph.AddComputePass<PassData>(
        "My Compute Pass",
        out var passData);

对应执行上下文也变为:

ComputeGraphContext

Unity 官方也把 AddComputePass 作为 URP 中运行 Compute Shader 的标准 Render Graph 路径。9

7.6Unsafe Pass

AddUnsafePass 是逃生口。

它允许使用一些 Render Graph 无法完整追踪的 Compatibility Mode 风格命令,例如更自由地手工设置 Render Target。

代价是:

Graph 看不懂的事情,也就很难优化。

Unity 文档明确提醒 Unsafe Pass 可能阻止 Render Pass 合并等优化。10

能用 Raster / Compute Pass 表达,就不要因为“写起来像以前的 CommandBuffer”而默认选择 Unsafe Pass。Render Graph 的价值恰恰来自系统能看懂你的资源访问。


8Resource Handle:为什么不是直接拿 Texture?

Render Graph 中经常看到:

TextureHandle
BufferHandle

它们不是“GPU 纹理对象本身”。

更准确地说,它们是:

Graph 中一个逻辑资源的引用。

8.1TextureHandle 的意义

假设:

TextureHandle temp =
    renderGraph.CreateTexture(desc);

这句话表达的是:

“这一帧的 Graph 中需要一个符合 desc 的逻辑纹理。”

并不应该理解为:

“此刻立刻永久 new 出一张 RenderTexture。”

这给 Render Graph 延迟分配、跳过无用分配、资源复用等行为留下空间。

8.2为什么 Handle 很重要?

因为如果用户一直握着真实物理资源:

Pass A 创建 RenderTexture
↓
Pass A 保存引用
↓
Pass B 保存引用
↓
Pass C 保存引用

Graph 很难判断:

你什么时候真的不再需要它?

而 Handle 把逻辑身份与底层物理分配解耦。

8.3Handle 的有效范围

Unity 文档特别说明,从当前帧数据得到的 TextureHandle 只对当前帧的当前 Render Graph 有效。11

因此不要把它当成普通 Unity Asset Handle 跨帧永久保存。


9Graph Resource 与 Imported Resource

9.1Graph 管理的临时资源

最理想的 Render Graph Resource 是:

只属于 Graph
↓
Graph 知道谁第一次用
↓
Graph 知道谁最后一次用
↓
Graph 决定何时分配 / 回收

例如:

TextureHandle temp =
    renderGraph.CreateTexture(desc);

这种资源非常适合作为临时中间结果。

9.2Imported Resource

现实中当然还有 Graph 外部资源:

  • 一个已有 RTHandle;
  • 持久化 RenderTexture;
  • 跨帧 History;
  • 外部系统创建的资源。

它们需要被 Import 进 Graph:

TextureHandle imported =
    renderGraph.ImportTexture(myRTHandle);

这意味着:

Graph 可以在本帧里使用它,但它不拥有完整生命周期。

Unity 文档明确指出,导入资源的释放仍由外部负责,而且 Imported Resource 会限制一部分 Pass Culling / 生命周期优化。12

这也是为什么:

把所有东西都做成 Persistent RT 再 Import 进来,虽然能工作,却可能把 Render Graph 最有价值的生命周期信息抹掉。


10ContextContainer:当前帧已经有什么?

自定义 Pass 不应该重复创建 URP 已经生成的资源。

在:

RecordRenderGraph(
    RenderGraph renderGraph,
    ContextContainer frameData)

中,可以从 ContextContainer 获取当前帧已有的数据。

例如:

var resourceData =
    frameData.Get<UniversalResourceData>();

TextureHandle color =
    resourceData.activeColorTexture;

还能取得:

UniversalCameraData
UniversalRenderingData
UniversalLightData

UniversalResourceData 则包含 Camera Color、Depth、Opaque Texture、GBuffer 等帧资源;具体哪些资源存在,与当前渲染路径和 Injection Point 有关。11

这体现了一个重要原则:

先查询 Graph 已经拥有的资源,再决定是否创建新的副本。


11第一个 Render Graph:复制 Camera Color

先写一个最小案例,只做:

Camera Color
    ↓
Copy Pass
    ↓
Temporary Texture

效果本身没有意义,但它能让我们专注于 Render Graph 的结构。

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

public sealed class CopyColorFeature : ScriptableRendererFeature
{
    private CopyColorPass pass;

    public override void Create()
    {
        pass = new CopyColorPass
        {
            renderPassEvent =
                RenderPassEvent.BeforeRenderingPostProcessing
        };
    }

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

    private sealed class CopyColorPass : ScriptableRenderPass
    {
        private sealed class PassData
        {
            public TextureHandle source;
        }

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

            TextureHandle source =
                resources.activeColorTexture;

            var desc =
                renderGraph.GetTextureDesc(source);

            desc.name = "_CopyColor";
            desc.clearBuffer = false;
            desc.depthBufferBits = 0;

            TextureHandle destination =
                renderGraph.CreateTexture(desc);

            using var builder =
                renderGraph.AddRasterRenderPass<PassData>(
                    "Copy Camera Color",
                    out var passData);

            passData.source = source;

            builder.UseTexture(
                source,
                AccessFlags.Read);

            builder.SetRenderAttachment(
                destination,
                0,
                AccessFlags.Write);

            builder.SetRenderFunc(
                static (
                    PassData data,
                    RasterGraphContext context) =>
                {
                    Blitter.BlitTexture(
                        context.cmd,
                        data.source,
                        new Vector4(1, 1, 0, 0),
                        0,
                        false);
                });

            // 教学示例的 destination 没有后续消费者。
            // 若想在调试器中观察这个 Pass,需要阻止它被判定为无用。
            builder.AllowPassCulling(false);
        }
    }
}

这段代码真正表达的是:

Loading diagram…

11.1UseTexture() 在表达什么?

builder.UseTexture(
    source,
    AccessFlags.Read);

不是单纯“把纹理传进去”。

它还告诉 Graph:

这个 Pass 对 source 有读取依赖。

11.2SetRenderAttachment() 在表达什么?

builder.SetRenderAttachment(
    destination,
    0,
    AccessFlags.Write);

它告诉 Graph:

destination 是当前 Raster Pass 的 Color Attachment,而且这个 Pass 会写它。

11.3SetRenderFunc() 什么时候执行?

最容易误解的是:

builder.SetRenderFunc(...)

调用这行时并不是立刻执行里面的 GPU 工作。

你是在把执行逻辑登记给 Render Graph。

等 Graph 进入执行阶段时,才真正调用这个函数并记录命令。

11.4为什么这里要 AllowPassCulling(false)?

因为这个教学案例的 _CopyColor 最后没人消费。

从最终输出看:

Copy Camera Color
→ 得到一张没人使用的 Texture

这正是一个应该被剔除的 Pass。

所以我们为了观察它,故意关闭 Culling。

真实项目中如果一个 Pass 必须靠大量 AllowPassCulling(false) 才能存在,应该先确认:

它是真的有 Side Effect,还是资源依赖没有正确描述?


12更常见的写法:AddBlitPass

如果需求只是:

Texture A
→ Fullscreen Material
→ Texture B

没有必要每次都手工展开整个 Raster Pass。

Unity 当前 URP 提供 AddBlitPass,它会替你生成对应的 Render Graph Pass。13

例如:

using UnityEngine.Rendering.RenderGraphModule.Util;

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

    if (resources.isActiveTargetBackBuffer)
        return;

    TextureHandle source =
        resources.activeColorTexture;

    var desc =
        renderGraph.GetTextureDesc(source);

    desc.name = "_MyPostProcess";
    desc.clearBuffer = false;
    desc.depthBufferBits = 0;

    TextureHandle destination =
        renderGraph.CreateTexture(desc);

    var parameters =
        new RenderGraphUtils.BlitMaterialParameters(
            source,
            destination,
            material,
            0);

    renderGraph.AddBlitPass(
        parameters,
        "My Post Process");

    // 不必再额外 destination → cameraColor blit 一次。
    // 直接告诉后续 URP:新的 cameraColor 就是 destination。
    resources.cameraColor = destination;
}

这里有一个非常漂亮的地方:

resources.cameraColor = destination;

它不是“把像素又复制了一次”。

而是在改变后续帧流程所引用的逻辑资源:

旧 cameraColor
    ↓
My Post Process
    ↓
destination
    ↓
后续 Pass 把 destination 当作 cameraColor

这比:

Camera Color → Temp → 再 Blit 回 Camera Color

少一次不必要的往返。

Unity 官方也把这种方式作为避免额外 Color Buffer Blit 的优化建议。13


13一个多 Pass 效果如何形成 Graph?

假设我们实现一个典型的两阶段 Blur:

Camera Color
↓
Horizontal Blur
↓
Temp A
↓
Vertical Blur
↓
Temp B
↓
后续渲染

Graph 可以表示为:

Loading diagram…

如果再加 Composite:

Loading diagram…

注意我们从来没有手写:

H 必须先于 V
V 必须先于 Composite

这种顺序可以从资源依赖推出:

H Write TempA
V Read  TempA

V Write TempB
C Read  TempB

这就是 Graph 真正开始发挥作用的地方。


14Render Graph 如何知道谁依赖谁?

可以从最经典的 Hazard 来理解。

14.1Read After Write:RAW

A Write X
B Read  X

B 必须看到 A 写入后的结果:

A→BA \rightarrow B

14.2Write After Read:WAR

A Read  X
B Write X

如果 B 提前覆盖 X,A 读到的就可能不是预期内容。

14.3Write After Write:WAW

A Write X
B Write X

如果两个写入有可观察的先后语义,也必须维护正确顺序。

Render Graph 能否正确推导这些关系,有一个前提:

你必须诚实声明资源访问。

如果代码偷偷读取了一张没有声明为 Read 的资源:

Graph 以为没有依赖
但真正的 Shader 却依赖它

系统就可能:

  • 错误重排;
  • 错误 Cull;
  • 错误 Alias;
  • 缺少 Barrier。

于是结果不再只是“优化差一点”,而可能直接渲染错误。

Render Graph 的声明式 API 不是形式主义。Read / Write 声明本身就是正确性的一部分。


15Texture 到底什么时候真正存在?

考虑:

TextureHandle temp =
    renderGraph.CreateTexture(desc);

更合适的心智模型是:

CreateTexture
↓
创建“逻辑资源描述”
↓
Graph 分析谁会使用它
↓
如果根本没人使用,可能无需分配
↓
如果需要,决定实际生命周期
↓
执行时取得物理资源

而不是:

CreateTexture
↓
立刻 new 一张永远存在的 GPU Texture

15.1资源生命周期

设某个资源 RR:

  • 第一次使用发生在 Pass sRs_R;
  • 最后一次使用发生在 Pass eRe_R。

那么可以把逻辑生命周期写成:

LR=[sR,eR]L_R = [s_R, e_R]

两个资源 AA、BB 如果满足:

eA<sBe_A < s_B

或者:

eB<sAe_B < s_A

那么二者生命周期完全不重叠。

这就是底层尝试 Memory Aliasing / Reuse 的理论空间。

15.2跨帧 History 为什么不同?

例如 TAA:

Frame N Color
↓
保存
↓
Frame N+1 读取

这个资源生命周期已经跨越了当前 Render Graph。

所以它不能简单地被视为“当前帧内部随时可回收的临时 Texture”。

Unity URP 为相机历史提供单独的 History 管理机制;这本质上也是在承认:

跨帧资源和 Frame-Local Transient Resource 是不同类型的问题。


16Raster、Compute 与 Unsafe Pass 应该怎么选?

可以先用一个非常实用的判断:

需求优先选择
写 Color / Depth、绘制 MeshRaster Pass
Dispatch Compute ShaderCompute Pass
常规 Fullscreen BlitAddBlitPass / Raster
旧 API 无法被正常表达Unsafe Pass
只是为了继续沿用旧 SetRenderTarget 思维不应成为使用 Unsafe 的理由

16.1Raster Pass

Graph 对 Attachment 关系最清楚:

Read Texture
Color Attachment
Depth Attachment

因此这是传统图形工作最自然的表达。

16.2Compute Pass

Compute 的输出经常是 Buffer 或 UAV Texture。

Render Graph 知道它在 Compute Queue 上产生什么、后面是谁消费,就有机会正确安排同步。

16.3Unsafe Pass

Unsafe 的真正含义不是:

“它会 Crash。”

而是:

你正在做 Graph 无法完整建模的工作。

信息越少,优化空间越少。


17Render Graph 如何优化一帧?

17.1Pass Culling

如果一个 Pass:

  • 输出没有消费者;
  • 没有声明必须保留的 Side Effect;

它就可能被移除。

这是典型的 Dead Code Elimination 思路,只不过对象从 CPU 指令变成了 Render Pass。

17.2Resource Lifetime

Graph 可以找出:

Create
↓
First Use
↓
...
↓
Last Use
↓
Release / Reuse

而不是把临时资源粗暴地保持一整帧甚至永久存在。

17.3Memory Aliasing / Reuse

两个逻辑资源生命周期不重叠,就有机会共享底层内存。

Unreal RDG 的文档对此表述得非常明确:Transient Allocator 会利用不相交的 Resource Lifetime 做 Memory Aliasing。4

17.4Barrier 与 Transition

Graph 可以根据资源读写推导:

上一用途
↓
Barrier / Transition
↓
下一用途

而不是让每个系统各自猜测资源当前状态。

17.5Async Compute

Async Compute 并不是把某个 Pass 标成 Compute 就会自动变快。

系统还必须分析:

  • 生产者是谁;
  • 消费者是谁;
  • 是否能与 Graphics 重叠;
  • 在哪里 Fence;
  • 平台是否支持。

Render Graph 的作用,是为这种分析提供完整依赖信息。

17.6Pass Merge

如果两个 Raster Pass 的 Attachment 与依赖允许,系统可能把它们合并为更低层的 Native Render Pass。

Unity 文档特别指出,在 Tile-Based Deferred Rendering 平台上,这可能减少 Tile Memory 与外部内存之间的数据搬运。3

17.7Render Graph 不是“自动让游戏变快”

它更像:

建立一个系统可以安全优化的前提。

如果你:

  • 到处 Import Persistent Texture;
  • 大量使用 Unsafe Pass;
  • 不准确声明 Read / Write;
  • 强制关闭 Pass Culling;
  • 做大量没必要的 Camera Color Copy;
  • 把所有任务人为串成一条长链;

Render Graph 也不会凭空消除这些设计问题。


18如何观察一张真实的 Unity Render Graph?

Unity 6 提供 Render Graph Viewer。

路径为:

Window
→ Analysis
→ Render Graph Viewer

它能显示当前场景的 Graph,包括 Unity 内置 Pass 和自定义 Pass。5

18.1看 Pass

Viewer 可以过滤:

  • Raster;
  • Compute;
  • Unsafe;
  • Culled。

最值得先看的是:

你以为自己写进去的 Pass,最终真的执行了吗?

18.2看 Resource Lifetime

Viewer 会把 Resource 和 Pass 放到同一个时间轴中。

这时前面抽象的:

First Use
Last Use
Read
Write

就真正可视化了。

18.3看 Culled Pass

如果一个 Pass 消失了:

不要第一反应就写:

builder.AllowPassCulling(false);

先看它为什么被 Cull:

  • 是最终没人消费输出?
  • 是 Camera Color 没有被更新?
  • 是 Side Effect 没声明?
  • 是你创建了 Texture 但没有真正形成下游依赖?

18.4Render Graph Viewer 与 Frame Debugger 的区别

可以粗略理解为:

工具更擅长回答
Render Graph ViewerPass / Resource / Lifetime / Dependency 是什么关系?
Frame Debugger实际 Draw / Pass 按什么顺序发生?
Rendering Debugger当前 Pipeline 的运行时渲染状态是什么?
GPU Profiler / RenderDocGPU 真正花时间在哪里、底层资源与命令是什么?

Render Graph Viewer 更偏:

“这张计划表是怎么组织的?”

而不是最终 GPU 微观性能分析的全部答案。


19Render Graph 与 Renderer Feature / Custom Pass

这几个概念经常被混在一起,是因为它们会同时出现在“自定义渲染”代码里。

但它们回答的是不同问题。

19.1Renderer Feature

URP 的 ScriptableRendererFeature 解决:

我如何给 URP 增加一个功能,并把一个或多个 Pass 插入 Renderer?

例如:

OutlineRendererFeature
├─ MaskPass
└─ CompositePass

19.2ScriptableRenderPass

它解决:

其中一道具体渲染工序是什么?

19.3Render Graph

它解决:

这些 Pass 读写哪些资源?它们之间形成什么依赖?资源如何被管理与调度?

因此:

Loading diagram…

Render Graph 没有取代 Renderer Feature,因为两者根本不在同一抽象层。

19.4HDRP Custom Pass 为什么看不到 RecordRenderGraph()?

HDRP 的公开用户扩展主要是:

Custom Pass Volume
→ CustomPass
→ Execute(CustomPassContext)

而不是 URP 的:

Renderer Feature
→ ScriptableRenderPass
→ RecordRenderGraph()

所以:

HDRP 支持 Render Graph,不代表 HDRP 的 CustomPass API 必须直接暴露 RecordRenderGraph()。

这也是理解“引擎内部架构”和“用户扩展 API”差异的一个好例子。


20Unreal:Render Dependency Graph

Unreal 的名字反而把重点说得更直接:

Render Dependency Graph,RDG。

重点是 Dependency。

Epic 当前文档把 RDG 定义为一种记录 Render Command 到 Graph、再进行编译与执行的 API;它负责 Transient Resource、Memory Aliasing、Barrier、Async Compute Fence、Pass Culling、Parallel Command Recording 和 Validation 等工作。4

20.1基本模型

Unreal 的典型结构可以简化成:

FRDGBuilder GraphBuilder(RHICmdList);

FRDGTexture* texture =
    GraphBuilder.CreateTexture(...);

GraphBuilder.AddPass(
    RDG_EVENT_NAME("MyPass"),
    passParameters,
    ERDGPassFlags::Raster,
    [passParameters](FRHICommandList& RHICmdList)
    {
        // Record GPU work
    });

GraphBuilder.Execute();

核心仍然是:

Create Resource Description
↓
Add Pass
↓
Declare Resource Parameters
↓
Graph 推导 Dependency
↓
Execute

20.2FRDGTexture 也不是立即分配实体

Epic 文档特别指出:

CreateTexture 首先创建的是资源描述,底层 RHI Resource 会在后续执行期间按需分配。4

这和 Unity TextureHandle 背后的思想非常接近:

逻辑资源
≠
立刻永久存在的物理 GPU 资源

20.3Pass Parameter 为什么重要?

Unreal 通过 Pass Parameter Struct 声明 RDG Resource。

RDG 会用这些参数分析:

  • Pass Dependency;
  • Resource Lifetime。

也就是说 Unreal 同样尽量避免让依赖仅仅隐藏在 Lambda 的任意代码里。

20.4RDG Insights

Unreal 也提供专门的 RDG Insights。

它能检查:

  • Resource Lifetime;
  • Pass Association;
  • Allocation Overlap;
  • Async Compute Fence;
  • Graph Culling;
  • Render Pass Merge;
  • Parallel Execution;
  • Transient Memory Layout。

这和 Unity Render Graph Viewer 的存在说明了同一件事:

当渲染系统从“命令序列”升级成“依赖图”以后,调试工具也必须能展示这张图。


21Bevy:从显式 RenderGraph 到 ECS Schedule

Bevy 是一个很有意思的对照,因为它在 2026 年刚刚做了一次方向非常明确的架构变化。

21.1Bevy 0.18 及以前:Node / Edge / Slot

旧 Bevy RenderGraph 的设计非常“图论直觉”:

Node
Edge
Slot
SubGraph

一个自定义渲染步骤需要实现 Render Node,再通过 Render Label 与 Edge 安排顺序。

概念上像:

Loading diagram…

这种设计的优点是 Graph 结构非常显式。

但代价也很明显:

Bevy 已经有一套成熟的 ECS Scheduler,却又为了渲染单独维护另一套 Node / Label / Edge 调度 API。

21.2Bevy 0.19:Render Graph as Systems

Bevy 0.19 在 2026 年 6 月发布时,正式移除了旧的 RenderGraph API,把 Render Pass 改为普通 ECS System,在 Core3d、Core2d 等 Schedule 中运行。14 15

旧写法大致需要:

Node
RenderLabel
add_render_graph_node
add_render_graph_edge

现在则更像:

fn my_render_pass(
    mut ctx: RenderContext,
    // 其他 ECS 参数
) {
    let encoder = ctx.command_encoder();

    // record rendering work
}

然后:

render_app.add_systems(
    Core3d,
    my_render_pass
        .after(main_pass)
        .before(post_process),
);

21.3这还是 Render Graph 吗?

这是一个非常有意思的问题。

如果把 Render Graph 狭义理解为:

“必须存在一个叫 RenderGraph 的 Node/Edge 数据结构。”

那么 Bevy 0.19 确实删除了旧 RenderGraph API。

但如果把核心问题理解为:

“如何表达渲染任务之间的依赖,并以拓扑关系调度工作?”

那么 Bevy 的 ECS Schedule 已经能表达同样的模式:

System A
.before(System B)

System B
.after(System A)

Bevy 自己也明确说明:随着 ECS Scheduler 成熟,它已经有能力表达过去 RenderGraph 承担的调度模式,因此没有必要维护重复的专用抽象。14

21.4Bevy 和 Unity / Unreal 的重要差异

这里也能看到一个关键区别。

Unity / Unreal Render Graph 的核心非常强调:

GPU Resource Dependency + Lifetime。

Bevy 0.19 的“Render Graph as Systems”更强调:

用统一 ECS Schedule 表达渲染工作顺序与并行机会。

两者重叠,但并不是完全相同的抽象重点。

所以不要因为都使用“Graph / Dependency”思想,就强行把:

Unity TextureHandle
=
Bevy ECS Resource

做一一映射。

它们解决问题的层级并不完全相同。


22Unity、Unreal 与 Bevy:相同思想,不同抽象

问题Unity Render GraphUnreal RDGBevy 0.19
核心组织单位Render PassRDG PassECS Render System
依赖主要如何表达Resource Read / WriteRDG Pass ParametersECS Schedule .before() / .after() 与 System Data Access
临时 GPU 资源生命周期Graph 管理RDG 管理更多由 Bevy Renderer / wgpu 资源体系处理
执行代码SetRenderFuncPass LambdaRender System
低层命令入口Graph Context / Command APIRHI Command ListRenderContext / wgpu Command Encoder
整帧图优化重点Culling、资源、同步、MergeCulling、Aliasing、Barrier、Async、ParallelScheduler 统一、并行调度方向
专用可视化Render Graph ViewerRDG Insights目前不对应 Unity/Unreal 同类完整 Resource Graph Viewer
当前架构特点Resource-drivenResource-drivenECS schedule-driven

这张表比较的是“架构角色”,不是类型一一对应。三套系统的职责边界、资源模型与底层图形抽象并不相同。


23从三个实现里能抽取什么共同思想?

23.1第一:依赖应该尽量显式

传统代码:

因为我把 B 写在 A 后面
所以 B 在 A 后执行

更现代的表达:

B 依赖 A 的输出
所以 B 必须在 A 后执行

或者 Bevy 的:

B.after(A)

依赖从“偶然的代码顺序”升级成“系统可以理解的数据”。

23.2第二:先知道更多,才能优化更多

单个 Pass 只能看到局部。

Graph / Scheduler 看到:

整个帧

于是才有可能:

  • 删除;
  • 重排;
  • 并行;
  • 复用;
  • 延迟分配;
  • 提前同步;
  • 合并。

23.3第三:抽象的目的不是少写几行代码

Render Graph 经常反而需要你写更多声明:

Read
Write
Attachment
PassData
Resource Handle

如果只比较代码长度,甚至可能觉得退步了。

但这些额外声明让系统知道以前无法知道的信息。

这是一种很典型的工程交换:

用更严格的约束换取全局正确性、可观测性和优化空间。


24常见误区

24.1Render Graph 就是 Render Pass 的列表?

不是。

列表只有顺序。

Graph 还必须表达:

  • Resource;
  • Read / Write;
  • Dependency;
  • Lifetime。

24.2Render Graph 会直接渲染东西?

不会。

真正的工作最终还是:

Draw
Dispatch
Copy

Render Graph 负责组织这些工作。

24.3使用 Render Graph 就一定更快?

不一定。

Render Graph 提供优化条件,不保证你的算法本身更快。

一个本来就做了十次不必要 Fullscreen Copy 的算法,不会因为放进 Render Graph 就自动变成一次。

24.4TextureHandle 就是一张 RenderTexture?

不是。

它首先是 Graph 中逻辑资源的 Handle。

把它当成普通持久对象跨帧保存,是错误的心智模型。

24.5SetRenderFunc() 时 Pass 就执行了吗?

不是。

它是在 Record 阶段登记执行逻辑。

真正调用发生在 Graph 的 Execute 阶段。

24.6我写 Pass 的顺序就是最终执行顺序吗?

不要依赖这种思维。

正确做法是准确描述资源依赖和必要的执行关系,让 Graph 决定合法顺序。

24.7能不能在同一个 Pass 里随便读写同一张 Texture?

不能把它当成默认安全行为。

URP Render Graph 文档明确限制常规 Render Pass 对同一纹理同时读写的场景;如果算法需要反馈、Framebuffer Fetch 或特殊底层行为,应使用专门支持的路径,而不是偷偷绕过资源声明。16

24.8Render Graph 与 Shader Graph 有关系吗?

除了名字都有 Graph,没有直接关系。

24.9URP 有 Render Graph,所以 HDRP 没有?

错误。

当前 Unity 功能对照中 URP 与 HDRP 都支持 Render Graph。6

24.10Render Graph 能替代 SRP 吗?

不能。

SRP 回答:

一条可编程 Render Pipeline 如何构建。

Render Graph 回答:

Pipeline 内部的一帧任务与资源如何组织。


25一套更稳妥的 Render Graph 思考顺序

当准备写一个自定义渲染功能时,可以先别想 API。

先写四件事:

1. 输入是什么?

例如:

Camera Color
Depth
Normal
Object Buffer

2. 输出是什么?

例如:

AO
Mask
Blurred Color
Final Color

3. 中间有哪些真正独立的工作?

例如:

Mask
Blur H
Blur V
Composite

4. 每项工作读什么、写什么?

例如:

Mask
Read:  Depth
Write: Mask

Blur H
Read:  Mask
Write: TempA

Blur V
Read:  TempA
Write: TempB

Composite
Read:  TempB, CameraColor
Write: Final

做到这里,Graph 基本已经自然出现了。

接下来才是把它翻译成 Unity API:

TextureHandle
AddRasterRenderPass
UseTexture
SetRenderAttachment
SetRenderFunc

而不是反过来:

“我看到了一个 AddRasterRenderPass,所以我要想办法往里面塞代码。”


26总结:Render Graph 真正改变了什么?

传统命令式渲染倾向于描述:

先执行 A
然后执行 B
然后执行 C

Render Graph 更倾向于描述:

A 读取什么,产生什么
B 依赖什么,产生什么
C 又消费什么

于是:

Loading diagram…

它真正带来的变化可以浓缩成三点:

  1. 从命令序列到依赖描述
    顺序不再只是“代码写在前后”,而可以由资源生产与消费关系推导。

  2. 从手动资源管理到生命周期管理
    Texture / Buffer 不必默认永久存在,Graph 可以理解其真正需要存在的区间。

  3. 从单个 Pass 的局部视角到整帧视角
    Pass Culling、Memory Reuse、Barrier、Async Compute、Pass Merge 等优化,都是建立在“系统知道整张图”之上。

所以 Render Graph 最值得记住的并不是:

AddRasterRenderPass()

而是这句话:

Render Graph 把一帧渲染从“一串必须照着执行的命令”,提升成“一组具有明确输入、输出和依赖关系的任务”。

一旦依赖被显式表达,渲染器才真正拥有了理解和优化整帧的机会。


注释

  1. Unity Technologies,Scriptable Render Pipeline Core。SRP Core 是 URP、HDRP 共用的公共渲染基础库。 ↩ ↩2

  2. Unity Technologies,URP Global Settings — Render Graph。当前文档说明 Compatibility Mode 会禁用 Render Graph,并建议新图形功能使用 Render Graph 路径。 ↩ ↩2

  3. Unity Technologies,Introduction to the render graph system in URP。说明 Record / Execute 模型,以及 Pass 合并、无用 Pass 剔除、资源分配和 Graphics / Compute 同步等优化。 ↩ ↩2 ↩3

  4. Epic Games,Render Dependency Graph in Unreal Engine 5.8。官方文档说明 RDG 的 Builder / Execute 模型、Transient Resource、Memory Aliasing、Barrier、Async Compute、Pass Culling、Validation 与 RDG Insights。 ↩ ↩2 ↩3 ↩4 ↩5

  5. Unity Technologies,Render Graph Viewer window reference。Viewer 可以查看 Raster / Compute / Unsafe / Culled Pass 以及纹理、Buffer 等资源。 ↩ ↩2

  6. Unity Technologies,Render pipeline feature comparison。当前功能表同时将 Render Graph 列为 URP 与 HDRP 支持项。 ↩ ↩2

  7. Unity Technologies,Write a render pass using the render graph system in URP。官方 ScriptableRenderPass.RecordRenderGraph() 工作流。 ↩

  8. Unity Technologies,Create a Custom Pass in a C# script。HDRP 当前源码文档中的 Custom Pass 模板仍以 Setup、Execute(CustomPassContext)、Cleanup 为主要入口。 ↩

  9. Unity Technologies,Run a compute shader in a render pass in URP。说明 AddComputePass 与 ComputeGraphContext 的使用。 ↩

  10. Unity Technologies,Use Compatibility Mode APIs in a render graph pass。说明 AddUnsafePass 的用途以及它对 Render Graph 优化能力的限制。 ↩

  11. Unity Technologies,Get data from the current frame in URP。说明 ContextContainer、UniversalResourceData 以及 TextureHandle 当前帧有效性的约束。 ↩ ↩2

  12. Unity Technologies,Import a texture into the render graph system in URP。说明 Imported Texture 的生命周期由外部管理,并会限制部分自动剔除能力。 ↩

  13. Unity Technologies,Blit using the render graph system in URP。说明 AddBlitPass、BlitMaterialParameters 以及通过更新 cameraColor 避免额外回拷。 ↩ ↩2

  14. Bevy Contributors,Bevy 0.19 — Render Graph as Systems,2026-06-19。Bevy 0.19 将旧 RenderGraph Node 架构替换为 ECS Schedule / System。 ↩ ↩2

  15. Bevy Contributors,Migration Guide 0.18 to 0.19 — Render Graph as Systems。迁移指南明确说明旧 RenderGraph API 被移除,Render Pass 改为运行在 Core3d / Core2d Schedule 中的系统。 ↩

  16. Unity Technologies,Use a texture in a render pass in URP。说明常规 Render Graph Pass 中纹理读写访问的约束。 ↩

CONTINUE EXPLORING

沿着这篇文章继续

在地图中查看

On this page

1为什么现代渲染器需要 Render Graph?1.1从一条简单的渲染流水线开始1.2局部视角为什么难以优化整帧?1.3Render Graph 要解决的核心问题2Render Graph 到底是什么?2.1Node、Edge 与 Resource2.2Render Graph 是 DAG 吗?2.3Render Graph 不是什么?2.3.1它不是 Shader Graph2.3.2它不是 Scene Graph2.3.3它也不只是“Render Pass 列表”3从立即执行到“先记录,再执行”3.1没有 Render Graph:执行顺序就是依赖3.2有 Render Graph:先描述整帧工作3.3Record / Build 阶段3.4Compile / Analyze 阶段3.5Execute 阶段4Render Graph 能替渲染器做什么?4.1自动推导 Pass 依赖4.2管理资源生命周期4.3Transient Resource 与内存复用4.4自动剔除无用 Pass4.5Resource State 与 Barrier4.6Graphics / Compute Queue 同步4.7Render Pass Merge5Render Graph 在整个渲染系统中的位置5.1Render Graph 和 Render Pass 是什么关系?5.2Render Graph 和 CommandBuffer 是什么关系?5.3Render Graph 会“产出”什么?6Unity 的 Render Graph 架构6.1Render Graph 属于 SRP Core,而不只是 URP6.2URP 为什么在 Unity 6 转向 Render Graph?6.3HDRP 与 Render Graph 是什么关系?7Unity Render Graph 的核心 API7.1RenderGraph:整张图的入口7.2RecordRenderGraph():记录这一帧的工作7.3PassData:把 Record 阶段的数据带到 Execute 阶段7.4Raster Pass7.5Compute Pass7.6Unsafe Pass8Resource Handle:为什么不是直接拿 Texture?8.1TextureHandle 的意义8.2为什么 Handle 很重要?8.3Handle 的有效范围9Graph Resource 与 Imported Resource9.1Graph 管理的临时资源9.2Imported Resource10ContextContainer:当前帧已经有什么?11第一个 Render Graph:复制 Camera Color11.1UseTexture() 在表达什么?11.2SetRenderAttachment() 在表达什么?11.3SetRenderFunc() 什么时候执行?11.4为什么这里要 AllowPassCulling(false)?12更常见的写法:AddBlitPass13一个多 Pass 效果如何形成 Graph?14Render Graph 如何知道谁依赖谁?14.1Read After Write:RAW14.2Write After Read:WAR14.3Write After Write:WAW15Texture 到底什么时候真正存在?15.1资源生命周期15.2跨帧 History 为什么不同?16Raster、Compute 与 Unsafe Pass 应该怎么选?16.1Raster Pass16.2Compute Pass16.3Unsafe Pass17Render Graph 如何优化一帧?17.1Pass Culling17.2Resource Lifetime17.3Memory Aliasing / Reuse17.4Barrier 与 Transition17.5Async Compute17.6Pass Merge17.7Render Graph 不是“自动让游戏变快”18如何观察一张真实的 Unity Render Graph?18.1看 Pass18.2看 Resource Lifetime18.3看 Culled Pass18.4Render Graph Viewer 与 Frame Debugger 的区别19Render Graph 与 Renderer Feature / Custom Pass19.1Renderer Feature19.2ScriptableRenderPass19.3Render Graph19.4HDRP Custom Pass 为什么看不到 RecordRenderGraph()?20Unreal:Render Dependency Graph20.1基本模型20.2FRDGTexture 也不是立即分配实体20.3Pass Parameter 为什么重要?20.4RDG Insights21Bevy:从显式 RenderGraph 到 ECS Schedule21.1Bevy 0.18 及以前:Node / Edge / Slot21.2Bevy 0.19:Render Graph as Systems21.3这还是 Render Graph 吗?21.4Bevy 和 Unity / Unreal 的重要差异22Unity、Unreal 与 Bevy:相同思想,不同抽象23从三个实现里能抽取什么共同思想?23.1第一:依赖应该尽量显式23.2第二:先知道更多,才能优化更多23.3第三:抽象的目的不是少写几行代码24常见误区24.1Render Graph 就是 Render Pass 的列表?24.2Render Graph 会直接渲染东西?24.3使用 Render Graph 就一定更快?24.4TextureHandle 就是一张 RenderTexture?24.5SetRenderFunc() 时 Pass 就执行了吗?24.6我写 Pass 的顺序就是最终执行顺序吗?24.7能不能在同一个 Pass 里随便读写同一张 Texture?24.8Render Graph 与 Shader Graph 有关系吗?24.9URP 有 Render Graph,所以 HDRP 没有?24.10Render Graph 能替代 SRP 吗?25一套更稳妥的 Render Graph 思考顺序1. 输入是什么?2. 输出是什么?3. 中间有哪些真正独立的工作?4. 每项工作读什么、写什么?26总结:Render Graph 真正改变了什么?注释