简介:DrawCall数量过多是实时渲染中CPU瓶颈的常见来源。本文对比逐物体直绘与合批渲染两类提交方案,并进一步拆解静态合批、动态合批与GPU实例化三种通用技术,从CPU/GPU开销、内存成本、灵活性、剔除行为与接入难度等维度分析差异,给出典型场景的选型依据与迁移注意事项,帮助开发者建立系统的渲染性能优化决策框架。
在实时渲染中,一帧画面的生成可以粗略划分为CPU阶段与GPU阶段。DrawCall(绘制调用)是CPU通过图形编程接口(Graphics API)发起一次渲染的命令:在渲染管线的应用阶段,CPU需要从硬盘或内存中读取模型、纹理等资源并上传到显存,设置本次渲染所需的渲染状态(材质、着色器、光照、混合模式等),准备好顶点数据与变换矩阵,最后调用一次DrawCall,把“这一批”数据交给GPU。可以简单理解为:DrawCall就是CPU对GPU说的一句“请开始渲染这个物体”。
单看一次DrawCall,指令本身并不重,真正的开销藏在周边环节:API调用与驱动层的参数校验、渲染状态的切换、CPU与GPU之间的命令提交与同步等。当场景中有成百上千个独立物体时,如果每个物体各自提交一次DrawCall,CPU会被大量重复的准备工作占满,GPU反而处于等待状态——这就是典型的“CPU受限”型渲染瓶颈。
因此,实际项目中围绕DrawCall形成了两类基本处理思路:一是保持逐物体提交的结构,仅做局部调优;二是通过合批(Batching)把多次绘制合并为少数几次提交。两种思路在成本结构、灵活性、适用范围上差异明显,值得系统对比,这也是本文的对比主题。
方案A:逐物体直绘(不合并提交)
指场景中每个可渲染物体独立走完“设置渲染状态—准备数据—提交DrawCall”的完整流程。每个物体保留独立的材质、网格与变换信息,渲染逻辑最直观,逻辑对象与绘制表现一一对应。
方案B:合批渲染(Batching)
指在提交前把多个物体的绘制合并为更少的DrawCall。业界通用的合批技术主要有三类:
直绘方案下,N个物体约等于N次DrawCall,每次调用都伴随状态设置与驱动层校验,CPU开销随物体数量线性增长。合批方案将N次提交压缩为1次或少数几次,调用与校验开销大幅下降;但需要注意,不同合批技术是把开销“转移”到了不同位置:静态合批把成本转移到构建期的数据合并,动态合批转移到每帧的顶点变换与拼接,GPU实例化则转移到实例数据的组织与着色器支持上。
直绘最灵活,每个物体可独立移动、更换材质、参与不同排序。静态合批合并后以批次为单位,物体不能再单独移动或单独换材质,需要变动时往往要重建批次。动态合批允许物体自由移动,但受“材质相同、顶点量足够小”等条件限制。GPU实例化的实例可独立移动并设置参数,但网格与材质必须相同,灵活性介于两者之间。
直绘的物体粒度细,视锥剔除效果好,视锥外物体不会产生GPU负担。合批后剔除粒度变粗:同一批次内只要有一个物体可见,整批都可能被提交处理,可能带来额外的顶点处理负担。常见的缓解做法是按空间区域分块合并,在“批次数量”与“剔除精度”之间取得平衡。
不透明物体通常可按材质排序以减少状态切换,合批与排序的目标一致;透明物体则需要按深度由远及近排序以保证混合结果正确,强行合批可能与渲染正确性冲突,通常需要保留逐物体排序或限制合批范围。
直绘零改造但优化空间也最小;静态合批需要预先标记静态物体并纳入构建流程,对频繁变动的场景维护成本高;动态合批通常由引擎按条件自动执行,接入成本低但可控性差;GPU实例化要求材质与着色器支持实例化语义,自定义着色器可能需要改造,接入门槛略高,但在大规模重复物体上收益最显著。
示意性伪代码对比:
// 方案A:逐物体直绘for obj in scene.visibleObjects:setRenderState(obj.material) // 材质、着色器、混合模式等状态setTransform(obj.matrix) // 变换矩阵submitDrawCall(obj.mesh) // N 个物体 → N 次 DrawCall// 方案B:GPU实例化示意mesh, material = sharedResource // 共用同一网格与材质for obj in scene.sameKindObjects:instanceData.append(obj.matrix, obj.color) // 收集实例差异setRenderState(material)submitDrawCallInstanced(mesh, instanceData) // N 个物体 → 1 次 DrawCall
| 对比维度 | 逐物体直绘 | 静态合批 | 动态合批 | GPU实例化 |
|---|---|---|---|---|
| DrawCall数量 | 高,与物体数线性相关 | 低 | 较低 | 最低 |
| CPU每帧开销 | 高 | 低 | 中,随规模上升 | 低 |
| 内存占用 | 基准 | 明显增加(网格副本) | 较低(临时缓冲) | 最低 |
| 物体可移动性 | 完全自由 | 合并后不可单独移动 | 自由 | 自由(实例级参数) |
| 网格/材质要求 | 无限制 | 无硬性要求 | 材质相同、顶点量小 | 网格与材质必须相同 |
| 接入改造量 | 无 | 需标记静态、构建期处理 | 基本自动 | 需着色器支持实例化 |
| 剔除精度 | 物体级 | 批次级,需分块缓解 | 物体级 | 实例级或批次级 |
| 典型场景 | 物体少、逻辑复杂 | 建筑、地形装饰等静态物 | 小型频繁移动物体 | 植被、碎石、人群等重复物 |
DrawCall本质上是CPU向GPU发起的一次绘制命令,其性能问题的根源在于大量重复提交带来的CPU侧开销。逐物体直绘结构清晰、灵活度最高,但物体规模大时容易让CPU成为瓶颈;合批渲染通过更少次数的提交换取性能,代价则体现为内存、灵活性、剔除精度与接入改造等不同形式的成本。静态合批、动态合批与GPU实例化分别在“静态不动、小型移动、大规模重复”三类场景中最具性价比。选型的关键不是追求DrawCall数字最低,而是先定位真实瓶颈,再按物体特性、更新频率与资源预算匹配相应技术,并以量化指标持续验证优化效果。