当然可以!以下是完整、详尽、可发布的博客文章,专注讲解 Unity 的静态合批与动态合批机制,并详细列出它们对 Shader 的要求和所有限制条件。文章结构清晰、技术深度足够,适合发布在 CSDN、掘金、知乎等技术平台。
urp默认隐藏动态合批,需要再这边开启动态合批的选项
🎯 Unity 静态合批 & 动态合批完全解析:原理、限制与实战优化
Unity 渲染优化的第一课,必须懂的就是 Draw Call 合批机制。本文聚焦 Unity 中的 静态合批(Static Batching) 和 动态合批(Dynamic Batching),从底层原理到 Shader 限制、失败原因、调试方法,带你一文吃透。
🧱 什么是 Draw Call 与合批?
在 Unity 中,每渲染一个物体,CPU 就会向 GPU 发送一个 Draw Call(绘制调用)。Draw Call 多意味着 CPU 压力大,可能引起帧率下降,特别是在移动端和 WebGL 平台。
Unity 为了优化性能,会尝试自动将多个渲染调用合并为一次 Draw Call,这就是“合批(Batching)”。
🟩 一、静态合批(Static Batching)
✅ 原理
静态合批是 Unity 在构建时将多个静态物体的网格数据打包成一个或多个大网格,运行时作为一个整体进行渲染,显著减少 Draw Call。
✅ 启用方式
- 选中 GameObject
- 在 Inspector 中勾选
Static
(或勾选 Static →Batching Static
) - Unity 构建时会对这些对象进行合并
✅ 条件汇总
条件 | 是否必需 | 说明 |
---|---|---|
勾选 Static 标志 | ✅ | 必须设置为 Static |
使用相同材质实例 | ✅ | 材质必须引用同一个 Material 对象 |
使用 MeshRenderer | ✅ | 不支持 SkinnedMeshRenderer |
Shader 必须兼容 | ✅ | 见下文 Shader 限制 |
不使用 MaterialPropertyBlock | ✅ | 会创建独立材质实例,破坏合批 |
⚠️ Shader 限制(静态合批)
静态合批的核心规则是:所有被合批的物体不再是“独立对象”,而是被合并成一个大网格。
所以 Shader 不允许访问对象独有的属性。
❌ 以下 Shader 写法会导致合批失败:
// 错误示例:访问对象唯一矩阵
float3 worldPos = mul(_Object2World, v.vertex).xyz;
✅ 推荐用法:
// 推荐写法:使用 Unity 内置宏
float4 clipPos = UnityObjectToClipPos(v.vertex);
❌ 禁用功能列表(会导致分批):
Shader 特性 | 说明 |
---|---|
_Object2World , _World2Object | 对象被合并后,不存在单独变换矩阵 |
GrabPass / 多 Pass Shader | 每个 Pass 是独立 Draw Call |
Shader Keyword 不一致 | _EMISSION , _NORMALMAP 等开关变化会拆分 Shader 变体 |
使用 MaterialPropertyBlock 修改参数 | 会使每个物体拥有独立材质实例,合批失败 |
💡 适用场景
- 城市建筑、地形、房屋、墙体等不会移动/旋转/缩放的对象
- 静态 UI 元素(如背景装饰)配合 SpriteAtlas 合批
🟨 二、动态合批(Dynamic Batching)
✅ 原理
动态合批是在运行时由 Unity 动态将多个小型对象的顶点数据合并成一个临时网格,从而减少 Draw Call。
Unity 每帧会重新组合这些物体的网格,虽然提升了绘制效率,但也会带来一定的 CPU 合批开销。
✅ 启用方式
Project Settings > Player > Other Settings
- ✅ 勾选
Dynamic Batching
(⚠️ 在 URP 中需在 URP Asset 勾选)
✅ 条件汇总
条件 | 是否必需 | 说明 |
---|---|---|
使用相同材质实例 | ✅ | 必须是同一个 Material 对象 |
每个 Mesh 顶点数 ≤ 300 | ✅ | 官方限制,超过即失败 |
使用 MeshRenderer | ✅ | 不支持 SkinnedMeshRenderer |
未启用 GPU Instancing | ✅ | Instancing 和动态合批互斥 |
Shader 结构必须简单 | ✅ | 顶点函数不能太复杂,不能用动画偏移 |
缩放需一致或接近 | ⚠️ | 非 uniform scale 可能破坏合批(如 X:2 Y:1 Z:1) |
⚠️ Shader 限制(动态合批)
与静态合批相比,动态合批对 Shader 要求更苛刻,因为它需要 CPU 快速合并多个对象的数据。
❌ 以下 Shader 特性将阻止动态合批:
特性 | 说明 |
---|---|
顶点函数复杂 | 含有顶点动画、扭曲、动态偏移等逻辑 |
非常量矩阵 | 顶点变换使用不确定变量会中断合批 |
使用不同 Shader Keyword | 会产生不同 Shader 变体 |
材质不同 | 即使 Shader 相同,材质参数不同也会失败 |
GrabPass、多 Pass Shader | 强制产生多个 Draw Call,无法合批 |
🧪 示例场景
- 掉落的金币、弹药、碎片等小物体
- 小型动态粒子替代物(如火花、树叶)
❌ 合批失败的常见原因汇总
原因 | 静态合批 | 动态合批 | 说明 |
---|---|---|---|
材质不同 | ❌ | ❌ | 不同材质一定不能合批 |
Shader Keyword 不一致 | ❌ | ❌ | 比如一个开启 _EMISSION ,另一个关闭 |
使用 MaterialPropertyBlock 设置属性 | ❌ | ❌ | 会实例化材质,打断合批 |
Shader 使用对象独立数据(如 _Object2World) | ❌ | ✅ | 静态合批合并后没有对象矩阵 |
使用复杂顶点动画 | ✅ | ❌ | 动态合批的顶点函数必须简单 |
Mesh 顶点数 > 300 | ✅ | ❌ | 动态合批失败,静态合批不限 |
使用 SkinnedMeshRenderer | ❌ | ❌ | 这类 Renderer 本身无法合批 |
非等比缩放(如 X=1.5 Y=1 Z=0.8) | ✅ | ⚠️ | 有概率影响动态合批稳定性 |
🛠️ 如何验证合批是否成功?
🔧 使用 Frame Debugger(首选)
-
打开路径:
Window > Analysis > Frame Debugger
-
点击左上角
Enable
-
在 Draw Call 列表中查找是否有:
Batched: Static
Batched: Dynamic
-
点击每个 Batch 可查看合批的对象和材质
🔧 使用 Profiler
-
打开:
Window > Analysis > Profiler
-
查看 Rendering 模块中的:
- Draw Calls(总绘制次数)
- Batches(实际提交的批次数)
✅ 实战优化建议
优化点 | 原因 |
---|---|
大量静止物体 → 使用静态合批 | 提升性能、减少运行时消耗 |
小物体顶点数 ≤ 300 → 可用动态合批 | 控制模型复杂度 |
统一使用共享材质 | 避免因材质不同导致分批 |
避免频繁使用 MaterialPropertyBlock | 会实例化材质、拆批 |
使用 UnityObjectToClipPos 替代 _Object2World | 保证静态合批兼容 |
用 Frame Debugger 验证结果 | 直观查看合批是否成功 |
📌 总结对比表:静态合批 vs 动态合批
项目 | 静态合批 | 动态合批 |
---|---|---|
触发方式 | 构建时 | 运行时 |
是否勾选 Static | ✅ 必须 | ❌ 不需要 |
Mesh 顶点限制 | ❌ 无限制 | ✅ ≤ 300 |
运行时性能开销 | 极低 | 较高(每帧打包) |
适用对象 | 静止对象 | 小动态物体 |
材质要求 | 同材质实例 | 同材质实例 |
Shader 要求 | 不能访问对象唯一数据 | 必须简单、轻量 |
📣 结语
Unity 提供的静态合批和动态合批,是开发者提升渲染性能最基本、也最有效的优化手段之一。理解它们的原理、限制与触发机制,可以帮助你从源头降低 Draw Call 数量,让你的游戏在中低端设备上依旧运行流畅。
开发不是一味堆特效,而是用合适的方式,做足够的表现。