Unity SRP:从 Camera 到 GPU 理解渲染管线架构
从 RenderPipeline、Camera、Culling、RendererList、CommandBuffer 到 Render Graph,沿着一帧的真实路径理解 Unity Scriptable Render Pipeline 的架构、核心组件与设计动机。
Unity 中经常能见到一串看起来彼此相似的名字:
- Render Pipeline
- Scriptable Render Pipeline
- Universal Render Pipeline
ScriptableRenderContextRendererScriptableRenderer- Render Pass
- Renderer Feature
- Render Graph
- SRP Batcher
如果只是分别查 API,很容易得到一种奇怪的结果:每个名词都大概认识,却不知道一帧究竟是怎样被它们串起来的。
真正值得先理解的不是某个类的成员函数,而是一条更根本的路径:
SRP 的本质,是让我们用 C# 决定这一帧渲染工作如何被组织,而不是让 C# 自己去光栅化三角形。
Unity 仍然负责大量底层工作:场景数据、原生渲染后端、图形 API 抽象、资源管理、平台差异以及最终与 GPU 的交互。SRP 则位于更高的一层,负责告诉 Unity:
哪台 Camera 要渲染什么?
哪些对象需要参与?
按什么顺序画?
使用哪些 Shader Pass?
写入哪个 Render Target?
什么时候生成阴影、深度、颜色和后处理结果?
本文会从一条极简的自定义 SRP 出发,沿着一帧真正经历的流程逐层拆开这些问题。
本文以 Unity 6 的 API 为主要参照。许多较早的 SRP 教程仍然直接使用 ScriptableRenderContext.DrawRenderers();在 Unity 6 中该 API 已标记为 Obsolete,官方建议改用 RendererList:先通过 CreateRendererList() 创建列表,再用 CommandBuffer.DrawRendererList() 调度绘制。本文会使用较新的写法,同时说明旧 API 背后的同一套思想。1
1为什么 Unity 需要 Scriptable Render Pipeline?
在理解 SRP 之前,可以先问一个更基本的问题:
游戏引擎不是本来就应该负责渲染吗?为什么还需要让开发者“编程渲染管线”?
因为“把一个 Mesh 画出来”和“决定一整帧应该怎样被组织”是两件不同的事情。
一个现代实时渲染器通常不只是:
找到物体
→ 画出来而更像:
Culling
↓
Shadow Map
↓
Depth Prepass
↓
Opaque
↓
Lighting
↓
SSAO / SSR / Decal / ...
↓
Transparent
↓
Post Processing
↓
Final Output不同项目对这条流程的需求可能完全不同。
移动游戏可能希望尽量减少 Render Target 和带宽;高端 PC 项目可能需要 Deferred、体积雾、复杂阴影和大量屏幕空间效果;VR 又特别在意双眼渲染、延迟和 GPU 时间。
如果整个 Render Loop 完全固定在引擎内部,那么开发者只能在有限的插槽中修改行为。
Unity 的 Built-in Render Pipeline 并不是完全不可扩展,它长期以来也提供 Command Buffer、Camera Event 等接口。但渲染流程的主体仍主要由引擎定义。
SRP 改变的是这一层边界。
Unity 将 SRP 定义为一个较薄的 API 层:开发者可以使用 C# 调度和配置渲染命令,这些命令随后交给 Unity 的底层图形架构,再发送给实际 Graphics API。URP 和 HDRP 都建立在 SRP 之上,也可以基于 SRP 编写自己的 Render Pipeline。2
可以粗略理解为:
1.1“Scriptable” 到底意味着什么?
这里的 Scriptable 并不是:
“用 C# 实现一个软件光栅化器。”
也不是:
“以后不需要 Shader 了。”
而是:
把 Render Loop 中相当一部分“什么时候做什么”的组织权交给 C#。
例如,我们可以在 C# 中决定:
先画 Shadow
还是先做 Depth Prepass?
Opaque 用什么规则筛选?
Transparent 怎样排序?
什么时候切换 Render Target?
什么时候 Dispatch Compute Shader?
什么时候做 Post Process?真正处理顶点、片元、采样纹理和光栅化的,依然是 Shader 与 GPU。
因此可以把职责粗略分成三层:
| 层 | 主要问题 |
|---|---|
| C# / SRP | 这一帧做什么、按什么顺序做 |
| Shader | 一次绘制中的顶点和像素怎么算 |
| GPU / Graphics API | 这些命令最终怎样在硬件上执行 |
这也是为什么学习 SRP 的重点不应该从 HLSL 开始,而应该先理解 Render Loop。
2先看整张图:一帧渲染经历了什么?
先不考虑阴影、光照和后处理,一条极简 SRP 可以抽象为:
如果进一步抽象,场景中的所有可渲染对象可以看成集合 。
Camera 和 Culling 决定可见集合 ,Filtering 再从中挑选当前 Pass 想画的对象,最后 Sorting 决定顺序:
这里的 才是当前一次绘制真正关心的对象集合。
例如第一次:
Cull
↓
只筛 Opaque
↓
Front-to-Back 排序
↓
Draw第二次则可以:
同一个 CullingResults
↓
只筛 Transparent
↓
Back-to-Front 排序
↓
Draw这已经暴露出 SRP 的一个重要设计:
“场景里有哪些东西可能可见”与“这一道渲染步骤实际要画哪些东西”是分开的。
后面许多 API 都只是围绕这个思想逐渐具体化。
3SRP 最核心的两个对象
一个自定义 SRP 最先遇到的两个类型是:
RenderPipelineAsset
RenderPipelineUnity 官方也把它们视为 SRP 的两个核心自定义元素。2
它们看起来很像,但职责完全不同。
3.1RenderPipelineAsset:配置与创建 Pipeline
RenderPipelineAsset 是一个 ScriptableObject。
它存在于 Project 中,可以序列化,也可以出现在 Inspector 里:
using UnityEngine;
using UnityEngine.Rendering;
[CreateAssetMenu(menuName = "Rendering/Tiny Render Pipeline")]
public sealed class TinyRenderPipelineAsset : RenderPipelineAsset
{
[SerializeField]
private Color clearColor = new(0.05f, 0.05f, 0.05f);
protected override RenderPipeline CreatePipeline()
{
return new TinyRenderPipeline(clearColor);
}
}它最关键的方法就是:
CreatePipeline()可以把 Asset 理解成:
Pipeline 的可序列化配置 + Factory。
例如它可以存:
Shadow Distance
MSAA
HDR
Render Scale
Lighting Quality
Post Processing Settings
...而真正负责每一帧渲染的不是 Asset,而是它创建出来的 RenderPipeline 实例。
3.2RenderPipeline:真正执行渲染
真正的 Render Loop 位于:
RenderPipeline中。
Unity 6 当前的核心入口是:
protected override void Render(
ScriptableRenderContext context,
List<Camera> cameras)Render() 就是 SRP 的主入口。Unity 会在需要渲染时调用它。3
最小骨架可以写成:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public sealed class TinyRenderPipeline : RenderPipeline
{
private readonly Color clearColor;
public TinyRenderPipeline(Color clearColor)
{
this.clearColor = clearColor;
}
protected override void Render(
ScriptableRenderContext context,
List<Camera> cameras)
{
foreach (Camera camera in cameras)
{
RenderCamera(context, camera);
}
}
private void RenderCamera(
ScriptableRenderContext context,
Camera camera)
{
// 后面逐步实现
}
}此时我们已经拥有了一条合法的“渲染管线”。
只不过它什么都没有画。
这件事本身很重要:
Render Pipeline 并不神秘。最小意义上的 Pipeline,就是 Unity 每帧调用的一段 C# Render Loop。
真正的复杂度来自后面不断增加的能力,而不是来自 RenderPipeline 这个类本身。
3.3Asset 和 Runtime Instance 为什么要分开?
如果把配置和执行全部塞进一个对象,会遇到一些生命周期问题。
Asset 更适合:
保存
序列化
Inspector 编辑
不同质量等级配置
创建 Runtime InstancePipeline 实例更适合:
持有运行时资源
执行 Render Loop
管理临时状态
创建 Buffer
执行每帧逻辑因此结构是:
Unity 当前使用的 Render Pipeline Asset 可以通过 Graphics Settings / Quality Settings 决定;GraphicsSettings.currentRenderPipeline 则表示当前质量等级实际生效的 RenderPipelineAsset。4
4写一个最小的 Scriptable Render Pipeline
接下来真正让它开始工作。
先暂时不画任何物体,只做三件事:
- 设置 Camera;
- 清空颜色和深度;
- 提交。
private void RenderCamera(
ScriptableRenderContext context,
Camera camera)
{
context.SetupCameraProperties(camera);
CommandBuffer cmd =
CommandBufferPool.Get("Tiny SRP");
cmd.ClearRenderTarget(
clearDepth: true,
clearColor: true,
backgroundColor: clearColor
);
context.ExecuteCommandBuffer(cmd);
context.Submit();
CommandBufferPool.Release(cmd);
}这段代码虽然简单,却已经引出了 SRP 中三个非常核心的角色。
4.1SetupCameraProperties()
它会调度 Camera 相关的全局 Shader 状态,例如 View、Projection 和裁剪平面等。5
也就是说 Camera 并不是只提供一个位置:
Camera
├─ View Matrix
├─ Projection Matrix
├─ Frustum
├─ Near / Far
├─ Culling Mask
└─ Rendering State渲染某台 Camera 前,需要让后续 Shader 与绘制工作处于这台 Camera 的上下文中。
4.2CommandBuffer
CommandBuffer 本质上是一份:
待执行的图形命令列表。
例如:
SetRenderTarget
ClearRenderTarget
SetGlobalTexture
DrawMesh
DrawRendererList
DispatchCompute
Blit
...它不是图片,也不是 GPU。
它更接近:
“之后请执行这些事情。”4.3Submit()
很多 SRP API 的行为不是:
调用这一行以后 GPU 立刻完成工作。
而是:
将工作调度到当前
ScriptableRenderContext。
ScriptableRenderContext.Submit() 才会把此前已经调度的命令提交给渲染循环执行。6
因此:
C# 调用 API
↓
Schedule Commands
↓
ScriptableRenderContext
↓
Submit
↓
Unity Graphics Backend
↓
Graphics API / GPUSubmit() 的意义是提交已经调度的渲染工作,而不是“等待 GPU 完成整帧”。不要把它理解成一个普通的 CPU → GPU 同步阻塞点。
现在我们的 Pipeline 能正确清屏,但还是看不到 Scene 中的 Mesh。
因为我们还没有回答一个重要的问题:
到底应该画哪些对象?
5Culling:为什么第一步不是直接 Draw?
假设 Scene 里有十万个 Renderer。
一个 Camera 可能只看得到其中几千个。
如果我们每一次 Pass 都从:
Scene 中所有对象开始重新筛选,成本和架构都会非常糟糕。
于是 SRP 首先需要进行 Culling。
5.1从 Camera 获取 Culling 参数
if (!camera.TryGetCullingParameters(
out ScriptableCullingParameters cullingParameters))
{
return;
}这些参数来自 Camera,可以包含:
- Culling Mask
- Culling Matrix
- Culling Planes
- Shadow Distance
- LOD 参数
- Occlusion Culling 选项
- 最大可见光数量
等信息。7
5.2执行 Cull
CullingResults cullingResults =
context.Cull(ref cullingParameters);得到:
CullingResults它代表后续渲染可以使用的可见性结果。
5.3后续多个 Pass 复用结果
Opaque、Transparent、Shadow 等不同步骤,可以从同一个或相关的可见性信息出发,再分别应用自己的绘制规则。
整体关系是:
5.4Culling 不只是“看物体在不在屏幕里”
最容易把 Culling 简化成:
Frustum Culling但 ScriptableCullingParameters 描述的范围更广。
例如 Unity 的 CullingOptions.OcclusionCull 可以让当前 Culling 操作包含 Occlusion Culling;该选项默认还会受到 Camera.useOcclusionCulling 的影响。7
所以更准确的理解是:
Culling 是根据当前 Camera 和管线配置,建立这一视图后续渲染所需的可见性数据。
Unity 6 中 URP/HDRP 还存在 GPU Occlusion Culling 等更现代的 GPU-driven 技术路径,那属于另一层优化机制,不应该简单等同于这里最基础的 context.Cull() 调用。
6从“可见”到“真正要画”:Sorting、Filtering 与 Drawing
有了 CullingResults,仍然不能直接说:
“全部画出来。”
因为某一道 Pass 通常只需要其中一部分对象。
例如:
Opaque Pass
只画不透明物体
Transparent Pass
只画透明物体
Shadow Pass
只画 Shadow Caster
Depth Pass
只需要能参与 Depth 的 Shader Pass这就是三个重要结构出现的原因:
| 类型 | 解决的问题 |
|---|---|
FilteringSettings | 哪些对象有资格进入这一批绘制? |
SortingSettings | 这些对象按什么顺序? |
DrawingSettings | 应该怎样画、选择哪个 Shader Pass? |
6.1FilteringSettings:画谁?
最简单的例子是区分 Opaque 与 Transparent:
var filtering = new FilteringSettings(
RenderQueueRange.opaque
);或者:
var filtering = new FilteringSettings(
RenderQueueRange.transparent
);FilteringSettings 还可以根据:
GameObject Layer
Rendering Layer
Render Queue
Motion Vector 状态
...继续过滤。8
因此:
CullingResults
↓
FilteringSettings
↓
当前 Pass 真正关心的对象6.2SortingSettings:先画谁?
Opaque 和 Transparent 往往不能采用完全相同的顺序。
对于不透明物体,一个常见策略是大致 Front-to-Back:
var sorting = new SortingSettings(camera)
{
criteria = SortingCriteria.CommonOpaque
};这样较近的不透明物体可以更早写入 Depth,后续被遮挡片元有机会更早被拒绝,同时 Unity 也会考虑减少状态切换。
透明对象则通常需要:
criteria = SortingCriteria.CommonTransparent;也就是包含 Back-to-Front 排序的常用透明规则。Unity 官方的 SortingCriteria 文档也明确区分了 CommonOpaque 与 CommonTransparent。9
因此顺序不是一个纯粹的美学问题,它会直接影响:
正确性
+
Overdraw
+
State Change
+
性能6.3DrawingSettings:怎样画?
DrawingSettings 决定的事情包括:
Sorting
Shader Pass
Per-object Data
Instancing
Override Material / Shader
...最基础的写法:
static readonly ShaderTagId ShaderTag =
new("SRPDefaultUnlit");
var drawing = new DrawingSettings(
ShaderTag,
sorting
)
{
enableInstancing = true
};这里出现了一个非常关键的东西:
ShaderTagId它把 Render Pipeline 的某个绘制阶段 和 Shader 中的某个 Pass 联系起来。
这会在后面详细解释。
7Unity 6:RendererList 把这些规则组合起来
很多经典 SRP 教程会写:
context.DrawRenderers(
cullingResults,
ref drawingSettings,
ref filteringSettings
);这个接口很好地展示了 SRP 思想,因此非常适合学习。
但是在 Unity 6 中,ScriptableRenderContext.DrawRenderers() 已经标记为 Obsolete,官方推荐使用 RendererList API。1
新的关系更接近:
代码如下:
private static readonly ShaderTagId ShaderTag =
new("SRPDefaultUnlit");
private static void DrawObjects(
ScriptableRenderContext context,
CommandBuffer cmd,
Camera camera,
CullingResults cullingResults,
bool transparent)
{
var sortingSettings = new SortingSettings(camera)
{
criteria = transparent
? SortingCriteria.CommonTransparent
: SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(
ShaderTag,
sortingSettings
)
{
enableInstancing = true
};
var filteringSettings = new FilteringSettings(
transparent
? RenderQueueRange.transparent
: RenderQueueRange.opaque
);
var rendererListParams = new RendererListParams(
cullingResults,
drawingSettings,
filteringSettings
);
RendererList rendererList =
context.CreateRendererList(ref rendererListParams);
cmd.DrawRendererList(rendererList);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
}这里非常值得停下来观察。
我们仍然没有:
foreach (Renderer renderer in scene)
{
renderer.Draw();
}而是在描述:
从已经 Cull 得到的结果中,按照这些 Filtering、Sorting 和 Drawing 规则,构造我想要绘制的一组可见对象。
这就是 RendererList 的意义。
Unity 将其定义为一组可见 GameObject;创建它时通常提供 CullingResults、DrawingSettings 与 FilteringSettings,然后通过 CommandBuffer.DrawRendererList() 将绘制加入命令流。1
8把 Opaque、Skybox 和 Transparent 串起来
现在已经可以真正构造一个非常小的 Render Loop:
Camera
↓
Cull
↓
Opaque
↓
Skybox
↓
Transparent
↓
SubmitSkybox 在现代 API 下也可以创建自己的 Renderer List:
RendererList skybox =
context.CreateSkyboxRendererList(camera);
cmd.DrawRendererList(skybox);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();于是完整的 Camera 渲染函数可以写成:
private void RenderCamera(
ScriptableRenderContext context,
Camera camera)
{
if (!camera.TryGetCullingParameters(
out var cullingParameters))
{
return;
}
CullingResults cullingResults =
context.Cull(ref cullingParameters);
context.SetupCameraProperties(camera);
CommandBuffer cmd =
CommandBufferPool.Get(camera.name);
cmd.ClearRenderTarget(
clearDepth: true,
clearColor: true,
backgroundColor: clearColor
);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// 1. Opaque
DrawObjects(
context,
cmd,
camera,
cullingResults,
transparent: false
);
// 2. Skybox
RendererList skybox =
context.CreateSkyboxRendererList(camera);
cmd.DrawRendererList(skybox);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// 3. Transparent
DrawObjects(
context,
cmd,
camera,
cullingResults,
transparent: true
);
context.Submit();
CommandBufferPool.Release(cmd);
}这已经非常接近一条最原始的 Forward Render Pipeline 骨架。
但它仍然缺少很多东西:
Lighting
Shadow
Depth Texture
Reflection Probe
Post Processing
Editor Gizmo
XR
Camera Stack
MSAA
HDR
...这反而正是这个示例的价值:
把现代管线中的复杂功能全部拿掉以后,下面仍然是 Camera → Cull → Select → Draw → Submit。
9一帧 SRP 的完整骨架
如果把前面的代码整理一下,一条教学用 SRP 可以写成:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public sealed class TinyRenderPipeline : RenderPipeline
{
private static readonly ShaderTagId ShaderTag =
new("SRPDefaultUnlit");
private readonly Color clearColor;
public TinyRenderPipeline(Color clearColor)
{
this.clearColor = clearColor;
}
protected override void Render(
ScriptableRenderContext context,
List<Camera> cameras)
{
BeginContextRendering(context, cameras);
foreach (Camera camera in cameras)
{
BeginCameraRendering(context, camera);
RenderCamera(context, camera);
EndCameraRendering(context, camera);
}
EndContextRendering(context, cameras);
}
private void RenderCamera(
ScriptableRenderContext context,
Camera camera)
{
if (!camera.TryGetCullingParameters(
out var cullingParameters))
{
return;
}
CullingResults cullingResults =
context.Cull(ref cullingParameters);
context.SetupCameraProperties(camera);
CommandBuffer cmd =
CommandBufferPool.Get(camera.name);
cmd.ClearRenderTarget(
clearDepth: true,
clearColor: true,
backgroundColor: clearColor
);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
DrawObjects(
context,
cmd,
camera,
cullingResults,
transparent: false
);
RendererList skybox =
context.CreateSkyboxRendererList(camera);
cmd.DrawRendererList(skybox);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
DrawObjects(
context,
cmd,
camera,
cullingResults,
transparent: true
);
context.Submit();
CommandBufferPool.Release(cmd);
}
private static void DrawObjects(
ScriptableRenderContext context,
CommandBuffer cmd,
Camera camera,
CullingResults cullingResults,
bool transparent)
{
var sortingSettings = new SortingSettings(camera)
{
criteria = transparent
? SortingCriteria.CommonTransparent
: SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(
ShaderTag,
sortingSettings
)
{
enableInstancing = true
};
var filteringSettings = new FilteringSettings(
transparent
? RenderQueueRange.transparent
: RenderQueueRange.opaque
);
var rendererListParams = new RendererListParams(
cullingResults,
drawingSettings,
filteringSettings
);
RendererList rendererList =
context.CreateRendererList(ref rendererListParams);
cmd.DrawRendererList(rendererList);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
}
}这里的:
BeginContextRendering(...)
BeginCameraRendering(...)
EndCameraRendering(...)
EndContextRendering(...)并不是 GPU 绘制的必要组成部分,而是在自定义 SRP 中正确触发 RenderPipelineManager 相应事件的入口。
URP 和 HDRP 会自动处理这些回调;自己实现 SRP 时,如果希望外部代码能够使用相应的 Rendering Event,则需要在合适的位置调用它们。Unity 6 还推荐使用 beginContextRendering / endContextRendering 替代旧的 Frame 版本,因为旧的 beginFrameRendering / endFrameRendering 会带来额外堆分配。10
10ScriptableRenderContext:C# 与 Unity 渲染后端之间的桥
到这里,ScriptableRenderContext 已经出现了很多次:
context.Cull(...)
context.SetupCameraProperties(...)
context.CreateRendererList(...)
context.ExecuteCommandBuffer(...)
context.Submit()那么它究竟是什么?
可以把整个关系画成:
Unity 官方将 ScriptableRenderContext 描述为自定义 C# Render Pipeline 与 Unity 低层图形代码之间的接口。2
它不是:
GPU也不是:
CommandBuffer更不是:
一张 Render Texture它代表的是当前 SRP 可以用来:
Cull
创建 RendererList
建立 Camera 状态
调度 CommandBuffer
创建 Shadow / Skybox RendererList
最终 Submit的一组渲染上下文能力。
11CommandBuffer 与 ScriptableRenderContext 有什么区别?
这两个概念很容易混。
11.1ScriptableRenderContext 更像“当前渲染会话的接口”
例如:
context.Cull(...)
context.CreateRendererList(...)
context.Submit()它知道当前 SRP 渲染所处的 Unity 上下文。
11.2CommandBuffer 更像“要执行的命令序列”
例如:
cmd.ClearRenderTarget(...);
cmd.SetGlobalTexture(...);
cmd.DrawMesh(...);
cmd.DrawRendererList(...);
cmd.DispatchCompute(...);最后:
context.ExecuteCommandBuffer(cmd);是把这组命令调度进 Context。
值得特别注意的是,ExecuteCommandBuffer() 并不等价于:
“GPU 此刻立即完成这些命令。”
官方描述是:该调用会把 Command Buffer 注册进 ScriptableRenderContext 的内部命令序列,实际执行在随后 Submit() 所提交的工作中发生。11
因此:
11.3为什么还需要两种 Draw?
你可能还会遇到:
cmd.DrawMesh(...)以及:
cmd.DrawRendererList(...)二者虽然都能产生 Draw,但解决的问题不同。
DrawMesh() 更像显式命令:
画这个 Mesh,用这个 Material。
而 RendererList 更像场景驱动的批量绘制描述:
从这台 Camera 的可见对象中,按照这些过滤、排序和 Shader Pass 规则,把符合条件的 Renderer 画出来。
典型场景渲染通常更需要后者。
12Renderer、Material、Shader 是怎样进入 SRP 的?
到这里还有一个很自然的疑问:
Scene 中明明摆的是
MeshRenderer、Material 和 Shader,为什么 Render Loop 里好像从来没有逐个访问它们?
因为这部分连接发生在 Culling、RendererList 和 Drawing 规则之间。
可以把关系理解成:
Scene 中的 Renderer 负责让对象具有可渲染表示,并关联 Material 等数据。
但 SRP 并不是:
foreach (Renderer renderer in scene)
{
...
}而更多是:
Scene
↓
Cull
↓
Visible Renderers
↓
Filtering / Drawing / Sorting
↓
RendererList
↓
Draw这让底层能够拥有更大的优化空间,而 Pipeline 只负责描述自己需要什么。
13ShaderTagId:Pipeline 与 Shader 之间的协议
前面的代码里写过:
private static readonly ShaderTagId ShaderTag =
new("SRPDefaultUnlit");为什么 C# 需要知道一个字符串?
因为一个 Shader 可能存在多个 Pass:
SubShader
{
Pass
{
Tags { "LightMode" = "MyForward" }
}
Pass
{
Tags { "LightMode" = "ShadowCaster" }
}
Pass
{
Tags { "LightMode" = "DepthOnly" }
}
}同一个对象可能在不同阶段执行不同 Shader Pass。
于是 Render Pipeline 需要表达:
这一次我要这个 Shader 的哪种角色?
这就是 LightMode / ShaderTagId 形成的一种约定。
例如自定义 SRP 完全可以约定:
TinyForward
TinyDepth
TinyShadowCasterC#:
new ShaderTagId("TinyForward");Shader:
Tags { "LightMode" = "TinyForward" }只要双方约定一致即可。
URP 中常见的:
UniversalForward
UniversalGBuffer
ShadowCaster
DepthOnly
SRPDefaultUnlit本质上也是 URP 自己定义的一套协议。
这非常重要,因为它意味着:
UniversalForward并不是“SRP 标准”。它是 URP 对自己 Render Loop 和 Shader 角色的定义。
14一个最小的 SRP Shader
为了配合前面的 SRPDefaultUnlit,可以使用一个最简单的 Shader:
Shader "TinySRP/Unlit"
{
Properties
{
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags
{
"Queue" = "Geometry"
}
Pass
{
Tags
{
"LightMode" = "SRPDefaultUnlit"
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
struct Attributes
{
float3 positionOS : POSITION;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
};
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
CBUFFER_END
Varyings Vert(Attributes input)
{
Varyings output;
output.positionCS =
TransformObjectToHClip(input.positionOS);
return output;
}
half4 Frag(Varyings input) : SV_Target
{
return _BaseColor;
}
ENDHLSL
}
}
}这里只做:
Object Space Position
↓
Clip Space Position
↓
输出 Base Color没有光照,没有阴影。
但这已经足以看清:
C# Render Pipeline
↓
ShaderTagId("SRPDefaultUnlit")
Shader
↓
LightMode = "SRPDefaultUnlit"
两边匹配
↓
RendererList 可以选择这个 Shader Pass15Render Pass 和 Shader Pass 不是一回事
这是 Unity Rendering 中非常容易混淆的一对名字。
15.1Shader Pass
存在于 ShaderLab:
Pass
{
Tags { "LightMode" = "ShadowCaster" }
HLSLPROGRAM
...
ENDHLSL
}它回答:
同一个 Shader 在这一种绘制角色下,该执行怎样的 GPU 程序和 Render State?
15.2Render Pass
则是渲染流程中的一个工作阶段,例如:
Shadow Pass
Depth Pass
Opaque Pass
SSAO Pass
Transparent Pass
Post Process Pass它回答:
这一阶段整个 Renderer 要完成什么工作?
一个 Render Pass 可能绘制几千个对象,而这些对象可能选择某个特定 Shader Pass。
因此关系更接近:
所以:
Render Pass 是 Pipeline 层的概念;Shader Pass 是 Shader 层的概念。
名字相同,是因为二者都表达了“同一流程的一次阶段”,但抽象层完全不同。
16Render Target:这些东西到底画到哪里?
到目前为止我们一直在说:
Draw
Draw
Draw但 GPU 产生的结果必须写到某个地方。
最基础的是:
Color Buffer
Depth Buffer颜色可以想象成:
深度则记录对应像素当前的 Depth:
当然真实 GPU 中的格式、精度、MSAA、压缩和 Tile Memory 要复杂得多,但在 Pipeline 层,我们首先只需要建立这个抽象。
16.1为什么现代 Pipeline 会有那么多 Texture?
因为很多渲染算法不是:
Scene
↓
Screen一次完成的。
例如 Deferred Rendering:
Geometry
↓
GBuffer
GBuffer + Lights
↓
LightingSSAO:
Depth + Normal
↓
AO Texture
↓
Lighting / CompositeBloom:
Scene Color
↓
Threshold
↓
Downsample
↓
Blur
↓
Upsample
↓
Composite因此一条现代 Render Pipeline 实际上更像:
也就是说:
现代 Render Pipeline 很大程度上就是“Pass 与 Resource 的网络”。
这句话会直接引出后面的 Render Graph。
17一条真正的 Pipeline 不只有 Opaque + Transparent
我们刚才写出来的 Tiny SRP:
Cull
↓
Opaque
↓
Skybox
↓
Transparent只是为了把骨架暴露出来。
真正的实时 Renderer 可能包括:
Culling
↓
Shadow Culling
↓
Shadow Maps
↓
Depth Prepass
↓
Depth Pyramid
↓
GBuffer / Forward Opaque
↓
Decals
↓
Lighting
↓
SSAO
↓
SSR
↓
Volumetric Lighting
↓
Transparent
↓
Distortion
↓
TAA / Upscaler
↓
Bloom
↓
Depth of Field
↓
Tonemapping
↓
UI
↓
Final Output而且其中很多步骤并不是简单 Raster Pass,还可能包含:
Compute Pass
Copy Pass
Ray Tracing Pass
Async Compute
GPU-driven Culling所以真正让一条 Pipeline 变复杂的,并不只是“代码行数很多”。
更本质的是:
Pass 数量、Resource 数量,以及它们之间的依赖关系迅速增加。
18阴影也只是另一种“从可见性到绘制”
以 Shadow Map 为例,思路其实仍然没有逃离我们前面建立的框架。
普通 Camera:
Camera
↓
Cull
↓
Visible Objects
↓
Draw to Camera TargetShadow:
Light View
↓
Shadow Culling
↓
Shadow Casters
↓
Draw Depth to Shadow MapUnity 的现代 ScriptableRenderContext 甚至提供:
CreateShadowRendererList(...)来为 Shadow 绘制创建对应的 Renderer List。1
这说明 Culling → Selection → RendererList → Draw 不是“只适用于 Opaque”的偶然写法,而是一种更加普遍的组织模式。
19光照是怎样加入这套结构的?
我们当前的 Shader 是 Unlit,因为真正做 Lighting 会立刻引出:
Visible Lights
Main Light
Additional Lights
Light Culling
Shadow Data
Reflection Probe
Light Probe
Shader Constant Buffer
Forward / Deferred但从架构角度看,它仍然只是继续丰富前面的数据流。
例如一个最朴素的 Forward Renderer:
也就是说 Pipeline 做的事情变成:
- 从
CullingResults获得可见 Light; - 整理当前 Lighting Strategy 需要的数据;
- 写入全局 Buffer / Constant;
- 选择相应 Shader Pass;
- Draw。
真正复杂的 URP/HDRP 正是在不断扩展这些步骤。
20为什么一条简单的 Render() 最终会失控?
如果只有:
Opaque
Transparent我们完全可以手写:
DrawOpaque();
DrawTransparent();但随着功能增加:
RenderShadow();
RenderDepth();
RenderOpaque();
RenderSSAO();
RenderSSR();
RenderTransparent();
RenderBloom();
RenderPostProcess();问题开始出现。
20.1问题一:Render Target 生命周期
例如:
AO Texture只在:
SSAO → Lighting之间需要存在。
之后它的 GPU Memory 就可能被别的临时资源复用。
如果所有 Texture 都手动维护:
Create
Use
Release资源生命周期会越来越难推理。
20.2问题二:Pass 之间存在依赖
例如:
Depth
↓
SSAO
↓
CompositeSSAO 必须等 Depth 完成。
而另一些工作可能彼此独立:
Shadow Map
Environment Processing理论上可能存在更复杂的调度空间。
20.3问题三:有些 Pass 其实不需要运行
假如:
Pass A
↓
生成 TempTexture但最终没有任何后续 Pass 读取这个 Texture,那么 A 对最终输出没有贡献。
如果渲染系统能看见完整依赖关系,就可能直接把它裁掉。
这正是 Render Graph 这类系统出现的背景。
21Render Graph 在 SRP 中处于什么位置?
Render Graph 很容易被误解成:
“Unity 又发明了一种新的 Render Pipeline。”
不是。
SRP 回答的是:
怎样构建和控制渲染管线?
Render Graph 更关注:
当管线里已经存在很多 Pass 时,它们怎样描述资源依赖和生命周期?
可以粗略画成:
传统命令式思维更像:
创建 TempA
↓
执行 Pass A
↓
切 RenderTarget
↓
执行 Pass B
↓
释放 TempA
↓
创建 TempB
...Render Graph 则更倾向于声明:
Pass A:
Write Texture A
Pass B:
Read Texture A
Write Texture B
Pass C:
Read Texture B
Write Camera Color也就是:
从“我一步一步手动管理资源”转向“我告诉系统依赖关系”。
这使系统有机会进行:
Resource Lifetime Analysis
Transient Resource Reuse
Pass Culling
Pass Scheduling
Debug Visualization等优化。
在 Unity 6 / URP 17 中,Render Graph 已成为新 URP 项目的默认路径。Unity 仍提供 Compatibility Mode 运行旧式非 Render Graph API,但官方升级文档明确说明,不再继续开发或改进不使用 Render Graph API 的渲染路径。URP 与 HDRP 当前都支持 Render Graph。12
但这篇文章不会继续深入:
TextureHandle
RasterGraphContext
IRasterRenderGraphBuilder
Pass Culling
Resource Aliasing因为那些属于 Render Graph 本身的问题。
这里最重要的结论只有:
Render Graph 没有取代 SRP;它是在现代复杂 SRP 内部帮助组织 Pass 与 Resource 的基础设施。
22SRP Core 又是什么?
除了 Unity Engine 自身提供的核心 SRP API,还有一个很容易与 SRP 本身混淆的名字:
Scriptable Render Pipeline Core也就是常见的:
com.unity.render-pipelines.coreSRP Core 提供大量构建 Render Pipeline 时可以复用的代码,例如:
Render Graph
RTHandle
Volume Framework
Core Utilities
Shader Library
平台相关辅助
公共渲染工具
...Unity 官方明确说明,URP 和 HDRP 都使用 SRP Core,其中包含平台相关图形 API 的样板代码、常见渲染操作工具以及 Shader Library。13
所以可以这样区分:
SRP
= Unity 允许你构建 Scriptable Render Pipeline 的核心机制
SRP Core
= Unity 提供的一组可复用渲染基础设施和工具库它们有关,但不是同一个概念。
23URP 与 HDRP 是怎样建立在 SRP 之上的?
现在终于可以回到日常开发最常见的:
URP
HDRP它们并不是:
SRP 的两个模式开关而是:
Unity 基于 SRP 构建好的两条具体 Render Pipeline。
结构可以粗略表示为:
23.1URP 做的事情
URP 在基础 SRP 上继续定义了大量自己的架构:
UniversalRenderPipeline
ScriptableRenderer
UniversalRenderer
ScriptableRendererFeature
ScriptableRenderPass
URP Lighting
URP Shader Tags
URP Render Graph Frame Data
...23.2HDRP 做的事情
HDRP 则建立另一套更加面向高端渲染的结构:
HDRenderPipeline
HDRP Lighting
GBuffer
Volumetrics
Custom Pass
HDRP Shader Pass
Ray Tracing
...所以:
ScriptableRenderer并不是 SRP 的标准组件。
它是 URP 的设计。
同样:
ScriptableRendererFeature和ScriptableRenderPass也不是所有 SRP 都必须拥有。
这解释了为什么 HDRP 没有 Renderer Feature,却依然是一条完全正常的 SRP。
24为什么学习 SRP 时不应该从 ScriptableRenderer 开始?
如果第一次理解 Unity Rendering 就从 URP 源码开始,很容易形成:
SRP
=
ScriptableRenderer
+
Renderer Feature
+
ScriptableRenderPass但这是把:
一个具体产品的架构误当成:
底层框架本身真正更底层的关系是:
RenderPipeline
↓
Render()
↓
ScriptableRenderContext
↓
Cull
↓
RendererList / Commands
↓
SubmitURP 的 ScriptableRenderer 是 Unity 在这套能力之上又设计出来的一层 Renderer 抽象。
理解这一点以后,再去看:
UniversalRenderer
ForwardRenderer
2D Renderer
Renderer Feature就会容易很多。
25从 SRP 到 Renderer Feature / Custom Pass
理解完整 SRP 后,一个非常实际的问题是:
如果我只是想做 Outline,有必要自己写一整条 Render Pipeline 吗?
通常没有。
这其实是两个不同层级的问题。
25.1改整条 Pipeline
例如你真的要设计:
新的 Lighting Model
新的 GBuffer Layout
特殊的 Visibility Pipeline
完全不同的 Shadow Architecture
研究性质 Renderer这时可能需要:
Custom SRP25.2只往现有 Pipeline 增加一步
例如:
Outline
X-Ray
Object ID
Scanner
特殊 Mask
Fullscreen Effect更合理的方式往往是扩展现有 Pipeline:
URP
↓
Renderer Feature
↓
ScriptableRenderPass
HDRP
↓
Custom Pass因此:
这也是为什么即使大部分游戏开发者永远不会亲手维护一条完整 SRP,理解 SRP 仍然很有价值:
只有知道整台机器怎样运行,才能真正理解那些扩展点究竟插在什么地方。
26一条现代 Render Pipeline 可以怎样理解?
到这里,可以把文章中出现的主要组件放回同一张地图。
如果只记住一张图,就应该是这张。
27常见概念混淆
27.1SRP vs URP / HDRP
SRP
= 构建 Scriptable Render Pipeline 的基础机制
URP / HDRP
= Unity 基于 SRP 构建的具体 Pipeline不是:
SRP = URP也不是:
HDRP 建立在 URP 上而是:
SRP
/ \
URP HDRP27.2Renderer vs ScriptableRenderer
Renderer 是 Unity Scene 中的 Component 基类之一,例如:
MeshRenderer
SkinnedMeshRenderer
ParticleSystemRenderer它代表一个可被渲染的场景对象。
而:
ScriptableRenderer是 URP 的 Renderer 架构。
二者名字相似,但不在一个层级。
27.3Render Pass vs Shader Pass
Render Pass
= Pipeline 的一个工作阶段
Shader Pass
= Shader 中的一套 GPU Program + Render State一次 Render Pass 可以让大量 Renderer 执行同一种角色的 Shader Pass。
27.4SRP vs SRP Core
SRP
= 渲染管线机制
SRP Core
= Unity 提供的共享渲染工具包URP 与 HDRP 都建立在 SRP 上,也都大量复用 SRP Core。13
27.5SRP vs SRP Batcher
名字尤其容易误导。
SRP Batcher 并不是:
“把整个 SRP 批处理。”
它是一种 Draw Call CPU 侧优化路径,目标主要是减少使用相同 Shader Variant 的绘制之间反复设置渲染状态和材质常量数据的成本。Unity 6 文档列出的兼容范围包括 URP、HDRP 和 Custom SRP,但不包括 Built-in Render Pipeline。14
所以:
SRP
= Render Pipeline Architecture
SRP Batcher
= Render State / Draw Submission Optimization完全不是同一个抽象层。
27.6Culling vs GPU Occlusion Culling
基础 SRP 中:
context.Cull(...)负责根据 ScriptableCullingParameters 建立可见性结果,并可以根据选项包含传统 Occlusion Culling。
现代 URP/HDRP 的 GPU Occlusion Culling 则属于更加 GPU-driven 的可见性优化机制。
它们都叫 Culling,但不要简单认为:
context.Cull()
=
GPU Occlusion Culling27.7CommandBuffer vs Render Graph
CommandBuffer
= 我记录一系列要执行的图形命令
Render Graph
= 我描述 Pass、Resource 和依赖关系Render Graph 最终仍然需要产生实际 GPU 工作。
因此 Render Graph 并不是“CommandBuffer 的另一个名字”,而是更高层的组织和调度模型。
28为什么理解 SRP 比记住 API 更重要?
Unity 的 API 会继续变化。
例如很多经典文章中的:
context.DrawRenderers(...)在 Unity 6 已经让位于 RendererList API。
URP 17 又进一步把自定义 ScriptableRenderPass 推向 Render Graph-first 的写法。12
未来一些:
Draw Submission
GPU-driven Rendering
Render Graph
Resource Management还可能继续演进。
但下面这些关系不会那么容易过时:
Camera
↓
Visibility
Visibility
↓
Selection
Selection
↓
Draw Work
Draw Work
↓
Resources / Render Targets
Passes
↓
Dependencies
Commands
↓
Graphics Backend
↓
GPU所以真正值得长期记住的是:
渲染 API 的名字会变,但 Render Pipeline 必须解决的问题不会凭空消失。
29总结:从 Camera 到 GPU
现在再回头看 SRP,一帧最核心的路径已经非常清楚。
1. Unity 进入 RenderPipeline.Render()
当前的 RenderPipelineAsset 创建 Runtime Pipeline 实例,Unity 在需要渲染时进入它的 Render()。
2. Pipeline 处理 Camera
为每台 Camera 建立 View、Projection 以及当前视图相关的渲染状态。
3. Culling
从 Camera 得到 ScriptableCullingParameters:
Camera
↓
Cull
↓
CullingResults将整个 Scene 收缩成后续渲染需要考虑的可见性数据。
6. CommandBuffer
记录:
Clear
Set Render Target
Draw RendererList
Draw Mesh
Dispatch Compute
...等具体 GPU 工作。
7. ScriptableRenderContext
作为 C# Pipeline 与 Unity 原生渲染后端之间的接口,收集和调度这些工作。
因此,SRP 最核心的心智模型可以压缩成:
Camera
↓
RenderPipeline.Render()
↓
Cull
↓
CullingResults
↓
Filter + Sort + Drawing Rules
↓
RendererList
↓
CommandBuffer
↓
ScriptableRenderContext
↓
Submit
↓
Graphics API
↓
GPUSRP 的本质不是“用 C# 实现渲染”,而是让 C# 获得组织 Render Loop 的权力:决定这一帧有哪些工作、它们以什么顺序发生、绘制哪些对象、使用哪些资源以及怎样把这些工作提交给 Unity 的底层图形系统。
理解这一层之后,URP 中为什么会出现:
ScriptableRenderer
Renderer Feature
ScriptableRenderPass
RecordRenderGraphHDRP 中为什么会出现:
Custom Pass
Injection Point以及现代 Unity 为什么越来越依赖:
Render Graph就不再是一堆彼此孤立的 API。
它们只是在继续回答三个不同层级的问题:
SRP
“整条 Render Pipeline 怎样运行?”
↓
自定义渲染
“怎样往现有 URP / HDRP 中插入自己的工作?”
↓
Render Graph
“越来越多的 Pass 和 Resource 应该怎样组织?”这也正好构成接下来两篇文章的方向。
注释
-
Unity Scripting API,ScriptableRenderContext.CreateRendererList、RendererListParams、CommandBuffer.DrawRendererList 与 ScriptableRenderContext.DrawRenderers,Unity 6.0。Unity 6 已将直接
DrawRenderers标记为 Obsolete,推荐通过 Renderer List API 描述和调度可见对象绘制。 ↩ ↩2 ↩3 ↩4 -
Unity Manual,Scriptable Render Pipeline fundamentals / 可编程渲染管线基础知识,Unity 6.0。用于说明 SRP 是允许 C# 调度和配置渲染命令的薄 API 层,以及 URP、HDRP 与 Custom SRP 都建立在 SRP 之上。 ↩ ↩2 ↩3
-
Unity Scripting API,RenderPipeline.Render,Unity 6.0。当前文档将
Render()定义为自定义 Render Pipeline 的入口,并使用ScriptableRenderContext与 Camera 列表作为渲染上下文。 ↩ -
Unity Scripting API,GraphicsSettings.currentRenderPipeline,Unity 6.0。说明当前实际生效的 Render Pipeline Asset 如何由 Graphics Settings 和 Quality Settings 决定。 ↩
-
Unity Scripting API,ScriptableRenderContext.SetupCameraProperties,Unity 6.0。该调用负责调度 Camera 相关的 View、Projection、Clipping Plane 等全局 Shader 状态。 ↩
-
Unity Scripting API,ScriptableRenderContext.Submit,Unity 6.0。官方定义为将已经调度的命令提交给 Rendering Loop 执行。 ↩
-
Unity Scripting API,ScriptableCullingParameters、CullingOptions,Unity 6.0。说明 Culling 参数来自
Camera.TryGetCullingParameters(),并由ScriptableRenderContext.Cull()产生CullingResults;CullingOptions.OcclusionCull可以控制 Occlusion Culling。 ↩ ↩2 -
Unity Scripting API,FilteringSettings 与 RenderQueueRange,Unity 6.0。
FilteringSettings可根据 Render Queue、GameObject Layer、Rendering Layer 等从可见对象中继续筛选子集。 ↩ -
Unity Scripting API,SortingCriteria,Unity 6.0。官方提供
CommonOpaque与CommonTransparent两种常用组合,其中透明对象需要 Back-to-Front 排序。 ↩ -
Unity Scripting API,RenderPipeline.BeginContextRendering、BeginCameraRendering、EndCameraRendering 与 RenderPipelineManager,Unity 6.0。自定义 SRP 若希望触发这些 Rendering Event,需要在 Render Loop 的相应位置调用;旧 Frame Rendering 回调目前主要为兼容保留。 ↩
-
Unity Scripting API,CommandBuffer 与 ScriptableRenderContext.ExecuteCommandBuffer,Unity 6.0。官方说明
ExecuteCommandBuffer()会把命令注册到 Context 中,随后通过Submit()提交执行。 ↩ -
Unity Manual,Upgrade to URP 17 (Unity 6)、Render Graph Viewer 与 Render Pipeline feature comparison,Unity 6.0。URP 17 在 Unity 6 中默认使用 Render Graph,新自定义 Pass 推荐使用 Render Graph API;Compatibility Mode 仍可运行旧路径,但官方说明不再继续开发或改进非 Render Graph 路径。当前 URP 与 HDRP 均支持 Render Graph。 ↩ ↩2
-
Unity Manual,Scriptable Render Pipeline Core,Unity 6.0。SRP Core 提供可复用的平台抽象样板、渲染工具与 Shader Library,并由 URP、HDRP 共同使用。 ↩ ↩2
-
Unity Manual,Scriptable Render Pipeline Batcher,Unity 6.0。SRP Batcher 主要通过减少具有相同 Shader Variant 的绘制之间的状态切换和 CPU 设置成本来优化 Draw Submission;支持 URP、HDRP 与 Custom SRP。 ↩
CONTINUE EXPLORING
沿着这篇文章继续
Unity 自定义渲染:Renderer Feature、Custom Pass 与 Render Graph
从 Render Pass 的基本概念出发,理解 URP 的 Renderer Feature / ScriptableRenderPass、HDRP 的 Custom Pass,以及它们与 SRP、Render Graph 之间真正的层级关系。
Unity 的 AI 大转弯:从 Muse 订阅到 CLI 开放
几年前 Unity Muse 卖资产生成和聊天,贵且难用;如今 Unite 2026 推出 Unity CLI 与 Pipeline,第三方 Agent 免费接入。这不是突发善心,而是战略、管理层与开发者生态共同推动的结果。