1. 先把“搬运量”这件事说清楚做移动端图形优化做了六七年我最常说的一句话是发热的本质不是算得多而是搬得多。画面卡了大家第一反应是“顶点多了”“像素算不过来了”但在移动平台上一测功耗往往发现真正的开销大头是内存带宽也就是 GPU 从显存里把数据来回搬运的体量。GPU 的 ALU 算一次数能耗是几皮焦级别但从显存里搬一个 32 位的纹素进缓存能耗要高出几个数量级。一次全屏后处理要把整块颜色缓冲读一遍再写一遍数据搬运量动辄几十 MB一张 2048×2048 的 RGBA 纹理一次采样就是 16MB 的潜在读取。别小看这些数字手机 SoC 的带宽是共享预算CPU、GPU、NPU、显示控制器全都挤在这条高速公路上GPU 多搬一点整体功耗就往上蹿一点机身温度跟着就上去了。1.1 发热不是“算”出来的是“搬”出来的用搬砖来类比就很直白。**Shader 计算是在工地上砌墙纹理读取和后处理读写就是卡车在拉砖。**砌墙再快砖拉不过来都是白搭反过来砖拉得太多路堵死卡车油耗上去整条工地整个 SoC的温度就爆了。移动端 GPU 和桌面端架构不一样桌面 GPU 不差带宽显存位宽动不动 256-bit、384-bitGDDR6 的带宽以几百 GB/s 计手机这边是 LPDDR5 或 LPDDR5X 共享内存带宽通常在 30~70 GB/s 这个量级还要和 CPU 抢。也就是说同样的绘制内容放在 PC 上可能完全没瓶颈一到手机就发热降频根源就在“搬运量”超标。所以做发烫优化先不要急着砍特效、降帧率先把每个 pass 的数据搬运量算清楚往往会发现几个“搬运惯犯”——纹理和后处理是每次都会上榜的两位。1.2 纹理和后处理凭什么被叫“惯犯”先说纹理。一张 2048×2048 的 RGBA32 纹理不压缩时占用 16MB 显存在支持 ASTC 压缩后可以压到 1~2MBASTC 8x8差距是 8 到 16 倍。**如果项目里的纹理没有正确压缩等于每个物件都拉着一大卡车砖在渲染发热不找你找谁。**更隐蔽的问题是纹理压缩格式选错了或者用了不支持硬件直接采样的格式比如把 PNG 直接塞进纹理内存GPU 要先解码再采样搬运量成倍往上翻。后处理就更典型了。一个 Bloom 效果本来粒子、模型、场景光照都跑完了突然为了“氛围感”加了三四个全屏 pass每个 pass 都要把整块屏幕读进来、写出去。一次全屏 pass 的搬运量 分辨率宽度 × 高度 × 每像素字节数 × 2读写。1080p 的 RGBA16F 缓冲一次 pass 就是 1920×1080×8×2 ≈ 33MB。一顿后处理链下来搬运几百 MB 是家常便饭。这不是“算得很复杂”而恰恰是“搬得很离谱”。所以这个系列走到第 4 篇我决定把这两个惯犯单独拎出来审一遍讲清楚它们为什么耗电、怎么优化、踩过的坑有哪些。2. 纹理优化从压缩、层级到采样设置把货藏在源头纹理优化不是一个开关就能搞定的它是一套从资产制作到运行时的组合拳。我见过太多项目美术出图的时候用 PNG 直出程序加载的时候也不做转换结果光纹理解码就把 CPU 干到降频。下面按优化优先级把纹理相关的关键点拆开讲。2.1 纹理压缩不是“压画质”而是“改货箱”很多新手一听纹理压缩第一反应是“画质会不会变差”。这里要澄清一个概念移动端的纹理压缩更多是选择一种 GPU 硬件可以直接读取的存储格式而不是普通的图片压缩。JPEG、PNG 这类格式GPU 没法直接采样必须先解码成 RGBA 原始数据放进显存这个过程既慢又占空间。而 ETC2、ASTC 这类格式手机 GPU 的纹理单元能直接识别和采样读取的时候无需要解压压在显存里的体积还小带宽消耗也小。ASTC 是目前移动端最推荐的格式它是 ARM 主导的可扩展纹理压缩标准Android 和 iOS 的设备基本都支持。ASTC 的核心优势是可以灵活选择 block 尺寸4x4 块质量最高但压缩率最低8x8 块质量和体积比较均衡12x12 块体积最小但可能出现明显色块。我在项目里通常会定一套规则使用场景推荐格式说明UI 图标/小图ETC2/RGBA 或 ASTC 6x6UI 讲究清晰度不建议压太狠3D 角色贴图ASTC 8x8兼顾体积和皮肤/布料渐变质量场景大纹理ASTC 10x10 或 12x12远景和地面的细节损失肉眼可接受法线贴图ASTC 6x6 或 8x8法线信息比较敏感压缩过狠会有光照瑕疵这里要特别提醒一个坑如果目标设备不支持某种格式运行时就必须回退回退过程往往就是先把纹理通过 CPU 解码成 RGBA再重新上传显存这个操作会让首发卡顿和发热双爆炸。所以上线前一定要用真机列表的 GPU 信息核对一下设备支持的纹理压缩格式不要想当然。2.2 mipmap给 GPU 准备一套“阶梯货架”mipmap 的原理很简单就是从原始纹理开始每级缩小一半生成一串”等比缩小“的纹理序列从 2048 一直到 1x1。渲染时GPU 根据当前 fragment 到摄像机的距离自动选择合适的层级采样。别小看这个机制它对带宽的影响是数量级的。如果一个 2048 的纹理用在只有 64 像素大小的远处物体上你不开 mipmapGPU 照样要加载那张 2048 的贴图纹素然后做大量降采样采样缓存命中率极低基本等于“杀鸡用牛刀还把刀磨得特别大”。开了 mipmap 之后GPU 会直接挑 64 或 32 像素那个层级去读读取量小了十几倍带宽压力自然就下来了。操作层面3D 场景的纹理建议无条件开启 mipmap。代价是显存增加约 33%但这和带宽节省相比完全值得。另一个容易被忽略的设置是LOD 偏移Texture LOD Bias它控制 GPU 偏向选哪个层级调正一点可以让 GPU 倾向用更小的 mipmap能进一步省带宽代价是远处纹理变模糊。我一般在低端机配置里会把 LOD Bias 拉高 0.5~1.0肉眼几乎看不出差别但发热体感改善明显。2.3 各向异性过滤与采样器设置的平衡各向异性过滤Anisotropic FilteringAF是为了解决地面、墙面这类几乎平行于视线方向的表面采样模糊的问题。AF 级别越高采样质量越好但采样次数也越多带宽消耗越高。在移动平台我建议中高端机用 4x低端机用 2x 或者干脆关掉。这个阈值不是拍脑袋定的实测中 4x 到 8x 的画质提升很小但带宽开销几乎翻倍性价比很低。另外还有一个常见误区采样状态Sampler State不要每个对象都新建一个尽量做成全局复用。有些引擎里如果纹理的过滤方式、寻址方式不一致会导致纹理描述符和采样状态频繁切换本来就紧张的带宽花在了无谓的状态切换上。实际的工程里我会把采样器按“TrilinearClamp”“BilinearRepeat”“PointClamp”等几个固定配置提前建好材质里只引用配置 id不动态创建。说到 Unity 项目还有个隐藏的坑**Streaming Mipmap 的触发距离如果设得太近角色跑两步就要加载更高精度的 mipmap加载任务瞬间打满 IO 和 CPU发热反而更高。**建议把触发距离放宽让系统提前异步加载避免瞬时尖峰。3. 后处理优化低分辨率、合并 pass、少来回后处理一直是我在项目中最警惕的部分因为它太“好用”了。美术说加点辉光就上一个 Bloom说色彩要电影感就加 Color Grading说景深要高级再加 DoF。每加一个效果就是往 GPU 的“搬运清单”里添一笔全屏读写。3.1 一次全屏 pass 的真实成本先说一个简单的计算公式一次全屏后处理 pass 的带宽开销 ≈ 屏幕分辨率 × 每像素字节数 × 2一次读 一次写。以 1080p 为例RGBA8 缓冲4 字节/像素1920×1080×4×2 ≈ 16.6MB如果是 HDR 场景缓冲通常是 RGBA16F8 字节/像素一次 pass 就是 33MB。**一个典型的 Bloom 效果先降采样到 1/2再降采样到 1/4模糊两三遍再和原图合成前前后后差不多 7~9 个 pass。**就算每个 pass 只操作半分辨率累加起来的搬运量也在 200MB 以上。一天玩一小时就是几十 GB 的数据搬运发热不找你找谁。所以后处理优化的第一原则是能不用的 pass 就不用能合并的 pass 必须合并。比如 Vignette暗角、Bloom 的最终合成、Color Grading如果引擎支持尽量写在同一个 shader 里用一张全屏 RT 采样一次完成而不是叠三四个 OnRenderImage。3.2 Bloom 降分辨率性价比最高的后处理优化Bloom辉光是发热大户中的大户很多项目的问题就出在它的实现方式上。产品级做法是把 Bloom 做在降分辨率缓冲上而不是全分辨率。通常流程是先把场景 RT 降采样到 1/2 分辨率甚至 1/4然后在这个低分辨率缓冲上做高斯模糊和叠加。由于模糊本身就是低频操作低分辨率下做几乎不影响观感但搬运量直接降到 1/4 或 1/16。实际参数可以参考1080p 下Bloom 的源分辨率用 540p模糊迭代 4 次半径 2~4 像素效果和 1080p 全分辨率做 6 次迭代的差距很小但带宽开销能差 5 倍以上。我在项目里测试过单纯把 Bloom 降一半分辨率整机功耗能掉 0.3W~0.5W机身温度下降 2~3 度画面观感几乎没变化这是后处理优化里投入产出比最高的一刀。色差的另一个细节不要在 HDR 的 16F 缓冲上重复做模糊最好先降采样到 8-bit LDR 缓冲再做效果叠加。很多效果在 LDR 下完全够用硬生生在 HDR 缓冲上来回读写带宽翻倍还感觉不到差异。这个取舍在移动端尤其重要。3.3 移动端 Tile GPU 下少“碰”缓冲比狂压特效更稳移动 GPU 大多是 Tile-based Deferred RenderingTBDR架构渲染完一个 tile 之后数据是存在片上高速缓存On-Chip Memory里的。如果当前渲染目标的内容不需要拿到外部显存去读写就能省下大量内存带宽。这也是为什么移动端极力推荐使用 FrameBuffer Fetch帧缓冲读取的原因——上一阶段算完的颜色不用出去绕一圈再回来采直接在 tile 里传给下一阶段。对于后处理链要注意引擎实现方式。很多后处理栈为了通用性会跑若干个全屏 blit pass每一下都从显存读写。这在桌面端没什么但在移动端就是灾难。有条件的话尽量选用支持“合并 pass”的后处理实现比如 GitHub 上一些开源的移动端后处理栈会把多个效果写进一个 shader通过关键字切换一次全屏绘制就把 Bloom、Color Grading、Vignette 全做了。本质上是把原来七八次全屏读写压缩到两三次。还有个很实际的经验不要对 UI 层做后处理。有些项目图省事把 UI 也放进屏幕空间特效里结果 UI 每一帧都被重新采样和读写。UI 一般应该独立一层在最后直接合成不参与任何后处理链。4. 实操一套可以照着抄的优化流程这一节我拿一个典型的中型 3D 手游项目举例走一遍纹理和后处理的优化流程。这项目当时的症状是中端机跑 10 分钟就发烫降频帧率从 45 掉到 25手感全无。用 PerfDog 一测功耗平均 5.8W其中 GPU 占了大头。4.1 纹理资产检查与压缩方案落地第一步先把项目的纹理资产清单拉出来检查压缩格式和尺寸。我们当时的检查脚本统计出项目里有 1200 张纹理其中约 40% 还是 RGBA32 未压缩格式主要原因是美术用的图片导入设置不对。处理步骤是按使用场景把纹理分成 UI、3D 模型、场景、特效四类每类指定不同的压缩格式和最大尺寸限制。对所有 3D 纹理批量开启 mipmap并设置 LOD Bias 全局偏移 0.5。对未压缩纹理统一走 ASTC 8x8Android和 ASTC 8x8iOS的转换格式不支持的旧机走 ETC2 回退。UI 纹理单独保留 ETC2 或 RGBA 线性格式避免 UI 文字和图标出现压缩脏色。这里最花时间的是处理“尝试压但压坏”的纹理法线贴图如果用 ASTC 10x10 压某些地方会出现明显的“麻点”高光也怪。我们的做法是单独筛出法线贴图列表统一用 ASTC 6x6压完在真机上拉几个场景跑了验证肉眼基本看不到差异后定稿。改完纹理这步中端机上的 GPU 负载立刻降了一截。PerfDog 显示 GPU 利用率从 82% 降到 65% 左右整机功耗降到 4.7W。4.2 后处理链重构降分辨率 合并 pass当时项目用了一个第三方的后处理栈默认跑 Bloom DoF Color Grading Tonemapping Vignette一共 12 个全屏 pass1080p 下每帧搬运量高达 300MB非常夸张。我们做了两件事第一Bloom 和 DoF 全部降到 1/4 分辨率执行。具体做法是先把场景 RT 用双线性降采样到 480p 的中间缓冲所有模糊类效果都在这个缓冲上跑最后再 upscale 合回主缓冲。模糊类效果本身是低频信息低分辨率执行基本不失真。第二把 Color Grading、Tonemapping、Vignette 合并到最终合成 pass。这三个效果的输入输出完全一致完全不必要分开执行。我们把它们浓缩成一个 shader一次全屏绘制全部完成RT 读写的次数从 3 次减到 1 次。优化后的后处理链从 12 个 pass 缩减到 4 个 pass降采样 pass、Bloom 模糊 pass×2、最终合成 pass总搬运量从 300MB/帧 降到 70MB/帧左右。这 230MB 的节省直接反映在整机功耗上又降了 0.6W。如果项目对画质要求更高还有一个折中方案让玩家可以选“性能模式”后处理渲染分辨率固定为屏幕分辨率的 75%高端机默认 100%。低端机的体验会平滑很多不至于一开特效就发烫掉帧。这本质上是用分辨率换带宽也是手游厂商最常用的“自适应画质”策略之一。4.3 用工具量化验证不要凭感觉优化优化的效果必须用数据验证不能凭“好像凉快了”这种玄学感觉。我在项目里固定用几套工具组合工具用途关键指标PerfDog整机功耗、帧率、CPU/GPU 占用整机平均功耗、帧率波动Unity Frame Debugger查每帧画了多少 drawcall、纹理绑定情况drawcall 数、RT 切换次数RenderDoc定位后处理 pass 的输入输出、资源状态RT 尺寸、格式、pass 顺序Xcode Metal System Trace / ARM Streamline查看 GPU 带宽和缓存命中率带宽利用率、L2 命中率优化过程中我习惯在改一个措施前后各测一次**同场景、同路线、同机型跑 15 分钟记录平均功耗和最高温度。**没有前后对照的优化都是耍流氓因为你根本分不清是哪个改动起作用了还是只是散热环境变了。RenderDoc 对查后处理特别有用。它会显示每个 pass 的渲染目标尺寸和格式你一眼就能发现哪些 pass 在 1080p 的 RGBA16F 上折腾。另一个常用技巧是材质 Debug View直接输出纹理的 mipmap 层级覆盖图检查场景纹理是否真的按照距离正确选择 mipmap 层级。5. 常见问题与排查技巧实录做优化这些年踩过的坑不少有些问题甚至反直觉。我把最常见的问题整理成一张速查表再挑几个典型展开讲。现象可能原因排查思路与对策纹理压缩后出现明显色块/麻点压缩格式选得过狠或法线贴图不该压这么狠法线贴图改用更小的 blockASTC 6x6UI 纹理避免过度压缩帧率高但机身烫GPU 带宽超限碎片化读写太多用 profiler 查看带宽指标重点排查后处理 pass 和未压缩纹理远处贴图闪烁/摩尔纹mipmap 没开或 LOD Bias 过小开启 mipmap调大 LOD BiasBloom 一开帧率就腰斩Bloom 在全分辨率 HDR 缓冲上多次模糊降到 1/4 分辨率利用低分辨率缓冲完成模糊最后再合成低端机比高端机烫得多分辨率/后处理参数未按档位适配实现画质分档低档位用 75% 分辨率渲染后处理关掉 DoF后处理链一堆 RT内存带宽爆炸过多全屏 pass 独立读写合并 pass减少 RT 切换尽量一次全屏绘制完成多个效果某些 Android 机型纹理花屏纹理压缩格式不支持回退逻辑出问题核对 GPU 支持的格式列表做好 ASTC/ETC2 的运行时判断5.1 为什么“画质全高”和“发热”是老冤家很多玩家抱怨“手机一开高画质就烫”这是正常的但也可以优化得更平衡。高画质意味着更大分辨率、更高精度的纹理、更复杂的后处理每一样都在往带宽上加码。我通常会给玩家提供“极致/高清/均衡/省电”四档每档背后其实就是一套参数组合极致原生分辨率渲染后处理 100% 分辨率纹理全高AF 8x高清渲染分辨率 90%后处理 75%纹理中高AF 4x均衡渲染分辨率 80%后处理 50% 纹理中AF 2x省电渲染分辨率 70%后处理 25%纹理低AF 关这四档对发热的影响差异很大但对玩家来说均衡档的画面观感依然过得去。真正应该避免的是“一刀切”让所有机型都跑同一套效果低端机体验糟糕又不甘心降画质。5.2 关于纹理坐标、UV 接缝和压缩格式的一个冷门坑再分享一个比较冷门的案例。有一个场景贴图压成 ASTC 8x8 后某些模型的 UV 接缝处出现了一条明显的“亮边”。排查了很久最后发现是 UV 跨了多个纹理 block 边界ASTC 在 block 边界压缩时产生了不连续。解决方法是把模型的 UV 做 4 像素的 padding或者改用 ASTC 6x6问题立刻消失。如果新版压缩格式引入不可见的边界瑕疵不要硬扛考虑在资源生产端做调整。5.3 优化后的复查和回归最后提醒一句**纹理和后处理优化做完之后一定要回归真机测试特别是帧率稳定性Frame Time 的 P1/P99和发热曲线。**有时候优化后平均帧率上去了但 1% Low最卡的一帧反而更难看了说明某些改动引入了偶发性的加载尖峰。这时候配合 Streaming Mipmap 的触发距离、纹理异步加载的队列长度把这些尖峰压平体验才算真正稳住。我个人在实际项目里最常用的收尾手段是在游戏设置页放一个“自动检测画质”按钮根据真机跑分自动给玩家推荐一组合适的参数。**与其让玩家在发热和画质之间反复纠结不如让设备自己说出它扛得住的档位。**这个思路比任何单一的优化技巧都更能提升口碑。做优化这么多年回头看纹理和后处理这两个“惯犯”之所以总在发热榜单里排名靠前核心原因就是它们和带宽的关系太密切了。带宽这东西平时看不见摸不着但每一份发热账单里都写着它的名字。当你把纹理压缩做到位、mipmap 全部打开、后处理链压到最低必要 pass 数的时候你会发现整机功耗降得比想象中更明显而画面观感却几乎没有牺牲。这大概就是图形优化最“值”的那部分工作了吧。
