Unity 自定义渲染:Renderer Feature、Custom Pass 与 Render Graph
从 Render Pass 的基本概念出发,理解 URP 的 Renderer Feature / ScriptableRenderPass、HDRP 的 Custom Pass,以及它们与 SRP、Render Graph 之间真正的层级关系。
Unity 的 URP 和 HDRP 已经包含一套完整的渲染流程:阴影、深度、Opaque、Transparent、后处理……大多数项目只要配置材质、灯光与 Volume,就能完成绝大部分画面。
但一旦你想做这些效果,问题就会出现:
- 只把某一批对象重新绘制一次;
- 给被墙挡住的角色做 X-Ray;
- 生成 Outline 所需的 Mask;
- 输出 Object ID、Selection Buffer 或自定义 Depth;
- 在透明物体之前插入扫描效果;
- 在后处理前后运行自己的全屏 Shader;
- 增加一段 Compute Shader 工作;
- 让一个效果由多个连续的 GPU 步骤组成。
它们的共同点并不是“需要一个更复杂的 Shader”,而是:默认渲染流程里缺少你需要的那一步。
这正是自定义 Render Pass 要解决的问题。
本文的边界
本文讨论的是:如何扩展 Unity 已经实现好的 URP / HDRP。SRP 如何从零构建一条完整 Render Pipeline,以及 Render Graph 更深入的编译、资源别名和调度机制,会分别留给独立文章。
本文以 Unity 6 时代的 URP Render Graph 工作流为主要参考。Unity 6 / URP 17 将 Render Graph 引入为默认路径;旧式、非 Render Graph 的 ScriptableRenderPass API 仍可通过 Compatibility Mode 使用,但 Unity 已明确说明不会继续开发或改进这条路径。1
1为什么需要“自定义渲染”?
渲染管线本质上不是“执行一个 Shader”,而是一系列按顺序发生的工作。
一个极度简化的帧可能类似:
如果你只是想让一个材质“变红”,修改 Shader 就够了;但如果你的需求是:
在 Opaque 结束后,把
HighlightLayer 中的对象再画一次,而且使用另一套材质。
那么 Shader 本身无法决定“在整条渲染管线的哪个时刻,再调度一次绘制”。你需要修改的是 render loop。
类似地,Outline 常常不是一个单独的 Shader 就能完成,而可能需要:
这里已经出现了两个独立的渲染步骤。
因此,自定义渲染真正关心的问题是:
如何在已有渲染流程的某个位置,增加一个或多个新的 GPU 工作单元?
URP 和 HDRP 都能做到,但它们暴露给用户的扩展 API 并不相同。
2先理解 Render Pass
在进入 ScriptableRenderPass、CustomPass 和 Render Graph 之前,最好先把 Render Pass 当作一个普通概念理解。
2.1Pass 是渲染流程中的一个工作单元
一个 Pass 可以做很多事,例如:
- 把一批 Renderer 画进 Color Buffer;
- 只写 Depth;
- 把角色画进 Mask Texture;
- 运行一个 Fullscreen Shader;
- 运行 Compute Shader;
- 把临时纹理合成回 Camera Color。
所以一个效果完全可能由多个 Pass 构成:
Outline
├─ Mask Pass
├─ Blur / Edge Detection Pass
└─ Composite Pass这里“一个 Feature 包含多个 Pass”的设计很自然:Feature 是功能,Pass 是工序。
2.2Render Pass 不等于 Shader Pass
Unity 中“Pass”这个词很容易制造混淆。
ShaderLab 中也有:
SubShader
{
Pass
{
// vertex / fragment shader
}
}这个 Pass {} 表示 一个 Shader 可被管线选择和执行的着色通道。
而本文所说的 Render Pass,则是 渲染流程中的工作单元。
二者可以发生关系:一个 Render Pass 在绘制对象时,可能选择 Material 中某个 Shader Pass;但它们并不是同一种抽象。
可以这样记:
| 名称 | 关注的问题 |
|---|---|
| Shader Pass | “这个 Material 用哪组 GPU Shader 状态和程序来画?” |
| Render Pass | “这一阶段要做什么渲染工作?” |
| Render Graph Pass | “这个渲染工作读什么、写什么,与其他工作如何形成依赖?” |
3这些机制和 SRP 是什么关系?
SRP,全称 Scriptable Render Pipeline,是 Unity 允许 C# 代码调度和配置渲染命令的一层框架。URP 和 HDRP 都建立在 SRP 之上;开发者也可以基于 SRP 创建自己的 Render Pipeline。2
所以关系不是:
SRP → URP → HDRP而是:
这里最重要的边界是:
ScriptableRendererFeature和ScriptableRenderPass不是 SRP 的统一 API,而是 URP 自己的扩展架构。
HDRP 是另一套 SRP 实现,因此没有义务提供同名的 Renderer Feature。它选择了 CustomPass 体系。
本文真正讨论哪一层?
SRP 回答的是“怎样构建一整条渲染管线”;本文回答的是“当 URP / HDRP 已经替你构建好管线以后,怎样进入其中并增加自己的步骤”。
4URP:Renderer Feature 与 ScriptableRenderPass
URP 的自定义渲染核心可以先压缩成两个类:
ScriptableRendererFeature
↓
ScriptableRenderPass官方当前的 URP 工作流也是:先创建继承自 ScriptableRenderPass 的自定义 Pass,再通过 ScriptableRendererFeature 或 RenderPipelineManager 把它注入帧渲染循环。3
4.1Renderer Feature 是什么?
ScriptableRendererFeature 更像一个功能入口、配置容器和 Pass 组织者。
它最典型的职责是:
- 创建 Pass;
- 配置 Pass;
- 决定哪些 Camera 使用它;
- 把一个或多个 Pass 加入 Renderer 的执行序列。
最小骨架如下:
using UnityEngine.Rendering.Universal;
public class HighlightRendererFeature : ScriptableRendererFeature
{
private HighlightPass _pass;
public override void Create()
{
_pass = new HighlightPass();
_pass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
}
public override void AddRenderPasses(
ScriptableRenderer renderer,
ref RenderingData renderingData)
{
renderer.EnqueuePass(_pass);
}
}官方 API 中,Create() 用于初始化 Feature 所需资源和 Pass;AddRenderPasses() 则通过 EnqueuePass() 把一个或多个 Pass 加入 Renderer 的渲染序列。Unity 会针对 Camera 调用 AddRenderPasses(),因此这里不适合反复创建昂贵资源。4
4.2ScriptableRenderPass 是什么?
ScriptableRenderPass 才是那道真正的“渲染工序”。
例如:
HighlightRendererFeature
├─ HighlightMaskPass
└─ HighlightCompositePassFeature 代表“Highlight”这个功能;两个 Pass 分别负责:
- 将目标对象写进 Mask;
- 从 Mask 找边缘并合成到屏幕。
这也解释了为什么 Unity 没有简单设计成:
RendererFeature.Execute();因为一个功能并不等于一次 GPU 工作。
4.3为什么 Feature 和 Pass 要分开?
这个分层带来几个重要好处。
首先是配置与执行分离。LayerMask、材质、开关、质量级别可以属于 Feature,而真正的 Draw / Blit / Compute 留在 Pass。
其次是组合。一个 Feature 可以组织多个 Pass:
public class OutlineFeature : ScriptableRendererFeature
{
private MaskPass _maskPass;
private CompositePass _compositePass;
public override void Create()
{
_maskPass = new MaskPass();
_compositePass = new CompositePass();
}
public override void AddRenderPasses(
ScriptableRenderer renderer,
ref RenderingData renderingData)
{
renderer.EnqueuePass(_maskPass);
renderer.EnqueuePass(_compositePass);
}
}最后是注入时机。Pass 自己可以通过 renderPassEvent 表示大致插入位置,例如 Opaque 之后、Transparent 之前或 Post Process 之后。
5一个完整的 URP 自定义 Pass 是怎样工作的?
先看一个比 Outline 更简单、但结构完整的例子:
把场景中一批对象用 Override Material 再绘制一次。
这种能力本身就能扩展成 Highlight、X-Ray、Selection、Stencil Mark 等许多效果。
5.1第一步:Feature 创建并注入 Pass
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
public class RedrawObjectsFeature : ScriptableRendererFeature
{
[SerializeField] private Material overrideMaterial;
[SerializeField] private LayerMask layerMask = ~0;
private RedrawObjectsPass _pass;
public override void Create()
{
_pass = new RedrawObjectsPass(overrideMaterial, layerMask)
{
renderPassEvent = RenderPassEvent.AfterRenderingOpaques
};
}
public override void AddRenderPasses(
ScriptableRenderer renderer,
ref RenderingData renderingData)
{
renderer.EnqueuePass(_pass);
}
private sealed class RedrawObjectsPass : ScriptableRenderPass
{
private readonly Material _overrideMaterial;
private readonly LayerMask _layerMask;
public RedrawObjectsPass(Material overrideMaterial, LayerMask layerMask)
{
_overrideMaterial = overrideMaterial;
_layerMask = layerMask;
}
private sealed class PassData
{
public RendererListHandle rendererList;
}
public override void RecordRenderGraph(
RenderGraph renderGraph,
ContextContainer frameData)
{
using var builder = renderGraph.AddRasterRenderPass<PassData>(
"Redraw Highlight Objects",
out var passData);
var renderingData = frameData.Get<UniversalRenderingData>();
var cameraData = frameData.Get<UniversalCameraData>();
var lightData = frameData.Get<UniversalLightData>();
var resources = frameData.Get<UniversalResourceData>();
var filter = new FilteringSettings(
RenderQueueRange.opaque,
_layerMask);
var shaderTag = new ShaderTagId("UniversalForward");
var drawing = RenderingUtils.CreateDrawingSettings(
shaderTag,
renderingData,
cameraData,
lightData,
cameraData.defaultOpaqueSortFlags);
drawing.overrideMaterial = _overrideMaterial;
var listParams = new RendererListParams(
renderingData.cullResults,
drawing,
filter);
passData.rendererList = renderGraph.CreateRendererList(listParams);
builder.UseRendererList(passData.rendererList);
builder.SetRenderAttachment(resources.activeColorTexture, 0);
builder.SetRenderAttachmentDepth(
resources.activeDepthTexture,
AccessFlags.Write);
builder.SetRenderFunc(
static (PassData data, RasterGraphContext context) =>
{
context.cmd.DrawRendererList(data.rendererList);
});
}
}
}这段代码和 Unity 官方 RendererList 示例采用同样的核心路径:通过 CullingResults + DrawingSettings + FilteringSettings 构造对象列表,转换成 RendererListHandle,声明 Color / Depth Attachment,再由 DrawRendererList() 执行绘制。5
5.2第二步:理解这段代码,而不是背 API
真正重要的是它表达了四件事。
1. “哪些对象要画?”
var filter = new FilteringSettings(
RenderQueueRange.opaque,
_layerMask);这里先从当前 Camera 已经完成的 Culling 结果中筛选对象。
你可以据此控制:
- Opaque / Transparent;
- Layer;
- Shader Pass / LightMode;
- 排序规则。
2. “这些对象用什么方式画?”
var drawing = RenderingUtils.CreateDrawingSettings(...);
drawing.overrideMaterial = _overrideMaterial;这一步决定绘制状态和材质。
因此同一批 Mesh 完全可以在主场景里正常绘制一次,再在自定义 Pass 中用纯色材质重新绘制。
3. “画到哪里?”
builder.SetRenderAttachment(resources.activeColorTexture, 0);
builder.SetRenderAttachmentDepth(
resources.activeDepthTexture,
AccessFlags.Write);这一步把输出资源显式告诉 Render Graph。
4. “真正执行什么 GPU 命令?”
context.cmd.DrawRendererList(data.rendererList);前面的工作在描述这个 Pass;这里才记录真正的绘制命令。
这正好引出 Unity 6 中非常关键的变化:RecordRenderGraph()。
6从 Execute 到 RecordRenderGraph
如果看过较老的 URP 教程,你很可能见过这样的结构:
public override void Execute(
ScriptableRenderContext context,
ref RenderingData renderingData)
{
var cmd = CommandBufferPool.Get();
// SetRenderTarget(...)
// Blit(...)
// DrawRenderers(...)
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}这种思维方式接近传统命令式渲染:
创建资源
→ 切 Render Target
→ Draw
→ Blit
→ 再切 Render Target
→ 释放资源问题在于,当渲染流程越来越复杂时,系统很难仅从这些命令判断:
- 哪个 Pass 真正需要哪个 Texture?
- 一个临时 Texture 最早什么时候需要创建?
- 最晚什么时候可以释放或复用?
- 某个 Pass 的结果没人使用时,可不可以直接删掉?
- 两个 Pass 之间何时需要 GPU synchronization?
- 哪些 Pass 可以合并为更高效的原生 Render Pass?
这就是 Render Graph 要解决的问题。
Unity 6 的 URP 17 对自定义 Pass 的写法做了重大调整:默认启用 Render Graph,并建议自定义渲染功能通过 Render Graph API 重写;旧式路径被保留为 Compatibility Mode。1
6.1RecordRenderGraph() 不是 Execute() 的改名
这是最重要的理解。
旧式 Execute() 更像在说:
请按照我给出的顺序执行这些命令。
RecordRenderGraph() 更像在说:
我要注册一个 Pass;它读取这些资源、写入这些资源,真正执行时请调用这个函数。
例如:
using var builder = renderGraph.AddRasterRenderPass<PassData>(
"My Pass",
out var passData);
builder.UseTexture(inputTexture, AccessFlags.Read);
builder.SetRenderAttachment(outputTexture, 0);
builder.SetRenderFunc(
static (PassData data, RasterGraphContext context) =>
{
// GPU commands
});这形成了两阶段模型:
SRP Core 的 Render Graph 文档也把流程描述为:注册 Pass 与资源、编译 Graph、执行 Graph;在编译阶段可以裁掉不贡献最终结果的 Pass,并计算资源生命周期和同步点。6
6.2为什么“显式声明依赖”这么重要?
假设一个 Outline 效果是:
MaskPass:
Read = Depth
Write = Mask
CompositePass:
Read = Mask, CameraColor
Write = CameraColorRender Graph 看到的是:
于是它能知道:
Mask在MaskPass之前不需要存在;CompositePass之后,如果再没人读取Mask,其物理资源可以回收或复用;- 如果
CompositePass被删掉且没有其他消费者,MaskPass也可能不再有意义; - 某些渲染阶段可以被合并;
- Graphics / Compute 队列之间的同步可以由系统推导。
Unity 官方对 URP Render Graph 的描述也明确包括:Pass 合并、避免创建未使用资源、裁掉最终结果不需要的 Pass、减少重复资源,以及自动同步 Graphics / Compute 队列。7
Render Graph 的关键不是“Graph”这个名字
真正的设计变化是:从“我手动控制每一步状态”转向“我声明工作与资源依赖,让系统拥有全局优化空间”。
7Renderer Feature、Render Pass 与 Render Graph 到底是什么关系?
这三个词经常一起出现,但它们并不是同一个层次的竞争方案。
可以把它们分别理解为:
| 层次 | 回答的问题 |
|---|---|
| Renderer Feature | “这个自定义功能怎样接入 URP、包含哪些 Pass?” |
| ScriptableRenderPass | “其中这一道渲染工作具体是什么?” |
| Render Graph | “这些 Pass 读写什么资源,彼此如何依赖和调度?” |
因此一个 Outline Feature 可以是:
OutlineRendererFeature
├─ MaskPass
│ ├─ Read: Depth
│ └─ Write: Mask
│
└─ CompositePass
├─ Read: Mask
├─ Read: CameraColor
└─ Write: CameraColorRender Graph 并没有取代 Renderer Feature。
如果把 Renderer Feature 删掉,你仍然需要某种机制回答:
“这些自定义 Pass 什么时候、针对哪些 Camera、以什么配置加入 URP?”
反过来,Renderer Feature 也无法替代 Render Graph,因为它不负责全局资源生命周期和依赖优化。
7.1一个 Pass 能否随意读写同一纹理?
Render Graph 会比旧式 CommandBuffer 模型更严格,因为严格正是优化的前提。
例如当前 URP 文档明确指出,在普通 Render Graph Pass 中不能把同一个纹理同时作为读取和写入对象;常见做法是使用临时 Texture,或者在确实需要低层控制时使用 UnsafePass。但 UnsafePass 会减少 URP 能做的优化,例如 Pass 合并。8
这类限制不是单纯“API 变麻烦了”,而是在强迫依赖关系变得可分析。
8URP 中几个典型用例
8.1Outline:一个 Feature,多个 Pass
Outline 是最适合理解整个体系的例子之一。
一种常见方案:
在 URP 中它可以自然组织为:
OutlineRendererFeature
├─ MaskPass : ScriptableRenderPass
├─ EdgePass : ScriptableRenderPass
└─ CompositePass : ScriptableRenderPass每个 Pass 在 RecordRenderGraph() 中声明自己的输入输出。
这样设计的价值并不仅仅是“代码整洁”。对于 Render Graph 来说,三道工作拥有明确边界,它才有机会分析资源生命周期和无效 Pass。
8.2X-Ray:重新绘制被遮挡对象
X-Ray 的核心通常不是复杂光照,而是:
同一个对象,再画一次,但改变 Depth Test、Material 或 Stencil 行为。
流程可能是:
正常场景完成 Opaque
↓
筛选 Character Layer
↓
使用 Override Material 重新 Draw
↓
通过 Depth / Stencil 只保留被遮挡部分如果需求只涉及“在某个时间重新绘制某些 Layer,并覆盖材质或深度/模板状态”,甚至不必自己实现完整 Renderer Feature;URP 已提供 Render Objects Renderer Feature,可按 Render Queue、Layer Mask、Shader Pass 等筛选对象并覆盖部分渲染状态。9
8.3Object ID Buffer:不要把所有问题都塞进 Camera Color
编辑器 Picking、对象选中描边、语义分割或后续 GPU 识别常常需要一个独立 Buffer:
Object A → ID 1
Object B → ID 2
Object C → ID 3这时自定义 Pass 的目标不是“直接产生最终画面”,而是生成中间数据:
这正是 Render Graph 的优势场景:中间 Texture 的消费者非常明确。
8.4Fullscreen Effect:先看看是否根本不用写代码
如果你的需求只是:
Camera Color
↓
Fullscreen Shader
↓
Camera Color例如:
- 黑白;
- Damage Flash;
- Glitch;
- Night Vision;
- 简单 Scanner Overlay;
- 自定义后处理;
那么 URP 已有 Full Screen Pass Renderer Feature,可以把全屏材质插入预定义 Injection Point,并按需请求 Color、Depth、Normal 或 Motion 等输入。10
工程上更重要的问题
不要因为“会写 Renderer Feature”就什么都自己写。能用预构建 Render Objects / Full Screen Pass 清晰表达需求时,它们通常比自定义代码更容易维护。
8.5Compute Shader:Render Pass 并不只等于 Raster
Render Graph 中的 Pass 不一定是传统 Draw Call。
一个效果也可能是:
Depth / Normal
↓
Compute Pass
↓
Result Texture
↓
Composite Raster Pass因此“Render Pass”更适合理解成 一段图形工作单元,而不是“画 Mesh 的步骤”。
9HDRP:为什么没有 Renderer Feature?
理解到这里,再看 HDRP 就很容易了。
HDRP 没有 ScriptableRendererFeature,也没有 URP 的 ScriptableRenderPass,最根本的原因是:
它们本来就是 URP 自己的扩展 API,而不是 SRP 规定的统一接口。
HDRP 自己提供的主要用户扩展机制叫 Custom Pass。当前 HDRP 官方主干文档将其描述为:可以在渲染管线的特定位置注入 Shader 和 C#,用于绘制对象、覆盖对象渲染属性、执行 Fullscreen Pass,以及读取 Color、Depth、Normal、Motion Vector 等 Camera Buffer。11
9.1Custom Pass Volume 到底是什么?需要继承吗?
CustomPass 只描述“这一道自定义渲染工作做什么”,HDRP 还需要另外一层信息:
- 这组 Pass 在哪个 Injection Point 执行?
- 它对所有 Camera 生效,还是只在某个空间区域生效?
- 多组 Custom Pass 同时存在时,执行顺序是什么?
- 这一组效果包含哪些
CustomPass?
这正是 CustomPassVolume 的职责。它本身是一个 MonoBehaviour,用于承载和调度一组 Custom Pass。12
大多数情况下,你不需要继承 CustomPassVolume。最常见的工作流完全可以在 Editor 中完成:
GameObject
↓
Volume
↓
Custom Pass
↓
Custom Pass Volume然后在 Inspector 里配置:
Mode
Injection Point
Priority
Custom Passes[]在 Custom Passes 列表中,你可以直接添加 HDRP 已经提供的 Pass,例如 Draw Renderers、FullScreen 等;只有当这些现成能力表达不了需求时,才需要自己继承 CustomPass 写 C#。11
因此可以先把二者记成:
Custom Pass Volume
= “在哪里、什么时候、对哪些 Camera、以什么顺序执行”
CustomPass
= “这一道自定义渲染具体做什么”例如,一个始终存在的角色描边,可以使用一个 Global Custom Pass Volume:
Custom Pass Volume
Mode = Global
Injection Point = Before Post Process
│
├─ Draw Renderers Custom Pass
│ └─ 把 Character Layer 写入 Mask / Custom Buffer
│
└─ FullScreen Custom Pass
└─ 根据 Mask 合成 Outline而一个只在玩家进入水下区域后生效的效果,可以使用 Local 模式:
Custom Pass Volume
Mode = Local
Collider = 水下区域
Fade Radius = 2 m
Injection Point = Before Post Process
│
└─ FullScreen Custom Pass
└─ 水下颜色、扭曲或其他屏幕效果Local Volume 需要空间边界;Camera 进入这个区域后,相应 Custom Pass 才参与渲染。Fade Radius 还可以把距离转换成 Custom Pass 可使用的淡入因子。Global 模式则没有空间边界,可以影响启用了 Custom Pass 的 Camera。12
另一个典型例子是 X-Ray:
Custom Pass Volume
Mode = Global
│
└─ Draw Renderers Custom Pass
├─ Layer Mask = Character
├─ Override Material = XRay
└─ Override Depth State这个场景甚至可能一行自定义 C# 都不需要写:如果 Draw Renderers Custom Pass 已经能表达对象筛选、材质覆盖和 Depth 状态,直接在 Inspector 中配置通常更简单。
先配置,再写代码
HDRP 的 Custom Pass 工作流并不意味着“每个效果都要继承 CustomPass”。先看 Draw Renderers / FullScreen 等现成 Pass 能否表达需求;只有在需要特殊 Draw、Compute、多 Buffer 协作或额外运行时逻辑时,再写自定义 CustomPass。
9.2Global Custom Pass 又是什么?
这里特别容易因为名字相似而混淆。HDRP 里至少要区分两个概念。
9.2.1Custom Pass Volume 的 Mode = Global
它仍然是一个普通的场景 GameObject:
Scene
└─ GameObject
└─ Custom Pass Volume
Mode = Global“Global”只是表示这个 Volume 没有空间边界,而不是说“不需要 Volume”。Unity 官方文档说明,Global Custom Pass Volume 可以影响场景中的 Camera;Local 模式则通过 Collider 确定空间范围。12
这适合:
- 某个 Scene 固有的描边;
- Scene 作者希望直接在 Inspector 调整的全屏效果;
- 与场景内容一起保存、一起编辑的自定义渲染。
9.2.2Global Custom Pass API
HDRP 还提供真正不依赖 Custom Pass Volume GameObject 的注册方式:Global Custom Pass API。官方说明它可以直接把一个 CustomPass 注册进 render loop,而不用修改用户场景或在运行时动态创建 Custom Pass Volume。13
例如:
var pass = new MyCustomPass();
CustomPassVolume.RegisterGlobalCustomPass(
CustomPassInjectionPoint.BeforePostProcess,
pass,
priority: 0f);这种方式更适合:
- 渲染插件;
- 项目级 Outline / Selection 系统;
- Debug Visualization;
- 不希望每个 Scene 都手动放一个 GameObject 的基础设施。
所以这三个概念最好分开:
| 机制 | 有无场景 GameObject | 主要用途 |
|---|---|---|
Custom Pass Volume, Mode = Global | 有 | Scene 中全局生效、Inspector 配置 |
Custom Pass Volume, Mode = Local | 有 | 与空间区域绑定的效果 |
| Global Custom Pass API | 无 | 由代码或插件直接注册项目级 Pass |
它们最终承载的工作单元仍然都是 CustomPass:
因此,如果一定要和 URP 做近似映射,可以写成:
URP HDRP
Renderer Feature ≈ Custom Pass Volume 的部分组织职责
ScriptableRenderPass ≈ CustomPass
renderPassEvent ≈ Injection Point但这只是工作流上的类比,不是 API 一一对应。尤其是 Global Custom Pass API 说明 HDRP 并不要求所有 Custom Pass 都必须经过场景里的 Volume。
9.3HDRP 的 CustomPass 长什么样?
当前 HDRP 官方主干文档提供的 C# 模板仍然是:
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
class MyCustomPass : CustomPass
{
protected override void Setup(
ScriptableRenderContext renderContext,
CommandBuffer cmd)
{
}
protected override void Execute(CustomPassContext ctx)
{
// Draw / Fullscreen / Compute commands
}
protected override void Cleanup()
{
}
}三个入口的职责非常直接:
Setup():分配长期需要的资源;Execute():描述这个 Custom Pass 在当前 Camera 上执行什么;Cleanup():释放Setup()中创建的资源。14
CustomPassContext 则给 Execute() 提供 Command Buffer、Camera Buffer、Culling Results、ScriptableRenderContext、HDRP Camera 数据等运行时上下文。
一个极简的 Fullscreen 思路可以写成:
class TintCustomPass : CustomPass
{
public Material material;
protected override void Execute(CustomPassContext ctx)
{
if (material == null)
return;
CoreUtils.SetRenderTarget(
ctx.cmd,
ctx.cameraColorBuffer,
ctx.cameraDepthBuffer);
CoreUtils.DrawFullScreen(ctx.cmd, material);
}
}实际项目还需要根据 Injection Point、目标 Buffer、MSAA、XR 与所用 Shader 的约束调整,但它很好地展示了 HDRP API 的编程风格:用户在 Execute() 中记录具体绘制或计算命令。
10HDRP CustomPass 为什么像旧式 ScriptableRenderPass.Execute?
这正是 URP 和 HDRP 今天最容易让人困惑的地方。
URP 的现代扩展入口是:
ScriptableRenderPass
↓
RecordRenderGraph()
↓
声明 Graph Pass 与资源依赖HDRP 的公开 Custom Pass API 则仍然是:
CustomPass
↓
Execute(CustomPassContext)
↓
Draw / Dispatch / Copy 等具体命令这里最重要的一点是:这两个函数并不处在相同的抽象层。
10.1Render Graph 是更高层的工作描述
Render Graph 的核心工作不是“直接 Draw”,而是先记录:
有哪些 Pass?
↓
每个 Pass 读取什么资源?
↓
写入什么资源?
↓
彼此有什么依赖?SRP Core 的 Render Graph 文档把生命周期概括为 Record → Compile → Execute:先记录 Pass 与资源,编译依赖和资源生命周期,最后才执行各个 Pass。6
真正执行某个 Pass 时,才会进一步产生:
Draw
Dispatch
Copy
Set Render Target
...所以抽象层应该理解为:
CommandBuffer、Raster/Compute Command Context 等都位于 Render Graph 下面更具体的执行层,而不是位于它上面。
10.2那 URP 和 HDRP 的差异在哪里?
这里不需要把两条 Pipeline 的内部调用链强行画成同一种结构。对使用者来说,更重要的是它们公开出来的扩展接口不同:
URP
ScriptableRendererFeature
↓
ScriptableRenderPass
↓
RecordRenderGraph()HDRP
Custom Pass Volume / Global Custom Pass API
↓
CustomPass
↓
Execute(CustomPassContext)URP 在 Unity 6 的现代工作流中,允许自定义 ScriptableRenderPass 通过 RecordRenderGraph() 直接声明 Render Graph Pass 及其资源依赖。
HDRP 的公开 CustomPass API 则仍然以 Setup / Execute / Cleanup 为主要入口;Execute(CustomPassContext) 中记录具体的 Draw、Dispatch、Copy 等工作。14
这并不表示 HDRP 没有 Render Graph。HDRP 本身同样建立在 Unity 的 Render Graph 基础设施之上,只是 Custom Pass 没有向用户暴露与 URP RecordRenderGraph() 对等的公开入口。15
因此,理解这一节时只需要记住两个层次:
Render Graph 描述更高层的 Pass、资源与依赖;真正执行某个 Pass 时,最终仍会落到 Draw、Dispatch、Copy 等具体图形或计算命令。
不必把两套 API 强行一一对应
URP 与 HDRP 都属于 SRP,也都使用 Unity 的 Render Graph 基础设施,但它们面向用户的自定义渲染 API 是分别设计和演进的。理解实际开发流程时,掌握各自公开的扩展入口,比追踪内部每一层如何桥接更有用。
11为什么 URP 和 HDRP 的扩展模型不同?
从设计上,可以把二者的思路粗略概括成:
11.1URP:Renderer + Feature
URP 的心智模型比较像:
Universal Renderer
├─ Built-in Passes
├─ Renderer Feature A
├─ Renderer Feature B
└─ Renderer Feature C因此“Renderer Feature”这个名字很合理:它是挂在某个 URP Renderer 上的功能扩展。
11.2HDRP:固定的高复杂 Render Loop + Injection Point
HDRP 的内部 render loop 本身就非常复杂,包含大量相互依赖的阶段和中间 Buffer。对用户来说,HDRP 更强调:
你想在哪个已经定义好的位置插进去?这个位置允许你访问哪些 Buffer?
因此它的扩展模型更偏向:
HDRP Render Loop
├─ ...
├─ Injection Point
│ └─ Custom Passes
├─ ...
├─ Injection Point
│ └─ Custom Passes
└─ ...Injection Point 并不仅仅决定“先后顺序”,还影响当前时刻哪些 Camera Buffer 已经存在、能否读取或写入,以及修改 Depth / Normal 后是否还能被后续 HDRP 阶段利用。HDRP 官方文档也专门把 Injection Point 与可访问 Buffer 的关系作为 Custom Pass 的核心约束来解释。16
因此,HDRP 没有 Renderer Feature 并不是“少了一层功能”,而是选择了另一种扩展边界。
12URP 与 HDRP:怎样建立正确的对应关系?
下面这张表比“Renderer Feature = CustomPass”更准确。
| 关注点 | URP | HDRP |
|---|---|---|
| 所属 Pipeline | Universal Render Pipeline | High Definition Render Pipeline |
| 功能组织 | ScriptableRendererFeature | CustomPassVolume / Global Custom Pass API |
| 单个自定义工作单元 | ScriptableRenderPass | CustomPass |
| 大致执行位置 | RenderPassEvent | Custom Pass Injection Point |
| 用户参与 Render Graph | RecordRenderGraph() 直接描述自定义 Graph Pass / Resource | CustomPass 不暴露对应的 RecordRenderGraph();HDRP 内部构建 Graph |
| 对象重绘 | RendererList / Render Objects Feature | Draw Renderers Custom Pass / 自定义 CustomPass |
| 全屏效果 | Full Screen Pass Renderer Feature / 自定义 Pass | Fullscreen Custom Pass / 自定义 CustomPass |
| Render Graph | 用户自定义 Pass 可直接录入 Graph | HDRP 内部使用 Graph,CustomPass API 仍是 Execute 风格 |
不要把对应关系看得太死
CustomPassVolume 并不是 HDRP 版 ScriptableRendererFeature 的同名替代物;它们只是都承担了一部分“组织自定义工作并选择注入时机”的职责。真正接近“一道工作单元”的对应关系是 ScriptableRenderPass ↔ CustomPass。
13面对一个效果,应该选择什么?
学习这些 API 的目的不是记住类名,而是能从需求反推正确的扩展层。
| 需求 | URP 优先考虑 | HDRP 优先考虑 |
|---|---|---|
| 单纯全屏 Shader | Full Screen Pass Renderer Feature | Fullscreen Custom Pass |
| 重新绘制某些 Layer | Render Objects Renderer Feature | Draw Renderers Custom Pass |
| Outline Mask | 自定义 ScriptableRenderPass | CustomPass |
| X-Ray / Occluded Character | Render Objects 或自定义 Pass | Draw Renderers Custom Pass |
| Object ID Buffer | 自定义 Render Graph Pass | 自定义 CustomPass |
| 多阶段 Blur / Composite | 一个 Feature + 多个 Pass | 一个 Volume / Global 注册 + 多个 CustomPass |
| Compute + Raster 组合 | Render Graph Compute / Raster Pass | CustomPass 中 Dispatch + 后续合成,或更深入 HDRP 扩展 |
| 只想改 Material 本身的视觉 | 先不要写自定义 Pass | 先不要写自定义 Pass |
13.1只是改表面效果?先别碰 Render Pass
如果需求只是:
- 改颜色;
- 改 BRDF;
- 加 Dissolve;
- 改法线;
- 做顶点动画;
那么 Shader / Shader Graph 往往已经足够。
自定义 Render Pass 的信号通常是:
你需要“额外画一次”“额外生成一个 Buffer”“换一个时机画”“在两个阶段之间插一段工作”。
13.2只重画一批对象?先看预构建 Feature / Pass
如果需求是:
Layer A
↓
Override Material
↓
在某个时间再画一次优先检查:
- URP:Render Objects Renderer Feature;
- HDRP:Draw Renderers Custom Pass。
13.3只是全屏处理?优先用 Fullscreen 工具
如果你的算法本质是:
而不需要重新遍历并绘制某些 Mesh,那么它更像 Fullscreen Pass,而不是 Object Draw Pass。
13.4需要多个中间结果?这时 Render Graph 的价值开始明显
例如:
Depth
↓
Downsample
↓
Compute Blur
↓
Temporal Accumulation
↓
CompositePass 越多、中间资源越多,显式描述依赖就越有价值。
14常见误区
14.1“Renderer Feature 就是 Render Pass”
不是。
Renderer Feature = 功能 / 配置 / Pass 组织
ScriptableRenderPass = 一道工作一个 Feature 可以包含多个 Pass。
14.2“Render Graph 取代了 Renderer Feature”
也不是。
Render Graph 解决的是 Pass 与资源的描述、生命周期和调度;Renderer Feature 解决的是自定义功能如何进入 URP。
二者处在不同层。
14.3“Render Graph Pass 就等于 ScriptableRenderPass”
也不能简单画等号。
ScriptableRenderPass 是 URP 暴露给用户的 Pass 抽象;一个 ScriptableRenderPass.RecordRenderGraph() 内部会向 Render Graph 注册实际的 Raster / Compute / Unsafe 等 Graph Pass。
因此概念上更接近:
ScriptableRenderPass
↓
RecordRenderGraph()
↓
一个或多个 Render Graph Pass而不是永远严格的一对一。
14.4“HDRP 没有 Renderer Feature,所以自定义能力更弱”
错误。
HDRP 有自己的 Custom Pass 扩展模型;差别是 API 设计与注入方式不同。
14.5“HDRP 使用 Render Graph,所以 CustomPass 一定有 RecordRenderGraph”
也错误。
HDRP 内部使用 Render Graph,但当前公开的 CustomPass C# 模板仍然通过 Execute(CustomPassContext) 暴露用户工作。1415
14.6“SRP Batcher 和 SRP 是同一个东西”
不是。
SRP 是 Scriptable Render Pipeline 架构;SRP Batcher 是针对兼容 Shader 的 CPU 侧渲染状态/数据提交优化。名字相近,但本文讨论的 Renderer Feature、Custom Pass、Render Graph 与“是否启用 SRP Batcher”不是同一个问题。
15最后建立一张完整的心智地图
理解这套体系时,需要区分两个维度:公开扩展 API 的结构,以及渲染工作的抽象层级。
15.1公开扩展 API 的结构
这张图回答的是:开发者从哪里进入 URP / HDRP,自定义工作由哪些公开类型组织?
15.2渲染工作的抽象层级
这张图回答的是:一次渲染工作从高层依赖描述,到真正发出 GPU 命令,抽象层如何逐步降低?
URP 的 RecordRenderGraph() 直接面向 Render Graph 的描述阶段;HDRP 的公开 CustomPass API 则主要让开发者在 Execute(CustomPassContext) 中记录具体渲染工作。这里不需要把二者强行画成同一条内部调用链。
这里可以得到几个最终结论。
第一,SRP 是框架,URP / HDRP 是两种具体实现。Renderer Feature 并不是 SRP 标准,而是 URP 自己的扩展体系。
第二,Renderer Feature 和 ScriptableRenderPass 不是同一个层次。Feature 组织功能,Pass 表示工作单元。
第三,Render Graph 也不是另一种 Renderer Feature。它描述 Pass 与 GPU 资源之间的依赖,并给 Pipeline 留出全局优化空间。
第四,HDRP 的 CustomPass 更接近 URP 的 ScriptableRenderPass,而 Custom Pass Volume 承担了一部分 Feature 式的组织与注入职责。
第五,也是最容易误解的一点:HDRP 内部使用 Render Graph,并不意味着公开的 CustomPass API 必须暴露 RecordRenderGraph()。 当前 HDRP 用户扩展依然主要通过 Execute(CustomPassContext) 工作,而 URP 已经把 Render Graph 更直接地带进 ScriptableRenderPass 的公开编程模型。
从需求出发,而不是从 API 出发
当你遇到一个新效果时,先问:它只是 Shader 变化,还是需要增加新的渲染步骤?如果需要新步骤,再问:它是重绘对象、全屏处理、生成中间 Buffer,还是多个 Pass 的组合?最后才去选择 Renderer Feature、ScriptableRenderPass、Custom Pass 或 Render Graph API。
如果继续向下追问“URP 和 HDRP 自己又是怎么通过 RenderPipeline、ScriptableRenderContext、Culling、RendererList 和 CommandBuffer 构成一整条管线的”,那已经进入 SRP 本身;而如果继续追问 Render Graph 如何做 Pass Culling、资源生命周期分析、Transient Resource 复用和同步,则是另一篇更专门的主题。
这两个方向,也正好构成理解 Unity Rendering Architecture 的下一层。
注释
-
Unity Manual,Upgrade to URP 17 (Unity 6)。官方说明 URP 17 引入 Render Graph,Unity 6 新 URP 项目默认启用它,同时保留 Compatibility Mode;非 Render Graph 路径不再继续开发或改进。 ↩ ↩2
-
Unity Manual,Scriptable Render Pipeline fundamentals。官方说明 SRP 是允许 C# 调度和配置渲染命令的薄 API 层,并明确指出 URP、HDRP 都建立在 SRP 之上。 ↩
-
Unity Manual,Custom render pass workflow in URP。官方工作流要求自定义 Pass 继承
ScriptableRenderPass,并通过 Renderer Feature 或RenderPipelineManager注入帧循环。 ↩ -
Unity Manual,Scriptable Renderer Feature API reference。该页说明
Create、AddRenderPasses、SetupRenderPasses等方法的职责,以及AddRenderPasses针对 Camera 的调用方式。 ↩ -
Unity Manual,Draw objects in the render graph system in URP。官方示例展示了
RendererListParams、RendererListHandle、UseRendererList、Color/Depth Attachment 与DrawRendererList的组合方式。 ↩ -
Unity Graphics / SRP Core,Render graph system in SRP Core。官方仓库说明 Render Graph 的记录、编译、执行阶段,以及 Pass Culling、资源生命周期和同步点分析。 ↩ ↩2
-
Unity Manual,Introduction to the render graph system in URP。官方列出了 URP 可基于 Render Graph 执行的 Pass 合并、无用资源避免、无用 Pass 裁剪、资源复用与 Graphics/Compute 队列同步等优化。 ↩
-
Unity Manual,Use a texture in a render pass in URP 与 Use compatibility-mode APIs in a render graph pass。官方说明普通 Render Graph Pass 对纹理读写与 Render Target 切换的约束,以及
UnsafePass会削弱部分优化能力。 ↩ -
Unity Manual,Render Objects Renderer Feature reference。官方说明该 Feature 可按 Render Queue、Layer Mask、Shader Pass 等筛选对象,并覆盖部分渲染行为。 ↩
-
Unity Manual,Full Screen Pass Renderer Feature reference。官方说明 Full Screen Pass 可在预定义 Injection Point 注入全屏效果,并按需请求 Depth、Normal、Color、Motion 等输入。 ↩
-
Unity Technologies Graphics repository,HDRP feature overview — Custom Passes。当前官方主干文档说明 Custom Pass 可在 HDRP 的指定位置注入 C# / Shader、绘制对象、覆盖渲染属性、执行全屏 Pass 并读取 Camera Buffer。 ↩ ↩2
-
Unity Technologies Graphics repository,Understand custom pass volumes 与 Custom Pass reference。官方主干文档说明 Global / Local、Injection Point、Priority 等 Custom Pass Volume 行为。 ↩ ↩2 ↩3
-
Unity HDRP Documentation 17.0,Global Custom Pass API 与
CustomPassVolumeAPI。官方说明 Global Custom Pass API 可以在不创建 Custom Pass Volume GameObject、不修改用户场景的情况下,通过RegisterGlobalCustomPass等 API 把CustomPass注册到指定 Injection Point;这与Custom Pass Volume的Mode = Global是两个不同概念。 ↩ -
Unity Technologies Graphics repository,Create a Custom Pass in a C# script。当前官方主干模板公开
Setup、Execute(CustomPassContext)与Cleanup三个入口,并说明Execute用于描述 HDRP 在 Custom Pass 中渲染什么。 ↩ ↩2 ↩3 -
Unity Technologies Graphics repository,HDRP feature overview — Render Graph。当前官方主干文档明确写明 HDRP 内部使用 Render Graph API 描述 render loop,并利用它优化 GPU Memory 与裁剪未使用 Pass。 ↩ ↩2
-
Unity Manual,HDRP Custom Pass Injection Points。该官方文档说明 Injection Point 会影响 Custom Pass 执行时机以及各 Camera Buffer 的可读写状态;具体 Injection Point 集合会随 HDRP 版本演进,应以当前项目所用 HDRP 包文档为准。 ↩
CONTINUE EXPLORING
沿着这篇文章继续
Unity 渲染管线全解:从 BRP 到 SRP,再到 URP、HDRP 与 Unified Rendering
Built-in、SRP、LWRP、URP、HDRP 各自是什么、解决了什么问题、代价在哪里;2026 年渲染战略与 Unified Rendering 的真实含义;以及不同管线之间如何迁移、迁移要花多少成本。
Unity SRP:从 Camera 到 GPU 理解渲染管线架构
从 RenderPipeline、Camera、Culling、RendererList、CommandBuffer 到 Render Graph,沿着一帧的真实路径理解 Unity Scriptable Render Pipeline 的架构、核心组件与设计动机。