想象你走进一家生意火爆的餐厅。大厨是 GPU,他手艺精湛但只认菜谱;服务员是 CPU,他的工作是跑堂递单。每位客人每点一道菜,服务员就要穿过拥挤的大堂跑到后厨,递上一张单子,等大厨说一句“收到”再跑回去。这张“单子”在渲染世界里有个名字——绘制指令,也就是 DrawCall。高峰时段,服务员光顾着来回跑腿递单,真正炒菜的后厨反而闲得发慌。菜品上得慢,问题不在大厨,而在递单次数太多了。 合批策略,就是餐厅老板想出来的“点单合并术”。给 VIP 包间提前备好整桌宴席,写在总单上,这叫静态合批;散客点了同款饮料,服务员等一等凑成一张单再下,这叫动态合批;十桌客人点了同样的红烧狮子头,大厨干脆按同一份菜谱一次开十口锅,这叫 GPU Instancing。三种方式殊途同归:让 CPU 少跑几趟,让 GPU 把时间花在真正画像素、上颜色上,而不是干等指令。读完这篇文章,你会彻底搞懂这三种合批手艺的原理、边界和取舍。

岗位关联

合批策略是渲染性能优化的基本功。掌握它,你就能在项目中快速定位“CPU 疯狂递单、GPU 闲着等菜”的瓶颈,把游戏帧率从卡顿拉回流畅,这是每位图形学与渲染工程师的必修课。

目录
  1. 可视化演示
  2. 1. 合批总览
  3. 2. 渲染瓶颈
  4. 3. DrawCall 本质
  5. 4. 静态合批
  6. 5. 动态合批
  7. 6. GPU实例化
  8. 7. 选型对比
  9. 8. 实战落地
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示合批策略的核心机制,通过开始/执行/监控/完成四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

合批策略:Draw Draw Call开销与合批动 静态合批的原理与适用场景 核心机制可视化 合批策略整体架构与关键组件

1. 合批总览

餐厅试运营第一天,服务员们就跑断了腿。客人点一杯水,跑一趟厨房;加一碟小菜,又跑一趟。老板很快发现,出菜慢的真凶不是大厨手艺,而是服务员来回递单的次数。于是老板立下第一条店规:能一起下的单,绝不分两次递。这条店规,就是渲染优化里的“合批”。

在渲染世界里,GPU 这位大厨的做菜速度相对固定,真正决定一帧画面快慢的,常常是 CPU 这位服务员递单的效率。CPU 每向 GPU 提交一次绘制指令,都要提前切换渲染状态、整理顶点数据、绑定贴图——这一整套准备动作,就是一次 DrawCall 的固定成本。合批的核心目标,就是减少这种递单次数,把多条绘制指令合并成一条。

常见的合批手段有三种。Static Batching(静态合批)在构建阶段就把不动的物体合并成大网格;Dynamic Batching(动态合批)让 CPU 在运行时把小网格临时拼装;GPU Instancing 则让 GPU 用一份网格数据批量绘制大量相同实例。三者适用边界不同,但目的只有一个:降低 DrawCall 数量,让 GPU 满负荷运转。

所以合批不是让大厨炒得更快,而是让大厨少被敲门打断。递单次数降下来,帧时间的天花板自然就顶开了。

Tframe≥Ndraw×Tsetup+Tgpu\displaystyle T_{frame} \geq N_{draw} \times T_{setup} + T_{gpu}
帧耗时大约等于“递单次数 × 每次递单的准备时间”再加上“大厨实际炒菜的时间”。所以递单次数越多,帧耗时越长。
  1. 帧总耗时T_frame 表示渲染一帧画面总共需要的时间,我们的优化目标就是让它尽量小。
    Tframe\displaystyle T_{frame}
  2. CPU 递单时间N_draw 是这一帧的 DrawCall 数量,T_setup 是每次递单的准备开销,两者相乘就是 CPU 花在递单上的总时间。
    Ndraw×Tsetup\displaystyle N_{draw} \times T_{setup}
  3. GPU 炒菜时间T_gpu 是 GPU 真正执行绘制、输出像素所需的时间,递单再快,这部分的物理时间也省不掉。
    +Tgpu\displaystyle + T_{gpu}

