Deep Read

性能优化-静态合批

被标记为 Static、共享同一材质的物体,Unity 会在 Build 时把它们的网格预先合并成一套大的顶点/索引缓冲。运行时不再逐物体绑定缓冲、上传变换,而是一次绑定、连续绘制。

要理解它到底省在哪,得先看清 CPU 画一个物体要做几步。

画一个物体的三步

从图形 API(DX/GL)看,绘制一个物体分三步,开销依次递减:

  • 设置渲染状态(SetPass):绑定 Shader、纹理、管线状态。走图形驱动的通用路径,要校验状态、重组资源绑定表,是三步里最贵的。
  • 设置逐物体数据:绑定这个物体的顶点/索引缓冲,把它的模型矩阵上传到 GPU 常量缓冲。
  • 发起 Draw Call:命令 GPU 按当前状态和缓冲画一次。

场景里成千上万个小物体,CPU 要为每个重复这套流程,提交本身就成了瓶颈。所有合批优化都是在削减这套重复流程里的某一步——分清砍的是哪一步,才知道每种合批解决的是什么问题。

静态合批砍掉的是第二步

前提要先说清:这批物体本来就共享同一材质。所以只要它们被连续绘制,渲染状态不变,SetPass 在合批前后都只有一次。静态合批省的不是 SetPass,而是第二步——逐物体的缓冲绑定和变换上传:

  • 顶点已在世界空间:合并时每个顶点已经乘过所属物体的模型矩阵,运行时不必再逐物体上传变换。
  • 缓冲只绑一次:所有子网格在同一套大缓冲里,绑定一次即可,不用逐物体 rebind。

于是每次 Draw Call 都变得极廉价:状态和缓冲都已就绪,剩下的只是指定一段索引范围让 GPU 去画。

至于 Draw Call 的数量,减少与否要看剔除:当合并进来的子网格连续可见时,引擎能用一次 DrawIndexed 画一整段连续索引区间,多个物体合成一次 Draw Call——这就是 Unity 面板里 "Saved by batching" 的来源。可一旦其中一部分被视锥/遮挡剔除打散,连续区间被切断,就退化成按可见区间多次发起。所以数量的减少是锦上添花、不保证;稳定省下的是逐物体那层绑定与变换开销

底层怎么合并缓冲

合并做两件事,外加一份内存代价。

顶点空间变换。 遍历每个 Static 物体,逐顶点乘以所属物体的模型矩阵,把世界空间坐标写进大顶点缓冲。这不是简单拼接——坐标系必须先统一到世界空间,运行时才能免去逐物体变换。

索引偏移重建。 所有索引合并进一个新缓冲,关键是对索引值做偏移:若第一个物体有 N 个顶点,第二个物体的索引要整体加 N,才能正确指向大顶点缓冲里的顶点。

内存代价。 合并后的大网格是原网格的世界空间副本,而原始网格仍然存在,两者并存。顶点越多、实例越多,这份副本越大,打包体积和运行时内存都随之上升。

小结

  • 省在哪:静态合批是 CPU 端的构建期预处理。前提是共享材质(SetPass 已经是一次),它砍的是逐物体的缓冲绑定和变换上传,让每次 Draw Call 变廉价;物体连续可见时顺带把多次 Draw Call 合成一次。
  • 底层做了什么:把顶点从局部空间变换到世界空间,重建一套带索引偏移的大顶点/索引缓冲。
  • 代价:大网格是原网格副本,增加打包体积和运行时内存。

它和 SRP Batcher性能优化-SRP合批SetPass Call 为什么贵 CPU 提交一次绘制,真正贵的不是 Draw Call 本身,而是它前面的 SetPass Call——切换渲染状态的那一步。每次 SetPass,CPU 端要: 1. 选择要使用的 Shader 变体(Variant),绑定对应的管线状态; 2. 把这个材质的所有属性从 CPU 内存写入 GPU 的常量缓冲区(CBuffer)。 这一步之所以慢,是因为它走的是图形驱动的通用路径:驱动要校验状态合法性、重新组织资源绑定表,材质数据每次都要重新走一遍 CPU→GPU 的上传。场景里材质一多,即使共用同一个 Shader,传统管线也要为每个材质完整做一遍 SetPass。 SRP Batcher 要解决的就是这个:让使用相同 Shader 变体的多个材质共享一次 SetPass。注意它减少的是 SetPass Call,Draw Call 的次数并不变——只是每次 Draw 变得极其廉价。 核心思路:材质数据常驻显存 传统批处理(Static/Dynamic Batching,见 [[性能优化-静态合批]])的思路是合并网格,把多个物体拼成一 优化的是不同步骤——SRP Batcher 砍的是 SetPass(材质数据常驻显存),静态合批砍的是逐物体几何绑定,两者维度不同、可以共存。只是在 SRP Batcher 已高效运作的 URP 下,静态合批的优先级明显降低了。