Loading
S7ARRY_SPACE
0%
INITIALIZING
Album Cover
焚蝶
铁痕电台
0:00
Xiaohongshu
从零开始的SRP学习(一)
DOC_ID // 373fd5ONLINE

从零开始的SRP学习(一)

2026-6-2
UPDATED: 2026-6-10
学习笔记
4785 CHARS
#UNITY#SRP
S7ARRY
type
Post
status
Published
date
Jun 2, 2026
slug
srp01
summary
tags
Unity
SRP
category
学习笔记
icon
password

Custom Render Pipeline

这篇是 Catlike Coding Custom SRP 系列第一章 Custom Render Pipeline 的学习笔记。原教程本身已经写得很细,所以这里不打算逐句翻译,而是按教程的推进顺序,把这一章的内容概括、代码结构和几个需要注意的点整理出来。
这一章的目标很简单:让 Unity 不再使用默认渲染管线,而是交给我们自己写的 SRP 来画一帧画面。刚开始它什么都不会画,在写代码的过程中逐步支持天空盒、Unlit 物体、不透明和透明物体的顺序、Scene View、Gizmos、错误 Shader 显示,以及多个相机。

1. A New Render Pipeline:新建管线

渲染管线负责决定一帧画面里“画什么、用什么画、按什么顺序画、画到哪里”。真实项目里还会牵涉光照、阴影、透明、后处理、体积效果等一堆内容。Unity 内置管线以前把这些流程封装好了,我们只能在有限的节点上插手;SRP 则允许我们用 C# 重新组织这套流程。

Project Setup : 项目设置

创建一个普通 3D 项目,不要选 URP 或 HDRP 模板。

Color Space

Edit - Project Settings - Player 中 将颜色空间设置为Linear

Scene Setup

然后准备一些测试物体,最好混合使用不同 Shader 和不同 Render Queue:
  • 一些 Unlit/Color 材质,用来验证当前管线支持的对象能正常绘制。
  • 一些 Standard Shader 材质,用来观察“不支持的 Shader”会发生什么。
  • 一些透明材质,用来观察透明物体和天空盒之间的绘制顺序问题。
这样准备工作就做好了,方便后面调试。

Pipeline Asset:管线资源入口

第一段代码是 CustomRenderPipelineAsset。它继承自 RenderPipelineAsset,本质上是一个 Project Asset,可以在 Graphics Settings 里被指定为当前项目使用的渲染管线。
它的核心职责是重写 CreatePipeline
可以把 RenderPipelineAsset 理解为“配置和入口”,把 RenderPipeline 理解为“真正干活的实例”。现在这个 Asset 还没有任何参数,后面教程里会逐渐把光照、阴影、批处理等设置放进来。
当你把这个 Asset 指到 Graphics Settings 后,如果 CreatePipeline 暂时返回 null,Unity 就真的什么都不画了。Game View、Scene View、材质预览都会失效。这说明我们不是在默认管线上加一点功能,而是直接接管了整个渲染入口。

Render Pipeline Instance:管线实例

CustomRenderPipeline 继承自 RenderPipeline,需要重写 Render 方法。Unity 2022 里常用的是这个版本:
旧的 Camera[] 版本仍然要保留,因为基类里它还是抽象方法,但实际教程里使用 List<Camera> 版本,避免每帧为相机数组产生额外分配。
这里第一次遇到 ScriptableRenderContext。它可以理解为我们写的 C# 管线和 Unity 底层渲染系统之间的接口。SRP 里的很多操作并不是直接调用 GPU,而是通过 context 记录和提交给 Unity 执行。

2. Rendering:单相机绘制

进入 Rendering 部分后,教程开始把“每个相机怎么渲染”单独拆成 CameraRenderer。这一步很自然:RenderPipeline 负责遍历相机,CameraRenderer 负责渲染某一个相机。
这样的结构也和 URP 里的 Scriptable Renderer 有点像。后面如果不同相机需要不同渲染方式,比如一个主视角相机、一个小地图相机、一个特殊效果相机,就可以在这个层次上扩展。

Camera Renderer:保存当前上下文和相机

CameraRenderer 里先保存两个字段:
每次渲染一个相机,就把当前的 context 和 camera 放进来。后续的 Setup、Culling、Draw、Submit 都围绕这两个对象展开。
从这一章开始,单个相机的渲染流程大致会长成这样:
这套顺序后面还会继续扩展,但第一章已经把骨架搭出来了。