2. 渲染瓶颈

为什么服务员跑腿这么慢?因为他每递一张单,不光要穿过大堂,还得在出菜口排队、和大厨确认菜单、等大厨回一句“收到”。遇到大厨刚好在忙别的菜,这一来一回的时间就更长了。在 CPU 与 GPU 的协作关系里,这段“递单等待”就是渲染瓶颈的根源。

CPU 提交一次 DrawCall 的完整链路是:引擎先把渲染状态(Shader、材质、相机矩阵)写入命令缓冲区,再把顶点数据从 CPU 内存搬运到 GPU 显存,最后通知显卡驱动执行。这些操作都需要系统调用和总线传输,每一步都有不可忽略的延迟。当场景里的物体数量从几十涨到几百、几千,CPU 就要在同一帧里反复执行这套流程。

帧时间由 CPU 时间和 GPU 时间共同决定。如果 CPU 处理 DrawCall 的时间超过了 GPU 完成上一帧渲染的时间,GPU 就只能空闲等待,画面上表现为掉帧、卡顿。移动端尤为明显——CPU 的算力本来就有限,场景一复杂,递单本身就占据了大量性能预算。

大厨要等菜料送到才开火,CPU 就是那个送菜的。后厨再快,送菜不及时,客人照样得饿肚子。

TCPU=Ndraw×Tsetup\displaystyle T_{CPU} = N_{draw} \times T_{setup}
CPU 的递单总时间等于递单次数乘以单次递单的准备时间。当 T_CPU 大于等于 GPU 的渲染时间,CPU 就成为了整条流水线的瓶颈。
  1. 递单次数N_draw 是同一帧内 CPU 需要提交的绘制指令总数,它直接决定了 CPU 要跑多少趟。
    Ndraw\displaystyle N_{draw}
  2. 单次准备时间T_setup 包含状态切换、顶点上传、驱动调用等固定开销,和物体复杂度关系不大。
    Tsetup\displaystyle T_{setup}
  3. CPU 总开销N_draw 与 T_setup 相乘,得到 CPU 在同一帧内的总递单消耗。当场景中物体数量翻倍,这一项基本上也翻倍。
    TCPU=Ndraw×Tsetup\displaystyle T_{CPU} = N_{draw} \times T_{setup}

3. DrawCall 本质

一张点单从服务员笔下诞生到后厨开火,要经历三道工序:先写清楚菜名和桌上号,再穿过拥挤的大堂递到出菜口,最后等着大厨看一眼安排进灶台。一次 DrawCall 的完整旅程,也有类似的三段开销。

第一段是状态设置。CPU 要告诉 GPU“现在用什么着色器、绑哪张贴图、开什么混合模式”,这些状态切换需要写入命令缓冲区。第二段是总线传输。顶点数据要从 CPU 内存搬到 GPU 显存,PCIe 总线带宽有限,数据越大搬运越慢。第三段是驱动响应。显卡驱动要把命令翻译成 GPU 能执行的底层指令,再等待 GPU 完成请求并返回状态。

这三段开销里,状态设置和驱动响应的成本几乎是固定的,与模型多复杂、贴图多大关系不大。所以优化思路就呼之欲出:与其减少单次递单的开销,不如减少递单的绝对值。批处理思想的核心,就是把“每道菜跑一趟”变成“同一桌的菜一趟送完”,让固定成本被更多物体摊薄。

与其优化写菜的速度,不如少写几张单。把固定的三件事合并到一次执行里,省下的时间远比抠单次调用的细节要多。

CdrawCall=Cstate+Cbus+Ccmd\displaystyle C_{drawCall} = C_{state} + C_{bus} + C_{cmd}
一次 DrawCall 的总成本 = 状态切换成本 + 总线传输成本 + 驱动命令成本。前两项和最后一项都高度固定,所以减少调用次数才是关键。
  1. 状态切换成本C_state 是 CPU 设置着色器、材质、混合模式等渲染状态的时间。
    Cstate\displaystyle C_{state}
  2. 总线传输成本C_bus 是把顶点、法线、UV 等几何数据从 CPU 内存搬运到 GPU 显存的时间。
    Cbus\displaystyle C_{bus}
  3. 驱动命令成本C_cmd 是显卡驱动把 API 调用转换成 GPU 指令并等待响应的固定开销。
    Ccmd\displaystyle C_{cmd}

