Unity-RenderGraph机制分析
RenderGraph(渲染图)解决的核心问题不是“换一种写 Pass 的语法”,而是让渲染管线先知道整帧会发生什么,再决定怎么执行。
传统 SRP 写法里,ScriptableRenderPass 往往在执行阶段直接申请临时 RT、设置 RenderTarget、读写纹理并提交 CommandBuffer。这样写很直观,但管线看到的是一串已经排好的命令,很难全局判断哪些资源可以复用、哪些 Pass 没有贡献、哪些 Render Target 切换可以合并。RenderGraph 把这件事拆成两步:先声明 Pass 与资源依赖,再由系统编译执行计划。

1. 从命令列表到资源依赖图
RenderGraph 可以理解成一张有向图:
- 节点是一个个渲染 Pass。
- 边是资源读写依赖,例如某个 Pass 写入 Color Texture,后面的 Bloom Pass 读取它。
- 资源是图里的对象,例如 Texture、Buffer、RendererList,而不是手动管理生命周期的裸 RTHandle。
这种组织方式的关键变化是:Pass 不再只告诉 GPU “现在做什么”,而是先告诉管线“我要读什么、写什么、产出什么”。管线拿到这些信息后,可以在执行前分析整帧。
例如一个后处理链路可能写成:
1 | |
如果使用传统命令式写法,临时纹理的申请、释放、读写关系通常散落在各个 Pass 里。RenderGraph 会把这些依赖集中记录下来,然后计算资源生命周期:BloomMip0 只需要活到 Bloom Blur 结束,之后它占用的内存就可能被别的临时纹理复用。
这也是 RenderGraph 相比普通 Pass 队列更有价值的地方。它不是简单地把 Execute() 改名,而是让管线拥有了优化所需的信息。
2. URP 中 RenderGraph 的位置
在较新的 URP 里,自定义 Pass 的入口从过去常见的 Execute() 逐渐转向 RecordRenderGraph()。直观理解:
1 | |
ScriptableRendererFeature 的职责没有消失。它仍然负责创建并注入自定义 ScriptableRenderPass。变化主要发生在 Pass 内部:以前在 Execute() 里直接拿 CommandBuffer 干活,现在要在 RecordRenderGraph() 里声明图节点、资源和真正执行函数。
一个很简化的结构如下:
1 | |
这段代码不应该当成跨版本模板直接复制。URP 的资源数据类、Blit 工具和 RenderGraph API 在不同 Unity 6 小版本里有调整。重点是它体现了 RenderGraph 的写法分工:
RecordRenderGraph()负责描述这个 Pass。PassData保存执行阶段需要的数据。UseTexture()、SetRenderAttachment()等调用负责声明资源读写。SetRenderFunc()里的函数才是真正记录 GPU 命令的地方。
3. RenderGraph 能优化什么
3.1 临时资源生命周期
屏幕空间效果经常产生大量临时纹理。HBAO、SSR、Bloom、TAA、Color Grading、Depth Pyramid 这类功能都可能需要中间 RT。传统写法里,开发者需要自己决定何时申请、何时释放、是否复用。
RenderGraph 通过读写声明可以知道资源最后一次被使用在哪里。一个临时纹理如果只在两个相邻 Pass 之间传递,就不必占用整帧生命周期。多个生命周期不重叠的纹理也可能复用同一块底层内存。
这对移动端尤其重要。很多移动 GPU 对带宽和 Render Target 存取非常敏感,多申请一张全分辨率 HDR RT,成本不只是显存占用,还包括清屏、Load、Store 和 Tile Memory 与系统内存之间的数据交换。
3.2 Pass 剔除
如果一个 Pass 的输出没有被后续 Pass 或最终 BackBuffer 使用,它理论上可以被剔除。RenderGraph 能从图依赖里发现这类无效节点。
当然,这个优化有前提:Pass 不能隐式产生外部副作用。比如写全局状态、读写不受图管理的资源、依赖某个外部 CommandBuffer 副作用,都可能让管线无法安全剔除。因此 RenderGraph 写法鼓励把输入输出显式声明出来。
3.3 Native Render Pass 合并
在支持的图形 API 和平台上,URP 可以结合 RenderGraph 信息尝试合并 Native Render Pass。这个优化在 TBDR(Tile-Based Deferred Rendering,基于 Tile 的延迟渲染)移动 GPU 上很有意义。
如果多个 Pass 对同一组附件进行连续操作,并且中间没有必须落回系统内存的依赖,管线可以减少 Render Target 的 Load/Store。对移动端来说,这类优化有时比减少一点 ALU 更重要,因为瓶颈经常在带宽和片上/片外交互。
但这不是“用了 RenderGraph 就一定更快”。如果 Pass 之间频繁采样前一个 Pass 的颜色结果,或者需要把颜色 RT 当普通 Texture 读取,就可能打断合并条件。RenderGraph 提供的是分析和调度基础,具体收益仍然取决于资源访问模式、平台和效果实现。
4. 自定义 Pass 迁移时要注意什么
4.1 不要把旧 Execute 代码原样塞进 RenderFunc
最常见误区是把旧版 Execute() 里的逻辑整体搬到 SetRenderFunc()。这样虽然可能跑起来,但很容易失去 RenderGraph 的意义。
需要拆开看:
- 创建临时纹理:尽量改成
renderGraph.CreateTexture()。 - 声明输入纹理:用
UseTexture()或对应 builder API。 - 声明输出附件:用
SetRenderAttachment()或对应写入 API。 - 真正绘制命令:放到
SetRenderFunc()。
也就是说,资源关系在外面声明,命令记录在里面执行。
4.2 PassData 只放必要数据
PassData 是执行函数的数据桥。它适合放 TextureHandle、BufferHandle、RendererListHandle、材质引用、少量参数。不要把大对象、临时容器或依赖当前帧外部状态的复杂对象随手塞进去。
RenderGraph 的一个设计目标是让执行阶段更可预测。PassData 越小、越明确,越容易看出这个 Pass 的输入输出。
4.3 区分 TextureHandle 和真实纹理
TextureHandle 不是普通 RenderTexture。它更像图里的资源引用。实际底层资源什么时候创建、是否复用、什么时候释放,由 RenderGraph 在编译和执行阶段决定。
这意味着不要在记录阶段假设自己已经拿到了真实 GPU 纹理,也不要在图外长期保存某个临时 TextureHandle。如果资源需要跨帧存在,例如历史帧 TAA、SSR history、上一帧深度等,通常要使用管线允许的持久资源管理方式,再在每帧导入或声明给 RenderGraph 使用。
4.4 注意版本差异
Unity RenderGraph 在 URP 中的接入经历过迁移阶段。Unity 6 里 RenderGraph 已经是 URP 的重要路径,但具体 API 名称、资源数据类型和示例代码会随 URP 包版本变化。写教程或项目封装时,不建议只记住某一段网上代码,而应该围绕这几个稳定概念组织:
1 | |
只要这个模型清楚,API 小变化通常比较容易适配。
5. 和传统 SRP Pass 写法的区别
可以用一句话概括:传统写法偏“我现在怎么画”,RenderGraph 写法偏“这一帧有哪些依赖”。
| 维度 | 传统 ScriptableRenderPass | RenderGraph 写法 |
|---|---|---|
| Pass 表达 | 手动按顺序入队并执行 | 入队后记录成图节点 |
| 临时 RT | 开发者显式申请释放 | 图系统根据生命周期管理 |
| 资源依赖 | 隐含在命令顺序里 | 通过读写声明显式表达 |
| Pass 剔除 | 很难全局判断 | 可基于最终依赖判断 |
| 移动端带宽优化 | 依赖手工组织 Pass | 更容易分析合并条件 |
| 调试方式 | 看 Frame Debugger 和命令 | 同时看图结构、资源生命周期、命令 |
这并不代表传统写法已经没有价值。对于很简单、一次性的渲染逻辑,命令式写法更直接。RenderGraph 的优势会在 Pass 数量变多、临时资源变多、平台差异变复杂时逐渐体现出来。
6. 适合用 RenderGraph 思考的效果
RenderGraph 特别适合资源链路清晰的屏幕空间效果,例如:
- Bloom:多级降采样、模糊、合成。
- SSAO / HBAO:深度法线输入、AO 中间结果、Blur、Composite。
- SSR:深度、法线、颜色、Hit Buffer、Resolve。
- TAA:当前帧颜色、Motion Vector、History、输出颜色。
- Depth Pyramid:深度输入、多级 Mip 输出。
这些效果的共同点是:输入输出明确,中间纹理多,Pass 之间依赖清楚。RenderGraph 可以把这种链路表达得更结构化。
反过来,如果一个 Pass 强依赖外部状态、跨帧副作用很多、资源生命周期不清晰,迁移到 RenderGraph 时就要先整理数据边界。否则只是换了 API,复杂度并不会消失。
7. 工程上的使用建议
先画资源链路,再写代码。
写 RenderGraph Pass 之前,先把输入、输出、中间资源画出来。比如“读 CameraColor 和 Depth,写 AOTexture,再读 AOTexture 合成回 CameraColor”。这比直接开写更不容易漏声明。
把持久资源和临时资源分开。
临时纹理交给 RenderGraph 管理;跨帧历史、缓存贴图、长期 Buffer 则用明确的生命周期管理,并在每帧接入图。
避免隐式副作用。
如果一个 Pass 修改了全局纹理、全局关键字或图外资源,要非常小心。隐式副作用会削弱 RenderGraph 的优化空间,也会让调试变困难。
不要为了“看起来现代”强行拆 Pass。
Pass 越多,图越细,调度信息越丰富,但也可能增加状态切换和理解成本。拆分应该服务于真实资源依赖,而不是把每两行命令都包装成一个 Pass。
用 Frame Debugger、RenderGraph Viewer 和平台抓帧一起看。
RenderGraph 能告诉你资源和 Pass 的逻辑关系,但最终性能仍然要看 GPU 抓帧。尤其是移动端,要关注 Render Target Load/Store、Resolve、带宽和 Tile Pass 是否被打断。
总结
RenderGraph 的本质是把渲染管线从“命令顺序管理”推进到“资源依赖管理”。它让 URP 在执行前知道一帧里有哪些 Pass、每个 Pass 读写哪些资源、资源生命周期在哪里开始和结束,从而为临时纹理复用、Pass 剔除、Native Render Pass 合并和跨平台调度提供基础。
对写自定义 URP Feature 来说,RenderGraph 最大的心智变化是:不要急着写命令,先把资源关系说清楚。RecordRenderGraph() 是声明阶段,SetRenderFunc() 才是执行阶段。只要理解这条边界,RenderGraph 就不是一套更绕的 API,而是一种更适合复杂渲染管线的组织方式。
真正落地时也不要迷信它。RenderGraph 提供优化可能性,不保证所有效果自动变快。好的 Pass 划分、清晰的资源生命周期、少做不必要的全分辨率中间图、避免隐式副作用,仍然是渲染优化的基本功。
参考资料
Unity Manual:Introduction to the render graph system in URP
Unity Manual:Write a render pass using the render graph system in URP