Drawing the Skybox:绘制天空盒

先尝试绘制天空盒:
但只写这一句时,画面不会立刻出现,因为 context 里的命令是延迟提交的。必须调用:
这也是 SRP 很重要的一个习惯:很多时候我们不是“调用一个 API 马上看到结果”,而是在往 context 里排命令,最后统一提交。
接着还要调用:
这一步会把相机的 view matrix、projection matrix 等状态设置进渲染上下文。没有它,天空盒虽然可能被画出来,但相机方向不会正确影响画面;之后画普通物体时也会缺少正确的 VP 矩阵。

Command Buffers:把一部分渲染命令放进缓冲

有些操作可以直接通过 context 调用,比如 DrawSkyboxDrawRenderers。但像清屏、Profiler sample 这些命令,需要通过 CommandBuffer 来记录。
创建一个可复用的 buffer:
之后每次执行 buffer 都走统一方法:
ExecuteCommandBuffer 会把 buffer 里的命令交给 context,但不会自动清空,所以复用 CommandBuffer 时一定要手动 Clear。这一点如果忘了,命令就会在后续帧里越积越多,调试起来会很混乱。
还需要用 BeginSampleEndSample 包住相机渲染过程。它们不改变画面,但会让 Profiler 和 Frame Debugger 里出现清晰的分组。对于 SRP 学习来说,Frame Debugger 几乎是必开的工具,它能直接告诉你这一帧到底执行了哪些 clear、draw call 和 sample。

Clearing the Render Target:清屏

画面最终会写入 Render Target,默认就是 framebuffer。问题是 Render Target 里之前可能已经有内容,如果不清掉,当前相机的结果可能和旧内容混在一起。
所以加入:
前两个参数分别表示是否清 depth 和 color。这里一开始两个都清。
这需要注意的点是:清屏最好放在 SetupCameraProperties 之后。否则 Unity 可能会用 Hidden/InternalClear shader 画一个全屏 quad 来清理;把相机属性设置好之后,Frame Debugger 里能看到更直接的 Clear (color+Z+stencil)。这个细节不影响理解 SRP 主流程,但能说明一件事:同样是“清屏”,放在不同状态下,底层执行方式可能不同。

Culling:剔除

现在能画天空盒了,但还没画场景物体。真正画物体之前,需要先剔除不可见对象。
流程是:
TryGetCullingParameters 根据相机生成剔除所需的参数。它可能失败,比如相机设置异常,所以把它封装成一个返回 bool 的 Cull 方法。
context.Cull(ref p) 会得到 CullingResults。后面 DrawRenderers 只会从这个结果里画东西。
这里可以把 Culling 理解为:相机不是面对整个场景直接画,而是先向 Unity 要一份“这个相机当前可能看见的 Renderer 列表”。视锥外的物体、被 Culling Mask 排除的 layer,都不会进入后续绘制。

Drawing Geometry:绘制Renderer

画普通物体用的是:
它至少需要三类信息。
第一类是 CullingResults,表示有哪些 Renderer 候选。
第二类是 DrawingSettings,表示用什么 Shader Pass、按什么规则排序。当前只支持 Unlit,所以指定:
这也解释了为什么 Standard Shader 一开始不会正常显示:它没有我们当前支持的这个 pass。
第三类是 FilteringSettings,表示进一步筛选哪些对象,比如只画 opaque,或者只画 transparent。
刚开始用 RenderQueueRange.all,这样不透明和透明都被一起画出来。随后通过 Frame Debugger 会发现问题:透明物体如果在 skybox 之前画,skybox 会把它覆盖掉,因为透明物体通常不写深度。

分开绘制 Opaque 和 Transparent

正确的基础顺序是:
不透明物体先画,并写入深度。天空盒在不透明之后画,可以利用深度测试跳过被物体挡住的部分。透明物体最后画,因为它需要和已经存在的颜色做混合。
对应代码思路是:
这段是本章很核心的一段。它把“画可见物体”拆成了两个 pass:不透明 pass 和透明 pass,中间插入天空盒。
透明物体用 CommonTransparent,通常会更偏向从远到近排序,这样混合结果更合理。不过原教程也提醒了,透明排序不是万能的。Unity 的排序通常是按对象而不是按每个三角形,交叉透明物体、大面积透明面片仍然可能出错。

