Unity Shader 系列(十六):URP Shader 性能优化实战
Shader 性能优化是 Unity 游戏开发中最具影响力、也最容易被忽视的技术方向。一个写得好的 Shader 可以在同等视觉质量下比粗糙实现快 5-10 倍,这在移动端上往往是游戏能否流畅运行的关键。本文从 Unity 官方工具(Frame Debugger、Shader Profiler、RenderDoc)的实际使用方法出发,深入讲解每一种优化技术,并提供完整的优化版 SDF UI Shader 作为综合案例。
工具篇:先量化,再优化
优化的第一原则是:不要盲目猜测瓶颈。Unity 提供了完整的分析工具链。
Frame Debugger
Window → Analysis → Frame Debugger,可以逐 Draw Call 查看每一步的渲染结果:
- 检查 overdraw:半透明物体叠加过多层(透明粒子是最常见的 overdraw 来源)
- 查看 Shader 变体:每个 Draw Call 右侧显示使用的 Shader 和关键字,快速发现变体膨胀
- 验证 深度测试:确认 Depth Priming 是否生效(Early-Z 优化)
GPU Usage(Profiler)
Window → Analysis → Profiler,切换到 GPU 标签页:
- VS Time(顶点着色器时间)高:检查顶点数量、顶点着色器复杂度
- FS Time(片段着色器时间)高:检查 overdraw、片段着色器复杂度、纹理采样数
- Memory Bandwidth(内存带宽)高:检查纹理尺寸、Mipmap 设置
RenderDoc 集成
在 Unity 中安装 RenderDoc 插件后,可以 Capture 单帧并在 RenderDoc 中查看每个 Draw Call 的:
- 实际执行的 DXBC/SPIRV 指令数
- 各纹理单元的采样计数
- ALU(算术逻辑单元)和 TEX(纹理单元)的使用比例
核心优化技术
技术一:精度优化(half vs float)
在 HLSL 中,float 是 32 位,half 是 16 位。移动端 GPU 对 half 的计算速度是 float 的两倍。
精度选择原则:
1 | |
踩坑警告:不要对 worldPos 使用 half,在大场景中会导致顶点位置精度不足,出现顶点抖动(特别是 SV_POSITION 推导出的 worldPos)。
技术二:discard 与 clip() 的正确使用
discard(或等价的 clip())在 URP 中有一个常见的误解:它在 tile-based 的移动端 GPU 上会禁用 Early-Z 优化。
1 | |
技术三:四面体法线 vs 六样本中心差分
在 SDF 光线步进中,法线估计是最频繁调用的操作。四面体法(Tetrahedral Normal)只需 4 次 SDF 采样,比标准的六样本中心差分节省 33%:
1 | |
技术四:Overdraw 控制与 SDF UI Shader
SDF 在 UI 渲染(TextMeshPro 的原理)中有重要应用:通过 fwidth 实现无锯齿的边缘,无需 MSAA。同时通过边界盒预测剔除无效片段,减少 overdraw:
1 | |
完整示例:优化版 SDF UI Shader
这个 Shader 综合运用了所有优化技术,可以直接用于 Unity UI Canvas 的自定义 Shader。
1 | |
Shader 变体优化
过多的 Shader 变体是另一个常见的性能问题:每个 #pragma multi_compile 产生 2 的 N 次方个变体,大量变体导致:
- 冷启动加载慢(Shader 编译)
- 内存占用增加(变体缓存)
- Shader.WarmUp 时间长
1 | |
GPU Instancing 与 SDF 的结合
当场景中有大量使用相同 SDF 材质的对象时(比如一堆相同的魔法水晶),GPU Instancing 可以大幅减少 Draw Call:
1 | |
常见性能陷阱
| 陷阱 | 症状 | 解决方案 |
|---|---|---|
大量 discard |
移动端 fill rate 下降,帧率不稳定 | 用 Alpha Blending 替代,或优化 discard 位置 |
| 纹理采样过多 | GPU Memory Bandwidth 高,发热严重 | 合并纹理通道(将多张贴图打包到 RGBA) |
| 过深的 Shader 分支 | 编译后指令数暴增 | 用 lerp 替代 if,或用 #pragma 特性开关 |
| 精度不一致 | 移动端数值精度错误、黑屏 | 统一精度规范,关键计算用 float |
| ShadowCaster 未优化 | 阴影贴图渲染开销大 | ShadowCaster Pass 中移除所有不必要的计算 |
| 未使用 Depth Priming | overdraw 严重 | 在 URP Asset 中开启 Depth Priming Mode |
常见踩坑
坑1:[unroll] 与 [loop] 的编译器行为
HLSL 中 [unroll] 强制展开循环,增大着色器大小但减少分支开销;[loop] 保留循环,减小大小但增加分支开销。对于 SDF 光线步进(通常 64-128 步),不要 使用 [unroll],否则编译后的指令数量会爆炸式增长(可能超过 GPU 硬件限制)。
坑2:移动端 fwidth 不可用fwidth、ddx、ddy(屏幕空间偏导数)在某些移动端 GPU 或特定渲染模式(如 Forward+ 中的某些情况)下可能不可用或精度极低。如果目标平台是移动端,需要提供不依赖 fwidth 的降级路径。
坑3:CBUFFER 对齐规则
HLSL 的 CBUFFER 有严格的 16 字节对齐规则:如果一个 float 变量后跟一个 float3,可能因为跨 16 字节边界而产生意外的内存布局。始终使用 Unity 的 CBUFFER_START/CBUFFER_END 宏,并注意变量排列顺序(将 float4 放在前面,float 放在后面)。