拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity性能优化:DrawCall、Batches、SetPassCalls深度解析

做Unity性能优化的人应该都对着Profiler面板发呆过。界面里DrawCall、Batches、SetPassCalls三个数字并排摆着看着都跟渲染性能有关但真要说明白它们各自代表什么、为什么有时候DrawCall不高游戏却还是卡、SetPassCalls这个指标到底能说明多少问题很多人其实是含糊的。我最早入行时也走了不少弯路一度把Batches当成DrawCall的别名闹出过优化了半天结果帧率纹丝不动的笑话。这篇文章就把这三个概念彻底讲透包括它们各自的含义、它们之间的协作逻辑以及在实际项目中怎么利用这些指标定位性能问题。不管你是刚入门的技术美术、移动端游戏开发者还是做渲染底层的老手这篇文章都能帮你把性能分析这块的认知补齐。1. 三个指标是什么先从概念上搞清楚在动手优化之前先别急着背数字得先弄清每一栏统计的到底是什么东西。很多人把Profiler里的数值直接当“性能好坏”的判断依据这是最大的误区。这三个指标虽然都来自渲染流程但描述的是不同环节的成本。1.1 DrawCallCPU向GPU发出的“请求”DrawCall字面意思就是一次绘制调用。你把一个网格、一套材质、一组变换矩阵打包好CPU通过图形APIOpenGL、DirectX、Vulkan、Metal向GPU发出一条“请把这堆东西画出来”的命令这就是一次DrawCall。用生活化的方式理解DrawCall就像是去饭店点菜。你每次招呼服务员“给我来一份宫保鸡丁”服务员都要跑一趟后厨、记一次单、后厨再按单做一次菜。哪怕你点的是同一道菜只要分两次说服务员和后厨就得分别处理两次。CPU和GPU之间的通信也是这个道理CPU每次发起绘制命令都要经过驱动层、渲染状态验证、数据打包然后才真正交给GPU执行。命令发得越多CPU要干的杂活就越多一旦CPU被这些杂活塞满游戏帧率就会卡在CPU的“点菜速度”上GPU再快也没用。在Unity的Profiler里DrawCall往往被统计成帧内所有绘制指令的总数。旧版本的Unity里这个数字最直观后来Unity引入了批处理机制这个数字的计算口径发生了变化于是就有了下面要说的Batches。1.2 Batches合批之后的真正渲染批次Batches是Unity在统计渲染数据时使用的一个单位它表示经过合批处理后GPU实际需要执行的渲染批次数量。这个概念很关键因为它和DrawCall并不是一回事。继续用点菜的比喻。假如你同桌三个人都点同样的宫保鸡丁正常情况下服务员要跑三趟。但如果你在点单的时候直接告诉服务员“三份宫保鸡丁一起上”那服务员只跑一趟后厨后厨也只需要做一次流程上三份菜。Unity的批处理就是干这件事的把能合并的绘制请求收集起来打包成一次提交减少CPU和GPU之间的往返次数。所以在Profiler中你会看到这样的现象场景里明明有3000个物体DrawCall显示可能是3000但Batches可能只有几百甚至更少这取决于这些物体是不是具备合批条件。Batches代表的是“真正提交给GPU执行的批次”是优化之后的结果。如果你在做优化时只盯着DrawCall看而忽略了Batches那就等于你点了三份菜却还在纠结服务员跑了几趟不关注最后实际执行了多少次绘制流程。1.3 SetPassCalls状态切换的“隐藏成本”SetPassCalls这个指标很多人会忽略但它在某些场景下比DrawCall更值得关注。它的含义是在一帧渲染过程中GPU切换Shader Pass渲染通道的次数。要理解SetPassCalls得先知道GPU是怎么处理渲染状态的。GPU在绘制一个物体之前需要先把当前的渲染状态配置好比如顶点着色器、片元着色器、混合模式、深度测试、模板缓冲等。这些状态被打包成一个Pass。如果你渲染的物体序列是物体A用Shader的Pass1物体B也用Pass1物体C还用Pass1那么GPU只配置一次状态就能连续绘制A、B、C。但如果物体C使用的是同一个Shader的Pass2或者干脆是另一个Shader那GPU就得停下来重新配置一遍状态这个过程就是一次SetPassCall。状态切换的开销是很大的。GPU内部的管线状态一旦切换之前的缓存优化可能就失效了有些渲染硬件在切换Pass时还要做额外的编译、同步操作一两次感觉不明显但如果一帧之内状态切换上千次CPU和GPU都被这些“准备工作”拖住帧率就下来了。1.4 三个指标最容易踩的认知误区关于这三个指标最常见的误解有三个第一把DrawCall等同于Batches。其实在Unity的现代版本中DrawCall更多是指“原始绘制请求的数量”Batches才是“批处理后的提交数量”。你看到DrawCall很高不代表性能一定崩还要看Batches是不是够低。第二以为SetPassCalls越低越好、越低越光荣。SetPassCalls反映的是状态切换次数它和Batches之间有关联但不是简单的正比关系。如果场景里所有物体都用一个Shader的一个Pass那SetPassCalls就会远小于Batches因为同一个Pass可以连续处理很多批次。但如果你故意把所有物体塞进同一个材质却因为渲染顺序问题导致Pass切换频繁那SetPassCalls还是会飙高。第三只看数字、不看场景。你的项目是移动端还是PC端跑的是低端机还是高性能显卡这三个指标的敏感程度完全不一样。移动端对DrawCall异常敏感因为移动GPU的CPU和GPU通信带宽有限低端手机上几千个DrawCall就能把帧率拉到十几帧。桌面端相对宽裕但SetPassCalls对桌面的驱动优化同样很敏感。2. 三者之间的关系从渲染管线的角度理解理解了各自定义还不够关键是把它们放进渲染管线的上下文里看。一帧画面从CPU生成绘制指令到GPU真正画出像素中间要经过好几道工序三个指标正好分布在不同的工序节点上。2.1 一次绘制请求要经过的三道工序先理清一帧渲染的宏观流程。CPU这边引擎要做剔除、排序、合批然后生成渲染命令。这些命令经过图形API接口带着网格数据、材质参数、矩阵变换等一堆信息进入GPU的命令缓冲区。GPU收到命令后先解析、配置渲染状态再把顶点变换、光栅化、片元着色逐步执行最终输出到帧缓冲。把这个流程映射到三个指标上DrawCall出现在CPU生成绘制命令的阶段它统计的是“引擎准备了多少次绘制请求”。Batches出现在合批以及最终提交的阶段它统计的是“经过合并优化后最终提交给GPU几个批次”。SetPassCalls出现在GPU状态配置阶段它统计的是“GPU在渲染这些批次时切换了几次Shader Pass”。所以你会发现三个指标其实是一条流水线上的三个观察点。CPU忙不忙看DrawCall和BatchesGPU状态切换多不多看SetPassCalls。它们互相影响但不完全等价。2.2 Batches和DrawCall为何不相等很多新手最困惑的就是为什么有时候DrawCall和Batches差很多有时候又几乎一样答案在于合批的触发条件。Unity中有几种合批方式静态合批、动态合批、GPU实例化、SRP Batcher。每种合批方式的技术原理不同能合并的范围也不同。静态合批处理的是标记为Static的场景物体它在运行时把共享材质的网格合并成一个大的网格从而用一次DrawCall绘制多个物体。这种方式下Batches会急剧下降。动态合批处理的是小网格物体它要求物体顶点数少、使用相同的材质、满足一定的缩放规则。因为CPU需要每个帧做一次合批计算所以合批成本可能反而高于收益。GPU实例化是通过一次提交绘制多个相同或不同的网格实例它要借助着色器的实例化支持。由于实例化同样会把多个物体合并到一个批次但每个物体还是可以有不同的变换矩阵、颜色等属性。SRP Batcher是通用渲染管线URP/HDRP里的一种合批机制它通过缓存材质属性、减少Shader的状态切换来提升性能对不同材质之间的切换优化尤其明显。当这些合批机制生效时DrawCall和Batches的差值就会拉大。反之如果场景里的物体各自使用不同的材质球、不同的Shader、不同的网格那么合批失败Batches就会贴近DrawCall的原始数量。所以如果你看到Profiler里这两个数字几乎相等基本可以判断当前场景有严重的合批阻力。2.3 SetPassCalls与Batches之间的比值说明了什么SetPassCalls和Batches的比值是判断场景渲染状态复杂度的一个很好用的参考。如果你的Batches是500SetPassCalls只有100说明平均每个Pass可以连续渲染5个批次状态切换被控制得很好。反过来Batches是500SetPassCalls也是500说明每渲染一个批次都要切换一次Pass那这帧的状态切换就非常频繁。为什么会出现SetPassCalls接近Batches的情形最常见的原因是物体渲染顺序混乱。假设场景里有两种材质材质A和材质B各自的Pass不同。引擎每帧按物体顺序提交绘制命令如果顺序是ABABAB那GPU就得在PassA、PassB之间反复横跳SetPassCalls特别高。如果顺序是AAAABBBB那GPU先连续处理A再连续处理BSetPassCalls就低多了。另一个原因是Shader写得特别复杂一个材质里塞了十几个Pass哪怕只渲染几个物体Pass切换的次数也会暴涨。还有可能是后处理效果、深度预通道、阴影通道等机制叠加让同一帧内容被多次渲染SetPassCalls自然飙升。在Profiler里看到SetPassCalls偏高时别急着调整材质数量先打开Frame Debugger一帧一帧去翻看看到底是哪个环节引发了状态切换。很多时候把场景的渲染队列整理一下SetPassCalls就能降下来根本不需要大改美术资源。3. 实操优化思路三套打法逐个击破知道概念、理解关系之后接下来的问题是怎么在项目里落地我按三个指标分别讲一套实操方案注意这里不是让你盲目地压每个指标而是结合你自己的项目瓶颈去选。3.1 降低Batches优先考虑的三类手段如果你的项目瓶颈在CPU线程Profiler里能明显看到渲染线程时间很长那就要集中精力降低Batches。第一优先做静态合批。把场景中不动的物体地板、墙体、柱子、摆放固定的装饰物都勾上Static选项。注意静态合批的代价是内存占用变大因为合并网格时把多个网格的数据合并成一份如果场景里大网格很多内存压力会很大。所以适合网格顶点数不多、复用度高的物体。第二动态合批要小心用。动态合批要求严格顶点属性必须匹配材质必须完全一致而且小网格才有收益。如果你的物体是大模型动态合批不仅没帮助每帧的合批计算还会增加CPU负担。所以小物体用动态合批大物体老老实实走GPU实例化。第三GPU实例化是现在的大趋势。它特别适合处理大量重复的物体比如树木、石头、子弹、粒子。实现时主要工作是在Shader里开启Instancing支持并且用MaterialPropertyBlock设置每实例的差异属性。移动平台如Vulkan、Metal对实例化特别友好能明显降低Batches。优化Batches的时候顺手能把LOD细节层级做了。LOD能减少远处物体的三角形数量同时也能减少参与合批的网格复杂度每个物体的渲染成本都降了Batches自然压力小。3.2 控制SetPassCalls的四个实用习惯第一渲染队列尽量稳定。Unity的渲染队列Queue分为Background、Geometry、AlphaTest、Transparent、Overlay等。不同队列之间切换时引擎往往要中断当前的合批状态。所以尽量把不透明物体放在Geometry队列透明物体放在Transparent队列不要在一个队列里面混杂不同渲染类型的材质。第二控制Shader Pass数量。Shader中Pass越多单物体切换状态的可能性越大。对移动端来说一个Shader超过4个Pass就值得警惕。能在一个Pass里解决的就不要拆开。比如多光源处理尽量用Forward或延迟渲染而不是每个光源都跑一次完整Pass。第三材质排序是隐藏的优化点。在不改变视觉结果的前提下调整物体提交顺序让相同Pass的物体连续绘制。项目里的做法是写一个自动排序脚本在Game视图运行时动态调整某些批次的提交次序实测对SetPassCalls的降低很明显。第四警惕阴影和深度通道。很多项目一看SetPassCalls高以为是材质太多但打开Frame Debugger后发现阴影Pass、深度预通道占了大头。比如平行光阴影Directional Shadow可能让每个物体多画两次后处理的全屏Pass也计入SetPassCalls。遇到这种情况需要结合场景需求降阴影分辨率、限制阴影距离或者改用一次性深度预通道Depth Prep来降低重复开销。3.3 不同项目阶段怎么取舍优化这三个指标不是越极致越好而是要看项目阶段。我见过不少团队在原型阶段就开始抠DrawCall结果美术资源迟迟不定过早的合批配置全部作废后期返工成了家常便饭。原型阶段先保证渲染效果能跑通不需要过多优化。可以关注Profiler但不要为了数值好看做大量合批工程。等美术资源基本定型进入内容填充阶段这时候做静态合批和GPU实例化收益最高。到了性能测试阶段再集中用Profiler逐帧分析针对瓶颈调渲染队列、降SetPassCalls。另外要记住一个原则移动端和PC端的优先级不同。移动端优先把Batches压下来因为移动GPU驱动的处理能力弱每批次的状态切换成本更高。桌面端GPU相对强悍Batches的敏感度低但SetPassCalls对驱动层的老旧线路影响更大尤其是Windows平台上的OpenGL状态切换频繁时帧率变化很明显。4. 常见问题与排查技巧实录优化做多了会遇到各种奇怪的现象。有些问题看起来和DrawCall、Batches、SetPassCalls无关但追根溯源还是这三兄弟在作祟。下面记录几个高频问题以及我自己的排查流程。4.1 Batches降了帧率却反而下降这大概是我见过最反直觉的情况之一。有次我帮一个项目做优化场景里有很多重复小物件我猜是动态合批在起作用。But优化后Profiler里Batches从800降到300帧率却从50帧掉到了45帧让我很困惑。查了一遍发现是动态合批在每帧计算时的CPU开销反噬了。那个项目里的场景物体虽然小但每个物体挂了自己的组件每帧位置都在变化动态合批需要每帧重新合并网格计算量比节省的绘制开销还要大。Batches数字降了但CPU总时间反而涨了。所以Batches并不是越低越好降低Batches的手段必须和场景特征匹配。固定物体用静态合批动态物体数量多才用GPU实例化而动态合批适合“数量多、单体小、材质统一、顶点数少”的物体。不看场景特征盲套方案就会踩这种坑。这里有一张对比表方便快速判断优化手段适用场景典型收益主要风险静态合批场景不动的大批量静态物体Batches显著下降内存占用增加动态物体无法参与动态合批小物体、顶点数少、材质统一的动态物体CPU合批后减少DrawCall每帧合批计算开销高网格稍大就亏本GPU实例化大量重复或相似物体Batches大幅下降支持每实例差异属性需要Shader支持写错代码易出仍画错物体的问题SRP BatcherURP/HDRP项目中的材质切换频繁场景减少状态切换SetPassCalls下降需要引入SRP部分旧Shader不兼容手动合批静态或低频变化的Mesh合并Batches直接下降操作繁琐影响碰撞体和美术修改流程4.2 用Profiler定位瓶颈的实操方法遇到帧率问题我的建议是先别管DrawCall先打开Unity Profiler的CPU模块看主线程、渲染线程各自的耗时。如果渲染线程耗时高再切到Rendering面板看DrawCall、Batches、SetPassCalls。具体分这么几步走第一步确定CPU还是GPU瓶颈。CPU瓶颈时主线程或渲染线程的时间很高GPU时间反而不高。GPU瓶颈时GPU时间占比高CPU相对空闲。这里有一个小技巧把游戏窗口缩得很小如果帧率大幅提升说明像素填充瓶颈更明显如果帧率反而不升不降那CPU侧的问题更大。第二步如果是CPU瓶颈看Rendering统计。这里要区分一种情况Profiler的Rendering数据并不完全等于实际GPU开销它更多的是渲染线程在CPU侧的提交耗时。所以当Batches降低后帧率上升说明瓶颈在CPU提交侧如果Batches降了帧率没变那就要去GPU端找问题。第三步打开Frame Debugger。它能一帧一帧回放绘制命令每一条命令里都标了当前事件类型Batch、SetPassCall、Shadow等。在这里你能直观看到到底哪些物体的绘制命令被合批了哪些没有SetPassCalls发生的位置在哪。这时候再把目标定位到具体物体优化果断得多。第四步看GPU端。Unity 2019以上版本可以用Profiler的GPU模块或者借助RenderDoc、Xcode Instruments、小米的PerfDog等外部工具。重点看Fragment Shader的占用和顶点处理时间排除像素填充或带宽问题。4.3 几个容易忽视的细节有几个细节是我在项目里碰过的坑单独列出来提醒一下。第一个材质球不共享会让合批失效。两个物体看起来用同一个Shader但从美术那边导入时各生成了一份材质球实例哪怕参数设置完全相同引擎也不会把它们合批。点开材质球看Inspector顶部的对象名旁边是否有数字后缀如果有那就是多个材质实例。第二个Mesh的读写选项会影响合批。在导入模型时模型的Read/Write选项如果开启了Unity会保留一份CPU可读的数据副本某些合批路径会因此失效。尤其是在移动端这个开关能关就关只保留GPU数据还能省内存。第三个阴影会让DrawCall、Batches、SetPassCalls三兄弟同时增加。阴影的Pass是额外的可能一个物体在Shadow Pass里渲染一次、Main Pass里再渲染一次。如果场景里灯光又多又亮阴影相关Pass的次数会非常惊人画面上根本看不出那么多东西性能却已经严重下降了。第四个ScriptableRendererFeature、后处理特效等也会产生额外的全屏Pass。你在Frame Debugger里看到的Fullscreen Mesh或者Blit命令都要计入SetPassCalls。如果这部分占比太高可以评估降低后处理叠加次数或者尝试Vulkan、Metal API因为新API对渲染状态切换的优化更好。第五个记得在真机上验证。Editor里的Profiler数据很有用但合批策略和Pass切换在真实设备上表现会有差异。尤其是iOS/Android设备驱动对状态切换的敏感度差别很大同一个场景在骁龙和天玑上的SetPassCalls成本不一样DrawCall的极限值也不一样。提交到商店前建议在最低配机型上做一次完整的Frame Debugger检查。5. 文章说到这里还是回到最初那个场景回头看Profiler面板里并排的三个数字它其实不是三个孤立的指标而是渲染流水线上不同环节的“血压计”。DrawCall告诉你CPU发出了多少绘制请求Batches告诉你经过引擎的合批后真正要提交几个批次SetPassCalls告诉你GPU为了画这些批次到底切换了多少次渲染状态。三者互有关联却各自测量不同维度的成本。我个人在实际项目里已经不再单纯追求某个数字降到多低而是先根据Profiler确认瓶颈是CPU还是GPU、是绘制提交还是状态切换再决定优化哪一项。有些项目DrawCall 2000却跑得很流畅因为合批和渲染队列做得很好有些项目DrawCall 300GPU却因为阴影和过度绘制卡成幻灯片。技术文档常教我们怎么定义这三个指标但真正让性能产生质变的是知道在什么时候、用什么手段去调整它们。最后分享一个小技巧也是我每次优化完成之后必做的事把优化前后的Frame Debugger截图存下来标上当时的帧耗时和机型信息。上面那些令人迷惑的性能问题很多时候对比两个版本的截帧记录就能看出端倪省得下次再去翻旧账。这个方法不复杂但长期积累下来的性能基线在新的优化需求出现时能省下大量调试时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门