Unity3D SRP Batcher原理分析
前言
Unity的SRP Batcher是一种高性能渲染批处理机制,专为可编程渲染管线(Scriptable Render Pipeline, SRP)设计,旨在显著降低Draw Call带来的CPU开销,提升渲染性能。它的核心原理与传统批处理(静态/动态批处理)和GPU Instancing不同,更侧重于减少CPU与GPU之间的通信开销和状态切换。
对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀!
核心问题:传统批处理的瓶颈
- Draw Call开销: 每次调用
DrawMesh
或DrawRenderer
,CPU都需要向GPU发送大量指令和数据(顶点、索引、材质属性、纹理、着色器状态等),涉及昂贵的线程同步和驱动层开销。 - 材质切换开销: 当相邻渲染对象使用不同的材质时,GPU需要切换状态(Shader程序、纹理、材质属性常量缓冲区等)。这种切换非常耗时。
- 传统批处理的限制:
- 静态批处理: 合并网格顶点,但仅适用于静态不动的对象,增加内存和磁盘占用。
- 动态批处理: 运行时合并顶点简单的网格,限制极多(顶点数、蒙皮、光照贴图等),且合并本身也有CPU开销。
- GPU Instancing: 高效绘制大量相同网格和材质的对象,但对材质变体(不同属性值)的支持有限且需要特定Shader编写。
SRP Batcher 的解决之道
SRP Batcher的核心思路是:将频繁变化的数据(每个对象特有的数据)与不频繁变化的数据(材质数据)分离管理,并利用GPU的持久化内存(Persistent Data)来最小化CPU-GPU通信和状态切换。
- 数据分离与重组:
- 每对象属性(Per-Object Data): 包括模型矩阵(
unity_ObjectToWorld
)、法线矩阵(unity_WorldToObject
)、光照探针数据、Lightmap索引/偏移等。这些数据每个渲染对象实例都不同。 - 每材质属性(Per-Material Data): 包括
_BaseColor
,_Metallic
,_Smoothness
,_MainTex_ST
以及纹理引用等。这些数据在共享同一材质的多个对象之间是相同的(或变化不频繁)。
- 常量缓冲区(Constant Buffer, CBUFFER)优化:
- 专用CBUFFER: SRP Batcher要求Shader必须使用特定的
CBUFFER
宏来声明这些属性:UnityPerDraw
: 包含每对象属性。UnityPerMaterial
: 包含每材质属性。
- 示例Shader代码片段:
// Per-Object数据 (每个实例不同)
CBUFFER_START(UnityPerDraw)float4x4 unity_ObjectToWorld;float4x4 unity_WorldToObject;// ... 其他每对象数据 (如LightProbe SH, Lightmap ST)
CBUFFER_END// Per-Material数据 (同一材质相同)
CBUFFER_START(UnityPerMaterial)float4 _BaseColor;float _Metallic;float _Smoothness;float4 _MainTex_ST;
CBUFFER_ENDsampler2D _MainTex; // 纹理在另一个特殊块中声明
-
- GPU端的持久化内存:
- SRP Batcher在GPU显存中开辟了一块持久化的内存区域。
- 材质数据(UnityPerMaterial): 当一个材质首次被渲染时,它的所有
UnityPerMaterial
数据(包括纹理绑定)会被上传到这个持久化区域,并长期驻留在GPU内存中。之后使用该材质的对象渲染时,不再需要重新上传这些数据。 - 对象数据槽(Per-Object Data Slots): 这块持久化内存也被组织成一个大的对象数据列表。每个可以被SRP Batcher批处理的对象实例,在这个列表中都有一个预分配的固定槽位(Slot)。
- 渲染流程优化:
- 准备阶段(CPU):
- SRP(如URP/HDRP的Culling过程)收集所有符合SRP Batcher条件的渲染对象(使用兼容Shader)。
- 按材质对这些对象进行排序(尽量减少材质切换)。
- 对于每个对象,CPU只准备它自己的每对象属性数据(
UnityPerDraw
内容)。
- 批处理提交(CPU -> GPU):
- 单次大块数据传输: CPU将当前帧所有需要渲染对象的每对象属性数据(仅仅是
UnityPerDraw
数据)一次性打包上传到GPU持久化内存中它们各自预分配的槽位里。这是一个非常高效的大块内存拷贝操作。 - 状态绑定: 对于一批共享同一材质的对象:
- GPU绑定该材质的顶点着色器(VS)和片元着色器(FS)(第一次或材质变化时绑定)。
- GPU绑定该材质在持久化内存中的材质属性数据地址(
UnityPerMaterial
)。这个绑定在材质未改变时保持不变。
- 绘制调用(Draw Call): CPU发出一个
DrawMesh
指令序列(可能包含多个子网格SubMesh)。关键点在于:- 这个
DrawMesh
调用不是为单个对象,而是为当前材质批内的所有对象。 - 在绘制每个对象时,顶点着色器(VS)知道如何根据对象的InstanceID(或一个专门的索引)去GPU持久化内存的对象数据列表中对应的预分配槽位里读取该对象特有的
UnityPerDraw
数据(如unity_ObjectToWorld
)。 - 片元着色器(FS)则直接访问绑定好的材质数据(
UnityPerMaterial
)和纹理。
- 这个
SRP Batcher 的关键优势
- 大幅减少Draw Call: 将共享同一材质的多个对象的渲染合并到一个或少数几个Draw Call中提交,显著降低了Draw Call数量带来的CPU开销。
- 消除材质切换开销: 因为材质数据(
UnityPerMaterial
+ 纹理)已持久化在GPU内存中,切换材质时只需要绑定一次Shader程序和材质数据地址,避免了传统管线中逐个设置大量材质属性和纹理绑定的巨大开销。绑定改变只发生在材质实际发生变化时。 - 高效的对象数据更新: 每帧只更新变化的每对象数据,并且是高效的大块内存传输到GPU预定义槽位,避免了传统方式中为每个对象单独设置常量缓冲区的开销。
- 内存友好: 相比静态批处理合并顶点数据导致内存膨胀,SRP Batcher只额外存储了每对象属性数据(相对较小),内存占用更优。
- 兼容动态对象: 对象可以自由移动、旋转、缩放(动态修改Transform),不受静态批处理的限制。
启用SRP Batcher的条件
- 使用SRP: 必须在URP或HDRP(或自定义SRP)中使用。
- 兼容的Shader:
- 必须使用
CBUFFER_START(UnityPerDraw)
和CBUFFER_START(UnityPerMaterial)
(或UnityPerMaterial
的别名)来声明属性。 - 不能使用材质属性块(
MaterialPropertyBlock
)。MPB会破坏材质数据的共享性。 - 满足SRP Batcher对Shader代码结构的其他要求(如顶点着色器输入结构)。
- 兼容的渲染对象:
- 使用兼容Shader的材质。
- 非蒙皮网格(SkinnedMeshRenderer通常不支持)。
- 未使用MPB。
检查工具: Unity Frame Debugger 窗口会明确标识哪些Draw Call是通过 "SRP Batch" 提交的。
SRP Batcher vs. GPU Instancing
- 目标场景不同: SRP Batcher主要优化不同材质数量有限,但共享材质的对象数量众多的场景(如场景中的大量不同位置的岩石、树木、家具)。GPU Instancing主要优化大量完全相同的网格和材质实例(如草、子弹、人群)。
- 数据驱动方式不同: SRP Batcher依赖GPU持久化内存和预分配槽位。GPU Instancing通过InstanceID和实例数据缓冲区传递数据。
- 兼容性: SRP Batcher对Shader的要求相对GPU Instancing更宽松一些(GPU Instancing需要更显式的支持)。两者可以共存,一个对象如果同时满足两者条件,通常会优先使用GPU Instancing(因为它更高效)。
总结
SRP Batcher是Unity SRP的核心优化技术,它通过分离对象数据与材质数据、利用GPU持久化内存、预分配对象数据槽位以及按材质排序后批量提交Draw Call的方式,极大地减少了CPU在设置渲染状态和提交Draw Call上的开销,特别是显著降低了材质切换的成本。要利用好SRP Batcher,关键在于编写符合其规范的Shader(正确使用UnityPerDraw
和UnityPerMaterial
CBUFFER)并避免使用MaterialPropertyBlock
。它在优化拥有大量共享材质的动态对象场景时效果尤为显著。
更多教学视
Unity3Dwww.bycwedu.com/promotion_channels/2146264125