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. 材质标量和向量等常量放入UnityPerMaterialCBUFFER_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 渲染提交更便宜了。