4. 静态合批

有一类客人是 VIP 包间,他们提前定好了整桌宴席,菜单不会变,客人也不会挪桌。后厨干脆在开席前就把菜全部备好,放进保温柜,客人一入座就能连续出菜。这类“提前备菜”的思路,对应渲染优化里的静态合批(Static Batching)。

静态合批针对运行时完全不动的物体——地板、墙壁、建筑、路灯这类场景静态物。引擎在构建或加载阶段,把这些物体按相同材质分组,合并成一张超大网格。合并完成后,原本几百个物体的几百次 DrawCall,变成了一张网格的一次 DrawCall。CPU 只需要递一次单,GPU 就能按合并网格里预留的顶点顺序连续画完。

但“提前备菜”要付出代价。合并后的网格没有“单个物体”的概念,视锥剔除只能按整个大网格来做:只要有一小块进入镜头,整个大网格都要送去 GPU。更直观的代价是内存:合并网格要同时保留原始网格数据和合并网格数据,显存占用明显增加。因此静态合批适合那些材质种类少、重复度极高且永不移动的场景物体。

提前备菜是宴席的终极大招,但保温柜要占后厨空间。内存换 DrawCall,这笔账只有在物体足够多、材质足够统一时才划算。

Nmerged=⌈NobjNperBatch⌉\displaystyle N_{merged} = \left\lceil \frac{N_{obj}}{N_{perBatch}} \right\rceil
合批后的 DrawCall 数量约等于“物体总数除以每个批次能容纳的物体数”后向上取整。物体数量被大批量地“打折”了。
  1. 物体总数N_obj 是场景中所有参与静态合批的物体数量。
    Nobj\displaystyle N_{obj}
  2. 每批容纳量N_perBatch 是一个合批网格最多能容纳的物体个数,受顶点数上限和材质一致性约束。
    NperBatch\displaystyle N_{perBatch}
  3. 向上取整\lceil \cdots \rceil 表示无论余数多少都进一位,因为哪怕只剩一个物体也要单独算一批。
    ⌈NobjNperBatch⌉\displaystyle \left\lceil \frac{N_{obj}}{N_{perBatch}} \right\rceil

5. 动态合批

散客区的情况就复杂了。客人随时来、随时点,菜品也会变。聪明的服务员想了个办法:看到有人点柠檬水,不急着马上下单,稍微等几秒,看有没有别的客人也点柠檬水,凑够几张单再跑一趟。这种“现场凑单”的策略,就是动态合批(Dynamic Batching)。

动态合批在运行时实时检测满足条件的小网格,把同一材质、且顶点数足够小的物体临时合并成一次 DrawCall 提交。它最大的优点是不需要提前标记物体,任何物体只要每帧满足条件就能自动合批。但它也有一个硬门槛:单个网格的顶点属性总量一般要求小于 300,否则 CPU 合并数据的成本会超过省下的递单时间。

合并过程本身也有开销。CPU 需要先按材质给物体分组,再逐帧计算每个物体的变换矩阵,把顶点数据重新排列进临时缓冲区。物体越多、顶点越多,这个排序和拷贝的过程就越慢。所以动态合批只适合小块头:UI 图标、小金币、碎粒子、小摆件。一旦网格复杂到动辄上千顶点,动态合批反而会让帧率雪上加霜。

等单是聪明的,但等太久的客人会走。动态合批这笔账要精打细算:顶点数小是前提,CPU 有富余是加分项,小物件凑单永远比大网格硬拼更划算。

Vobj≤Vlimit=300\displaystyle V_{obj} \leq V_{limit} = 300
参与动态合批的单个网格顶点属性总量必须小于等于 300。超过这个数,引擎会放弃合批,单独绘制该物体。
  1. 物体顶点数V_obj 是单个网格包含的顶点属性总数(位置、法线、UV 等叠加计算)。
    Vobj\displaystyle V_{obj}
  2. 合批上限V_limit 是引擎允许动态合批的顶点上限,常规值为 300。
    Vlimit=300\displaystyle V_{limit} = 300
  3. 比较判定\leq 表示“小于等于”,只有满足这个不等式的物体才能被装入动态合批。
    Vobj≤Vlimit\displaystyle V_{obj} \leq V_{limit}

