一、先看一个常见的困惑
打开 Unity Profiler,你会看到两个指标:
Batches: 200 SetPass Calls: 35很多人只关注Draw Call / Batches,但其实SetPass Calls 才是性能杀手。
为什么?因为一次 SetPass 的成本,通常是一次 Draw Call 的几倍到几十倍。
二、什么是 SetPass Call?
定义
SetPass Call=切换渲染状态的调用次数。
当 GPU 要用一个新的"渲染配置"绘制物体时,CPU 必须先告诉 GPU:
- “换 Shader!”
- “换纹理!”
- “换混合模式!”
- “换深度状态!”
- …
这一整套"切换配置"的操作,就是一次 SetPass Call。
用做菜类比 🍳
回到之前 OpenGL 状态机的类比:
SetPass Call = 换菜谱、换锅、换调料的过程 Draw Call = "开火炒菜!"的那一下- 换一次菜谱(SetPass):耗时 10 分钟(切菜、洗锅、准备调料)
- 炒一盘菜(Draw Call):耗时 1 分钟
- 炒 10 盘同样的菜:10 分钟准备 + 10 分钟炒 = 20 分钟
- 炒 10 盘不同菜:10 × (10 分钟准备 + 1 分钟炒) = 110 分钟 😱
SetPass 是"准备成本",Draw 是"执行成本"。准备成本远大于执行成本。
三、SetPass 到底做了什么? 🔧
一次 SetPass 触发时,GPU 驱动层要做:
1. 上传新 Shader 到 GPU(如果没上传过) 2. 绑定 Shader 程序 3. 绑定纹理(可能多张) 4. 上传 Uniform 参数(矩阵、颜色、光照参数...) 5. 设置渲染状态(Blend、ZTest、Cull...) 6. 校验状态合法性 7. 提交 Command Buffer 到 GPU这些操作大部分在 CPU 侧完成,是驱动层的开销。所以 SetPass 是典型的CPU 瓶颈,GPU 反而闲着。
四、SetPass Calls vs Batches vs Draw Calls 🔍
三个概念常被混淆,一次讲清:
Draw Call
GPU 层面:CPU 调用glDrawElements/DrawIndexedPrimitive的次数。
Batches(批次)
Unity 层面:Unity 打包提交的批次数。
- 1 个 Batch = 1 个或多个物体合并后的 1 次绘制
- 合批优化(动态合批、静态合批、GPU Instancing)会减少 Batches
SetPass Calls
渲染状态切换次数:每次切换 Shader / 材质 / 纹理组合都算一次。
关系
1 SetPass = 1 次状态切换 + N 个 Draw Call 例如: 用同一个材质画 10 个物体: 1 SetPass + 10 Draw Calls(如果没合批) 1 SetPass + 1 Draw Call (如果合批成功) 用 10 个不同材质各画 1 个物体: 10 SetPass + 10 Draw Calls直观对比表
| 场景 | SetPass | Batches | Draw Call | 性能 |
|---|---|---|---|---|
| 100 个相同材质,无合批 | 1 | 100 | 100 | 中等 |
| 100 个相同材质,合批 | 1 | 1 | 1 | 极好 |
| 100 个不同材质 | 100 | 100 | 100 | 极差 |
| 100 个物体,50 种材质 | 50 | 100 | 100 | 差 |
五、为什么 SetPass 这么昂贵? 💸
1. CPU-GPU 通信成本
每次 SetPass 都涉及 CPU 到 GPU 的命令提交,需要:
- Command Buffer 打包
- 驱动层校验
- 可能的 GPU 停顿(等前一批命令完成才能切状态)
2. GPU 状态重建
现代 GPU 依赖Pipeline State,状态切换往往触发:
- 缓存刷新
- 内部状态机重置
- 有时甚至需要重编译 Shader 变体
3. 破坏并行性
GPU 喜欢连续执行相同任务:
好: 1000 次相同状态的绘制 → GPU 流水线满负荷 坏: 1000 次不同状态的切换 → GPU 走走停停六、如何看 SetPass Calls?
Unity Statistics 窗口
Game 视图 → 右上角 Stats会显示:
Batches: 200 Saved by batching: 150 SetPass calls: 35Unity Profiler
Window → Analysis → Profiler → Rendering看SetPass Calls曲线,识别峰值场景。
Frame Debugger
Window → Analysis → Frame Debugger逐帧查看每一步 SetPass 触发的原因:
- 左侧列表中,每个"SetPass ***"就是一次状态切换
- 点击可看到具体切换了什么(纹理不同?Shader 不同?)
七、什么情况会触发新的 SetPass? 🔥
触发 SetPass 的因素(每换一个都可能新增 SetPass)
- 不同的 Shader(最常见)
- 不同的材质(哪怕 Shader 相同,材质不同也算)
- 不同的纹理(材质相同但纹理不同)
- 不同的 Uniform 值(颜色、参数不同)
- 不同的 Render State(Blend / ZTest / Cull 等)
- 不同的 Keyword(Shader 变体不同)
- Lightmap / Light Probe影响
- 多 Pass Shader(每个 Pass 一次)
举例:同一个 Shader 也会多次 SetPass
// 材质 A:红色 + 纹理 1// 材质 B:蓝色 + 纹理 1// 材质 C:红色 + 纹理 2// 即使 Shader 都相同,3 个材质 = 3 次 SetPass除非用GPU Instancing / MaterialPropertyBlock,才可能减少 SetPass。
八、优化策略 🎯
策略 1:合并材质
核心原则:能用同一个材质,就别新建。
// ❌ 每个物体一个材质(Instance 化)foreach(varobjinobjects){obj.material.color=Color.red;// 触发材质克隆!}// ✅ 用 MaterialPropertyBlockMaterialPropertyBlockmpb=newMaterialPropertyBlock();foreach(varobjinobjects){mpb.SetColor("_Color",Color.red);obj.GetComponent<Renderer>().SetPropertyBlock(mpb);}// MaterialPropertyBlock 不会破坏合批和 SetPass 复用⚠️陷阱:访问renderer.material(不是sharedMaterial)会自动创建材质副本,导致 SetPass 翻倍!
策略 2:纹理合并(图集/Atlas)
多个物体用同一张大图里的不同区域,让它们共享材质:
❌ 100 个 UI 元素,每个用独立小图 → 100 SetPass ✅ 100 个 UI 元素,共用 1 个图集 → 1 SetPass策略 3:GPU Instancing
同一 Mesh + 同一 Material,渲染大量副本:
#pragma multi_compile_instancing// 场景中 1000 棵树,启用 Instancing// 结果:1 SetPass + 1 Draw Call(而不是 1000 SetPass)策略 4:控制 Shader 变体
Shader Keyword 会产生变体,不同变体 = 不同 Shader = 不同 SetPass:
物体 A 用 _NORMALMAP 版本 物体 B 用 _NORMALMAP + _EMISSION 版本 → 2 SetPass优化:
- 减少
multi_compile组合 - 用
shader_feature(只打包用到的) - 统一场景的 Shader 特性开关
策略 5:按材质排序渲染
Unity 内部会尽量按材质排序,但你也可以手动优化:
// ✅ 让同材质的物体连着渲染// (对透明物体不适用,透明必须按深度排)策略 6:减少多 Pass
一个 Shader 有 2 个 Pass → 每个物体触发 2 次 SetPass。
❌ 手写多 Pass 实现描边(边框 Pass + 主 Pass) ✅ 单 Pass 中用 fwidth 或后处理实现描边策略 7:UI 优化
- Sprite Atlas 合并
- 避免"字体图集"和"图片图集"交叉
- 静态部分合并到一张背景图
- Canvas 拆分(静/动分离)
策略 8:光照优化
- 减少实时光(每盏实时光可能触发额外 Pass)
- 使用Lightmap烘焙
- 减少 Light Probe 复杂度
- 使用URP的 Single Pass Forward
九、经验值参考 📊
目标值(移动端):
| 项目类型 | SetPass Calls | Draw Calls |
|---|---|---|
| 休闲游戏 | < 30 | < 100 |
| 中型 3D | < 60 | < 300 |
| 大型 3D | < 100 | < 500 |
| 端游/主机 | < 300 | < 2000 |
触发警戒的数值:
- 移动端 SetPass > 100 → 需要优化
- 移动端 SetPass > 200 → 严重问题
十、常见误区 🚫
❌ 误区 1:“合批能减少 SetPass”
部分错误。合批减少的是Draw Call,不一定减少 SetPass:
- 同一材质的多个物体合批 → SetPass 已经是 1,合批只是减少 Draw Call
- 不同材质的物体 → 合不了批,SetPass 也降不下来
❌ 误区 2:“SetPass 少就一定快”
部分错误。SetPass 少代表 CPU 端好,但 GPU 端可能因为:
- 单个 Draw Call 里画的物体过多、太复杂
- Shader 太重
- Overdraw 严重
依然可能卡。要综合看 CPU/GPU 各自的瓶颈。
❌ 误区 3:“Draw Call 比 SetPass 重要”
通常反了。Draw Call 的成本近年来大幅降低(现代 API 如 Metal / Vulkan 让 DC 便宜了很多),而SetPass 依然是昂贵的状态切换。
❌ 误区 4:“MaterialPropertyBlock 会破坏合批”
错误。MPB不会破坏静态合批和 GPU Instancing。相反,它可以在同一材质基础上做属性变化,同时保持合批,是"低成本改颜色"的最佳方案。
⚠️ 但 MPB 会破坏动态合批(Unity 的 legacy dynamic batching),因为动态合批要求所有属性一致。
❌ 误区 5:“我的场景就 30 个物体,不需要优化 SetPass”
30 个物体如果每个都用独立材质 = 30 SetPass,这在移动端已经是不小的开销了。关键看材质数量,不是物体数量。
十一、实战诊断流程 🕵️
步骤 1:看指标
SetPass > 100(移动)/ > 300(PC)→ 需要优化步骤 2:打开 Frame Debugger
找出触发 SetPass 的原因:
- 大量 UI 各自图集 → 合并图集
- 每个角色独立材质 → 用 MPB
- 多光源触发额外 Pass → 减光源
步骤 3:检查材质使用
// 场景中有多少个不同的材质实例?varallMaterials=newHashSet<Material>();foreach(varrinFindObjectsOfType<Renderer>()){foreach(varminr.sharedMaterials){allMaterials.Add(m);}}Debug.Log($"材质总数:{allMaterials.Count}");材质数 ≈ SetPass 数(理想情况)。
步骤 4:检查 Shader 变体
Edit → Project Settings → Graphics → Shader Preloading变体过多会导致 SetPass 分裂。
十二、进阶:SRP Batcher(URP/HDRP 的救星)
URP/HDRP 的SRP Batcher是针对 SetPass 优化的黑科技:
原理:
- 传统渲染:每个材质切换都要重新上传所有 Uniform
- SRP Batcher:Shader 相同时,只更新变化的 Per-Object 数据
效果:
- 大幅降低CPU 端渲染成本
- 就算材质不同(参数不同),只要 Shader 相同,几乎"免费"
启用条件:
- URP / HDRP
- Shader 兼容 SRP Batcher(用 CBUFFER 分组)
Graphics Settings → Scriptable Render Pipeline Settings → SRP Batcher ✅🎁 一句话总结
SetPass Calls 是"换菜谱"的次数,Draw Calls 是"开火炒菜"的次数。换菜谱的成本远大于炒菜本身,所以优化的核心是尽量用同一份"菜谱"——合并材质、共享纹理、启用 Instancing、用 MaterialPropertyBlock。SetPass 是 CPU 端的性能红线,尤其在移动端,降 SetPass 比降 DC 更重要。💡