ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity Overdraw优化:从GPU发热原理到移动端降耗实战

Unity Overdraw优化:从GPU发热原理到移动端降耗实战 手机玩到某个关头背面开始发烫帧率从 60 掉到 45卡顿说来就来。如果你已经排查过 Draw Call也优化过 CPU 主循环帧耗时却还是压不下去那大概率是 GPU 侧出了问题。而 GPU 侧最容易被人忽略、又经常“默默烧钱”的一个指标就是 Unity Overdraw。拿刷墙来比喻同一面墙你来回刷了 N 遍漆每一遍都真实消耗涂料和人工但最后墙并没有因此变得更硬、更白纯粹是浪费。Overdraw 就是同一块屏幕上同一个像素被反复绘制了 N 遍GPU 每画一遍都要跑一遍光栅化、像素着色器再把结果写回帧缓冲重复得越多功耗越高手机自然越来越烫。这篇文章会从 Overdraw 的发热原理讲到检测手段再把 UI、粒子、半透明、阴影、后处理这几大重灾区逐个拆开最后给出我实际项目里用过的优化动作和验证方法。适合同样在做发烫优化、而且已经处理完 CPU 和 Draw Call 的 Unity 开发者。1. Overdraw 的发热原理GPU 像素管线的重复劳动1.1 一个像素被画了 N 遍GPU 就多烧 N 遍的电要讲清楚 Overdraw得先回顾 GPU 绘制一块画面的基本过程。顶点数据从 CPU 送进来GPU 先做顶点变换和裁剪然后把三角形光栅化生成一个个像素位置。接下来的像素着色器会对这些像素逐个执行材质计算最后把颜色写进帧缓冲。同一个像素位置如果在同一帧里被多个物体覆盖就会被反复执行每一套完整流程。举个例子一个 UI 弹窗叠了五层半透明图片最底层是一张全屏遮罩上面有背景框、按钮底图、按钮文字、再叠一层光效。用户看到的是最上面的效果但 GPU 实际把遮罩、背景框、按钮底图都完完整整画了一遍。像素着色器的所有指令包括纹理采样、颜色计算、混合操作每一层都要跑一次。在移动端 GPU 里这个消耗会被放大。因为移动 GPU 通常把屏幕分成很多小块Tile每个 Tile 在芯片内部完成渲染后再写回显存。Overdraw 越高意味着 Tile 内要处理的像素着色次数越多芯片的计算单元忙得更久功耗自然更高。你摸到手机背面发烫很多情况下就是在为这些看不见的重复工作买单。1.2 从 FillRate 到带宽移动端为什么扛不住Overdraw 影响的并不仅仅是 GPU 计算单元更致命的是显存带宽。现代移动 SoC 里内存控制器的带宽是固定的而读写帧缓冲、读写贴图都要占用这部分带宽。整个系统里 CPU 要读写内存、GPU 要采样纹理、还要写帧缓冲大家挤在同一条高速公路上。算一笔简单的账假设 1280×720 的分辨率RGBA8 每像素 4 字节平均 Overdraw 是 8跑 60 帧那么一秒内写帧缓冲的数据量大约是1280 × 720 × 8 × 4 × 60算下来接近 1.77 GB/s。如果把分辨率提到 2560×1440这个数字直接变成 7.1 GB/s 左右。再看移动端内存带宽LPDDR4X 一般在 17~34 GB/sLPDDR5 在 50 GB/s 左右听起来数字不小但 CPU、贴图采样、顶点数据、GPU 其他读写操作都要分走带宽。Overdraw 一旦失控带宽占用就会成倍上涨芯片功耗跟着拉升热量散发出来手机就到降频边缘了。所以移动端项目里分辨率和 Overdraw 是必须一起控制的。很多人只盯着分辨率调 Render Scale却忽略了一个画面上几十层重叠的粒子、UI、半透明网格就算分辨率压得再低Overdraw 照样能把带宽耗尽。1.3 不同场景的 Overdraw 参考值我整理了一份自己在项目里常用的经验参考值方便你判断当前场景的严重程度。场景平均 Overdraw说明纯天空盒 简单地面1~2基本没有重叠正常水平常规打斗场景2~4角色、技能、地面装饰产生一定重叠复杂 UI 弹窗5~8多层图片叠加、半透明遮罩值得警惕粒子特效密集场景8~12满屏半透明粒子叠加发热重灾区堆叠多个全屏后处理等效再翻 3~5 倍Bloom 的多次降采样升采样会全屏读写当然这个数值不是绝对的。同样 Overdraw 是 5一个只采样一次颜色的半透明粒子和一个要算阴影、反射、折射的复杂半透明 Shader发热代价完全不同。但作为最初筛查参考值能帮你快速锁定重点场景。2. 先把 Overdraw 看透Scene、Frame Debugger 和真机 Profiler 三件套2.1 Scene 视图 Overdraw 模式一秒钟找出重灾区很多项目优化 Overdraw 靠的是肉眼猜觉得“这里好像有点亮”“那里感觉叠了很多层”。实际上 Unity 早就提供了 Overdraw 可视化模式。在旧版内置渲染管线里Scene 窗口左上角把绘制模式切到 Overdraw就能看到彩色覆盖层。URP 里现在更推荐用 Rendering Debugger路径是 Window Analysis Rendering Debugger在 Rendering 选项卡里的 Debug Mode 中选择 Overdraw。这个模式会把绘制次数映射成颜色越亮、越偏红的区域说明被绘制次数越多。你不需要逐帧分析一眼扫过去就能看到哪些屏幕区域是“刷了好几遍漆”的地方。打开之后我发现很多项目的弹窗界面、战斗技能特效中心、角色脚下阴影叠加处全是红得发亮的区域这些地方就是发热的元凶。不过要泼一盆冷水编辑器里的 Overdraw 视图反映的是绘制次数不直接等于真实渲染耗时。有些区域虽然偏红但 Shader 很便宜优化收益有限有些区域颜色不是特别亮但 Shader 里做了五六次纹理采样反而是真正的性能瓶颈。所以 Overdraw 视图用来定位重灾区具体优化优先级还要结合 Shader 成本和真机能耗来判断。2.2 Frame Debugger 逐 Draw Call 分析看到底是谁在重复画Overdraw 视图告诉你哪里有问题Frame Debugger 告诉你是什么东西在重复绘制。打开 Window Analysis Frame Debugger在播放状态下点击 Enable左侧会出现当前帧的所有 Draw Call 列表。逐条点开右侧能查看这个 Draw Call 使用的 Mesh、材质、PassScene 视图里也会高亮显示这次绘制的内容。实际排查时我会从可疑区域反推比如 UI 弹窗区域 Overdraw 很高就在 Frame Debugger 里往下翻先把所有 Draw Mesh 里引用的 UI 元素 Mesh 找出来看看同一个 Image 是不是被画了好几次。一个常见情况同一张角色立绘为了让边缘发光美术叠了三个半透明材质层每层都是一个独立的 Draw Call 和完整的像素着色流程。在 Frame Debugger 里你能清楚看到同一个网格出现三次这就是典型可优化的 Overdraw 来源。另一个容易被忽略的是基础通道和附加光源通道。前向渲染里一个物体如果被多盏实时灯照射每多一盏逐像素灯就多一次完整的着色 Pass。Frame Debugger 里你能看到同一个模型在同一帧里被反复绘制这种情况下单纯压缩 Overdraw 没用还需要从光照方案上动手。2.3 真机 Profiler不要只看 Frame Time编辑器里的 Overdraw 视图和 Frame Debugger 只能说明绘制次数真机的实际影响必须用 Profiler 和硬件工具确认。Unity Profiler 连接真机后切换到 GPU Usage 或 Rendering 模块能看到渲染循环的耗时分布。但只看 Frame Time 还不够因为 Overdraw 导致的带宽压力有时并不直接体现在 GPU 时长上而是体现在整机功耗和降频上。有条件的话Android 平台可以用高通或 ARM 的 GPU 性能分析工具看总线利用率和 GPU Active CyclesiOS 平台可以用 Xcode 的 Metal System Trace 或 Instruments 里的 GPU 项目。这些工具对大多数人来说有点重但至少要在同一台真机上做“控制变量”测试固定同一个镜头、同一段场景、同一个角色动作跑三分钟记录帧率曲线和机身温度再对比优化前后的数据。只有这样得出的优化结论才靠谱光在编辑器里看数据是没有说服力的。3. 五大 Overdraw 重灾区UI、粒子、半透明、阴影与后处理3.1 UGUI 的重复绘制图片叠图片阴影叠阴影UI 是 Overdraw 最容易被忽视的重灾区。UGUI 的渲染机制是把同一 Canvas 下的所有 UI 元素按层级顺序绘制元素相互遮挡时遮挡区域就会被重复填充。一个常见的弹窗界面往往由全屏半透明遮罩、底部背景框、渐变底图、内容面板、多个按钮、文字、图标叠加而成。你以为用户看到的是一个精致界面实际上 GPU 把整个弹窗区域反复画了五六遍。更糟糕的是很多项目的图片资源没有打图集一个按钮底图、一个图标、一个背景各自占用独立的纹理材质合批断裂Draw Call 上升的同时半透明层叠也没减少。Shadow 组件和 Outline 组件也会让同一元素多绘制一遍。我见过一些项目一个按钮为了做立体感挂了一个 Shadow 和一个 Outline整个按钮区域的实际绘制次数瞬间多了两遍。而且 UI 在全屏状态下参与 Overdraw 时通常是一个全屏覆盖层就算是“很便宜”的纯色 Image也会额外触发全屏像素写入发热影响不小。3.2 Particle System 的透明混合满屏都是半透明粒子系统是移动端 Overdraw 的另一座大山。ParticleSystem 默认的渲染方式就是生成大量面向摄像机的四边形每个四边形都使用半透明混合Alpha Blend 或 Additive。半透明混合意味着 GPU 必须把粒子颜色和背景颜色做一次混合运算既不能像不透明物体那样直接覆盖也不能用简单的 Z-Buffer 剔除。粒子层数一多整个屏幕几乎每个像素都被反复读写。项目里常见的情况是一个大招特效由三四个粒子系统叠加而成有拖尾、有爆点、有散射、有光晕每个系统再发射几百个粒子粒子尺寸还拉得很大。粒子之间互相重叠粒子和角色、粒子和场景也重叠最终这些区域的实际 Overdraw 能轻松超过 10。更麻烦的是粒子通常会动几乎每一帧的覆盖区域都在变化缓存优化很难生效GPU 只能老老实实把每一层都算一遍。3.3 半透明 Mesh玻璃、水面、空气墙场景里的半透明网格是第三种常见来源。玻璃墙、水面、全息投影、光柱、隐形护盾这些物体为了视觉效果必须走半透明队列。半透明物体之间以及半透明物体和不透明物体之间遮挡部分都会产生额外的绘制。大面积半透明是移动端的“重罪”。一块覆盖屏幕 1/3 的水面如果 Shader 里还叠加了反射、折射、菲涅尔计算每个像素的着色成本都非常高再加上水面上漂浮的粒子特效、岸边物体的半透明倒影整个画面区域等于被反复刷了好几层漆。还有一些项目喜欢用半透明网格做“空气墙”或者场景氛围光这些看似不起眼的透明片叠得多了整片屏幕都会被拖慢。3.4 实时光照与阴影动态物体的隐形成本严格意义上实时光照和阴影并不完全等于 Overdraw但它们在发热上有着同样的“每像素重复计算”特征。一个在实时阴影范围内的动态物体像素着色器需要计算阴影衰减地面、其他物体上的投射区域也会因此多一次纹理采样和乘法运算。如果场景里有多盏实时灯光每增加一盏逐像素光源在这个光源照射范围内的物体就要多执行一遍完整的着色流程相当于给画面多涂了一层“漆”。Unity 的前向渲染尤其明显。URP 里虽然对主光源有优化但多光源叠加时每个物体可能因为不同光源组合产生额外的 Pass。这种情况和 Overdraw 本质上是同一类问题同一像素在同一帧里的着色次数被重复放大。处理光源数量比一味减少物体 Mesh 面数更紧急。3.5 后处理全屏 Pass每一个特效都在叠层后处理极其隐蔽因为它不会出现在你常规检查的 3D 场景里但它的每一次全屏 Pass 都在对整个屏幕做读写。以 Bloom 为例先对全屏做一遍亮度阈值提取然后连续几次降采样、模糊、升采样每一个步骤都是一次全屏或半全屏的纹理读取和写回最后还要把 Bloom 结果和原场景做一次合成。整个过程下来等效 Overdraw 翻了 3 到 5 倍哪怕原场景 Overdraw 只有 2加完 Bloom 之后整个屏幕区域的实际读写次数也非常可怕。还有景深、抗锯齿、色彩分级、暗角。这些特效单独看都不重但叠在一起就是一场全屏带宽灾难。移动端项目里常见的问题是为了“画质好”把后处理全开结果一打起来就疯狂发烫掉帧。发热优化这块后处理往往是最值钱的一刀。4. 对症下药每一类 Overdraw 的优化动作清单4.1 UI用图集预烘焙阴影而不是用组件实时生成处理 UI Overdraw核心思路是“少叠层、能合就合”。第一步是排查弹窗、主界面、战斗结算界面这些高频界面把每个元素的 Image 层数记录下来砍掉视觉上可有可无的层级。全屏半透明遮罩能改用局部遮罩就别用全屏按钮底图、背景底图可以优先考虑合并成一张完整图集。第二步是禁用 Shadow 和 Outline 组件让美术直接在贴图里画好阴影和描边效果。表面上看这只是把阴影改成了贴图素材实际上它同时减少了半透明层叠、额外 Draw Call 和纹理采样一举三得。第三步是拆分 Canvas。把动态元素和静态元素拆开动态列表的滚动区域单独放一个 Canvas背景、标题、按钮等不常变动的元素放另一个 Canvas能有效减少 UI 重建导致的额外开销。第四步是检查 RaycastTarget。不需要接收点击的 Image 和 Text 把 RaycastTarget 关掉这样既减少 UI 事件检测的计算也间接降低一些不必要的界面逻辑压力。最终目标是把复杂界面的平均 Overdraw 压到 3 以下。4.2 粒子画得小一点活少一点粒子优化的第一原则是控制粒子在屏幕上的覆盖面积。粒子面积越大半透明混合的成本越高。把粒子的尺寸上限收紧从一个几十米的巨大光柱缩小到 3~5 米的范围内视觉冲击可能只差一点但 Overdraw 能下降一个量级。第二原则是控制同时存在的粒子系统数量。我见过一些战斗场景同一时刻有六个粒子系统同时运行它们互相叠加屏幕中央全是接近白色的一片。这种时候你应该做优先级取舍或者让特效团队用更少的系统达到同样的视觉表现。三个粒子系统能表达的效果不要用六个。另一个常见优化点是关闭粒子系统的 Shadow Casting 和 Receive Shadow粒子的阴影计算对画面贡献很低却能让每个粒子像素的着色成本翻倍。第三原则是资源复用。粒子贴图尽量打进图集避免不同粒子系统使用不同材质。相同材质的粒子系统数量减少后合批更容易Draw Call 和 Overdraw 压力都会缓解。额外说一句Additive 混合并不比 Alpha Blend 便宜多少别再迷信“改成 Additive 就不烧了”它一样要读写帧缓冲。4.3 半透明从 Transparent 队列挪到 Opaque 队列半透明对象最容易想到的优化是换渲染队列。很多所谓“半透明”效果其实只是想在表面留一些高光和边缘透光并不需要真正的 Alpha 混合。把这些对象从 Transparent 队列改成 Opaque 队列配合 Alpha Clip可以让它们参与不透明物体的深度测试被遮挡的像素可以在早期阶段被丢弃大幅减少重复着色。对于必须保持半透明的物体比如水面、玻璃优化重点放在降低 Shader 复杂度上。水面 Shader 里如果同时有反射采样、折射采样、法线多次扰动、菲涅尔计算那每个像素的着色成本都相当高。移动端精简版水面一般只需要采样一次颜色、一次法线配合简单高光就足够。半透明特效之间的排序也要处理好让同一区域内的半透明层数尽量少不要让两块大面积半透明完全重叠。场景布局上至少别让半透明水面上再叠满屏粒子不然 Overdraw 一定爆表。4.4 阴影距离、烘焙光照、深度预写光照和阴影的优化优先做的事情是减少实时阴影影响范围。URP 里的 Shadow Distance 参数能控制阴影贴图实际生效的距离。距离设得太大远处没必要的物体也会参与阴影计算还会增加地面上的阴影采样开销。移动端项目一般把 Shadow Distance 控制在 20~50 米角色周围有影子就行远山远树就不要强行算阴影了。静态场景的光照尽量全烘焙。Lightmap 加光照探针的组合在移动端的效果和性能平衡很好。动态物体需要阴影时让它们接收主光的阴影即可关闭次要光源的实时阴影。顺便提一个很多团队容易忽略的点URP 里可以开启 Depth Priming Mode让物体先统一画一遍深度再进入正式着色阶段。这个方案对不透明物体的 Overdraw 压制很有效但会额外增加一次深度全屏 Pass具体收益要看场景复杂度值得在项目里实际测一轮再决定用不用。4.5 后处理参数瘦身移动端不过度堆叠后处理的优化优先级最高因为直接砍效果见效最快。Bloom 在移动端是发热大户迭代次数从 5 次降到 2~3 次模糊范围适当收窄画质损失在大屏手机上通常可以接受但功耗能明显下降。Render Scale 从 1.0 降到 0.9 或 0.85像素总量变成原来的 81% 或 72%全屏 Pass 的读写开销也跟着下降这个参数在后处理越多时收益越明显。更激进的做法是用 Dynamic Resolution。URP 支持动态分辨率当 GPU 负载超过阈值时自动降低渲染分辨率帧率稳定后再恢复。这套机制适合战斗场景的特效峰值保护。最后提醒一句所有后处理效果加在一起要统计一下总 Pass 数。一个 Bloom 加一个景深加一个色调映射就是好几轮全屏读写。如果项目对发热敏感宁愿美术去调整 Bloom 强度也不要让多个全屏特效无脑叠加。5. 移动端 GPU 架构差异优化优先级也要分机型5.1 PowerVR 的 HSR能自动隐藏表面消除但半透明依然挡不住在 iOS 设备上GPU 通常采用类似 PowerVR 的 Tile-Based Deferred Rendering 架构内置了 Hidden Surface RemovalHSR。这个机制可以在渲染早期阶段识别出被遮挡的像素直接在 Tile 内存里把它丢掉后面的像素着色器根本不会执行。所以在纯不透明场景里哪怕 Overdraw 达到 3 或 4PowerVR 也可能通过 HSR 自动消掉大部分无效像素实际发热没有理论上那么夸张。但 HSR 对半透明物体基本没什么用。半透明必须保持绘制顺序前景、背景、粒子、UI 一层一层混下来HSR 不能因为后面的半透明层遮挡了前面的像素就去掉它它们天然就是互相依赖的。所以你会看到一种现象同一个 Overdraw 严重的粒子特效在 iPhone 上可能温温热在中高端 Android 上却已经烫得降频了。做优化时千万不要因为测试机是 iPhone 就放松对 UI 和粒子的 Overdraw 管控。5.2 Adreno 和 Mali 的 Early-Z 与绘制顺序敏感性现在的 Adreno 和 Mali 也都是基于 Tile 的架构也有 Early-Z、Forward Pixel Kill 一类优化。关键是这些优化非常依赖绘制顺序和深度状态。如果物体不是大致按由近到远排列的后面的物体会在深度测试前就进入像素着色阶段白白浪费一堆计算。Unity 的 Opaque 队列在排序策略上并不保证严格从近到远很多时候是按材质、Shader、队列顺序排的所以实际场景中不透明物体的 Overdraw 依然是真实存在的开销。Mali 的 Forward Pixel Kill 也要求深度信息已经准备好。深度数据不可用、物体用了复杂 Alpha 测试这些早期剔除机制都会失效。这就是为什么有些场景明明看起来不复杂在中端 Android 上运行却特别烫。如果项目要覆盖中低端 Android 设备不能在优化时只依赖 GPU 的自动隐藏。人工控制绘制顺序、考虑开启 Depth Prepass或者减少同屏物体数量才是更可靠的方案。5.3 实测数据同一个 Overdraw 场景在不同机型上的表现我自己在项目里测过一个战斗场景不透明部分 Overdraw 平均 3透明粒子部分平均 8画面上还叠加了 Bloom 后处理。新 iPhone 上帧率稳定 60机身只是温温的同场景放到一台中端 Android骁龙 7 系列级别帧率只能稳定在 40 左右背面明显发烫。后来我们把粒子的覆盖面积缩小Bloom 迭代次数从 5 砍到 2透明 Overdraw 从 8 降到 4 左右这台 Android 设备终于能稳 55 帧以上iPhone 这边也变得更省电。这个案例想说明的是优化不能只看“能不能跑 60 帧”。同一份 Overdraw 开销在旗舰机型上可能感知不强在中低端机型上就是体验灾难。只要你的发行目标里包含 Android就必须按最差设备做基准来优化别被高端机的体感骗过去。6. 优化完怎么验证数据对比与回归防线6.1 记录基线数据优化前先量化不要凭感觉Overdraw 优化很容易陷入“改了一堆东西但不知道哪个有效”的困境。所以第一步是建立基线优化前在同一台真机上用同一个游戏包、同一段场景路径、同一个镜头角度跑一遍并记录数据。需要记录的主要是 FPS、Frame Time、GPU Usage、机身温度。还要在编辑器里用 Rendering Debugger 的 Overdraw 模式截一张图把红色区域分布作为视觉基线。优化后再跑同一条路径记录相同数据。对比 GPU 耗时和温度曲线通常能明显看到改善。机身温度测试时要注意环境温度一致最好放在相同材质的桌面上跑同样时长避免空调风向、手机壳隔热这些变量影响结论。我自己习惯的做法是连续跑 10 分钟取后 5 分钟的平均帧率和峰值温度这样比只跑 1 分钟稳定得多。6.2 把 Overdraw 检查加进日常流程防止回归最后提一个团队协作层面的建议把 Overdraw 视图截图当成版本评审的固定检查项。尤其是特效、UI、新场景提交时看 Overdraw 截图里有没有新出现的红色高亮区域。粒子系统提交时限制同时叠加的层数UI 设计稿评审时检查半透明层级Shader 统一走 URP 标准流程避免有人为了“效果”塞进自定义双 Pass 半透明 Shader。版本迭代过程中Overdraw 的问题很容易在不知不觉中回来。今天加一个光效明天加一个全屏特效后天调整后处理参数每步看起来都不严重积累到一定程度又回到发烫的老路。有了截图对比和性能基线的流程至少能在合入前发现趋势而不是等玩家把差评写出来。从我个人的实际体会来看Overdraw 这类问题最反直觉的地方在于它不像 Draw Call 那样一眼能数出来也不像 CPU 慢那样能靠堆代码解决它藏在一帧画面的“重叠”里。只有习惯了用 Overdraw 视图扫场景、用 Profiler 量化收益、把优化动作落到日常流程里发烫优化才算是真正闭环。最后分享一个我自己常用的土办法把优化前和优化后的 Overdraw 截图各存一份每次版本回归时翻出来对比哪个版本开始屏幕上的红色区域悄悄变大了顺着那次改动往下查基本都能找到是哪一帧、哪一个特效、哪一层 UI 把“漆”又刷厚了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进