Unity SRP/URP实战:从原理到自定义渲染管线

发布时间:2026/10/16 16:27:29
Unity SRP/URP实战:从原理到自定义渲染管线 从什么时候开始觉得内置管线不够用的对我来说是在某项目里反复调整半透明物体的深度写入折腾自定义后处理却只能依赖OnRenderImage还在用各种奇技淫巧的时候。那一刻特别想要一种“我能自己决定画面先画什么、后画什么、每一帧的RenderTarget怎么流转”的能力。后来真正打开可编程渲染管线SRP的大门才明白——Unity其实把整个渲染流程的调度权从引擎手里拿了出来交到了开发者手里。这篇笔记不是官方文档翻译而是我自己从零折腾 SRP / URP / HDRP 的过程中整理出来的知识骨架、实操记录和排查经验。无论你是刚准备从内置管线迁移到 URP还是已经在用 URP 但想搞明白背后的 RenderPipelineAsset 和 Renderer Feature 机制这篇内容应该都能帮你省掉不少弯路上的时间。它解决的核心问题很简单让你的每一帧渲染不再是被引擎隐藏掉的黑盒而是可由自己掌控的流水线。1. SRP到底是什么从一个绕不开的底层变化说起1.1 一句话理解SRP与内置管线的本质差异早期Unity版本包括很多老项目还在用的内置管线是“固定的”引擎定义好了一套渲染流程比如先渲染不透明物体、再做天空盒、再处理透明物体、最后叠加后处理特效。你可以在中间挂一些脚本、塞一些CommandBuffer但整体顺序和资源管理方式是不动的。这套架构的优势是简单、稳定缺点是定制成本高尤其是在你希望为某个效果做性能层的优化时你会发现可操作的空间很窄。SRP则完全不同。它把“每一帧怎么渲染”定义为一个可由你编写和控制的C#脚本逻辑。你创建自己的渲染管线实例告诉Unity这一帧我要先做视锥剔除再把所有不透明的物件画进一个RT然后做一次全屏Blit紧接着画天空盒……你甚至可以让管线完全走一条非传统的路径比如在移动端为了节省Overdraw自定义一套极简的渲染顺序。从“引擎决定”到“开发者决定”这就是SRP最核心的变化。1.2 为什么Unity敢把渲染交给你C与C#的分工逻辑很多人第一次接触SRP会问要是全用C#调度渲染性能不爆炸吗答案是不会。因为SRP并不是让你用C#去逐顶点算光照C#层做的只是“描述指令”——建立一个绘制命令列表真正执行这些指令的底层依然是高效率的C/图形API层。具体来说你在C#侧写的是类似“把第3个相机画面裁剪一次然后把可见物体逐个提交给GPU”的调度逻辑。Unity底层会把这个调度翻译成图形API调用。这就是为什么SRP在移动端也能跑得动的原因C#层降低的是灵活性成本而不是逐帧昂贵的同步开销。只要你不傻到在每帧循环里做内存分配和字符串拼接SRP的C#调度成本完全可以接受。1.3 SRP家族URP / HDRP / 自定义管线怎么选SRP是一个总称落到实际项目里Unity官方给的是两条现成的实现路径URPUniversal Render Pipeline和HDRPHigh Definition Render Pipeline外加你可以完全从零写一套自己的自定义管线。特性URPHDRP完全自定义定位跨平台通用、移动端友好高保真画质、主机/PC端全场景可控、学习成本极高光照能力支持单Pass Forward、简化阴影超大规模实时光照、体积光照取决于你的实现后处理封装了Volume框架高质量后处理栈需要自己写Blit适合项目中小型游戏、跨端产品AAA级视觉项目定制引擎风格的渲染实验学习门槛中低高极高我自己的建议很简单初学 SRP 一定先从 URP 入手。HDRP 功能丰富但参数爆炸Shader 也是另一套写法在你没搞懂渲染管线的整体逻辑之前直接上 HDRP 会一头雾水。自定义管线可以当作进阶练习但不要一上来就全自研。2. 理解运行流程才能看懂渲染逻辑2.1 一个最简单的渲染帧是怎么跑完的不管是URP也好还是你自己写迷你管线也好一帧渲染的流程其实可以概括为几个阶段。理解了这个流程你就获得了渲染排期的“上帝视角”。首先是相机裁剪。Unity通过CullingResults告诉你当前相机能看到哪些物体这一步相当于把你场景里的可见对象挑出来。然后是绘制命令的录制你通过ScriptableRenderContext去建立DrawRenderers调用指定要绘制的物体、使用的Shader Pass、和材质属性。紧接着是提交把录制好的命令交给图形API执行。最后是呈现Present把最终渲染结果输出到屏幕。这一套流程里最容易被忽视的是排序与状态变化。GPU是一台状态机频繁切换Shader、切换材质、切换RenderTarget都会产生额外开销。SRP的目的之一就是让你能够通过合理的批处理策略减少状态切换。2.2 SRP Batcher为什么你的Draw Call还是降不下来很多人在URP项目里遇到一个现象已经开启SRP Batcher了Draw Call还是高得吓人。理解这个问题的前提是知道SRP Batcher到底在优化什么。在传统批处理机制中材质属性的提交是“每个材质对象单独绑定一次Shader属性”。假如场景里有100个位置不同但材质相同的箱子传统方式会给100个物体分别设置坐标矩阵属性然后一一提交。SRP Batcher的工作原理则是在GPU端维护一个CPU上传的材质属性缓冲区CBUFFER当多个物体使用相同的管线/Shader变体时它们可以共享提交命令只需要在缓冲区里改变对应属性块的偏移量。要触发SRP Batcher你的Shader必须满足几个硬性条件所有材质属性必须声明在一个名叫 UnityPerMaterial 的CBUFFER里不能使用内置管线时代的那种逐对象非CBUFFER传参逻辑Shader要声明SRP Batcher兼容的关键词。我在实操中发现很多网上拿来的老旧Shader根本没过CBUFFER这关自然合批不了。2.3 RenderGraph新版本URP的性能钥匙如果你用的Unity版本较新会发现在URP中新增了RenderGraph模式。这个机制把整帧的资源读写关系整理成一张有向无环图DAG。打个比方传统渲染像是一间只有一个大桌子的厨房每个人都要占一块地方洗菜切菜而RenderGraph则会先规划好洗菜的水槽、切菜的案板在哪里谁需要就把食材送过去甚至能让不同步骤复用同一个水槽从而省掉大量临时RT的分配开销。但代价也很明显它对你的渲染代码有约束。比如自定义Renderer Feature里如果你还在老式地获取临时RT并手动释放RenderGraph模式下容易出问题。所以新项目可以直接用RenderGraph模式老项目升级还是要谨慎测试。3. 动手实践构建自己的可编程渲染管线3.1 工程准备与管线资产创建思路是一回事动手是另一回事。我们先把一个最小的管线跑起来。首先创建一个自定义管线资产脚本。这个脚本的作用是让Unity能够在Project窗口中创建一份Pipeline Asset资产文件。using UnityEngine; using UnityEngine.Rendering; [CreateAssetMenu(fileName BasicPipelineAsset, menuName Rendering/BasicPipeline)] public class BasicPipelineAsset : RenderPipelineAsset { protected override RenderPipeline CreatePipeline() { return new BasicPipelineInstance(); } }接着创建对应的管线实例类。真正的渲染逻辑都写在Render方法里。最简单的情况下我们甚至可以先什么都不画只把相机清理成一种颜色让管线能跑通。using UnityEngine; using UnityEngine.Rendering; public class BasicPipelineInstance : RenderPipeline { protected override void Render(ScriptableRenderContext context, Camera[] cameras) { foreach (var camera in cameras) { if (!camera.enabled) continue; // 开始一个上下文命令作用域 context.BeginSample(BasicPipeline); context.SetupCameraProperties(camera); // 清理RT的颜色为黑色 var cmd new CommandBuffer(); cmd.name Clear; cmd.ClearRenderTarget(true, true, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); // 提交并结束作用域 context.EndSample(BasicPipeline); context.Submit(); } } }这段代码没有画任何物体但它能让你明白SRP的最小执行单元是什么。在Project窗口里右键创建BasicPipeline资产然后把它拖到渲染设置里帧调试器里就能看到你的自定义管线被调用了。能跑通这一小步后面的扩展就有了基础。3.2 最简Unlit Shader在SRP里跑通第一帧管道空跑是没有意义的得画点东西出来。我们在SRP里写一个最简单的Unlit Shader只输出一个颜色不带光照。Shader Custom/BasicUnlit { Properties { _BaseColor (Base Color, Color) (1,1,1,1) } SubShader { Tags { RenderPipeline UniversalPipeline } Pass { Name ForwardUnlit Cull Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl CBUFFER_START(UnityPerMaterial) half4 _BaseColor; CBUFFER_END struct Attributes { float4 positionOS : POSITION; }; struct Varyings { float4 positionCS : SV_POSITION; }; Varyings vert (Attributes input) { Varyings output; VertexPositionInputs positionInputs GetVertexPositionInputs(input.positionOS.xyz); output.positionCS positionInputs.positionCS; return output; } half4 frag (Varyings input) : SV_Target { return _BaseColor; } ENDHLSL } } }这里有几个关键点需要注意。一是 Tags 里声明了 RenderPipeline UniversalPipeline这样URP才会正确识别这个Shader。二是属性使用了 CBUFFER_START(UnityPerMaterial) 包起来这是SRP Batcher合批的前提条件。三是从 Core.hlsl 里引用了 GetVertexPositionInputs 这个工具函数避免自己去手动拼接矩阵计算极大减少出错率。在URP里材质球切换为这个Shader然后放到场景中你就能看到单色物体渲染出来了。如果你用 Frame Debugger 抓一帧会看到绘制命令里出现了你的Shader名字这代表它已经成功进入SRP的渲染流程。3.3 写一个Renderer Feature给画面加描边如果说渲染管线定义了“全局流程”那么Renderer Feature则是URP为“局部特殊效果”留出来的扩展口。典型的用法是你希望在特定物体的渲染完成后对整屏幕做一个描边效果。要自定义一个Renderer Feature通常新建两个类一个Feature类描述何时添加Pass一个Pass类描述具体如何执行渲染。using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class OutlineFeature : ScriptableRendererFeature { [System.Serializable] public class OutlineSettings { public Material material; public RenderPassEvent renderPassEvent RenderPassEvent.AfterRenderingOpaques; } public OutlineSettings settings new OutlineSettings(); private OutlinePass outlinePass; public override void Create() { outlinePass new OutlinePass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.material ! null) { renderer.EnqueuePass(outlinePass); } } }public class OutlinePass : ScriptableRenderPass { private OutlineFeature.OutlineSettings settings; private RenderTargetIdentifier source; public OutlinePass(OutlineFeature.OutlineSettings settings) { this.settings settings; renderEvent settings.renderPassEvent; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { source renderingData.cameraData.renderer.cameraColorTarget; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd CommandBufferPool.Get(Outline); cmd.Blit(source, source, settings.material); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }重点解释一下 RenderPassEvent它决定了你的Pass插在URP流程中的哪一个位置。常见的有 BeforeRenderingOpaques、AfterRenderingOpaques、BeforeRenderingTransparents、AfterRenderingPostProcessing等。描边特效一般放在不透明物体画完之后这样既能看到轮廓又不会过早地被透明物体遮挡逻辑干扰。实际的描边Shader通常做法是用深度法线贴图检测场景边缘或者用后处理对全屏纹理做边缘检测。无论用哪种方案Renderer Feature都为你提供了注入点。3.4 Shader跨管线兼容CGPROGRAM与HLSLPROGRAM的选择在SRP时代一个重要决定是写Shader时到底用CGPROGRAM还是HLSLPROGRAM。内置管线的年代CGPROGRAM是主流。那时候很多代码可以在Windows和部分移动平台上正常工作。但CGPROGRAM在跨平台方面逐渐暴露问题部分语法在Mali/Adreno等移动GPU上解析异常GCC平台的边界行为不统一还很难和URP的ShaderLibrary无缝协作。SRP时代Unity官方已经推荐 HLSLPROGRAM。原因很实在URP/HDRP里提供的大量工具函数和着色库都是用HLSL写的你用CGPROGRAM等于把自己关在门外连 GetVertexPositionInputs 都引用不到。我个人的习惯是凡是新写的SRP Shader一律用HLSLPROGRAM。老Shader如果只在URP里用也可以慢慢替换不一定要一次全改但新写的一定不要再用CGPROGRAM。4. 真正重要的事URP项目性能调优实录4.1 光照与阴影参数配置很多项目从内置管线迁移到URP后发现画面变平、变暗第一反应是“URP画质不行”。实际上多半是光照设置没跟上。URP默认的主光源平行光阴影距离Shadow Distance往往偏小。我在一个开放场景中实测默认的20米阴影距离在稍大一点的场景里就会出现远处阴影突然消失的怪异现象。把Shadow Distance调到40-60米同时开启Shadow Cascades画面立体感会立刻提升。代价是阴影贴图内存占用增加这点在移动端尤其注意。移动端我推荐用两套级联Two Cascades而不是默认的四级联。三级/四级级联更适合PC和主机。同时在URP的Pipeline Asset里阴影分辨率建议控制在1024或2048不要盲目上4096。如果场景中物体密集就优先保证级联数而不是单张阴影图分辨率。4.2 后处理与RenderScale的取舍URP内置后处理是基于Volume框架的。新手最容易犯的错是没有正确创建Global Volume导致调整了Bloom却看不到任何效果。正确做法是新建一个Global Volume添加Volume Override然后把Bloom、Color Adjustments等效果加上。后处理有一个硬性性能成本全屏像素读写。尤其是Bloom它要做多级降采样和模糊在低端移动设备上可能吃掉2-3毫秒的GPU时间。我的优化经验是按平台配置RenderScale高端PCRenderScale1.0可以开Bloom甚至加一点Vignette。中端手机RenderScale0.9或0.85Bloom强度调低。低端手机RenderScale0.8Bloom直接关掉用Color Adjustments和Tonemapping调风格。RenderScale小于1时整个精度会下降UI如果也跟着被Scale就会变糊。URP里UI默认在主相机的Overlay下通常不受影响但具体项目还要验证。4.3 合批策略的实测对比我做过一个对比测试同一个场景2025个Cube对象完全不优化时Draw Call约2000。开启SRP Batcher后Draw Call降到200-300。再对相同网格的物体启用GPU InstancingDraw Call进一步降到几十。再把小物件标记为Static Batching最终整个场景Draw Call稳定在30左右。但要注意SRP Batcher和Static Batching是两种不同的机制。SRP Batcher针对材质属性提交的CPU开销Static Batching则是把多个静态物体合并成一个网格减少提交批次两者可以同时开。GPU Instancing则适合草、树木、小道具这些重复网格物体。三者并不互斥配合使用效果最好。而一旦场景中有几个物体用了非SRP Batcher兼容的Shader你会发现效果大打折扣——因为合批可能会被“踢”成多个批次。5. 实战问题排查我踩过的那些坑5.1 紫屏/Pink材质排查清单URP项目里最著名的一个问题是把内置管线的Shader赋给材质后场景变成一片紫粉色。这个颜色本质上是Unity找不到Shader可用后的“错误提示色”。我总结了一份排查清单症状检查项整个材质变紫Shader是否兼容URP在Inspector里搜索 Universal 关键词部分物体变紫ShaderPass的LightMode标签是否正确URP通常使用 UniversalForward 或 SRPDefaultUnlitBuild后变紫Build Settings里是否包含URP相关依赖Project Settings里的Graphics配置管线资产是否被替换只有后处理变紫Volume组件里的Shader是否引用了URP后处理库检查Package是否安装最底层的逻辑是URP不会自动使用内置管线的Shader你要么把材质替换为URP Shader要么确保自定义Shader声明了 UniversalPipeline 标签。5.2 SRP Batcher不生效的排查方向如果你开了SRP Batcher但Frame Debugger里显示批次还是被打断可以从这几个方向排查。第一确认Shader是否符合SRP Batcher要求。打开Shader的编译后变体查看它是否有 _Color、_BaseMap 等属性在 UnityPerMaterial 块里。第二步打开Frame Debugger找到当前绘制物体的GK Command查看右边Compressed State里的SRP Batch可以会标注“SRP Batch为否因为Shader属性不兼容”类似的原因。还有一类常见问题是在CommandBuffer里直接改了全部材质的Float属性。这种做法会让每一个材质属性都变成“非持久化”SRP Batcher计算时会判定属性无法复用而拆批。正确做法是尽量使用材质属性块MaterialPropertyBlock来修改个别实例属性并保持在CBUFFER里。5.3 ScriptableRenderContext不要把Execute当Submit自定义管线的早期我犯过一个错以为 ExecuteCommandBuffer 调用后渲染就立刻执行了。实际上 ScriptableRenderContext 是一套延迟执行的APIExecuteCommandBuffer 只是把命令“挂到队列”里真正的GPU提交发生在 Submit() 或者其他同步调用时。这个差异的后果是如果你在 ExecuteCommandBuffer 之后立即读取RenderTexture的内容比如用ReadPixels可能读到的是上一帧的旧数据甚至报错。要保证命令真正执行后再读取需要插入GPU同步逻辑。排查思路是一切和GPU结果相关的操作都要考虑上下文提交时机。5.4 Queue与RenderQueue的错位透明物体为何顺序错乱SRP允许你自己定渲染顺序但默认给两种主要的RenderQueue不透明物体Opaque和透明物体Transparent。当你自定义渲染Pass时如果Pass设置的RenderQueueTag和Shader里的Queue不一致就可能出现先画了透明物体再画不透明物体的错乱画面。我记得有一次写一个半透明描边效果设置的是 AfterRenderingOpaques但Shader的Queue写成了Transparent结果描边总是在该出现的位置时隐时现。检查后发现是PassEvent和Queue描述不一致导致的。经验是写自定义Pass前先想清楚这个Pass里的对象属于“哪一类型”的渲染队列再决定RenderPassEvent。6. 接下来你可以怎么学几条经验建议如果你是刚开始接触SRP我建议的学习路径是这样的。第一步不要急着写代码先把URP的Pipeline Asset创建出来用鼠标点一遍里面的参数理解每个选项大概影响什么。第二步写一个最简Unlit Shader沿用上面的模板把颜色改成从脚本传入的材质属性观察SRP Batcher开关带来的性能差异。第三步做一个Renderer Feature哪怕只是做一个全屏颜色偏移效果也能帮你理解URP的渲染事件机制。第四步再看HDRP的文档这时候你会发现很多东西是相通的。调试工具方面Frame Debugger必须学会看它能逐条列出URP执行了哪些绘制命令。遇到GPU端性能问题时用RenderDoc抓帧分析会比凭感觉猜高效得多。CPU侧的Profiler也可以关注 ScriptableRenderContext 相关的耗时如果发现Submit占据了高峰说明一帧内的命令提交组织还有优化空间。最后说一点个人感受。SRP的上手曲线比内置管线陡但一旦你理解了“渲染是一帧一帧调度出来的”这个模型很多以前靠对齐参数都解决不了的问题会变成逻辑推理的结果。比如半透明排序不对你会想到是Queue设置问题而不是去盲调Shader代码。再比如某个效果在移动端性能爆炸你会自然地思考能否把绘制提前到更省带宽的环节。这种掌控感才是SRP真正诱人的地方。如果你正在项目里尝试SRP遇到具体报错或异常表现欢迎在下面留言。我踩过的坑大概率你也绕不开。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询