6. GPU实例化

婚礼现场,十桌客人不约而同点了红烧狮子头。大厨面对十张几乎一样的单子,没有选择频繁开火,而是让配菜师傅准备好十份肉丸,自己一次性调好十份酱汁,按同一份菜谱同时下锅。一次备料、一次调味、十份成品——这就是 GPU Instancing 的智慧。

GPU Instancing 的思路是:CPU 只提交一次网格和材质,然后提交一份“实例属性列表”,里面记录每个实例的位置、旋转、缩放、颜色等差异。GPU 在顶点着色器和片元着色器里循环处理这个列表,用同一份几何数据绘制出大量形状相同、但变换各异的物体。

实例化对重复物体多的场景极其有效。树、草、石头、路灯、人群——这些物体网格相同、材质相同,唯一的区别就是位置和朝向。实例化把它们的 DrawCall 从“每个物体一次”压缩到“所有实例一次”。不过它也有适用边界:实例之间不能有独立的骨骼动画、不能有完全不同的材质参数,实例数量也受硬件限制,超出上限后需要分批提交。

一份菜谱、十口锅,大厨的效率直接翻倍。GPU Instancing 是重复物体场景里性价比最高的一招,使用它的前提只有一个:不要在小事情上讲特例。

Msaved=(n−1)×Vper×Bper\displaystyle M_{saved} = (n - 1) \times V_{per} \times B_{per}
实例化节省的显存带宽 = (实例数 - 1) × 单个网格顶点数 × 每顶点字节数。n 越大,省下的带宽越可观。
  1. 重复实例数n 是场景中复用同一个网格的实例总数,比如 100 棵树。
    n\displaystyle n
  2. 单实例数据量V_per 是单个网格的顶点数,B_per 是每个顶点占用的字节数(位置、法线、UV 等)。
    Vper×Bper\displaystyle V_{per} \times B_{per}
  3. 带宽节省量(n - 1) 表示除第一份数据必须完整上传外,其余 n-1 份实例的几何数据都无需重复传输。
    Msaved=(n−1)×Vper×Bper\displaystyle M_{saved} = (n - 1) \times V_{per} \times B_{per}

7. 选型对比

老板终于开始总结经营心得:包间宴席用提前备菜,散客小点用现场凑单,团购大单用批量制作。三种策略没有绝对的好坏,关键看客人是什么类型、点的菜是什么性质。选型对比,就是把这本账算清楚。

静态合批适合永不移动、材质统一、重复度高的场景物体,代价是内存占用上涨和剔除粒度变粗。动态合批适合顶点数小、可移动的小物件,代价是 CPU 要消耗算力做临时合并。GPU Instancing 适合大量重复的相同网格,代价是材质差异必须收敛到实例属性里。三者并不互斥——一个城市街景里,建筑用静态合批、飞行的金币用动态合批、绿化带里的树用实例化,完全可以在同一帧里共存。

选型时还要注意副作用叠加。静态合批后网格不能逐物体剔除,可能造成大量不可见三角形送进 GPU;动态合批排序在物体数量多时有额外开销;实例化受平台支持度影响,移动端的老旧 GPU 可能对实例数量有严格上限。正确的做法不是拍脑袋选一种,而是先用分析工具定位瓶颈,再根据物体运动状态、网格复杂度、材质差异程度做决策。

点单策略没有银弹,只有看客人吃饭的习惯。把三种合批看作工具箱里的三把扳手,对不同规格的螺丝用对应的那一个。

Costdynamic=Nobj×Tsort,Nobj≤300\displaystyle Cost_{dynamic} = N_{obj} \times T_{sort}, \quad N_{obj} \leq 300
动态合批的 CPU 排序成本 = 参与合批的物体数量 × 单个物体排序耗时。物体数越多,合批本身的成本也越高。
  1. 参与物体数N_obj 是尝试进行动态合批的物体数量,物体越多,排序越耗时。
    Nobj\displaystyle N_{obj}
  2. 排序耗时T_sort 是 CPU 为每个物体计算变换、拷贝顶点数据所消耗的单位时间。
    Tsort\displaystyle T_{sort}
  3. 条件约束N_obj \leq 300 提醒我们——动态合批只有在物体数量可控时才划算,数量大了 CPU 反而先被合批过程拖垮。
    Costdynamic=Nobj×Tsort\displaystyle Cost_{dynamic} = N_{obj} \times T_{sort}

