“像素山谷”工作室的渲染主程小葵最近很头疼。新机型上,她精心打造的《山谷小镇》掉帧严重,从 60 帧跌到 20 帧。奇怪的是,GPU 这颗“显卡心脏”的利用率只有 30%,看起来闲得很,CPU 却忙得快要冒烟。她请来资深的 GPU 师傅“大黄”诊断,大黄只看了一眼渲染日志就笑了:“你一帧给我发了 800 单,每单都要我停车、核对地址、重新热车。我这辆卡车再能装,也经不起你把它当出租车开呀。” 小葵恍然大悟:在 3D 渲染管线里,CPU 是“调度员”,GPU 是“卡车司机”。而 Draw Call,就是 CPU 每帧向 GPU 下达的一次“发车指令”——“把这个物体从这个位置画出来”。每一条指令都有固定开销:校验数据、切换状态、打包命令。物体越多、指令越多,CPU 越忙;卡车虽然快,却要一趟趟空等。解决思路也像快递一样朴素:把去往同一个方向、使用同一种“车厢”(材质)的包裹,装进同一个集装箱,一次发车。这,就是 Draw Call 批处理。 本文从小葵的“集装箱改造日记”出发,带你理解批处理的基本原理、三种主流方案(静态合批、动态合批、GPU 实例化)、配套的纹理图集技巧,以及工程实战中的取舍与排错。看完你就能明白:为什么 GPU 很闲、游戏却卡成 PPT,以及如何让帧率起死回生。
学会 Draw Call 批处理,你就掌握了实时渲染性能优化的“第一性原理”。无论做游戏、数字孪生还是 AI 生成内容的可视化落地,这项技能都能让你的渲染帧率产生肉眼可见的提升,是图形与可视化工程师晋级路上的必修课。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
基本概念:一帧就是一班送货周期
《山谷小镇》上新机型的第一天,小葵就被玩家的吐槽淹没:“卡成 PPT 了!”她打开 Profiler 一看,CPU 在渲染线程上耗了 14ms,GPU 却只忙了 4ms,而 60 帧留给一帧的时间只有 16.7ms。大黄师傅叼着烟点评:“这不是车跑得慢,是你发车发得太多了。”
在 3D 渲染管线里,CPU 负责“想清楚画什么”,GPU 负责“动手画出来”。想清楚画什么的结果,就是一条条绘制命令:画这个网格、用这个材质、放在这个位置。每一条命令,就叫一个 Draw Call(绘制调用)——你可以把它想象成调度员对司机喊出的一句“发车口令”。一帧要画的物体越多,Draw Call 就越多,就像快递站要发的单子越多,调度员要做的派单动作就越繁琐。
很多人以为渲染慢就是 GPU 不行,但在物体数量庞大的场景里,真正的瓶颈往往是 CPU。每次 Draw Call,CPU 都要做驱动校验、状态检查、命令打包,这些工作在物体很小的时候也一笔不少:
所以小葵的任务很清晰:让 CPU 少发车。批处理,就是把几十上百个零散的“小包裹”合并成几趟“大集装箱”,让调度员省下大把时间。接下来的每一节,都是小葵的装箱实战记录。
- 帧总耗时 \(T_{\text{frame}}\)渲染一帧画面花掉的总时间,它必须小于 16.7ms 才能保住 60 帧每秒。它由 CPU 准备时间和 GPU 执行时间两部分构成。
- Draw Call 数量 \(N_{\text{draw}}\)一帧里 CPU 发出的绘制指令条数。物体越多、拆得越碎,这个数就越大,是优化最该盯住的变量。
- 单次调用开销 \(C_{\text{call}}\)每条指令的固定成本,包括驱动校验、状态检查与命令打包,它与物体大小无关,哪怕画一个点也要付这么多。
- GPU 渲染耗时 \(T_{\text{gpu}}\)GPU 真正执行光栅化、着色、写帧缓冲的时间,这部分通常与三角形数量成正比,而不是与物体个数成正比。
批处理原理:为什么集装箱能省钱
小葵在调度室蹲了一夜,把大黄每次发车的动作拆解开:核验派单表(驱动校验)、检查车厢状态(渲染状态检查)、把包裹搬上车(命令打包)、发车(提交命令)。她惊愕地发现,不管包裹多重,这四步动作一样都不能少——这就是 Draw Call 的固定开销。既然每单都要付一次“起步价”,那把多个包裹塞进同一辆车,起步价不就只付一次了吗?
批处理的思路,就是让一次发车同时拉走多个包裹,从而摊薄这份固定开销。但包裹不能随便捆:GPU 的“车厢”由材质决定,换了材质,就要重新喷漆、重新布置车厢,这是所有状态切换里最贵的一种。所以合批的第一铁律是——同材质才能同车。渲染引擎的渲染队列按材质排序,原理也是如此:让同材质的物体聚在一起,减少“换车厢”的次数。材质、网格、顶点格式等条件共同决定了合并策略的分类,也就是后面要讲的静态合批、动态合批与 GPU 实例化三兄弟。
如果把一个 Draw Call 比作一次发车,批处理就是快递行业的集装箱革命:同样的货物,发车次数从一千降到了几十,调度员终于能喝口茶了。接下来,小葵会给你展示三种装箱方案:焊死的、现拼的、还有递清单的。
- 物体总数 \(N_{\text{obj}}\)场景中需要绘制的物体个数,是合批的输入原材料,一帧 1000 个物体就是 1000 件待装车的包裹。
- 单批容量 \(K_{\text{batch}}\)一个批次最多能装下的物体数量,受顶点数上限、材质一致、网格可合并性等条件约束,是“集装箱”的容积。
- 向上取整 \(\lceil \cdot \rceil\)就算最后一箱只装了一件包裹,也得发一趟车,所以批次数必须向上取整,不能四舍五入。
- 合并后批次数 \(N_{\text{batch}}\)优化后的发车次数,它直接取代帧耗时公式里的 \(N_{\text{draw}}\),目标是把成百上千压到几十。
静态批处理:把不挪窝的家具焊成一体
小镇广场上,花坛、长椅、路灯都是“钉子户”,十年不挪窝。小葵想:既然这些包裹永远不会动,为什么不提前把它们焊成一体?于是她把广场上所有共享同一材质的静态物体,按区域焊成了几块“巨无霸”,从此大黄经过广场只需要停一次车。
这就是 静态批处理(Static Batching):在场景加载阶段,把不动的网格合并成一个大的组合网格,渲染时一次 Draw Call 画完。它的优点是“一劳永逸”——合并工作在运行前就完成了,运行时零额外消耗。但天下没有免费的午餐,焊死的大铁箱有两个代价:一是顶点数据要被复制进新的合并缓冲,内存近似翻倍;二是只能按合并后的整体做视锥剔除,不能再单独判断某把长椅是否在屏幕外。
“焊死的大铁箱”很能装,但占仓库,而且不能单独退货。所以静态合批最适合“永远不动、数量庞大、共享材质”的场景,比如室内装修、建筑外立面、城市街道设施——小葵的广场改造大获成功,Draw Call 从 400 降到了 50。
- 场景原始内存 \(M_{\text{scene}}\)所有小网格原本占用的顶点与索引内存,是所有小包裹各自的“占地面积”之和。
- 合并缓冲内存 \(M_{\text{merged}}\)合批后新生成的大网格缓冲,顶点数据是从各处复制过来的,原始数据并不会消失。
- 静态合批总内存 \(M_{\text{static}}\)新旧两份同时存在,所以内存近似翻倍,这就是“焊死”要付的仓库租金。
动态批处理:每帧现拼现发的拼箱服务
街上的行人、小狗、飘动的旗子天天乱跑,不能焊死。小葵于是改成“当日现拼”:每天清晨,把要去同一方向的包裹临时装进一个集装箱,发完就拆。第二天路线变了,再重新拼。这就是动态批处理的日常。
动态批处理(Dynamic Batching) 每帧把符合条件的移动物体临时合并,提交一次 Draw Call。它的工作流程是:每帧把各物体的顶点变换到世界空间,写入一块临时合并缓冲,再一次性提交给 GPU。但有三个硬性限制:批次内总顶点数有上限(Unity 里大约是 900 个顶点)、参与合批的物体必须使用相同材质、骨骼蒙皮网格等特殊网格不能参与。超过上限,引擎只能默默拆成两个批次。
现拼现发有好处也有成本——拼箱本身要花 CPU 时间。如果包裹太大太多,拼箱的功夫可能比省下的发车还贵,所以动态合批在移动端要慎用。小葵给大黄的规矩是:小件、同材质、总数少,才值得每天现拼。
- 第 i 个物体顶点数 \(v_i\)每个参与合批的小物体自己的顶点数量,是包裹的体积。
- 求和符号 \(\sum\)同一批次的顶点数要逐件累加,代表拼箱后包裹的总体积。
- 顶点上限 \(V_{\text{max}}\)动态合批的顶点预算(如 900),超过这个数引擎无法用单批处理,只能另开一批。
- 约束关系 \(\le\)它保证了“一箱装得下”是合批成立的前提,装不下时宁可拆箱也不要强塞。
GPU实例化:一张清单种出千棵树
山谷里有 5000 株一模一样的薰衣草。小葵没把它们焊成一体,而是给大黄递了一张清单:“草长这样(网格),这是 5000 个种植坐标(变换矩阵),你按清单种。”大黄开着车按清单跑,一趟活全干完。这就是 GPU Instancing 的朴素思想。
GPU 实例化(Instancing) 的理念正是如此:网格只有一份,每帧只需提交“网格 + 实例数据表”(每个实例的变换矩阵、颜色等属性),GPU 内部循环绘制所有实例。与合批“合并顶点”不同,实例化是“复用顶点、只换属性”,所以它不需要把 5000 份顶点数据复制来复制去,CPU 和内存压力都小得多。实例数据通常存放在专门的实例缓冲(StructuredBuffer)里,GPU 每处理一个实例就读取一行数据。
与合批的“合并顶点数据”不同,实例化是“复用同一份顶点、只换属性”,所以它特别适合草、树、路灯、人群这类“同模型大量出现”的对象。小葵的薰衣草山谷,Draw Call 从 5000 直接降到了 1。
- 实例数 \(N_{\text{inst}}\)需要绘制的副本总数,比如 5000 株薰衣草,这个数决定清单有多长。
- 单实例数据量 \(S_{\text{data}}\)每个实例只需传变换矩阵和可选的颜色等属性,通常只有几十到一百多字节。
- 总带宽 \(B_{\text{inst}}\)GPU 读取这份清单消耗的显存带宽,它控制着实例化方案的“运费”。
纹理图集:一面墙贴满所有招牌
小镇夜市有 20 家小店,每家招牌都是一块独立的布。大黄每画完一家,就要换一块布(纹理切换),换来换去刹车片都磨没了。小葵索性把 20 块招牌印到同一张巨型喷绘布上,画整条街时只需挂一次布。这张巨型喷绘布,就是 纹理图集(Texture Atlas)。
为什么图集能省 Draw Call?因为材质切换是 GPU 最贵的状态切换之一,而纹理引用是材质定义的一部分。多张小纹理打包成一张大纹理后,原本“材质不同”的网格现在引用同一张纹理,材质就“相同”了,合批条件被满足。代价是要做 UV 重映射:把原来指向小块纹理的 UV 坐标,换算成指向大图中对应区块的坐标。
图集不是万能药:大图占显存、mipmap 可能渗色、还要留 padding。但为了“同车同材质”这个合批前提,把这 20 块招牌印到一张布上,是性价比极高的一招。小葵的夜市一条街,从此一帧一趟车。
- 原始 UV 坐标 \((u, v)\)物体在小纹理上的采样坐标,范围是 0 到 1,是“招牌布上的具体位置”。
- 行列数 \(C, R\)图集把大图划分成 \(R\) 行、\(C\) 列,每个小格对应一张原来的小纹理。
- 区块索引 \((i, j)\)当前小块纹理位于第 \(i\) 行、第 \(j\) 列,它决定平移量,相当于“这块招牌挂在大布的哪个位置”。
- 映射后的坐标 \((u', v')\)新的 UV 坐标,GPU 用它在整张大图上采样,材质得以统一,合批随之成立。
工程实践:先看行车记录仪,再动手装箱
经历了几轮折腾,小葵学乖了:动手装箱前,先看行车记录仪。Profiler 会告诉她:到底是调度室忙(CPU 瓶颈),还是大黄在路上堵(GPU 瓶颈)?如果 GPU 已经 95% 满载,合批救不了帧率,得先减面或降分辨率;如果 CPU 的 Render Thread 时间爆表、Draw Call 计数高得吓人,才轮到合批发威。
优化的优先级有讲究:第一优先永远是“不画看不见的东西”——视锥剔除和遮挡剔除能让你白赚 30%~60% 的性能;然后是 LOD,远处的物体换轻量模型;最后才轮到合批。合批内部也有顺序:静态合批最划算,GPU 实例化次之,动态合批因为每帧有拼箱成本要放最后。渲染队列会按材质排序,进一步减少状态切换。
移动端尤其要小心:小卡车的油箱小(带宽与功耗有限),频繁发车和过度合批导致的内存上涨都可能翻车。小葵的最终工作流是:先剔除,再 LOD,然后静态合批带头、实例化殿后、动态合批压轴——这台“先计算后装箱”的铁律,让《山谷小镇》在新机型上稳稳回到了 60 帧。
- 场景物体总数 \(N_{\text{total}}\)关卡里摆的所有物体,无论看得到看不到,它们都挂在渲染列表里等待被安排。
- 可见比例 \(P_{\text{visible}}\)经过视锥剔除、遮挡剔除后仍然可能出现在屏幕上的物体占比,通常在 30%~70% 之间。
- 可见物体数 \(N_{\text{visible}}\)真正需要进入渲染管线的数量,是 Draw Call 的上限来源,先砍它等于免费砍掉大量发车。
问题排查:合批失败与收益评估
上线第二天,测试同学报 bug:“某个街区合批后反而更卡!”小葵打开 Frame Debugger 一看,集装箱被拆得七零八落——原来里面混了几件“特殊包装”的包裹。合批条件有多挑剔?她踩过的坑能写一页纸:两个物体哪怕 Shader 相同,只要材质参数不同(颜色、贴图引用),就是不同材质,不能同车;光照模式、阴影设置、顶点属性格式不一致,引擎也会拒绝合批。
动态合批还有自己的陷阱:它是“每帧现拼”,拼箱本身的 CPU 成本可能超过省下的发车成本。当物体数量大、顶点多、变换频繁时,动态合批往往得不偿失。遇到“合批后更卡”,优先检查三件事:材质是不是同一罐漆、光照模式是否统一、顶点格式是否兼容;然后用 Frame Debugger 逐帧确认合批是否真的生效。
评估一次优化,不能只看 Draw Call 数字这一项指标:用加速比衡量收益,同时盯着内存增量和剔除粒度。合批是工程权衡的艺术,不是把数字压到最小就赢了——小葵最终在“发车次数、仓库租金、掉头灵活性”之间找到了平衡,山谷小镇的快递网络从此高效运转。
- 优化前调用数 \(N_{\text{before}}\)合批改造前每帧的 Draw Call 数量,是“没装箱时的发车次数”。
- 优化后调用数 \(N_{\text{after}}\)合批改造后每帧的 Draw Call 数量,是“装箱后的发车次数”。
- 加速比 \(R_{\text{speedup}}\)两者相除得到收益倍数。它必须配合内存增量一起看:加速比高但内存翻三倍,未必划算。
AI 应用场景
场景1:开放世界游戏的海量植被渲染
阶段:AI 内容生成后的渲染落地
解决的问题:AI 生成工具一夜之间种了 10 万棵树、5 万丛花。如果每棵一个 Draw Call,CPU 直接瘫痪——这正是“GPU 很闲、CPU 冒烟”的 CPU-bound 噩梦。用静态合批处理不动的山体巨石,用 GPU 实例化处理几十万同种树木,用纹理图集统一不同植被的材质,才能让 AI 生成的美术资源跑得动。
具体案例:某开放世界项目中,10 万棵树木先经过视锥剔除与 LOD 过滤到 2 万棵可见,再按树种分组做 GPU Instancing,最终每帧 Draw Call 从几十万降到两百以内,主机端稳定 30 帧、PC 端稳定 60 帧。
场景2:数字孪生城市的批量设备可视化
阶段:数字孪生数据驱动的实时可视化
解决的问题:城市级 BIM 模型包含上百万个构件:路灯、井盖、消火栓、门窗。传统方式一个构件一次 Draw Call,浏览器端直接卡死。把重复构件按类型做静态合批,把动态点位用实例化渲染,能支撑城市级场景的流畅漫游。
具体案例:某智慧园区项目把 3.6 万个路灯全部静态合批为 12 个批次,把 8000 个智能井盖用实例化渲染,整体帧率从 8fps 提升到 45fps,同时显存占用只增加了 15%。
场景3:自动驾驶仿真环境的批量渲染
阶段:AI 仿真训练数据合成
解决的问题:自动驾驶仿真一次要渲染上百辆同款车、上千个行人,还要生成多视角数据。逐物体绘制导致仿真吞吐量极低,数据生成速度拖慢模型训练。GPU Instancing 让同型号车辆、同动作行人共享网格,大幅提升一帧内的有效物体数。
具体案例:某仿真平台用 GPU Instancing + 纹理图集将同场景车辆行人 Draw Call 减少 78%,单节点仿真吞吐量从每天 2000 帧提升到 6000 帧,合成数据成本显著下降。
Python 演示
# -*- coding: utf-8 -*-# 模拟 Draw Call 批处理: 按材质分组 + 顶点上限装箱# 场景是 2000 个散落在街边的小物件, 分属 4 种材质 class RenderObject: # 一个待渲染物体: 名称 / 材质 ID / 顶点数 def __init__(self, name, material_id, vertex_count): self.name = name self.material_id = material_id self.vertex_count = vertex_count class BatchPlanner: # 批处理规划器: 统计合批前后的 Draw Call 数量与 CPU 耗时 def __init__(self, call_cost=0.008, switch_cost=0.015, budget_ms=16.7): self.call_cost = call_cost # 单次 Draw Call 固定开销(ms) self.switch_cost = switch_cost # 每次材质/状态切换开销(ms) self.budget_ms = budget_ms # 一帧预算: 16.7ms def count_batches(self, objects, max_vertices=900): # 先按材质分组, 再对每组按顶点上限拆批 groups = {} for obj in objects: groups.setdefault(obj.material_id, []).append(obj) total_batches = 0 for material_id in groups: batch_vertices = 0 for obj in groups[material_id]: if batch_vertices + obj.vertex_count > max_vertices: total_batches += 1 batch_vertices = 0 batch_vertices += obj.vertex_count if batch_vertices > 0: total_batches += 1 return total_batches def estimate_cpu_ms(self, batch_count): # 每次发车都要付一次调用费 + 一次状态切换费 return batch_count * (self.call_cost + self.switch_cost) def main(): # 街道场景: 2000 个 240 顶点的小物件, 材质 ID 循环为 0/1/2/3 objects = [RenderObject("obj_%d" % i, i % 4, 240) for i in range(2000)] planner = BatchPlanner() before = len(objects) # 不合批: 一个物体就是一个 Draw Call after = planner.count_batches(objects) # 合批后 t_before = planner.estimate_cpu_ms(before) t_after = planner.estimate_cpu_ms(after) print("before batching: draws = %d, cpu = %.2f ms" % (before, t_before)) print("after batching: draws = %d, cpu = %.2f ms" % (after, t_after)) print("speedup = %.1f x" % (float(before) / after)) if t_after < planner.budget_ms: print("result: within %.1f ms frame budget, smooth" % planner.budget_ms) else: print("result: still over budget, need more culling or batching") if __name__ == "__main__": main()
objects = [RenderObject("obj_%d" % i, i % 4, 240) for i in range(2000)]生成 2000 个小物件,材质 ID 在 0~3 间循环,模拟四种不同“车厢”。groups.setdefault(obj.material_id, []).append(obj)按材质 ID 分组,材质不同不能同车,这是合批的第一铁律。if batch_vertices + obj.vertex_count > max_vertices:顶点数超过 900 就“另开一箱”,模拟动态合批的顶点上限约束。before = len(objects)合批前每个物体一次 Draw Call,2000 个物体就是 2000 次发车。float(before) / after计算合批加速比,衡量这轮优化的收益倍数。
常见误区
误区以为 Draw Call 越小性能越好,于是把所有模型都塞进一个大网格里,Draw Call 变成 1,结果帧率反而没提升,内存还爆了。
正解合批是“用空间换时间”的工程权衡。静态合批让内存近似翻倍、让剔除粒度变大——只要庞大网格有一部分可见,整块都要进渲染管线。真正的目标是“让 CPU 不再是瓶颈”:把 Draw Call 从 5000 压到 200,CPU 已经轻松留在 16.7ms 预算内,再往下压就是负收益了。
为何常见:新手容易把单一指标当成排行榜分数来比,忽略了内存、剔除粒度和动态合批成本这些同样昂贵的代价。
误区觉得引擎勾上 Dynamic Batching 选项,就会自动魔法般合批,CPU 躺着省力。
正解动态合批每帧都要把所有参与物体的顶点变换到世界空间、写入临时缓冲、再提交 GPU,这本身就是 CPU 开销。当参与物体数量大、顶点多时,拼箱成本可能超过省下的发车成本,出现“合批后更卡”的怪象。这正是小葵在夜市踩过的坑。
为何常见:“动态”这个前缀听起来像自动化魔法,加上引擎默认勾选,让人误以为它是免费的。
误区明明用了同一个 Shader 材质,引擎却不合批,怀疑是引擎渲染管线的 bug。
正解合批条件比想象中严苛:哪怕 Shader 相同,只要材质参数(颜色、贴图引用)不同,就是“不同材质”;光照模式、阴影设置、顶点属性格式不一致,同样会阻断合批。Frame Debugger 的“为什么合不批”面板会逐条列出原因,逐项核对比甩锅给引擎高效得多。
为何常见:渲染引擎的合批决策是黑盒,初学者看不到内部判定逻辑,自然容易归因于 bug。
误区觉得 Instancing 就是“种树种草”用的,城市里建筑各不相同,用不上。
正解实例化的本质是“同一份网格 + 不同的变换/属性”。大量相同的路灯、井盖、门窗、桌椅、人群、道具副本,都是天然的实例化对象。建筑外形不同,但同类门窗、栏杆、玻璃幕墙单元往往相同,照样能用。变体差异用每实例数据(矩阵、颜色、缩放)表达即可。
为何常见:教程和案例最爱拿草和树举例,形成“实例化 = 植被优化”的刻板印象。
面试题
Q1. 在 3D 渲染管线中,Draw Call 指的是什么?
- A.GPU 向 CPU 回传渲染结果的信号
- B.CPU 向 GPU 发出的单次绘制指令← 正确答案
- C.显卡厂商提供的硬件型号代号
- D.纹理在显存中的存储格式名称
答案:B。Draw Call 是 CPU 通过图形 API 发给 GPU 的“画这个物体”命令,包含网格、材质、变换等关键信息。GPU 向 CPU 回传的是读取结果,与指令定义无关。B 正确,其余选项描述的都是别的概念。
Q2. 某场景 GPU 利用率只有 35%,CPU 帧耗时接近预算上限,最可能的原因是?
- A.显卡性能太弱导致渲染过慢
- B.每帧 Draw Call 过多致 CPU 瓶颈← 正确答案
- C.屏幕分辨率设置得过高导致卡顿
- D.显存容量不足导致纹理数据丢失
答案:B。GPU 闲、CPU 忙是典型的 CPU-bound 特征,正是帧耗时公式里 N_draw 过大造成的。显卡弱会表现为 GPU 满载,分辨率过高会拉高 GPU/填充率压力,显存不足则会出现纹理丢失而非单纯掉帧。
Q3. 60 帧每秒渲染目标下,单帧可用的总预算约为?
- A.16.7 毫秒← 正确答案
- B.33.3 毫秒
- C.60.0 毫秒
- D.100.0 毫秒
答案:A。1 秒等于 1000 毫秒,除以 60 帧约等于 16.7ms。这是 CPU 指令准备与 GPU 渲染共享的总预算,一旦超出,帧率就会跌破 60,表现为肉眼可见的卡顿。
Q4. 关于静态批处理,下列说法正确的是?
- A.每帧重新合并所有网格且不增加内存
- B.适合频繁移动的物体且成本极低
- C.静态网格提前合并,减调用但增内存← 正确答案
- D.合批后每个物体仍可独立视锥剔除
答案:C。静态合批把不动的网格在加载期焊成大网格,Draw Call 下降但顶点数据被复制进新缓冲,内存上升;合并后只能按整体剔除,D 与事实相反。A/B 描述的是动态合批的特征,且动态合批也不免费。
Q5. Unity 中动态合批对单批顶点数的主流限制约为?
- A.上限约三百顶点
- B.上限约九百顶点← 正确答案
- C.上限六万五顶点
- D.完全没有上限
答案:B。动态合批要求批次内总顶点数不超过约 900 个(不同平台略有差异),超过则另起一批。65536 是单个网格索引的经典上限,与动态合批的每帧合并约束无关。
Q6. 以下哪些情况会导致合批失败(无法合并)?
- A.使用了不同的材质实例← 正确答案
- B.光照与阴影模式不一致← 正确答案
- C.顶点属性格式不相同← 正确答案
- D.场景中位置完全相同
答案:ABC。材质实例不同(即使 Shader 相同,参数不同也算不同材质)、光照/阴影模式不同、顶点格式不同,都会让 GPU 无法用同一渲染状态绘制,从而阻断合批。位置相同不影响合批条件,反而可能触发其他优化。
Q7. 纹理图集降低 Draw Call 的核心原因是?
- A.让纹理占用的显存变小
- B.让多网格共用材质满足合批← 正确答案
- C.让 GPU 采样纹理速度翻倍
- D.让 CPU 不再处理纹理数据
答案:B。图集把多张小纹理装进一张大纹理,多个网格引用同一张图就拥有了“同一材质”,从而满足合批前提,减少纹理切换与 Draw Call。图集本身并不保证显存变小,也不直接加速采样。
Q8. 关于 GPU Instancing,下列说法正确的是?
- A.适合绘制大量相同网格的物体← 正确答案
- B.一次调用可绘制多个不同变换实例← 正确答案
- C.每实例的顶点数据须单独保存
- D.与动态合批相比 CPU 开销更低← 正确答案
答案:ABD。Instancing 复用同一份顶点数据,靠每实例数据(变换矩阵、颜色等)区分副本,所以 C 错误;它不需要每帧在 CPU 端重建顶点缓冲,CPU 开销通常显著低于动态合批。A、B、D 都是实例化的典型特征。
Q9. 静态合批后内存显著上升的根本原因是?
- A.纹理被复制到多个不同画布
- B.顶点数据被复制进新的合并缓冲← 正确答案
- C.帧缓冲的分辨率被自动提高
- D.渲染状态缓存无法被释放
答案:B。合批把各小网格的顶点复制并拼接成新的大网格缓冲,原始数据仍保留,因此内存近似翻倍。纹理复制、帧缓冲分辨率、状态缓存都与静态合批没有直接关系。
Q10. 动态合批的局限性包括以下哪些?
- A.批次内总顶点数存在硬上限← 正确答案
- B.参与合批物体须用相同材质← 正确答案
- C.每帧合并本身会消耗 CPU← 正确答案
- D.参与合批的物体必须静止
答案:ABC。动态合批专门服务“会动”的物体,D 刚好说反;顶点上限、同材质要求以及每帧拼箱的 CPU 成本,都是动态合批的真实局限。这也是它在移动端需要谨慎使用的原因。