Unity-SRP Batcher 原理与使用

对 SRP Batcher 的个人理解整理。之前的笔记比较零散,现在系统梳理一下,顺便验证是否有理解偏差。

1. 概述:SRP Batcher 是什么

SRP Batcher 是 Unity Scriptable Render Pipeline(URP / HDRP)中的一个 CPU 端优化机制。它不会减少实际的 DrawCall 数量,但能显著降低每个 DrawCall 的 CPU 开销——特别是渲染状态切换和 Buffer 绑定这部分。

一句话概括:用 GPU 内存换 CPU 状态切换时间。

核心思想是:把低频变化的数据(材质属性)持久化缓存在 GPU 上,CPU 在材质不变的情况下只需要更新物体级别的数据(MVP 矩阵等),省去了大量重复的 Buffer 绑定和状态设置。


2. 如何使用

2.1 在 URP Asset 中启用

  • 选中 URP 配置文件(UniversalRenderPipelineAsset)
  • Advanced 部分勾选 SRP Batcher
  • 较新的 URP 版本中默认是开启的

2.2 Shader 中声明 CBuffer

SRP Batcher 要求材质属性声明在名为 UnityPerMaterial 的 CBuffer 中:

1
2
3
4
5
6
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST;
half _Cutoff;
half _Smoothness;
CBUFFER_END

Unity 内置的 Lit / Unlit Shader 已经按此规范声明,但如果你写自定义 Shader,必须手动加上 CBUFFER_START(UnityPerMaterial) / CBUFFER_END,否则 SRP Batcher 不会生效,Unity 会 fallback 到传统渲染路径。


3. 核心原理:CPU 与 GPU 的数据分离架构

高频变化的数据(物体级别:MVP 矩阵、Color 等)和低频变化的数据(全局设置、材质固有属性)在 GPU 上做物理分离。

SRP Batcher 在底层维护了两个概念上的 Buffer 区域:

Buffer 内容 更新频率
PerBuffer(单个 CBuffer 槽位) 同一 Shader Variant 共享的材质属性(_BaseColor、_Smoothness 等) 仅材质切换时更新
PerDraw(引擎内部维护的 Buffer) 每个物体的独有数据(MVP 矩阵、Lightmap UV、Instance ID、_BaseColor 等) 每帧按物体更新,高频

3.1 具体工作流程

  1. 准备阶段(CPU):将材质数据上传到 GPU 的持久 Buffer 中,原地保留
  2. 渲染阶段:当遍历可见物体列表时:
    • 如果物体使用的 Shader Variant 和上一个物体相同 → 跳过所有材质属性上传,只更新 PerDraw 数据(矩阵等),直接发 Draw
    • 如果 Shader Variant 变了 → 更新 PerBuffer 中的材质属性,再发 Draw

也就是说,CPU 在材质不变的情况下,只更新物体级别的数据,不切换渲染状态,也不重新绑定 Buffer

3.2 架构对比

传统渲染(无 SRP Batcher):

1
2
3
物体 A: 绑定材质 Buffer → 上传属性 → 绑定物体 Buffer → 上传矩阵 → Draw
物体 B: 绑定材质 Buffer → 上传属性 → 绑定物体 Buffer → 上传矩阵 → Draw (同一材质也要重新绑定)
物体 C: 切换 Shader → 绑定新材质 Buffer → 上传属性 → ...

有 SRP Batcher:

1
2
3
物体 A: 材质已驻留 GPU → 更新物体矩阵 → Draw
物体 B: 材质已驻留 GPU → 更新物体矩阵 → Draw (材质相同,跳过)
物体 C: 切换 Shader → 上传新材质到 GPU → 更新物体矩阵 → Draw

关键区别:在同一材质批量渲染时,SRP Batcher 省去了每物体都要做的 Buffer 绑定和属性上传。


4. 为什么材质属性必须在一个 CBuffer 里?

这是 SRP Batcher 高效工作的重要前提,有两个原因:

4.1 固定内存布局

引擎需要保证 GPU Buffer 的内存布局在运行时是确定且固定的。如果 Shader 属性分散在多个 CBuffer 里:

  • 引擎无法保证多个 Buffer 之间的布局关系是固定的
  • 可能需要运行时逐个绑定多个 Buffer,或不断重组布局
  • PerDraw 数据也无法干净地分离出来

4.2 内存拷贝效率

