SRP Batcher 主要优化的是:CPU 为连续绘制准备和绑定 Shader 数据的开销。
它通常不会减少 Draw Call 数量,也不会让 GPU 少画几个三角形。
先记住这组对比:
GPU Instancing: 多个实例尽可能放进一次绘制。 SRP Batcher: 绘制次数可以不变, 但让 CPU 更便宜地组织这些绘制。下面拆开讲。
一、一个 Draw Call,成本不只是“调用 Draw”
假设场景里有三块石头:
石头 A:红色材质 石头 B:绿色材质 石头 C:蓝色材质它们使用同一个 Shader Variant,只是材质参数不同。
CPU 在提交绘制之前,通常需要准备:
使用哪个 Shader? 使用哪个 Shader Variant? 绑定哪些纹理? 材质颜色、粗糙度等参数是什么? 物体的变换矩阵是什么? 使用哪个网格?然后才是:
Draw可以用一个简化公式表示:
[
T_{\text{CPU渲染}}
T_{\text{剔除与排序}}
+
T_{\text{状态和数据准备}}
+
T_{\text{绘制命令组织}}
]
SRP Batcher 重点优化中间的“状态和数据准备”,以及相关的绘制提交路径。
它不是把全部 CPU 渲染成本都消除。
二、传统路径的问题:很多时间花在“准备画”,而不是“画”
用概念流程表示:
石头 A: 整理材质数据 → 设置相关状态 → 绘制 石头 B: 整理材质数据 → 设置相关状态 → 绘制 石头 C: 整理材质数据 → 设置相关状态 → 绘制底层本来就可能有各种缓存,并非每次都完全从零处理。
但随着物体和材质增多,CPU 仍可能花费大量时间在:
- 收集、整理 Shader 参数;
- 更新和绑定常量数据;
- 处理不同材质的设置;
- 重复经过较重的状态准备路径。
尤其是:
大量物体使用不同材质,但这些材质实际使用同一个 Shader Variant。
SRP Batcher 正是针对这类情况提供高效路径。
三、核心机制一:让材质数据持久保存在 GPU 缓冲中
考虑材质参数:
CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float _Metallic; float _Smoothness; CBUFFER_END这些参数属于材质:
石头 A 材质: 红色、金属度 0、光滑度 0.2 石头 B 材质: 绿色、金属度 0、光滑度 0.4SRP Batcher 的一个关键思路是:
材质数据上传后,在 GPU 侧持久保存;材质未变化时,避免反复重新准备、上传同一份数据。
概念上:
GPU 材质数据区: 材质 A → 一份参数数据 材质 B → 一份参数数据 材质 C → 一份参数数据绘制不同物体时,使用对应的数据,而不是每次都重新打包同样的材质参数。
注意两点:
- 材质参数改变后,相关数据仍然需要更新。
- “数据持久保存”不意味着不再绑定资源,或完全没有状态切换。
省掉的是大量重复工作,不是让设置成本变成零。
四、核心机制二:高效处理每个物体自己的数据
同一材质可以被多个物体使用,但每个物体的位置不同:
石头 A:位置 (0, 0, 0) 石头 B:位置 (5, 0, 0) 石头 C:位置 (10, 0, 0)因此,不能把所有数据都当成材质数据。
常见分类是:
| 数据类别 | 例子 | 常见常量缓冲约定 |
|---|---|---|
| 每材质数据 | 颜色、金属度、光滑度 | UnityPerMaterial |
| 每物体数据 | 变换矩阵、部分光照相关数据 | UnityPerDraw |
| 每帧/相机等数据 | 相机和环境相关参数 | 由管线组织 |
SRP Batcher 同时提供了更高效的每物体数据组织和绑定路径。
但物体动了,矩阵仍然要更新;物体要画,绘制命令仍然存在。
它不是跳过必要工作,而是:
让这些工作按运行时易于批量处理的数据布局进行。
五、为什么名字叫 Batcher,却不减少 Draw Call?
因为这里的 Batch,不能直接理解成“一个绘制命令”。
例如:
一个 SRP Batch ├── Draw 石头 A ├── Draw 石头 B ├── Draw 石头 C └── Draw 石头 D里面仍然可以有多个 Draw Call。
它表示的是:
一段可以通过 SRP Batcher 高效连续处理的绘制序列。
概念对比:
普通路径: 较重的准备 → Draw 较重的准备 → Draw 较重的准备 → DrawSRP Batcher 路径: 复用兼容的准备状态 ├── 较轻量的数据绑定 → Draw ├── 较轻量的数据绑定 → Draw └── 较轻量的数据绑定 → Draw所以看到:
开启前:1000 Draw Calls 开启后:1000 Draw Calls不能说明 SRP Batcher 没生效。
要看 CPU 渲染相关耗时,而不只是绘制次数。
六、关键是 Shader Variant,不是“材质必须相同”
这是最容易混淆的地方。
情况一:材质不同,但 Variant 相同
材质 A:红色 材质 B:绿色 材质 C:蓝色 全部使用: 同一个 Shader 的同一个 Variant这是 SRP Batcher 很适合优化的情况。
材质可以不同,网格也可以不同。
情况二:Shader 文件相同,但 Variant 不同
例如:
材质 A:开启 _NORMALMAP 材质 B:关闭 _NORMALMAP 材质 C:开启 _ALPHATEST_ON虽然都来自同一个 Shader,但关键词组合不同,可能对应不同的 Shader Variant。
这会使连续绘制需要切换程序或相关状态,影响批处理连续性。
因此:
相同 Shader 文件 ≠ 相同 Shader Variant不过,也不能认为“Variant 相同就一定进入同一个 SRP Batch”。实际分组还受 Pass、渲染顺序及其他条件影响。
优化方向是减少不必要的 Variant 和状态切换,而不是为了凑批次破坏正确的渲染顺序。
七、Shader 怎样才算兼容?
典型要求包括:
1. 材质标量和向量等常量放入UnityPerMaterial
CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float4 _BaseMap_ST; float _Smoothness; CBUFFER_END纹理和采样器不是这样塞进常量缓冲:
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);2. 引擎要求的每物体常量遵循UnityPerDraw布局
使用 URP 提供的 Shader 库时,相关声明通常由库处理,不要随意重复定义。
3. 多个 Pass 保持兼容的数据布局
例如,不能因为不同 Pass 或关键词,随意改变同一材质常量缓冲的布局。
最稳妥的方式是共享相应声明,并参考当前 URP/HDRP 版本的官方 Shader。
4. 注意MaterialPropertyBlock
在常见的 Unity SRP Batcher 路径中,使用MaterialPropertyBlock会使对应 Renderer 无法走 SRP Batcher 兼容路径。
因此:
“用 MPB 避免创建材质实例”与“维持 SRP Batcher”之间,可能需要权衡。
具体应以项目 Unity 版本和实际渲染路径验证。
八、它与 GPU Instancing 的区别
| 对比 | SRP Batcher | 传统 GPU Instancing |
|---|---|---|
| 主要目的 | 降低 CPU 状态和数据准备成本 | 合并多个实例的绘制 |
| 是否通常减少 Draw Call | 不一定,通常不是核心收益 | 是 |
| 网格是否必须相同 | 不要求 | 同一实例化绘制通常要求相同网格 |
| 材质是否必须相同 | 不要求,但需满足兼容条件 | 通常共享材质和绘制状态 |
| 差异数据如何处理 | 材质和每物体数据路径 | 实例数据 |
| 典型场景 | 很多不同物体和材质,共用少量 Variant | 大量重复树木、石头、草等 |
同一个物体不能想当然地同时吃到两者全部收益。
Unity 会根据具体渲染路径和兼容条件选择处理方式;传统 Renderer 路径与较新的 GPU 驱动渲染路径也不能简单混为一谈。
九、什么时候效果明显?什么时候几乎没用?
更容易获益
物体数量多 材质数量多 Shader Variant 相对少 CPU 渲染准备成为瓶颈例如:
一个城市有大量不同建筑和材质,但材质都基于少数几种 Shader Variant。
提升可能不明显
Draw Call 本来就少 大量时间消耗在其他 CPU 系统 GPU 已经成为主要瓶颈 大量绘制不兼容 SRP Batcher Variant 和状态频繁切换例如,满屏透明粒子导致 GPU Overdraw 极高:
CPU 提交更快了 但 GPU 仍然要反复计算和混合大量像素帧率可能变化不大。
SRP Batcher 不直接减少阴影采样、片元计算、透明叠加或纹理带宽。
十、怎么确认它真的有用?
建议验证三个问题:
1. Shader 是否兼容?
检查 Shader Inspector 中的 SRP Batcher 兼容信息。具体界面随版本不同。
2. 实际绘制是否走了对应路径?
使用 Frame Debugger 查看:
- SRP Batch 分组;
- 使用的 Shader 和 Variant;
- 绘制之间为什么发生分组变化。
不要只看项目里“已开启”这个选项。
3. CPU 时间是否下降?
在相同场景、相同设置下对比开关:
- 主线程相关渲染耗时;
- 渲染线程耗时;
- GPU 耗时;
- 最终帧时间。
最好在目标设备的 Player 中测试,并排除 Shader 首次使用、资源加载等干扰。
最后一句话
SRP Batcher 不是“让 GPU 少画”,而是“让 CPU 少为每次绘制重复准备”。
最典型的结果是:
物体数量不变 Draw Call 数量基本不变 画面不变 GPU 工作量大致不变 但 CPU 渲染提交更便宜了。