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从一条简单的渲染流水线开始
假设我们正在实现一个很普通的延迟渲染器:
如果只看“执行顺序”,这似乎只是一串 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
+
SchedulingRender Graph 并不是凭空制造新的 GPU 能力。
它只是让渲染器拥有足够的信息,去更正确、更集中地利用这些能力。
2Render Graph 到底是什么?
最简单的定义是:
Render Graph 是一帧渲染任务及其资源依赖关系的高层表示。
例如:
SSAO
Read: Depth, Normal
Write: AO
Lighting
Read: GBuffer, AO
Write: HDRColor
Tonemap
Read: HDRColor
Write: FinalColor可以得到:
这里真正重要的不是图长什么样,而是:
边不再只是“程序员先写 A 再写 B”,而是“B 需要 A 产生的资源”。
如果 Pass A 写入资源 ,Pass 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
└─ WheelRender 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();代码顺序同时承担两个职责:
- 表达算法流程;
- 隐式表达依赖。
问题是系统很难知道:
ComputeSSAO()为什么必须在DrawLighting()前面?
它只能看到“你就是这么调用的”。
3.2有 Render Graph:先描述整帧工作
Render Graph 更接近:
SSAO:
Read Depth
Read Normal
Write AO
Lighting:
Read GBuffer
Read AO
Write HDRColor于是:
执行关系从资源关系中被推导出来。
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 之间使用:
资源 B 只在 Pass 5 到 Pass 7 之间使用:
如果两个生命周期不重叠:
那么底层有机会让它们复用同一块物理显存。
注意:
逻辑上仍然是两张 Texture,但物理内存不一定需要同时存在两份。
4.3Transient Resource 与内存复用
这种“只在本帧某个区间存在”的资源通常被称为 Transient Resource。
它特别适合:
- Blur 临时纹理;
- Bloom Pyramid;
- SSAO 中间结果;
- Lighting 临时 Buffer;
- 一次性 Object Mask。
Render Graph 知道资源的 first use / last use,才能比“每个系统自己维护一个长期 RenderTexture”做出更好的整体决策。
4.4自动剔除无用 Pass
假设:
如果 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:
系统必须在合适的位置建立同步:
Graphics
↓ Fence
Compute
↓ Fence / Join
GraphicsUnity 官方也把 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 在整个渲染系统中的位置
可以把大致关系理解为:
在 Unity 中,再具体一点:
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 PassRender Graph 关心的是:
5.2Render Graph 和 CommandBuffer 是什么关系?
它们也不是替代关系。
CommandBuffer 描述更低层的 GPU 命令:
Draw
Dispatch
Copy
SetViewportRender 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。
但这里要区分两个问题:
- Render Pipeline 内部是否使用 Render Graph;
- 用户扩展 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);对应执行上下文也变为:
ComputeGraphContextUnity 官方也把 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
UniversalLightDataUniversalResourceData 则包含 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);
}
}
}这段代码真正表达的是:
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 可以表示为:
如果再加 Composite:
注意我们从来没有手写:
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 XB 必须看到 A 写入后的结果:
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 Texture15.1资源生命周期
设某个资源 :
- 第一次使用发生在 Pass ;
- 最后一次使用发生在 Pass 。
那么可以把逻辑生命周期写成:
两个资源 、 如果满足:
或者:
那么二者生命周期完全不重叠。
这就是底层尝试 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、绘制 Mesh | Raster Pass |
| Dispatch Compute Shader | Compute Pass |
| 常规 Fullscreen Blit | AddBlitPass / 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 Viewer | Pass / Resource / Lifetime / Dependency 是什么关系? |
| Frame Debugger | 实际 Draw / Pass 按什么顺序发生? |
| Rendering Debugger | 当前 Pipeline 的运行时渲染状态是什么? |
| GPU Profiler / RenderDoc | GPU 真正花时间在哪里、底层资源与命令是什么? |
Render Graph Viewer 更偏:
“这张计划表是怎么组织的?”
而不是最终 GPU 微观性能分析的全部答案。
19Render Graph 与 Renderer Feature / Custom Pass
这几个概念经常被混在一起,是因为它们会同时出现在“自定义渲染”代码里。
但它们回答的是不同问题。
19.1Renderer Feature
URP 的 ScriptableRendererFeature 解决:
我如何给 URP 增加一个功能,并把一个或多个 Pass 插入 Renderer?
例如:
OutlineRendererFeature
├─ MaskPass
└─ CompositePass19.2ScriptableRenderPass
它解决:
其中一道具体渲染工序是什么?
19.3Render Graph
它解决:
这些 Pass 读写哪些资源?它们之间形成什么依赖?资源如何被管理与调度?
因此:
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
↓
Execute20.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 安排顺序。
概念上像:
这种设计的优点是 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 Graph | Unreal RDG | Bevy 0.19 |
|---|---|---|---|
| 核心组织单位 | Render Pass | RDG Pass | ECS Render System |
| 依赖主要如何表达 | Resource Read / Write | RDG Pass Parameters | ECS Schedule .before() / .after() 与 System Data Access |
| 临时 GPU 资源生命周期 | Graph 管理 | RDG 管理 | 更多由 Bevy Renderer / wgpu 资源体系处理 |
| 执行代码 | SetRenderFunc | Pass Lambda | Render System |
| 低层命令入口 | Graph Context / Command API | RHI Command List | RenderContext / wgpu Command Encoder |
| 整帧图优化重点 | Culling、资源、同步、Merge | Culling、Aliasing、Barrier、Async、Parallel | Scheduler 统一、并行调度方向 |
| 专用可视化 | Render Graph Viewer | RDG Insights | 目前不对应 Unity/Unreal 同类完整 Resource Graph Viewer |
| 当前架构特点 | Resource-driven | Resource-driven | ECS 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
CopyRender 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 Buffer2. 输出是什么?
例如:
AO
Mask
Blurred Color
Final Color3. 中间有哪些真正独立的工作?
例如:
Mask
Blur H
Blur V
Composite4. 每项工作读什么、写什么?
例如:
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
然后执行 CRender Graph 更倾向于描述:
A 读取什么,产生什么
B 依赖什么,产生什么
C 又消费什么于是:
它真正带来的变化可以浓缩成三点:
-
从命令序列到依赖描述
顺序不再只是“代码写在前后”,而可以由资源生产与消费关系推导。 -
从手动资源管理到生命周期管理
Texture / Buffer 不必默认永久存在,Graph 可以理解其真正需要存在的区间。 -
从单个 Pass 的局部视角到整帧视角
Pass Culling、Memory Reuse、Barrier、Async Compute、Pass Merge 等优化,都是建立在“系统知道整张图”之上。
所以 Render Graph 最值得记住的并不是:
AddRasterRenderPass()而是这句话:
Render Graph 把一帧渲染从“一串必须照着执行的命令”,提升成“一组具有明确输入、输出和依赖关系的任务”。
一旦依赖被显式表达,渲染器才真正拥有了理解和优化整帧的机会。
注释
-
Unity Technologies,Scriptable Render Pipeline Core。SRP Core 是 URP、HDRP 共用的公共渲染基础库。 ↩ ↩2
-
Unity Technologies,URP Global Settings — Render Graph。当前文档说明 Compatibility Mode 会禁用 Render Graph,并建议新图形功能使用 Render Graph 路径。 ↩ ↩2
-
Unity Technologies,Introduction to the render graph system in URP。说明 Record / Execute 模型,以及 Pass 合并、无用 Pass 剔除、资源分配和 Graphics / Compute 同步等优化。 ↩ ↩2 ↩3
-
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
-
Unity Technologies,Render Graph Viewer window reference。Viewer 可以查看 Raster / Compute / Unsafe / Culled Pass 以及纹理、Buffer 等资源。 ↩ ↩2
-
Unity Technologies,Render pipeline feature comparison。当前功能表同时将 Render Graph 列为 URP 与 HDRP 支持项。 ↩ ↩2
-
Unity Technologies,Write a render pass using the render graph system in URP。官方
ScriptableRenderPass.RecordRenderGraph()工作流。 ↩ -
Unity Technologies,Create a Custom Pass in a C# script。HDRP 当前源码文档中的 Custom Pass 模板仍以
Setup、Execute(CustomPassContext)、Cleanup为主要入口。 ↩ -
Unity Technologies,Run a compute shader in a render pass in URP。说明
AddComputePass与ComputeGraphContext的使用。 ↩ -
Unity Technologies,Use Compatibility Mode APIs in a render graph pass。说明
AddUnsafePass的用途以及它对 Render Graph 优化能力的限制。 ↩ -
Unity Technologies,Get data from the current frame in URP。说明
ContextContainer、UniversalResourceData以及TextureHandle当前帧有效性的约束。 ↩ ↩2 -
Unity Technologies,Import a texture into the render graph system in URP。说明 Imported Texture 的生命周期由外部管理,并会限制部分自动剔除能力。 ↩
-
Unity Technologies,Blit using the render graph system in URP。说明
AddBlitPass、BlitMaterialParameters以及通过更新cameraColor避免额外回拷。 ↩ ↩2 -
Bevy Contributors,Bevy 0.19 — Render Graph as Systems,2026-06-19。Bevy 0.19 将旧 RenderGraph Node 架构替换为 ECS Schedule / System。 ↩ ↩2
-
Bevy Contributors,Migration Guide 0.18 to 0.19 — Render Graph as Systems。迁移指南明确说明旧
RenderGraphAPI 被移除,Render Pass 改为运行在Core3d/Core2dSchedule 中的系统。 ↩ -
Unity Technologies,Use a texture in a render pass in URP。说明常规 Render Graph Pass 中纹理读写访问的约束。 ↩
CONTINUE EXPLORING