如果所有材质属性在一个 CBuffer 里,CPU 可以用一次内存拷贝(memcpy)把整个块送上 GPU。分散到多个 CBuffer 意味着多次单独的提交操作,效率低得多。

4.3 SRP 中定义的标准 CBuffer 布局

SRP 预定义了三个标准 CBuffer,SHader 中应严格遵循:

CBuffer 名称 用途 生命周期
UnityPerMaterial 材质级属性(颜色、贴图、参数) 材质切换时更新
UnityPerDraw 物体级属性(矩阵、LM UV、Instance ID) 每物体更新,SRP 自动管理
UnityInstancing GPU Instancing 相关数据 Instancing 启用时使用

自定义 Shader 中若遗漏 CBUFFER_START(UnityPerMaterial)/CBUFFER_END,SRP Batcher 会检测到 CBuffer 布局不兼容,自动退化为非 batched 渲染。


5. SRP Batcher vs GPU Instancing

这是两个容易混淆的优化,需要明确区分:

SRP Batcher GPU Instancing
目标 降低每个 DrawCall 的 CPU 开销 减少 DrawCall 数量
DrawCall 数量 不变 显著减少(N 合 1)
同一 Mesh 要求 ❌ 不需要 ✅ 需要相同 Mesh
同一 Material 要求 ✅ 需要同一 Shader Variant ✅ 需要完全相同的材质
适用场景 场景中大量不同 Mesh 但使用同一 Shader 大量相同 Mesh 的物体(草、树、粒子)
原理 持久化 GPU Buffer,跳过重复绑定 一次性提交多个 Instance 数据

两者可以共存。 Unity 在 SRP 中会先尝试 Instancing(如果能合批),退而求其次走 SRP Batcher(同 Shader 不同 Mesh),最后 fallback 到传统 Draw。


6. 如何在 Frame Debugger 中查看

开启 Frame Debugger(Window → Analysis → Frame Debugger),点击一个 DrawCall:

  • Batching 显示为 “SRP Batch” → SRP Batcher 生效
  • 显示 “DrawDynamic” 或 “SetPassCall” → 没有命中 SRP Batcher

常见的导致 SRP Batcher 退出的原因:

  1. Shader 中材质属性没有放在 CBUFFER_START(UnityPerMaterial) / CBUFFER_END
  2. 使用了 Multi-Pass Shader
  3. Shader 包含了 Procedural Instancing(#pragma instancing_options procedural),且没有正确处理
  4. 材质使用了不同 Shader Variant(例如关键字状态不同)

7. 优缺点与限制

优点

  • 显著降低 CPU 开销:在大量同 Shader 物体的场景下,CPU 端的渲染提交耗时可以降低 50%~80%
  • 无需合 Mesh:不需要像 Static Batching 那样合并模型,不同 Mesh 也可以受益
  • 透明集成:开启后对项目无侵入,Shader 符合规范即可自动生效

缺点与限制

限制 说明
仅 SRP 管线 不适用于 Built-in Render Pipeline
同 Shader Variant 要求 即使同一 Shader,关键字状态不同也不会 batch
Multi-Pass 不支持 使用了多 Pass 的 Shader 会禁用 SRP Batcher
CBuffer 布局必须一致 所有 Variant 的 UnityPerMaterial CBuffer 声明必须完全相同
GPU 内存占用 材质属性常驻 GPU,多材质时会有额外的显存开销

8. 总结

  • 本质:CPU 端的提交优化,不减少 DrawCall 数量
  • 原理:把材质数据持久化缓存在 GPU,省去重复的 Buffer 绑定和状态设置
  • 代价:牺牲 GPU 内存,换来 CPU 状态切换时间的降低
  • 关键:Shader 中声明 CBUFFER_START(UnityPerMaterial) / CBUFFER_END,且保证所有 Variant 的 CBuffer 布局一致
  • 优势场景:大量不同 Mesh 但使用同一种 Shader 的场景(如开放世界的建筑、物件)
  • 与 Instancing 关系:互补,可共存

对比你的原始笔记,主要补充的内容:

  • 完整的 CBuffer 体系(UnityPerDraw / UnityInstancing)说明
  • 与 GPU Instancing 的详细对比
  • Frame Debugger 验证方法
  • SRP Batcher 工作流程图解
  • 优缺点和限制清单
  • 针对”优化 SetPassCall” 说法的更加精确的描述

参考