3. Editor Rendering:管线编辑器支持

到这里,Game View 里已经能画一些东西了。但作为一条学习中的管线,只能在 Game View 里显示还不够。我们还需要让它在 Unity 编辑器里有更好的反馈。

Drawing Legacy Shaders:渲染不支持对象

当前管线只支持 SRPDefaultUnlit。如果场景里有 Standard Shader,它们没有被当前 DrawingSettings 接受,就会直接不显示。
从渲染角度看,这没问题;从开发角度看,这样是不方便的。因为很难判断一个物体是被剔除了、layer 不对、相机没看到,还是 Shader 不支持。
教程的做法是额外收集一些 Unity 旧管线常见 pass:
然后在正常绘制之后再画一遍这些“不被当前 SRP 支持”的对象。
一开始它们可能显示成黑色,因为我们没有给 Standard Shader 准备它期望的光照数据和内置变量。于是进一步使用 Unity 的错误材质。

Error Material:粉色错误材质

Unity 里经典的粉色材质来自:
把它设置到 DrawingSettings.overrideMaterial 后,所有不支持的对象会被统一画成错误颜色。

Partial Class:拆分编辑器代码

绘制错误 Shader、Gizmos、Scene View UI 这些都属于编辑器辅助功能,不应该混进最终 build 的运行时代码里。
教程用 partial classCameraRenderer 拆成两个文件:
  • CameraRenderer.cs:运行时核心逻辑。
  • CameraRenderer.Editor.cs:只在 Unity Editor 中存在的逻辑。
并配合:
以及 partial method:
如果某个 partial method 没有实现,编译器会把调用也去掉。这样运行时代码里可以保留调用点,而 build 时不会因为 Editor-only 方法不存在而失败。
渲染管线本身会有很多“只服务于编辑器调试”的逻辑,把它们分出去之后,主流程会干净很多。

Drawing Gizmos:Scene View 的Gizmos绘制

自定义 SRP 不会自动帮我们画 Gizmos。通过:
把 Gizmos 加回来。
这里分成 PreImageEffects 和 PostImageEffects,是因为有些 Gizmos 需要在后处理前画,有些需要在后处理后画。第一章还没有后处理,所以两个都调用即可。

Drawing Unity UI:Scene View 里的 UI 绘制

Screen Space - Overlay 的 UI 在 Game View 里不是由我们的 RP 直接绘制的,Frame Debugger 里也能看到它走的是单独流程。
但 Scene View 中的 UI 需要额外处理。用:
把 Scene View 需要的 UI 几何提交进去。这个操作要发生在 Culling 之前,因为它可能会向场景中添加需要被剔除和绘制的几何。
所以相机渲染流程里,PrepareForSceneWindow 要放在 Cull 前面。

4. Multiple Cameras:多相机

最后一部分处理多个相机。Unity 会把当前需要渲染的相机列表传给 RenderPipeline,管线按顺序渲染它们。相机的 Depth 值决定顺序,数值小的先画,数值大的后画。
复制 Main Camera,改名为 Secondary Camera,把 Depth 设为 0,并给它换一个 tag。这里 tag 只是标记,不影响渲染。真正影响渲染内容的是相机位置、Clear Flags、Culling Mask、Viewport Rect 等设置。

Two Cameras:同一场景会被渲染两次

有两个相机时,我们的循环会调用两次:
因此同一套 CameraRenderer.Render 流程会执行两遍。Frame Debugger 里可以看到两次 clear、cull 后的 draw、skybox 等操作。
刚开始画面看起来可能没变化,因为第二个相机渲染前又清了一次 Render Target。也就是说,第一个相机画完后,第二个相机把它的结果覆盖掉了。
这也是多相机最容易误解的地方:多一个相机不代表自动叠加,叠加还是覆盖,取决于后一个相机怎么清屏、画什么 layer、画到哪个 viewport。

Dealing with Changing Buffer Names:区分不同相机调试信息

