ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

游戏优化实战:Overdraw导致手机发热的排查与削减方案

游戏优化实战:Overdraw导致手机发热的排查与削减方案 项目发热排查到了第三轮CPU侧优化做了Draw Call也压下来了帧率看着还算正常可手机背面依然烫得能煎鸡蛋。这时候我基本可以断定问题出在GPU的像素填充阶段——换句话说Overdraw。这期是发烫优化系列的第3篇我想把事情讲透一点Overdraw到底是怎么把GPU热量拉起来的怎么把那些看不见的重复刷漆揪出来以及真正动手优化时应该从哪些地方下手。1. Overdraw与发热之间的因果链GPU每一次重复上色都是有代价的1.1 Overdraw到底在刷什么像素填充率才是核心先抛开Unity里的专业术语用装修刷墙来理解。你家里有一面墙最终看到的颜色是面漆的颜色但在刷面漆之前你可能刷了底漆、腻子层、甚至旧的墙皮没铲干净又多刷了两层。站在外面看墙面是最后那层漆的颜色可前面每一遍漆都是花过钱、花过时间、耗过人工的只是被盖住了看不到。Overdraw在渲染里的意思完全一样一个屏幕像素在一帧里被Fragment Shader处理了多次后面绘制的内容把前面绘制的结果盖住了但前面那几次GPU运算已经发生过了花掉的算力收不回来。在Unity里Overdraw的定义就是单个像素在一帧画面中被重复绘制的次数。假设屏幕分辨率是1080p约200万个像素如果平均每个像素被画了3次那这一帧GPU实际处理的像素量就是600万多出来的400万次纯粹浪费。移动GPU的像素处理能力本来就被功耗和散热锁得死死的这种浪费会直接转换成热量手机自然发烫。这里要引入一个专业概念填充率Fill Rate。填充率指的是GPU在一秒内能处理多少像素单位通常是百万像素/秒。分辨率越高、Overdraw越严重、帧率要求越高每秒钟需要处理的像素总量就越大。一旦超过硬件能力上限帧率就会往下掉GPU为了追帧率又会强行拉高频率频率一高功耗跟着涨热量堆积后触发降频最终陷入帧率越来越低、机身越来越烫的恶性循环。1.2 为什么半透明是重灾区Early-Z与混合的秘密如果所有物体都是不透明的Overdraw其实不会那么致命因为GPU有隐藏面消除机制。移动GPU基本都是Tiled架构在执行Fragment Shader之前会做一个逐像素深度测试也就是Early-Z。对于不透明物体如果这个像素已经被更近的物体覆盖了那它根本不会进入后面的着色计算直接被丢弃所以不透明物体之间的Overdraw很多情况下是无效成本或者被硬件优化掉了。问题出在半透明物体。半透明物体需要和后面已经画好的颜色做混合你必须先拿到背景颜色才能算出半透明效果所以它没法在Early-Z阶段被剔除。一个半透明粒子、一片半透明玻璃、一块半透明UI面板盖在画面上它下面那些已经画好的像素全都会被保留一次次叠加计算。这就是为什么很多人发现场景里没有几个物体Overdraw却高得离谱——大概率是半透明物体堆出来的。尤其粒子系统几百个半透明粒子叠在一起屏幕中心区域轻松画上十几遍甚至几十遍。这也是我说Overdraw和发热直接挂钩的核心原因半透明导致的Overdraw是无法被硬件自动优化的每一层都要实打实跑一遍Fragment Shader。1.3 发热不是玄学功耗与频率的负反馈再往深一层说GPU不是一直满负荷运转的它的功耗和当前频率以及活跃的硬件单元成正比。像素填充阶段会大量占用GPU里的Shader ALU阵列、纹理采样单元和混合单元这些模块一忙起来电流就会往上飙。移动设备没有主动散热热量只能通过机身慢慢散发表面温度一高系统会强制降频保护硬件游戏帧率随之暴跌。我自己的经验是用Profiler看GPU Frame Time往往只能看到结果真正让人体感烫的其实是持续的高负载。哪怕平均帧率是60只要GPU的Fragment阶段长时间占用超过70%手机握在手里十分钟必发热。优化Overdraw的本质不是把帧率从30提到60而是把GPU从每帧处理500万像素降回每帧处理200万像素从源头减少发热。2. 让Overdraw显形Scene视图、Frame Debugger和真机Profiler三件套2.1 Scene视图的Overdraw模式先宏观定位Unity的Scene视图自带Overdraw可视化模式在Scene窗口左上角的Draw Mode下拉菜单里选择Overdraw场景就会切换成一套特殊的配色方案。大致规则是白色区域表示基本没有重复绘制颜色越偏暖色红、品红说明该区域被重复绘制的次数越多。不同Unity版本的色带映射略有差异但判读逻辑是一样的——先找最刺眼的区域。实际操作时有一个小技巧先把Scene视图的摄像机调整到与Game摄像机一致的角度再进入Play模式拖视角观察。这样你在Scene里看到的红色区域基本就是玩家屏幕上会发烫的地方。我一般会从俯视角、主视角和侧视角各看一遍把高Overdraw区域用截图记录下来再去Game视图里对照是哪些物件。这里要提醒一下Scene视图的Overdraw模式主要反映的是场景物体的重复绘制Screen Space类型的UI和后处理Pass并不一定完整显示在这个视图里。UI的Overdraw问题要用其他工具来看稍后细说。所以Scene视图的作用是宏观定位不能当最终结论。2.2 Frame Debugger逐个Draw Call揪出元凶定位到大致区域后还需要知道到底是哪个Draw Call在重复覆盖。Unity的Frame DebuggerWindow Analysis Frame Debugger可以一帧一帧地回放所有渲染事件每一笔Draw Call、每一个RenderPass都能看到。操作方法是进入Play模式后打开Frame Debugger点Enabled冻结当前帧然后在左侧事件列表里逐条浏览。看Frame Debugger的时候重点关注三类事件一是Render.TransparentGeometry队列里的大Mesh半透明大物体的Draw Call会直接暴露二是ParticleSystem的批量绘制事件粒子系统通常会一次性提交大量半透明四边形三是后处理Post-processing的全屏Pass比如Bloom的模糊和合成这些Pass一出现就是整屏像素再算一遍等于无条件把Overdraw翻倍。配合Frame Debugger右上角的Info面板可以查看当前事件的Shader、Pass、RenderQueue、顶点数和三角形数。如果一个Draw Call的三角形数量很少、但它在事件列表里排得很靠后盖在已有画面上你就可以断定它是Overdraw贡献者。看到可疑对象后直接在Hierarchy里点开对应物体确认用这种方式能快速定位90%以上高Overdraw的源头。2.3 真机ProfileFragment阶段是评估的最终裁判编辑器里看到的Overdraw分布再严重最终还是要落到真机上量化。因为移动GPU和PC GPU在像素处理策略上有差异编辑器里的表现只能作为参考真机上的Fragment阶段耗时才是硬指标。如果你用Android机调试推荐用Android GPU InspectorAGI抓帧。它能按渲染阶段统计耗时重点看Fragment Stage的占比和Bandwidth带宽占用。Fragment阶段高、或者总体GPU Busy高基本就能坐实是像素填充压力过大。高通平台也可以配合Snapdragon Profiler看GPU频率曲线观察高负载时频率是否被拉满。如果是iOS设备用Xcode自带的Metal GPU Capture。抓帧后看Shader Profiler里的Pixel/Fragment处理时间再结合Device的功耗记录发热情况一目了然。真机Profiler的意义不仅在于验证还在于提供优化前后的对比数据——没有基线数据你就说不清楚优化到底有没有效果。所以项目从一开始就要养成抓真机GPU数据的习惯。3. 五个最容易反复刷漆的高发区UI、粒子、后处理、阴影与半透明材质3.1 UI全屏半透明底加多层弹窗想不烫都难UI是很多团队容易忽视的Overdraw重灾区。一个常见的界面全屏半透明黑底遮罩、两层弹窗背景、按钮上的半透明高光、文本的Outline和Shadow组件……这些都叠在屏幕同一区域内一次点击可能触发四五层半透明绘制。更要命的是Screen Space UI是每帧重绘的哪怕画面静止不动UI层依然在持续烧GPU。UGUI里最容易忽略的是Image组件默认开启的Raycast Target以及Text的Outline和Shadow效果。Shadow和Outline本质上是把同一段文字/图形复制平移了好几次画出来叠加区域会翻倍甚至翻三倍。当下流行的大面积高斯模糊、磨砂玻璃UI成本就更大了——一个全屏模糊效果往往需要多次采样等于把全屏Overdraw再乘一个系数。UI Overdraw的检测建议用Profiler里的UI模块也可以配合Frame Debugger查看Canvas的渲染事件。真机上一张纯色半透明背景在一整帧里如果排在最前面底下所有UI和3D场景都要被它再混合一遍发热贡献非常大。3.2 粒子与大雾景粒子的叠加问题粒子系统天生就是Overdraw制造机。每个粒子都是一个独立的半透明四边形几百个粒子叠在一起时屏幕中心区域的重复绘制次数可能达到几十次。尤其是烟雾、火焰、光晕、拖尾这类特效本身贴图就带大范围的半透明过渡叠加后几乎没有一处像素是只画一遍的。最典型的是移动端游戏里的大范围环境雾效美术为了氛围放了一大团半透明烟雾粒子把这团烟雾罩在场景前面。从相机角度看雾片覆盖的区域内所有场景物体都在被这团雾反复混合Overdraw直接从1涨到4以上。这种设计在PC上无所谓在移动端就是灾难。平时排查粒子Overdraw时我会直接用每粒子屏幕面积 × 粒子数量来估算压力假设一个粒子占屏幕1%的面积场景里有300个粒子且它们大部分堆叠在中心区域——中心像素的实际被覆盖次数不是300乘以1%那是分散情况而是几十粒子在同一个像素上叠加这就是Overdraw噩梦的来源。3.3 后处理链全屏Pass的隐形成本后处理是另一个全屏无差别刷漆的环节。Bloom泛光通常需要先把画面采样下来做若干次降采样模糊再和原图混合DOF景深需要计算散景再合成SSAO、Color Grading也都是逐像素Pass。每多一个全屏Pass屏幕上的每个像素就至少多画一遍Overdraw增加1。移动端后处理还要考虑RenderTexture切换的开销。内置渲染管线里随便挂一个后处理组件可能就多出3~5个全屏PassGPU Fragment阶段的耗时直接翻倍。URP里用Volume后处理虽然方便但如果同时开Bloom加DOF加Film Grain同一帧的全屏Pass数量依然不容小觑。有个容易忽略的地方MSAA和HDR叠加后处理时像素填充的压力不是简单相加。MSAA会在屏幕边缘多做几次采样某些移动GPU上这种sample级别的开销会放大Overdraw的代价。所以在移动端如果场景本身半透明和粒子很多我会建议关掉MSAA后处理分辨率也单独降半。3.4 阴影与多Pass材质重复上色的来源阴影对Overdraw的贡献往往比较隐蔽。平行光的Shadow Map渲染本身会额外绘制一遍场景如果场景里还有大量半透明物体会投射阴影那Shadow Pass里也要再处理一遍这些半透明物体的深度。很多团队为了省事给所有材质都开了Cast Shadows包括大面积的半透明墙面、玻璃、甚至粒子系统——这些都会在Shadow Pass里再被画一次。多Pass材质同理。游戏里常见的描边效果、外发光效果用两个Pass实现先画背面放大轮廓再画正面正常颜色。这就是一帧内同一个物体被画了两次甚至三次。角色身上再多挂几个特效组件、披风材质是双面渲染……整个角色的GPU开销会成倍往上走。我把这两个问题单独拎出来说是因为它们和半透明材质还不一样很多团队知道粒子烧性能但完全意识不到一个普通的双层Pass Shader或一个没关阴影的半透明片也在暗地里给Overdraw添柴火。底下用一个表格总结高发区及其特征方便对照排查。高发区域典型表现额外代价UI全屏半透明底、多层弹窗、Shadow组件静止界面也在反复混合粒子特效烟雾、拖尾、光晕大面积叠加同像素被反复着色后处理Bloom、DOF等多个全屏Pass每Pass等于整屏Overdraw1阴影半透明物体Cast ShadowsShadow Pass多一遍绘制多Pass材质描边、双面渲染、外发光同一物体被画多次4. 开始动手削减Shader、粒子系统和UI层的具体优化手法4.1 UI层从层级、Raycast Target到贴图改造UI的Overdraw优化是最容易见效的。第一步关闭所有不需要响应点击的Image和Text的Raycast Target。游戏里绝大多数UI元素不需要接收事件留着Raycast Target只会让Canvas的射线检测变重虽然它本身不直接增加Overdraw但减少不必要的元素能降低UI重建和批处理成本间接缓解GPU压力。第二步是砍掉Shadow和Outline组件。很多UI设计稿里的阴影效果完全可以用一张预烘焙阴影贴图替代。如果团队用的是TextMeshPro建议用TMP自带的Shadow/Outline写法去控制开销或者直接让美术把描边画进图集贴图里。实测同一个战斗界面只把20多个Shadow组件删掉UI模块的Overdraw就降低了30%以上。第三步是处理大面积半透明底。全屏黑色的半透明遮罩如果透明度低于80%基本可以直接改成不透明纯色UI视觉差别极小GPU省一大笔混合操作。磨砂模糊效果优先用RenderTexture把背景降采样后做一次模糊再贴成UI背景图别用多个半透明Image叠出模糊感。这里的原则是能不透明的绝不用半透明能预烘焙的绝不做运行时叠加。4.2 粒子系统调参之外的Shader整改粒子Overdraw的优化第一步肯定是调参数减少粒子发射数量、缩短生命周期、减小粒子大小、关掉不需要的Trail拖尾。这些都是常规手段但很多时候调完参数粒子效果就不好看了所以我更推荐从Shader层面想办法。常用的一个手段是给粒子材质加上Cutout控制粒子贴图的透明区域直接clip掉而不是参与混合。很多人担心Alpha Clip在移动端会破坏Early-Z确实有这个风险但粒子本来就是半透明队列本身就不走Early-Z优化通道所以用Clip把完全透明的像素剔除只对剩下的半透明像素做混合实际能减少不少Fragment负载。实现方式是在Shader的fragment函数里加一行clip(texColor.a - _Cutoff);比如把粒子贴图上完全透明的区域直接discard让GPU跳过混合计算。美术通常会留一层很薄的半透明边缘来保证圆滑过渡把_Cutoff控制在0.05到0.1之间视觉上基本无损但Overdraw能低一截。另一个思路是把整片半透明粒子合并成一张全屏贴图或者用Camera方向的Quad去模拟大范围光晕减少粒子数量。早期一个项目里环境雾气效果用了200多个半透明粒子后来换成一张逐帧播放的Flipbook贴图同样是雾粒子数量直接降到30GPU Fragment时间降了一半。这个方案对美术资源要求高一些但收益非常明显。4.3 后处理与摄像机分辨率、剔除和Pass数量后处理优化的核心思路是降分辨率和减Pass。URP的Bloom通常支持Downsample参数把它从1改成2或4计算量会成倍下降。DOF和Bloom同时挂的时候建议把Bloom的迭代次数减少或者把DOF放到半分辨率去做。实在不行就砍掉某个效果——移动端上一个后处理就够了别什么都想要。对于内置渲染管线我更推荐直接把后处理的RenderTexture分辨率改为屏幕分辨率的1/2或1/4。很多玩家在手机上根本分辨不出半分辨率Bloom和全分辨率Bloom的差别但GPU负载差好几倍。这里需要美术和程序达成共识移动端后处理的目标不是画面惊艳而是画面好看且散热压得住。摄像机侧有一个容易被忽视的优化减小Far Clip Plane。远裁剪面每拉远一点点视野里可能就多出成千上万个三角形和成片的小像素区域这些小区域虽然单个面积不大但数量上去后同样拉高填充压力。配合Occlusion Culling遮挡剔除把被挡住的室内物体裁掉才能真正做到没看到的东西不画。注意Occlusion Culling需要在Lighting窗口里先烘焙很多项目没做这个等于白白让GPU画了一堆被墙挡住的物体。4.4 一个综合案例把平均Overdraw从3.8降到1.9拿一个真实项目举例。之前接到一个MMO战斗场景的发热报告手机上GPU Frame Time稳定在13到14毫秒机身烫手。我用Scene Overdraw模式一看整个屏幕几乎都是红色尤其主城中心区域场景里密集的装饰物加上大雾粒子和复杂UI把Overdraw顶到了平均3.8倍。优化动作按优先级排先关掉所有UI多余组件的Raycast Target删掉20多个Shadow和Outline组件然后把背景雾粒子从250个降到60个材质里加了Clip裁剪透明区域接着把Bloom的后处理分辨率降到1/2关掉MSAA最后给每个非玩家角色关掉Cast Shadows替换成简单假阴影。一轮改完Overdraw平均值从3.8降到1.9GPU Frame Time从13.5毫秒降到8.2毫秒机身温度明显下降。整个优化过程没有动任何大美术资源纯粹是技术层面的减法——很多时候Overdraw高不是因为东西多而是因为一堆东西在同一个像素上重复劳动。5. 用数据说话优化前后的GPU时间、Fill-rate与机身温度验证5.1 记录基线Frame Time、Fill-rate与功耗优化前必须先记录基线数据否则改完效果根本说不清楚。我习惯在固定场景、固定机型的条件下记录以下几项平均帧率、Profiler里的GPU Frame Time、GPU Fragment阶段耗时、平均Overdraw估算值、以及机身温度曲线。平均Overdraw估算值可以这么算找一台固定分辨率的测试机把屏幕总像素数和Per-Frame Fragment Count做对比。部分Profiler工具能看到已渲染像素总数用它除以屏幕像素总数就是平均Overdraw。Unity自带的Profiler在GPU模块不一定直接显示这个值但Frame Debugger里每个Draw Call的屏幕覆盖面积可以粗略估算配合AGI的统计会更准。经验参考平均Overdraw在1到2倍之间是健康水平3倍以上就该警惕5倍以上基本必然发烫。机身温度需要注意测量方式。Android端可以用adb命令读取热区温度adb shell cat /sys/class/thermal/thermal_zone*/temp输出值通常是毫摄氏度比如47000代表47摄氏度。对比不同时间段读取的值画出发热曲线。iOS端可以用Xcode的Energy Log或Metal System Trace看温度状态。测温度时尽量让手机处于同一环境温度、同一帧率模式下跑同一段场景至少跑10分钟再记录避免刚开机和玩了半小时的数据混在一起。5.2 用真机Profiler验证Fragment阶段消耗优化完成后重新在相同机型、相同场景、相同路径下抓一次AGI或Xcode GPU Capture重点看Fragment阶段和Texture Bandwidth的变化。Fragment阶段从8毫秒降到4.5毫秒带宽占用下降这种数据最直观也最容易跟策划对齐——你说场景卡拿数据说话别拿感觉说话。如果优化后Fragment阶段没有明显下降那说明问题可能不再Overdraw而是Shader本身的计算量太大。比如一个复杂的BlinnPhong加上多层纹理采样就算只画一遍Fragment开销也很高。这种情况就要去优化Shader本身的复杂度而不是继续砍Overdraw。这也是为什么我一直强调Profiler数据是最终裁判因为它可以帮你区分画了很多遍和每一遍都很贵这两种不同的问题。5.3 可持续监控上线后的Overdraw巡检方法优化做完不代表一劳永逸。后续美术提了新的场景资源、加了新的特效、UI叠了新面板Overdraw随时可能反弹。我建议在项目里建立一套定期巡检机制每个月抽一个版本用统一机型跑一遍核心场景导出Overdraw热力截图和GPU时间曲线跟基线版本做对比。Unity的Scene视图Overdraw模式可以作为日常检查工具每次新场景提交前让场景美术自己切一遍Overdraw视图看一眼红色区域如果占屏幕比例太高打回修改后再提交。这个习惯养成之后Overdraw问题会少很多。真机数据巡检可以做成一个简单的自动化流程固定路径录屏回放用AGI命令行抓帧解析出Fragment耗时写入CI报表超过阈值自动报警。虽然搭建起来要花点时间但对长期维护的项目来说非常值。我自己踩过的一个坑是只优化了编辑器里看到的Overdraw忽略了真机上的分辨率适配。同一款游戏在低端机上是1080P渲染在高端机上是1440P渲染屏幕像素量差了近一倍Overdraw的绝对值也跟着翻倍。所以巡检时一定要带上不同分辨率档位的机型不能只看一款旗舰机。另一个经验是Overdraw和发热之间的关系有滞后性——不是帧时间一涨温度就立刻上来而是连续几分钟高负载后温度才明显升高。测试时至少持续运行五到十分钟别跑一个关卡就下结论。优化Overdraw这事跟装修刷墙一样最怕的就是差不多得了。每一遍被盖住的漆最后都会变成热量从手机背面透出来。多 Debug、多量化、多巡检你手里的项目才能真正做到看起来挺好摸起来不烫。
返回列表