8. 实战落地

老板把三种策略写进了员工手册:先观察客流高峰,再定服务流程,最后看客人满意度。这套方法论对应到渲染优化,就是“定位 → 分析 → 改造 → 验证”四步流程,每一步都有对应的工具和指标。

第一步定位:用 Profiler(如 Unity Profiler、RenderDoc、Xcode Instruments)抓一帧,看 DrawCall 数量是否超标、CPU 提交耗时是否超过 GPU 渲染耗时。第二步分析:观察瓶颈物体的材质是否统一、是否有 Static 标记、顶点数分布如何,找出最值得合并的目标。第三步改造:给不动物体勾选 Static 并统一材质;把零散小物件的贴图合并进同一张 Atlas;对重复的植被、石块启用 GPU Instancing。第四步验证:用真机数据对比改造前后的 DrawCall 数量和 P95 帧时间,确认收益且没有引入明显副作用。

实操中有几个容易踩的坑:静态合批后网格无法动态剔除,要留意超大合并网格带来的无效绘制;动态合批在物体多时 CPU 开销反而升高;实例化要求所有实例共享材质,遇到需要独特色彩的物体时,要把颜色放进实例属性而不是拆材质。每一步都要和美术、TA 协作推进,避免为了合批牺牲画面表现或内存预算。

员工手册最重要的一句话:合批的目标永远是让顾客更快吃上饭,不是让后厨更省事。每一次优化都要落到帧时间、内存、画面表现三个指标上,用数据说话。

Gain=Nbefore−NafterNbefore\displaystyle Gain = \frac{N_{before} - N_{after}}{N_{before}}
合批收益 = (合批前 DrawCall 数 - 合批后 DrawCall 数) ÷ 合批前 DrawCall 数。数值越接近 1,说明优化越彻底。
  1. 合批前基数N_before 是优化前单帧的 DrawCall 总数,是优化的起点数据。
    Nbefore\displaystyle N_{before}
  2. 合批后余量N_after 是优化后单帧的 DrawCall 总数,体现合批策略的实际效果。
    Nafter\displaystyle N_{after}
  3. 收益比例差值占优化前的比例,Gain 越接近 1,表示干掉的无用 DrawCall 越多。
    Gain=Nbefore−NafterNbefore\displaystyle Gain = \frac{N_{before} - N_{after}}{N_{before}}

AI 应用场景

情景1:开放世界场景搭建

阶段:场景资产管理 / 资源优化

解决的问题:开放世界地图中,建筑、道路、路灯等静态物体动辄数千个。传统做法每个物体一次 DrawCall,CPU 提交密度不堪重负。利用静态合批,关卡美术在编辑阶段就为物体打上 Static 标记,引擎构建时自动按材质合并网格。

具体案例:某开放世界项目的地铁站场景,改造前 2500 个静态物体产生 1800+ DrawCall,开启静态合批并统一了墙面与家具材质后,DrawCall 降到 210,Loading 时间仅增加 6%。

情景2:移动端虚拟商店

阶段:运行时性能调优

解决的问题:电商 3D 展厅里,用户会旋转、缩放查看商品。商品标签、金币特效、气泡提示等小物件不断生成销毁,不能静态合批。利用动态合批特性,把顶点数控制在 300 以内并统一材质,让这些小物件自动“凑单”。

具体案例:某移动端虚拟试装应用,首饰展示场景有 400+ 悬停小物件,开启动态合批后 CPU 帧耗时从 38ms 降到 22ms,低端机上的卡顿明显缓解。

情景3:自然场景生态渲染

阶段:GPU 资源调度 / 场景实例化

解决的问题:森林、草原、城市绿化中的植被和人群有海量重复网格,动态合批和静态合批都难以满足。利用 GPU Instancing,把同一种树的所有实例数据打包成缓冲,GPU 一次绘制完成数百棵树的渲染。

