URP 与 Built-in 渲染管线SubShader Pass 执行机制全解
2026/4/29 15:01:28 网站建设 项目流程

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 PipelineURPHDRP
正式名称Legacy / Built-in RPUniversal Render PipelineHigh 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 / CommandBufferScriptableRendererFeatureCustom 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 BatchingGPU InstancingSRP Batcher
减少内容Draw CallDraw CallSetPass 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 会独立完成一次完整的绘制,并可通过ZTestBlendStencil等状态控制写入目标。

Built-in 多 Pass 执行时序(单物体)

Pass #0

Vertex→Fragment→Write Depth + Color

Pass #1

Vertex→Fragment→Blend over Pass#0

Pass #2 …

依序执行,各自独立的渲染状态

⚠️

注意:顺序影响结果

Pass 的执行顺序直接影响BlendStencil的累积结果。例如,先写深度再做描边是常见模式,顺序颠倒会导致描边被遮挡。

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 LightModeBuilt-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-inURP
多 Pass 执行全部顺序执行每类 LightMode 仅执行第一个
主光照 Pass TagForwardBaseUniversalForward
附加光 PassForwardAdd(逐灯)打包进 UniversalForward(无 ForwardAdd)
SRP Batcher不支持原生支持,需 CBUFFER 声明规范
描边等多 Pass 效果第二个 Pass 直接写改用 ScriptableRendererFeature
无 LightMode Pass当作 Always 执行被忽略,不执行
深度纹理生成Camera DepthTextureModeDepthOnly Pass + URP Asset 开启

从 Built-in 迁移到 URP 的检查清单

📋

迁移 Shader 时需逐项确认

① 将所有ForwardBaseUniversalForward,删除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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询