SetPass Calls 深度解析:比 Draw Call 更重要的性能指标

SetPass Calls 深度解析:比 Draw Call 更重要的性能指标

一、先看一个常见的困惑

打开 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

直观对比表

场景SetPassBatchesDraw Call性能
100 个相同材质,无合批1100100中等
100 个相同材质,合批111极好
100 个不同材质100100100极差
100 个物体,50 种材质50100100

五、为什么 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: 35

Unity Profiler

Window → Analysis → Profiler → Rendering

SetPass Calls曲线,识别峰值场景。

Frame Debugger

Window → Analysis → Frame Debugger

逐帧查看每一步 SetPass 触发的原因:

  • 左侧列表中,每个"SetPass ***"就是一次状态切换
  • 点击可看到具体切换了什么(纹理不同?Shader 不同?)

七、什么情况会触发新的 SetPass? 🔥

触发 SetPass 的因素(每换一个都可能新增 SetPass)

  1. 不同的 Shader(最常见)
  2. 不同的材质(哪怕 Shader 相同,材质不同也算)
  3. 不同的纹理(材质相同但纹理不同)
  4. 不同的 Uniform 值(颜色、参数不同)
  5. 不同的 Render State(Blend / ZTest / Cull 等)
  6. 不同的 Keyword(Shader 变体不同)
  7. Lightmap / Light Probe影响
  8. 多 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 CallsDraw 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 更重要。💡