具体案例:某 PC 端开放世界游戏,森林场景 5000 棵树 + 2000 块石头,实例化后 DrawCall 仅 8 次,植被渲染耗时降低 70%,显存占用保持不变。

Python 演示

python81 行
class DrawCallBatchSimulator:    """模拟渲染场景中的合批决策过程"""     def __init__(self):        self.render_objects = []        self.drawcalls_before = 0        self.drawcalls_after = 0        self.combined_mesh_vertices = 0        self.bytes_per_vertex = 32  # position + normal + uv + color     def add_object(self, name, vertices, material, is_static):        self.render_objects.append({            "name": name,            "vertices": vertices,            "material": material,            "is_static": is_static        })     def count_drawcalls(self):        # 合批前 每个物体一次DrawCall        self.drawcalls_before = len(self.render_objects)        return self.drawcalls_before     def apply_batching(self, max_vertices=300):        # 模拟合批过程        static_groups = {}        dynamic_groups = {}        solo_drawcalls = 0         for obj in self.render_objects:            mat = obj["material"]            if obj["is_static"]:                # 静态物体按材质分组 合并成一个大网格                static_groups.setdefault(mat, []).append(obj)            elif obj["vertices"] <= max_vertices:                # 动态小网格 满足顶点限制则凑单                dynamic_groups.setdefault(mat, []).append(obj)            else:                # 大网格且会动 无法合批 单独绘制                solo_drawcalls += 1         self.drawcalls_after = solo_drawcalls + len(static_groups) + len(dynamic_groups)        self.combined_mesh_vertices = sum(            sum(o["vertices"] for o in objs)            for objs in static_groups.values()        )        return self.drawcalls_after     def memory_estimate(self):        return self.combined_mesh_vertices * self.bytes_per_vertex     def batch_ratio(self):        return 1 - self.drawcalls_after / self.drawcalls_before     def report(self):        ratio = self.batch_ratio()        print("=== 合批模拟报告 ===")        print(f"合批前 DrawCall: {self.drawcalls_before}")        print(f"合批后 DrawCall: {self.drawcalls_after}")        print(f"合批率: {ratio:.1%}")        print(f"静态合并网格顶点数: {self.combined_mesh_vertices}")        print(f"额外显存开销: {self.memory_estimate() / 1024:.1f} KB")  def main():    sim = DrawCallBatchSimulator()    # 场景 一条街道    for i in range(12):        sim.add_object(f"house_{i}", 1200, "brick_material", is_static=True)    for i in range(20):        sim.add_object(f"coin_{i}", 24, "gold_material", is_static=False)    for i in range(3):        sim.add_object(f"car_{i}", 2400, "car_material", is_static=False)     sim.count_drawcalls()    sim.apply_batching()    sim.report()  if __name__ == "__main__":    main()
目的:用一个小模拟器演示合批决策过程——静态物体按材质合并成组、动态小物体在顶点限制内凑单、超限大物体单独绘制,最终输出合批前后的 DrawCall 对比与显存代价。
  1. self.drawcalls_before = len(self.render_objects)合批前每个渲染物体都对应一次 DrawCall,这是优化的基线。
  2. static_groups.setdefault(mat, []).append(obj)静态物体按材质分组,相同材质的物体才有资格合并成同一张网格。
  3. elif obj["vertices"] <= max_vertices动态合批的硬门槛:顶点数超过 300 的小物件会被直接排除。
  4. solo_drawcalls += 1既不静态又超顶点限制的物体(如跑动的车)只能单独绘制,不计入合批。
  5. self.drawcalls_after = solo_drawcalls + len(static_groups) + len(dynamic_groups)合批后的 DrawCall 等于“落单物体数 + 静态分组数 + 动态分组数”。
  6. self.combined_mesh_vertices统计静态合批合并出来的大网格总顶点数,用来估算显存增量。
  7. batch_ratio(self)合批率越接近 100%,说明削掉的冗余 DrawCall 越多。

常见误区

误区我只要把场景里的物体 Inspector 里的 Static 勾上,引擎就会自动帮我合并所有静态物体,DrawCall 立刻降下来。

