01
渲染管线总览:Built-in vs URP/HDRP
Unity 渲染管线历史上经历了两个重要时代。早期的Built-in Render Pipeline(内置管线,也称 Legacy Pipeline)是随 Unity 3/4/5 时代共同成长的"老一代"渲染架构,功能齐全但定制空间有限。2018 年起,Unity 引入了基于Scriptable Render Pipeline(SRP)的全新框架,官方同时推出了面向移动/主机的轻量化方案 URP(Universal RP)和面向高端机器的 HDRP(High Definition RP)。
| 维度 | Built-in Pipeline | URP | HDRP |
|---|---|---|---|
| 正式名称 | Legacy / Built-in RP | Universal Render Pipeline | High Definition RP |
| 主要定位 | 全平台通用(老项目) | 移动、主机、PC 中低端 | PC、主机高端写实 |
| Shader 语言 | CG / HLSL(Surface Shader) | HLSL(ShaderLab + URP库) | HLSL(ShaderLab + HDRP库) |
| 默认光照模型 | Forward / Deferred 可选 | Forward(URP 12+ 支持 Deferred) | Deferred 为主 |
| SRP Batcher | 不支持 | 原生支持 | 原生支持 |
| 多 Pass Shader | 完整支持 | 受限(见第5章) | 受限 |
| 后处理系统 | Post Processing Stack v2(独立包) | 内置 Volume 框架 | 内置 Volume 框架 |
| 自定义渲染特性 | Camera Render / CommandBuffer | ScriptableRendererFeature | Custom Pass(更细粒度) |
💡
关键认知
URP 和 HDRP 本质上是 SRP 框架的两种"官方实现"。SRP 框架允许开发者完全自定义 GPU 命令提交顺序,而 Built-in 管线的渲染逻辑深埋在 C++ 引擎层,可定制程度低。
02
架构差异:Forward、Deferred 与 SRP Batcher
2.1 Forward Rendering(前向渲染)
前向渲染是最直白的渲染路径:每个物体对每盏灯光都执行一次完整的顶点+片元着色,结果直接写入帧缓冲。
复杂度是O(物体数 × 灯光数),灯光多时性能急剧下降。Built-in 的 Forward 路径通过ForwardAddPass 为每盏附加光再走一遍 Shader,URP 则将所有实时灯数据打包进常量缓冲(CBUFFER),使用UniversalForward单 Pass 一次性计算。
2.2 Deferred Rendering(延迟渲染)
延迟渲染先把几何信息写入多个G-Buffer(几何缓冲区),再在屏幕空间一次性完成所有光照计算,彻底解耦了"物体数"与"灯光数"的性能耦合。
复杂度降为O(物体数 + 灯光数),但无法支持 MSAA 且半透明物体仍需前向渲染。URP 12+ 开始支持 Deferred 路径,HDRP 默认使用 Deferred。
2.3 SRP Batcher:渲染批次的革命
Built-in 管线的一大性能瓶颈在于SetPass Call——每次切换材质,CPU 都要重新上传 Shader 参数,产生昂贵的 GPU 状态切换。SRP Batcher 通过以下机制彻底改变了这一现状:
⚡
SRP Batcher 核心思想
SRP Batcher 不减少 Draw Call 数量,而是将所有使用同一 Shader Variant 的物体的材质属性(Per-Material 数据)和物体变换矩阵(Per-Object 数据)各自打包进专用的持久化 GPU 缓冲区(CBUFFER)。一旦数据已在 GPU 上,后续帧无需重复上传,大幅降低 CPU 开销。
Shader "Custom/SRPBatcherDemo" { Properties { _BaseColor ("Base Color", Color) = (1,1,1,1) _BaseMap ("Base Map", 2D) = "white" {} } SubShader { HLSLPROGRAM // ✅ SRP Batcher 要求:所有 per-material 属性必须在统一 CBUFFER 中声明 CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float4 _BaseMap_ST; // 纹理 ST 也必须在此 CBUFFER 内 CBUFFER_END // ❌ 错误示例:在 CBUFFER 外声明属性会破坏 SRP Batcher 兼容性 // float4 _SomeValue; ← 这行会让 SRP Batcher 标红 ENDHLSL } }| 特性 | Built-in Dynamic Batching | GPU Instancing | SRP Batcher |
|---|---|---|---|
| 减少内容 | Draw Call | Draw Call | SetPass Call(CPU 开销) |
| 要求 | 顶点数 < 900,相同材质 | 相同 Mesh + 材质 | 相同 Shader Variant,CBUFFER 声明规范 |
| 多 Pass 兼容 | 受限 | 受限 | 第一个 Pass 才兼容 |
| 动态合批 | 是 | 否 | 是(Shader 层面) |
03
SubShader 与多 Pass 执行顺序
3.1 SubShader 选择机制
当一个物体需要渲染时,Unity 依次遍历 Shader 中所有SubShader块,选中第一个满足当前平台硬件能力(Tags + LOD)的 SubShader 执行。
Shader "Custom/PipelineSelect" { // SubShader 0:高端平台(支持几何着色器 / SM5.0) SubShader { Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" } LOD 300 // URP Pass... } // SubShader 1:Built-in 管线,前向渲染 SubShader { Tags { "RenderType"="Opaque" } LOD 200 // Built-in Pass... } // Fallback:最低要求,任何平台均可执行 Fallback "Diffuse" } // Unity 从 SubShader 0 开始,选中第一个满足平台能力和 LOD 要求的3.2 多 Pass 在 Built-in 中的执行顺序
在 Built-in 管线中,一个 SubShader 内的多个 Pass 按从上到下、顺序执行的原则运行。每个 Pass 会独立完成一次完整的绘制,并可通过ZTest、Blend、Stencil等状态控制写入目标。
Built-in 多 Pass 执行时序(单物体)
Pass #0
Vertex→Fragment→Write Depth + Color
Pass #1
Vertex→Fragment→Blend over Pass#0
Pass #2 …
依序执行,各自独立的渲染状态
⚠️
注意:顺序影响结果
Pass 的执行顺序直接影响Blend、Stencil的累积结果。例如,先写深度再做描边是常见模式,顺序颠倒会导致描边被遮挡。
3.3 Pass 的 LightMode 如何影响执行时机
除了"物体渲染时顺序执行"的逻辑,每个 Pass 还通过Tags { "LightMode" = "..." }声明自己属于哪个渲染阶段。渲染管线会在特定阶段主动查找并调用对应 LightMode 的 Pass,而不是简单"从头跑到尾"。
SubShader { // Pass A:主光照 —— 前向渲染阶段被调用 Pass { Tags { "LightMode" = "UniversalForward" } // HLSL 光照计算... } // Pass B:阴影投射 —— Shadow Map 阶段被调用(与 A 独立) Pass { Tags { "LightMode" = "ShadowCaster" } // 输出深度... } // Pass C:深度预写 —— Depth Prepass 阶段被调用 Pass { Tags { "LightMode" = "DepthOnly" } // 仅输出深度,不写颜色... } } // 三个 Pass 分属不同渲染阶段,由管线按需调用,互不干扰04
LightMode Tag 完全手册
LightModeTag 是管线与 Shader Pass 之间的约定协议: 管线在每个渲染阶段扫描场景中所有材质的 Pass,只执行匹配当前阶段 LightMode 的那个 Pass。
4.1 Built-in 管线常用 LightMode
"ForwardBase"
前向基础 Pass
处理场景中的主方向光(Directional Light)、环境光(Ambient)和所有 Baked 阴影。每个物体只执行一次。
Built-in 专用
"ForwardAdd"
前向附加 Pass
每盏实时点光、聚光灯都触发一次此 Pass,叠加到 ForwardBase 结果上。灯越多,Draw Call 越高。
Built-in 专用
"ShadowCaster"
投影 Pass
将物体深度信息写入 Shadow Map。Built-in 和 URP/HDRP 通用,可在此 Pass 中实现 Alpha 裁剪阴影。
通用
"Deferred"
延迟渲染 Pass
将 Albedo / Normal / 金属度 / 粗糙度等信息写入 G-Buffer,供后续 Lighting Pass 读取。
Built-in Deferred
"PrepassBase"
旧式延迟(遗留)
旧版 Legacy Deferred(已废弃)的法线/深度预处理 Pass,仅在 Legacy Deferred 路径中使用。
已废弃
"Always"
始终执行
不受渲染路径影响,无论何种管线都会执行。常用于特效、调试或需要无条件写入某 Buffer 的场景。
通用
4.2 URP 专属 LightMode Tag
"UniversalForward"
URP 主光照 Pass
URP 前向渲染阶段的核心 Pass,处理所有实时灯光(主平行光 + 多盏点光/聚光灯打包在一起)。每个物体仅执行第一个带此 Tag 的 Pass。
URP 专用
"UniversalForwardOnly"
仅前向执行
URP 12+ 引入,在 Deferred 路径下依然强制走前向渲染。适合不支持 G-Buffer 写入的特殊材质(如半透明)。
URP 12+
"ShadowCaster"
阴影投射 Pass
同 Built-in,写入 Shadow Map。在 URP 中可复用,也支持 _ALPHATEST_ON 关键字驱动的 Alpha Cutout 阴影。
通用
"DepthOnly"
深度预写 Pass
URP Depth Prepass 阶段使用,仅写入深度缓冲,不写颜色。为后续 SSAO、Depth of Field 等后处理提供 _CameraDepthTexture。
URP 专用
"DepthNormalsOnly"
深度+法线预写
URP 14+ 新增,同时写入深度和视空间法线到 _CameraDepthNormalsTexture,供 SSAO 等使用,比 DepthOnly 携带更多信息。
URP 14+
"Universal2D"
2D 渲染 Pass
配合 2D Renderer 使用,URP 2D 渲染器在 Sprite 渲染阶段调用此 Pass。3D 项目几乎用不到。
URP 2D
SubShader { Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry" } // ── Pass 1:主渲染 ────────────────────────────── Pass { Name "URPForward" Tags { "LightMode" = "UniversalForward" } // ✅ URP 主光照计算 } // ── Pass 2:阴影投射 ──────────────────────────── Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 // 不写颜色 // 写深度到 Shadow Map } // ── Pass 3:深度预写 ───────────────────────────── Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ZWrite On ColorMask 0 // 仅写 _CameraDepthTexture,后处理 SSAO/DOF 需要 } }✅
LightMode 匹配规则
当 Pass 没有声明 LightMode Tag 时,默认值为"Always"。在 URP 中,没有 LightMode 或 LightMode 不被识别的 Pass 会被直接忽略,不会执行。
05
URP 多 Pass 限制与解决方案
5.1 核心限制:URP 默认只执行第一个 UniversalForward Pass
这是 URP 与 Built-in 管线最容易踩坑的差异之一。在 Built-in 管线中,一个 SubShader 中的多个 Pass 会依序全部执行;而 URP 的渲染器(ForwardRenderer / UniversalRenderer)在处理不透明物体时,只会为每个物体调用第一个标记为"UniversalForward"的 Pass。
SubShader { Tags { "RenderPipeline" = "UniversalPipeline" } // ✅ Pass #0 —— URP 会执行这个 Pass { Tags { "LightMode" = "UniversalForward" } // 主渲染逻辑... } // ❌ Pass #1 —— URP 会直接跳过!同类型第二个 Pass 不执行 Pass { Tags { "LightMode" = "UniversalForward" } // 描边逻辑:在 URP 中永远不会运行 Cull Front // ... } } // 解决方案:使用 ScriptableRendererFeature 替代第二个 UniversalForward Pass🚨
为什么 URP 要这样设计?
URP 引入SRP Batcher时,要求每个物体在一次 Draw Call 中处理完所有光照,天然与"多 Pass 分多次绘制"相矛盾。 同时,URP 的ScriptableRenderPass系统将"附加效果"的职责从 Shader Pass 转移到了ScriptableRendererFeature, 让多 Pass 效果以 Render Feature 的形式由 CPU 侧调度,而非硬塞在同一个 Shader 里。
5.2 各 Pass 类型在 URP 中的执行情况
| Pass LightMode | Built-in 中 | URP 中 | 备注 |
|---|---|---|---|
UniversalForward | 不识别 | 执行(仅第一个) | 多个只执行 index=0 |
ShadowCaster | 执行 | 执行 | 阴影阶段独立调用 |
DepthOnly | 不识别 | 执行 | Depth Prepass 阶段 |
DepthNormalsOnly | 不识别 | 执行(URP 14+) | 提供法线信息给 SSAO |
ForwardBase | 执行 | 忽略 | URP 不认 Built-in Tag |
ForwardAdd | 执行 | 忽略 | URP 不使用逐灯 Pass |
| 无 Tag / Always | 执行 | 忽略 | URP 严格按 LightMode 匹配 |
5.3 解决方案:如何在 URP 中实现"多 Pass 效果"
方案一:使用 ScriptableRendererFeature(推荐)
将附加效果(描边、X光、毛发 shell layer)拆出为独立的ScriptableRenderPass, 以ScriptableRendererFeature注入到 URP Renderer 的特定阶段(BeforeRenderingOpaques、AfterRenderingOpaques 等)。 这是 URP 官方推荐的扩展方式,与 SRP Batcher 完全兼容。
using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; // 1. 继承 ScriptableRendererFeature,在 URP Asset 的 Renderer 上挂载 public class OutlineFeature : ScriptableRendererFeature { private OutlineRenderPass _pass; public override void Create() { _pass = new OutlineRenderPass(); // 指定此 Pass 注入到哪个渲染阶段 _pass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(_pass); } } // 2. 继承 ScriptableRenderPass,在 Execute 中用 CommandBuffer 绘制描边 public class OutlineRenderPass : ScriptableRenderPass { public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get("Outline Pass"); // 在此使用 cmd.DrawRenderer / cmd.DrawMesh 绘制描边 context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }方案二:Stencil Buffer 分帧实现描边等效果
利用 Stencil 测试:第一个UniversalForwardPass 写入 Stencil 值,然后通过 Renderer Feature 对全屏 Stencil 区域做后处理,实现类似多 Pass 的视觉效果。
方案三:使用 UsePass 引用其他 Shader 的 Pass
UsePass指令可以直接引用另一个 Shader 中特定名称的 Pass,避免代码重复。注意 Pass 名称需要全大写。
SubShader { Tags { "RenderPipeline" = "UniversalPipeline" } Pass { Name "MY_FORWARD" // Pass 名称必须全大写才能被 UsePass 引用 Tags { "LightMode" = "UniversalForward" } // ... } // 直接复用另一个 Shader 中名为 "SHADOWCASTER" 的 Pass UsePass "Universal Render Pipeline/Lit/SHADOWCASTER" UsePass "Universal Render Pipeline/Lit/DEPTHONLY" }方案四:材质堆叠(Multi-Material Renderer)
在MeshRenderer组件上挂载多个材质(Materials 数组),Unity 会为每个 Sub-Mesh 或使用第一个 Sub-Mesh 的情况下为每个材质分别执行 Draw Call,等效于多 Pass 效果,且完全兼容 SRP Batcher。
06
实战代码:URP 多 Pass Shader 模板
下面是一个在 URP 中正确实现"不透明 PBR + 深度预写 + 阴影投射"三 Pass 组合的完整 Shader 模板, 同时满足 SRP Batcher 兼容条件(所有 per-material 属性在CBUFFER_START(UnityPerMaterial)中声明)。
Shader "Custom/URPMultiPassTemplate" { Properties { _BaseMap ("Base Map", 2D) = "white" {} _BaseColor ("Base Color", Color) = (1, 1, 1, 1) _Metallic ("Metallic", Range(0,1)) = 0.0 _Smoothness("Smoothness",Range(0,1)) = 0.5 _Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5 } SubShader { Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry" } // ════════════════════════════════════════════════ // PASS 1 —— UniversalForward(主光照) // ════════════════════════════════════════════════ Pass { Name "URPForward" Tags { "LightMode" = "UniversalForward" } ZWrite On ZTest LEqual Cull Back HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" // ✅ SRP Batcher:所有 per-material 属性必须在此 CBUFFER 内 CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float4 _BaseMap_ST; float _Metallic; float _Smoothness; float _Cutoff; CBUFFER_END TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float2 uv : TEXCOORD1; float3 positionWS : TEXCOORD2; }; Varyings vert(Attributes IN) { Varyings OUT; VertexPositionInputs posInputs = GetVertexPositionInputs(IN.positionOS.xyz); VertexNormalInputs nrmInputs = GetVertexNormalInputs(IN.normalOS); OUT.positionCS = posInputs.positionCS; OUT.positionWS = posInputs.positionWS; OUT.normalWS = nrmInputs.normalWS; OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap); return OUT; } half4 frag(Varyings IN) : SV_Target { half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv); half4 baseColor = texColor * _BaseColor; InputData inputData = (InputData)0; inputData.normalWS = normalize(IN.normalWS); inputData.positionWS = IN.positionWS; inputData.viewDirectionWS= GetWorldSpaceNormalizeViewDir(IN.positionWS); inputData.shadowCoord = TransformWorldToShadowCoord(IN.positionWS); SurfaceData surfaceData = (SurfaceData)0; surfaceData.albedo = baseColor.rgb; surfaceData.metallic = _Metallic; surfaceData.smoothness = _Smoothness; surfaceData.alpha = baseColor.a; surfaceData.normalTS = half3(0,0,1); return UniversalFragmentPBR(inputData, surfaceData); } ENDHLSL } // ════════════════════════════════════════════════ // PASS 2 —— ShadowCaster(阴影投射) // ════════════════════════════════════════════════ Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull Back HLSLPROGRAM #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl" ENDHLSL } // ════════════════════════════════════════════════ // PASS 3 —— DepthOnly(深度预写) // ════════════════════════════════════════════════ Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ZWrite On ColorMask 0 Cull Back HLSLPROGRAM #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl" ENDHLSL } } FallBack "Hidden/Universal Render Pipeline/FallbackError" }🔍
SRP Batcher 兼容性检查
在 Unity Editor 中选中 Shader 资产,Inspector 面板最下方会显示"SRP Batcher: compatible"或列出不兼容原因(通常是有属性未放入 UnityPerMaterial CBUFFER)。
07
总结与迁移建议
核心要点速查
| 知识点 | Built-in | URP |
|---|---|---|
| 多 Pass 执行 | 全部顺序执行 | 每类 LightMode 仅执行第一个 |
| 主光照 Pass Tag | ForwardBase | UniversalForward |
| 附加光 Pass | ForwardAdd(逐灯) | 打包进 UniversalForward(无 ForwardAdd) |
| SRP Batcher | 不支持 | 原生支持,需 CBUFFER 声明规范 |
| 描边等多 Pass 效果 | 第二个 Pass 直接写 | 改用 ScriptableRendererFeature |
| 无 LightMode Pass | 当作 Always 执行 | 被忽略,不执行 |
| 深度纹理生成 | Camera DepthTextureMode | DepthOnly Pass + URP Asset 开启 |
从 Built-in 迁移到 URP 的检查清单
📋
迁移 Shader 时需逐项确认
① 将所有ForwardBase→UniversalForward,删除ForwardAddPass
② 所有 per-material 属性放入CBUFFER_START(UnityPerMaterial) ... CBUFFER_END
③ 将UnityCG.cginc引用改为Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl
④ Surface Shader 无法在 URP 中使用,需改写为手写顶点/片元 Shader
⑤ 多 Pass 描边/外发光效果改用ScriptableRendererFeature实现
⑥ 检查GrabPass用法——URP 不支持,改用_CameraOpaqueTexture