ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

DrawCall、Batches、SetPassCalls:Unity渲染优化三大指标深度解析

DrawCall、Batches、SetPassCalls:Unity渲染优化三大指标深度解析 打开Unity的Profiler找到Frame Debugger满屏的绿色条目里夹着几行黄色底部统计着三个数字Batches 57SetPassCalls 83DrawCall 21。我见过太多新手对着这三列数据发呆搞不清DrawCall和Batches是不是一回事也不知道为什么明明Batches降下来了游戏在手机上还是掉帧。这三个数字分别对应渲染管线里三种不同维度的开销把它们拆开看优化思路才能捋清楚。这篇文章就用我自己排查过的一个项目来说明DrawCall、Batches、SetPassCalls之间到底是什么关系以及那些容易被忽略的坑。1. DrawCall、Batches、SetPassCalls到底是什么1.1 DrawCall是CPU对GPU的一次“开工指令”DrawCall这个词很多入门教程解释成“CPU调用图形API的次数”这个说法不算错但不完整。一次DrawCall的本质是CPU向GPU下达一条绘制指令带上网格数据、材质属性、变换矩阵GPU拿到之后才开始真正干活。这个过程中CPU的压力主要不在“发指令”本身而在发指令之前的准备工作验证Shader状态、检查顶点格式、绑定纹理和缓冲区、更新常量数据这些操作在底层API里每一项都不便宜。现代图形API之所以在D3D12、Vulkan上能做性能提升核心就是把这些重复的状态验证和提交开销压缩了。但Unity层的一次DrawCall到底层可能会变成一条或几条指令引擎层做得越细底层API的优势就越难发挥出来。所以DrawCall高的时候CPU的渲染线程会被这些提交动作占满一帧的CPU耗时就上去了。1.2 Batches是合批后的真实提交数量Batches在Unity里翻译为“批次”它是引擎在每一帧实际提交给渲染管线的绘制批次数。如果一个DrawCall能一次画多个物体那Batches的数量就会小于物体的数量。换句话说Batches是经过静态批处理、动态批处理、GPU Instancing这些机制合并之后的最终批次统计本质上可以把它理解成“优化后的DrawCall数量”。但要注意Batches的统计口径并不总是等于Frame Debugger里的逐条绘制记录。Stats面板里的Batches包含了天空盒、阴影、UI、粒子等所有渲染队列的批次而Frame Debugger更偏向逐条事件两者在编辑器下经常会有微小的数字差。理解这一点以后看到“为什么Profiler和Stats面板数字对不上”就不会慌了。1.3 SetPassCalls是渲染状态切换的代价SetPassCalls指渲染状态切换的次数。GPU不是拿到一个DrawCall就直接开画的它要先确定一大堆状态用哪个Shader、哪个Pass、深度测试怎么开、混合模式怎么设、当前材质绑了什么贴图和参数。这些状态一旦切换GPU管线内部的很多缓存就失效了指令重新配置又要花时间。SetPassCalls统计的就是这个“切换渲染状态”的动作发生了多少次。最能说明问题的一个例子是静态批处理合并了一堆不同材质的物体。场景里100个箱子被合并成一个大网格Batches确实降到1了但每个箱子用不同颜色材质渲染这个大网格时引擎还是得在每个子网格之间切换材质状态SetPassCalls照样是100。Batches低但性能没变好问题常常出在这里。所以SetPassCalls不能被Batches这个数字掩盖掉。指标衡量对象主要开销来源优化手段DrawCallCPU提交绘制命令的次数命令验证、数据绑定、提交逻辑合批、减少网格数量、简化渲染路径Batches合批后实际提交的批次数量近似等于优化后的DrawCall静态批处理、动态批处理、GPU InstancingSetPassCalls渲染状态切换次数Shader切换、Pass切换、材质参数绑定SRP Batcher、减少材质种类、避免多Pass Shader2. 从一帧渲染命令流看三者的真实关系2.1 没有合批时的一帧命令长什么样要理解三者关系直接把一帧的渲染命令流写出来最清楚。假设场景里只有4个物体分别用材质A和材质B每个物体一个网格引擎没有做任何合批那么CPU提交给GPU的逻辑大概是这样SetPass(Material A) Draw(Mesh1, Transform1) Draw(Mesh2, Transform2) SetPass(Material B) Draw(Mesh3, Transform3) Draw(Mesh4, Transform4)这段逻辑里SetPass发生了2次Draw发生了4次Batches则为4。注意同一个材质下的两个物体GPU状态不需要变化所以SetPass只在换材质时触发一次。这里的额外开销主要是每次Draw前后的状态验证和提交而不是SetPass本身。这也是为什么有经验的开发者会说“同材质物体就算没合批代价也没有想象中那么恐怖真正刁难人的是材质切换”。2.2 合批之后命令流变成了什么样如果上面4个物体中前两个满足动态合批或静态合批条件命令流会变成这样SetPass(Material A) Draw(Mesh1 Mesh2) SetPass(Material B) Draw(Mesh3 Mesh4)合并后Draw从4次变成2次Batches也从4变成2SetPass不变仍然是一次材质A、一次材质B。合批的本质其实是在CPU端把多个小网格的数据临时拼成一个大网格或者提前在场景构建时拼好从而减少提交次数。GPU端顶点数据总量并没有减少但每次提交的固定开销已经不存在了CPU渲染线程的压力会明显下降。移动端那些大量同材质小物体比如场景里几百个砖块、路灯、栅栏的优化靠的就是这个逻辑。动态批处理属于“每帧临时拼”静态批处理属于“构建时提前拼”但方向一致让命令流里的Draw条目标少Batches降下来。2.3 为什么Batches低并不一定代表性能好Batches低不等于帧率好这个观点值得多聊几句。先看一种情况静态批处理把所有Static物体合并成一个大网格但物体之间用了50种不同材质。这个时候Batches可能只有个位数因为引擎把大网格当作一个批次来统计但真正渲染时GPU需要在大网格的不同子网格之间来回切换材质SetPassCalls仍然很高。Frame Debugger里会看到同一个“Batch”下面穿插着大量状态切换记录性能依旧上不去。再看另一种情况项目用了高分辨率后处理、多层半透明粒子、或者复杂的逐片元光照ShaderGPU的片元着色器已经满载了这时候DrawCall再低也救不了帧率。优化一定要先定位瓶颈在CPU侧还是GPU侧。Profiler里如果Render Thread一大片说明CPU渲染提交有问题应该去动Batches和SetPassCalls如果GPU时间已经爆掉再去压批次是毫无意义的。3. Unity四种批处理机制的原理与选型3.1 静态批处理拿内存换DrawCall静态批处理是Unity最“暴力”的合批方式。只要把物体标记为Static或者运行时调用StaticBatchingUtility.Combine引擎会在构建或进入PlayMode时把这些物体合并成一个或几个大网格。合并后渲染时一次提交就是一大块数据DrawCall和Batch数量会非常漂亮。它能兼容不同材质这也是它比动态批处理更灵活的地方但代价是不同材质的SetPass问题依然存在。静态批处理最明显的问题是内存。合并后的大网格会把原网格的顶点数据复制一份并且顶点坐标已经变换到世界空间顶点数据不能共享导致内存翻倍。在PC上还好移动端内存紧张的项目里几千个静态物体合并下来内存涨几十MB很正常所以在移动项目里要谨慎使用。另一个问题是Culling失效合并后的网格是一整块一个子物体在视锥内就会把整块送进渲染管线遮挡剔除和视锥剔除都不能对内部单独处理。适合静态批处理的场景是大量同类型、不会移动、材质种类可控的建筑、地形装饰、场景道具。如果要做运行时合批可以考虑自己调Mesh.CombineMeshes合批完成后将原始Mesh引用置空配Addressables按需加载能有效控制内存。3.2 动态批处理每帧在CPU端现合并动态批处理的好处是物体可以任意移动代价是CPU在每一帧都要做顶点数据的拷贝和合并。正因为是“现合现用”动态批处理的限制非常多网格顶点数要小于900如果Shader用了雾效上限降到300材质必须完全相同Mesh不能包含多套UV等复杂属性物体数量多时CPU合并本身就会喧宾夺主。一个典型的反面案例是大量使用动态批处理的小石子场景结果DrawCall没降多少CPU主线程却因为合并顶点数据飙升帧率比没开之前还差。我个人的经验是动态批处理最适合控制在一批少于10个物体、总顶点几千以内的小场景比如飞行的弹壳、散落的小金币、碎玻璃。大型场景里就算开启动态合批大部分物体也合不起来不如关掉它靠SRP Batcher和Instancing来撑场面。URP工程里一旦启用了SRP Batcher动态批处理基本不会再参与常规物体的提交反而会让不兼容SRP Batcher的Shader失去合批路径这点在项目初期就要想清楚。3.3 GPU Instancing一次提交绘制海量同名物体GPU Instancing是处理“成百上千个同网格同材质物体”的利器。它的原理不是把顶点数据拷到一起而是把一个网格的顶点数据只提交一次然后把每个实例的变换矩阵和属性打包成数组一起提交。GPU在顶点着色器里通过实例ID取出当前物体的矩阵把顶点变换到正确位置。整个提交过程只需要一次DrawCall实例数量多时CPU开销增长很慢内存几乎不增加性价比极高。Unity的Standard Shader和URP的Lit Shader默认支持Instancing打勾即可。每个实例如果需要差异化颜色可以用MaterialPropertyBlock传数据注意只能传Shader能识别的属性。单个DrawCall能提交的实例数量上限受常量缓冲区容量限制Unity下通常一个批次最多1023个实例超过会自动拆成多个批次。对于草、树叶、大量重复的装饰物如果连CPU提交实例矩阵都嫌多那就上Graphics.DrawMeshInstancedIndirect把实例数据放到Compute Buffer里交给GPU自己扩展生成能渲染出漫天遍野的森林但对代码和边界处理的要求也高适合有一定经验的开发者使用。3.4 SRP Batcher靠持久化CBUFFER干掉多余的SetPassSRP Batcher是Scriptable Render Pipeline下的一套状态缓存机制URP和HDRP默认开启。它的思路和传统批处理完全不同不合并网格、不减少DrawCall而是让所有Shader公共属性常驻显存切换物体时不需要重新绑定材质状态只需要更新物体自身的Transform数据。这种“状态持久化”让SetPassCalls大幅度下降在URP项目里经常能看到Batches没怎么变但SetPassCalls少了一半的情况。想要吃到SRP Batcher的红利Shader必须按规则来所有材质属性放在CBUFFER_START(UnityPerMaterial)和CBUFFER_END之间引擎内置的unity_ObjectToWorld、unity_WorldToObject等属性也要在CBUFFER里。只要Shader里有一次裸声明、或者用了MaterialPropertyBlock这个物体就会退出SRP Batcher路径。URP工程里如果发现Frame Debugger中大量绘制显示为黄色大概率就是Shader兼容性出了问题。Shader Graph生成的Shader默认兼容手写的Shader要注意检查。机制原理是否复制网格物体可移动核心限制典型场景静态批处理构建时合并网格是内存翻倍否需标记Static内存高、Culling粒度粗建筑、地形装饰、大批静态道具动态批处理每帧CPU合并顶点是每帧拷贝可以900顶点上限、材质必须相同、CPU开销大小金币、碎片、小规模动态物体GPU Instancing一份网格渲染多个实例否可以同网格同材质、属性由数组传递草、树叶、人群、重复物件SRP Batcher持久化材质状态减少SetPass否可以Shader必须符合CBUFFER规则URP/HDRP下同Shader多物体场景4. 从Profiler和Frame Debugger定位真正的瓶颈4.1 Stats面板和Profiler的统计口径要分清Game视图右上角的Stats面板是很多人的第一站。面板里Batches和SetPassCalls是实时统计的数字直观但并不精确。它统计的是一帧中所有渲染队列的批次总数包括天空盒、编辑器调试网格、UI、后处理等很多人在构建版本后对比数值发现和编辑器里完全对不上就是因为统计口径和运行模式不同。真要分析性能应该以Profiler的Rendering模块为主。打开Profiler切到Rendering区域关键看三个值DrawCall、SetPassCall、BatchCount。CPU耗时看Render Thread和Main Thread之间的比例如果Render Thread时间明显高于Main Thread说明渲染提交端压力大。也可以勾选GPU Profiler把分析切到GPU侧看到GPU耗时后对比一下CPU时间基本就能判断是提交瓶颈还是着色器瓶颈。4.2 用Frame Debugger查“为什么没合批”Frame Debugger是我排查合批问题的第一工具。打开后逐帧播放每一帧里所有的渲染事件都会列出来每一条事件都有颜色绿色表示参与合批的绘制黄色表示这一条因为某种原因没能进入合批新Shader切换、透明物体排序、顶点格式不兼容都会让它变黄。选中黄色事件右侧Details面板通常会直接写出原因比如材质不同、Mesh不是Static之类。这比看数字猜原因快得多。需要注意Frame Debugger在没有打开游戏视图时是无统计意义的有些检查项还要在非Game视图下才能看清楚。平时排查最好在独立Game窗口下操作方便点击场景里的物体看对应的DrawCall。开启Frame Debugger后Profiler的DrawCall统计可能暂时失真这是引擎在调试模式下的正常表现不需要担心。4.3 一个50个箱子的案例拆解我之前做过一个性能测试场景50个箱子同一个Cube网格同一个材质。没有做任何合批时Frame Debugger显示50条DrawBatches50SetPassCalls1因为材质一样GPU状态从头到尾没换过。这时候瓶颈纯粹在CPU提交次数上解决方案很简单直接开GPU Instancing勾上之后Frame Debugger里50个绘制缩成1条Batches变成1SetPassCalls保持1帧耗时立刻降下来。然后把其中2个箱子用MaterialPropertyBlock改了颜色Instancing失效Frame Debugger里会多出2条黄色因为材质属性无法通过CBUFFER传给实例化ShaderUnity只能为它们单独提交。最后我把50个箱子改成50个不同颜色的材质引擎再怎么做Instancing也没用只有靠SRP Batcher才能让SetPassCalls从50降到接近1。这个案例直观地说明了三种优化机制各管什么Instancing管同材质多物体SRP Batcher管同Shader多材质状态切换静态批处理管的是多物体合网格。5. 实际项目中遇到的坑与排查方法5.1 Batches不高但帧率低问题出在哪遇到这种场景先别急着继续压批次打开Profiler看GPU时间。如果GPU占用已经接近满了那问题在片元着色器、填充率、overdraw或者后处理强度上面和DrawCall没太大关系。移动端最常见的隐性杀手是半透明物体和无序透明排序同一个屏幕上叠了几层全屏半透明粒子GPU每一帧都要反复混合好几遍Batches可能只有20但GPU已经被磨到严重掉帧。另一种情况是CPU侧的脚本逻辑消耗太高渲染线程一直等主线程导致帧率崩盘。这种问题在时间轴上看得非常清楚Main Thread很长Render Thread明明有空间却在等待。这时候把注意力放在GC、物理、路径寻路和Animator更新上去压Batches就是缘木求鱼了。我的习惯是用Profiler先看一帧的大块时间分布再决定动手方向从来没有一次是直奔批次数值去的。5.2 动态批处理为什么一直没生效开启动态批处理后Frame Debugger里还是几百条黄色检查顺序通常是第一看材质必须是同一个材质对象不是两个看起来一样的材质第二看网格顶点数超过900上限直接放弃如果Shader里有雾上限更要降到300这个条款很多人压根没注意第三看Mesh属性带lightmap、顶点色、切线、多UV的网格动态批处理都会拒绝第四看非等比缩放Transform的scale每个轴不一样会直接破坏合批条件。另外要特别提醒动态批处理不会合并带骨骼动画的Mesh也不处理使用多Pass Shader的物体。即使是同一个材质只要Shader里有多个Pass每个Pass都会单独提交。项目里如果用了半透明物体且深度写入打开前后绘制顺序一变合批就自动退场。调试动态批处理时除了Frame Debugger还可以在Project Settings里关掉动态批处理再对比一帧变化越明显说明机制生效的物体越多没变化说明根本没用上。5.3 静态批处理后内存翻倍怎么办静态批处理的内存翻倍问题在移动端项目里经常是绕不开的坎。合并后的网格会单独存在原始网格又不会自动释放两个数据叠在内存里场景一大就让人头大。我的处理方案是在PC编辑器里先用Profiler看内存快照确认合并网格占了多少再决定是否值得。如果合并后Batches确实降到可以接受的范围但内存超标可以考虑自己用Mesh.CombineMeshes手动合并合并完成后把原始Mesh从Resources或Addressables引用里释放掉。要注意的是手动合批后如果还想让物件之间独立Culling合并网格会被当整体处理这个逻辑和引擎的静态批处理一模一样。所以手动合并也只适合那些生命周期稳定、不会频繁卸载的静态物件。对于地形周围的石头、树木这类重复度特别高的东西我更推荐用GPU Instancing搭配Tree/Detail系统内存占用低灵活度还高远比静态批处理省心。5.4 SRP Batcher在URP里不生效的排查清单SRP Batcher不生效时最典型的特征是Frame Debugger里大量黄色条目同时Profiler里的SRP Batcher统计数据很低。先按这个清单查Shader里所有材质属性是否都放进CBUFFER_START(UnityPerMaterial)/CBUFFER_END是否用了MaterialPropertyBlock用了就绕回传统路径Shader是否包含多Pass或带其他不被支持的特性材质球是否被动态创建且属性是脚本里赋值的有些运行时动态材质不会自动满足SRP Batcher的绑定规则。还有一个常见问题是ScriptableRenderPipeline被反复赋值或重建导致SRP Batcher缓存失效。遇到这种情况检查一下是否在运行时创建了额外RenderPipelineAsset或者Quality Settings里切换了不同的Pipeline Asset。排查时配合Frame Debugger逐条翻选中黄色绘制项看Details里的原因提示多数情况一眼就能看出是哪块不兼容。SRP Batcher处理的是“状态切换”层面的优化和顶点合并不冲突解决完兼容性问题SetPassCalls的下降通常非常明显。我个人在实际项目里的体会是不要被某一个指标绑架。DrawCall、Batches、SetPassCalls这三个数各有各的意义单独看任何一个都可能被误导。真正有效的流程是先确认瓶颈在CPU提交还是在GPU着色再用Frame Debugger逐条定位合批原因最后根据物体是否可动、材质是否统一、Shader是否兼容来决定用哪种批处理机制。场景里大量重复且不动的东西静态或Instancing优先动态小物件交给Instancing或动态批处理兜底能写标准URP Shader的地方就尽量让SRP Batcher吃得满。按这个思路调过几个场景后帧耗时基本都能回到正常范围。
返回列表