正解Static 标记只是告诉引擎“这个物体不会动”,合批还需要同时满足多个条件:启用了静态合批选项、物体材质完全相同、使用的网格数据可合并。哪怕只是贴图不同,合并也会被打断。用“包间客人”来比喻:客人定了位子(Static 标记),但点的是完全不同口味的菜(材质不同),后厨还是没法一锅出。

为何常见:因为 Inspector 的 Static 勾选框太显眼,给人一种“一键优化”的错觉,而合批的底层约束藏在引擎文档和 Profiler 里,初学者很少去深挖。

误区只要物体顶点数少、材质一样,动态合批就能自动工作,想合多少合多少。

正解动态合批的“顶点限制 300”是刚性的,但很多人忽略了另一个约束:合批过程本身的 CPU 开销会随物体数量增长。100 个小金币凑单很划算,1000 个小金币凑单时,CPU 光排序、拷贝顶点就忙不过来了。这就像服务员看到 50 桌客人都点柠檬水,于是把 50 张单攒在一起下——他省了跑腿次数,但站在后厨门口等配齐的时间反而更久。

为何常见:因为教程里只强调“顶点少就能合批”,很少有人把合批自身的开销讲清楚,“免费”的错觉就这样形成了。

误区GPU Instancing 这么强,我把场景里所有树都做成实例化,反正一次 DrawCall 就画完了。

正解实例化有两大限制:一是所有实例必须共享同一个网格和材质,不能有独立的骨骼动画,也不能有不同的 Shader;二是单个实例批次能容纳的实例数量受硬件和引擎限制,一般从几百到几千不等,超出上限会被拆成多个批次。用“一次做十份红烧狮子头”的比喻:十份可以一锅出,一百份也许要分十锅,而且只限于“做法完全相同的狮子头”。

为何常见:看多了“实例化 = 一次 DrawCall”的宣传语,容易把“一次”理解成“无限”,忽略了批次容量和材质统一性这两个实际前提。

误区DrawCall 减少了,性能一定变好,合批策略越多越好,无脑把所有物体都合批就完事了。

正解合批是用别的资源换 DrawCall。静态合批把多个网格并成一个大网格,会让视锥剔除失效——露了一小块墙,整面墙的三角形都得送进 GPU;动态合批消耗 CPU 算力;实例化限制材质差异。在某些场景里,合批带来的额外绘制开销可能比省下的 CPU 时间还大。真正的优化原则是:先定位瓶颈,再决定要不要合批。

为何常见:性能优化的世界里,“减少 X”听起来总是正确,但渲染是一个资源置换系统,只盯着 DrawCall 一个指标,就会忽略剔除、内存、CPU 算力等其他成本。

面试题

Q1. 合批的直接目的是减少什么?

单选题 · easy

  • A.减少顶点总数
  • B.减少材质数量
  • C.减少DrawCall数量← 正确答案
  • D.减少纹理数量

答案:C。合批的核心目标是减少 CPU 向 GPU 提交绘制指令的次数,也就是 DrawCall 数量。顶点、材质、纹理的数量会影响渲染开销,但合批本身并不直接减少它们;合批是把多次提交合并成一次,相当于服务员少跑几趟厨房,而不是把菜变少。

Q2. 静态合批发生合并的前提条件是什么?

单选题 · easy

  • A.共享相同网格资源
  • B.使用完全一致的材质← 正确答案
  • C.拥有大量动态顶点
  • D.放置在同一个空间位置

答案:B。静态合批是把不动物体按材质分组后合并,材质完全一致才有资格进入同一批次。共享网格或位置相邻不会触发合批,拥有动态顶点则意味着物体在移动,违背了“静态”的前提。就像包间客人虽然坐在同一桌,但点的菜不一样,后厨仍然无法一锅出。

Q3. 动态合批对单个网格顶点数的一般限制是多少?

单选题 · easy

  • A.仅限300个顶点以内← 正确答案
  • B.可支持上千个顶点
  • C.没有顶点数量限制
  • D.限制由贴图大小决定

答案:A。动态合批要求单个网格的顶点属性总量小于 300,这是引擎的刚性门槛。上千顶点的大网格如果强行合批,CPU 每帧做顶点数据拷贝和矩阵变换的开销会超过省下的递单时间。所以动态合批只适合小物件“凑单”,就像服务员只敢为小饮料凑单,不敢为大菜拼桌。