一开始所有相机的 CommandBuffer 都叫 Render Camera。两个相机连续渲染时,Profiler 或 Frame Debugger 里可能会把相邻同名 sample 合并,看不清哪一段属于哪个相机。
教程在 Editor 下把 buffer name 改成相机名:
然后 BeginSampleEndSample 也统一使用 SampleName
这里有两个原因:
第一,Profiler / Frame Debugger 里能看到 Main CameraSecondary Camera,调试多个相机时更直观。
第二,buffer name 和 sample name 要一致,否则 Play Mode 下可能出现 BeginSample / EndSample 数量不匹配的警告。
不过 camera.name 在 Editor 里会产生 GC Alloc,所以教程只在 Editor 下这样做。Build 里仍然使用固定的 Render Camera。后面还用 Profiler.BeginSample("Editor Only") 把这部分编辑器分配标出来,避免误以为 runtime 也有这笔开销。

Layers:Culling Mask 决定相机能看到哪些 Layer

相机的 Culling Mask 会参与剔除。把一些 Standard Shader 物体放到 Ignore Raycast layer,然后让 Main Camera 排除这个 layer,再让 Secondary Camera 只看这个 layer。
这样两个相机看到的是不同集合:
  • Main Camera 画普通 Unlit 对象。
  • Secondary Camera 画不支持的 Standard Shader 对象。
因为 Secondary Camera 后渲染,如果它清掉了颜色,最终就只会看到 Secondary Camera 的结果;如果它只清深度,就能和 Main Camera 的结果组合起来。
这也解释了之前调试里容易混淆的一点:Tag 不控制渲染,Layer 和 Culling Mask 才控制相机能不能看到对象。

Clear Flags:决定多相机是覆盖还是叠加

Clear Flags 决定相机渲染前要清掉哪些内容。Unity 的相机里常见几种设置:
Clear Flags
大致效果
Skybox
清理并绘制天空盒
Solid Color
清成背景色
Depth Only
只清深度,保留之前相机的颜色结果
Nothing
颜色和深度都保留
教程把相机的 clearFlags 读出来:
然后用它决定 ClearRenderTarget 的参数。
多相机叠加时,第二个相机的 Clear Flags 很关键。如果 Secondary Camera 是 Skybox 或 Color,它会把 Main Camera 的颜色结果清掉,最后看起来像只有第二个相机生效。如果 Secondary Camera 是 Depth Only,它会保留之前的颜色,只清掉深度,然后再绘制自己看到的对象。
这也是这一节最实用的结论:想让第二个相机作为叠加层,通常先从 Depth Only 开始检查。
Nothing 则会连深度也保留。这样两个相机更像是在同一个深度缓冲里连续画,前一个相机写入的深度会继续影响后一个相机。它有时有用,但也更容易产生“为什么后画的东西被前一个相机挡住”的疑惑。
教程最后还提到 Viewport Rect。相机不一定要画满整张 Render Target,可以只画一块区域,比如小地图或分屏。这个时候清屏也只作用在对应 viewport 区域里。

总结

本章建立了一条渲染管线最基本的执行顺序。
先有 RenderPipelineAsset,让 Unity 能在项目设置里找到我们的管线。然后有 RenderPipeline,接收每帧的 ScriptableRenderContext 和相机列表。接着把单个相机的渲染拆到 CameraRenderer,在里面完成 Setup、Culling、Draw、Submit。
画物体时,先根据相机做 Culling,再通过 DrawingSettings 指定 Shader Pass,通过 FilteringSettings 指定 Render Queue,通过 SortingSettings 指定绘制顺序。透明和不透明要拆开,天空盒要插在中间,这样基础画面才符合预期。
编辑器部分:一条自定义管线不仅要能有正确的效果,还要能被调试。错误 Shader 要明显显示,Gizmos 要能画,Scene View 里的 UI 要能看见,Frame Debugger 和 Profiler 里的 sample 要清楚。
多相机部分:每个相机都会重新走一遍渲染流程,最终是覆盖还是组合,取决于 Depth、Culling Mask、Clear Flags 和 Viewport Rect。实际项目里做 UI 相机、小地图相机、角色展示相机时,这几个设置都会直接影响结果。
学完这一章后,代码还很简单,但 SRP 的基本工作方式和流程已经能看出来了。后面加入光照、阴影、Draw Call 优化和更复杂的材质支持,本质上都会继续挂在这套流程上。

参考

NAVIGATION // Related Articles
Loading...