Q4. GPU Instancing 最适合哪种场景?

单选题 · mid

  • A.大量重复且相同的物体← 正确答案
  • B.少量高精度的角色模型
  • C.地形表面的大片纹理渲染
  • D.半透明液体表面的渲染

答案:A。GPU Instancing 擅长用同一网格和材质绘制大量相同实例,比如 1000 棵树、500 块石头。少量高精度角色模型需要独立动画,地形和液体需要专门的渲染路径,都不适合实例化。它就像大厨用同一份菜谱一次做十份狮子头,前提是十份菜必须一模一样。

Q5. 静态合批的主要内存代价是什么?

单选题 · mid

  • A.增加显存的占用← 正确答案
  • B.降低材质质量
  • C.减少场景光照
  • D.加快加载速度

答案:A。静态合批在合并网格时,既要保留原始网格供碰撞检测等逻辑使用,又要保存合并后的大网格供渲染,两份数据叠加导致显存和内存占用上升。它不会降低材质质量、减少光照或加快加载,反而可能增加加载时间。这就像提前备好的宴席菜要多占几个保温柜。

Q6. 动态合批为什么不适合大型网格?

单选题 · mid

  • A.超出顶点限制会拆分← 正确答案
  • B.材质会被引擎替换
  • C.网格变成透明状态
  • D.加载到内存中失败

答案:A。动态合批的适用前提是顶点属性总量小于 300,大型网格一旦超出限制,引擎就会放弃合批、单独绘制该物体,不会再产生任何合并行为。材质不会被替换,网格也不会变透明或加载失败。就好比服务员想为“整只烤全羊”凑单,但后厨说这道菜必须单独做。

Q7. 为什么合批后的物体可能无法被视锥剔除?

单选题 · hard

  • A.合批网格被视为一个整体← 正确答案
  • B.剔除逻辑被引擎自动关闭
  • C.合批后顶点数据被加密保护
  • D.GPU 无法识别合批后的网格

答案:A。合批把多个物体合并成一张大网格,剔除算法只能以整张网格为单位判断。只要大网格的一小部分进入镜头,整张网格的三角形都会提交给 GPU,哪怕其他部分不可见。这在静态合批中尤为明显。就像一桌宴席只要有一道菜上桌,整桌菜都算“已在出菜流程中”,无法只端走其中一盘。

Q8. 以下哪种情况会导致合批失败?

单选题 · hard

  • A.物体设置了动态标记
  • B.网格材质各不相同← 正确答案
  • C.物体位置不在原点
  • D.网格顶点数少于三百

答案:B。合批的核心条件之一是材质完全一致。材质不同意味着渲染状态不同,GPU 无法在一次提交里切换多套状态,所以合批必然失败。物体位置不影响合批,顶点数少于三百正好满足动态合批条件,设置动态标记也不一定会导致失败。材质不一致,就像同一张单子上写了川菜和粤菜,大厨没法同时开两个灶。

Q9. 合批策略可能带来哪些副作用?

多选题 · mid

  • A.网格合并增加显存占用← 正确答案
  • B.无法逐物体进行视锥剔除← 正确答案
  • C.降低渲染画面的整体分辨率
  • D.增加场景制作与维护复杂度← 正确答案

答案:ABD。合批用资源换时间:静态合批合并网格会抬升显存,合批后剔除粒度变大无法逐物体剔除,同时合批对材质和顶点数的约束也增加了美术和程序在场景制作时的协商成本。合批不会降低分辨率——分辨率是渲染管线参数,与 DrawCall 数量无关。C 是典型干扰项。

Q10. 关于三种合批策略,下列说法正确的是?

多选题 · hard

  • A.静态合批会提升内存开销← 正确答案
  • B.实例化适合大量相同网格← 正确答案
  • C.动态合批可以处理大型网格
  • D.合批后剔除粒度会变大← 正确答案

答案:ABD。静态合批合并网格必然增加显存与内存;GPU Instancing 正是为大量相同网格而设计的;任何合批都会把多个物体变成一个大整体,从而让剔除粒度变大。动态合批严格限制在 300 顶点以内,大型网格不在适用范围内。合批本质是“用内存换 CPU 时间、用剔除粒度换 DrawCall 数量”的权衡。