移动端游戏发烫优化:纹理压缩与后处理的带宽实战指南 📅 发布时间:2026/9/14 20:51:37 👁 浏览次数: 前几篇我把手机游戏发烫的根源从CPU的绘制指令、Overdraw的像素填充聊到了着色器复杂度今天换一个更隐蔽的方向——纹理和后处理。这两个东西在项目里看起来是人畜无害的“静态资源”和“收尾特效”但等到真机一跑它们往往是让GPU满头大汗地把数据从内存搬到显存、从渲染目标搬到采样器里的两个“惯犯”。我先说个判断标准如果你发现某个手机场景帧率不稳、机身发热但CPU占用不高、渲染状态切换也不多那大概率不是“算力不够”而是“搬运量太大”。这里的“搬运量”专业一点叫内存带宽消耗。纹理的采样命中和后处理的每一次全屏读写都是在跟带宽较劲。搞清楚这笔账你就知道为什么有时候少点两个特效反而比删掉半张场景更管用。这个系列前面几篇偏重“怎么查”这一篇我想换个节奏把纹理和后处理放在一起做一次系统的“拆案”先解释为什么它们搬运量大再给一套可以直接抄作业的优化流程最后把我这些年踩过的坑一次性掏出来。1. 先看案发现场GPU 到底在“搬”什么很多人在做发烫优化时有个误区以为GPU的发热只跟“计算强度”有关比如像素填充率、顶点数、Shader里的复杂光照。实际上移动端GPU的发热大头往往不在“算”而在“取数”。你可以把GPU想象成一个仓库搬运工Shader只是仓库里的记账员记账员再聪明运算再快只要搬运工来回跑的路程太长仓库外就会先“热”起来。1.1 “搬运量”这个词其实是在说带宽移动端目前普遍是统一内存架构GPU和CPU共用同一块系统内存通常就是LPDDR4或者LPDDR5。这个内存的带宽是固定的几十GB/s听起来不少但要在高分辨率、高帧率下同时喂给渲染管线和显示控制器实际余量非常紧张。一旦某一帧里纹理采样量激增或者后处理连续做几个全屏Pass内存带宽就会成为理论瓶颈。我自己衡量发热项目时习惯先把“带宽消耗”估算出来。一个最简单的方式数据总量 分辨率 × 每像素字节数 × 读写次数 × Pass数 × 帧率。如果这一项在60帧下已经超过设备内存带宽的30%以上那这项目基本就属于“搬运量超标”还没算上纹理采样和顶点数据发热几乎是必然的。有个直观类比把GPU想成一个外卖配送员。Shader运算相当于在厨房里炒菜炒菜再快如果每一单都要骑车跑5公里取食材人就会累瘫。纹理采样、后处理读写就是这些“取食材”的路程路程越远、次数越勤功耗越高。1.2 为什么偏偏是纹理和后处理渲染场景里消耗带宽的地方有很多比如顶点缓冲、深度测试、像素填充但真正能把带宽吃穿的就两类一是逐像素采样高分辨率纹理二是对全屏渲染目标做反复读写。纹理在于“高频重复”后处理在于“全屏放大”。我常用一张表格来给朋友解释这几个发热源的差异发热源数据规模开销特征典型优化路径CPU绘制指令每帧成百上千次指令调度、驱动层开销合批、减少DrawCallOverdraw像素着色反复执行与画面对角线大小成正比减少半透明层叠、LOD纹理采样带宽贴图尺寸 × 采样次数受纹理压缩、Mipmap影响巨大压缩纹理、开Mipmap、图集后处理全屏Pass分辨率 × 每像素字节 × 多次读写固定开销不分场景便宜降分辨率、合并Pass、关效果做移动端优化的人常说“能省则省”但在纹理和后处理上省的不是“量”而是“每次读取搬多少数据”。哪怕一张贴图在场景里只占很小的屏幕区域只要GPU把整张纹理灌到缓存里采样它产生的搬运量就按整张图的分辨率计算这才是真正的坑。2. 纹理一卷贴图背后的五次搬家纹理之所以是“惯犯”因为它不是一个单一环节的问题而是从打包、上传、缓存、采样到Mipmap选层每一环节都在搬运数据。一张看起来不大的纹理经过一条完整链路后实际搬运量可能比你想象的翻好几倍。2.1 从原始位图到屏幕像素要走多远纹理从磁盘到最终出现在屏幕上大致要经过五站读取文件并解压、上传到系统内存、GPU驱动再拷贝到显存、进入纹理单元的缓存L2/TMU附近、最后被Shader采样到寄存器里参与计算。中间任何一层缓存没命中数据就得重新从更大、更慢的存储层级里拉功耗一下就上去了。很多项目发热问题就出在“贴图尺寸”和“实际呈现大小”严重不匹配。比如场景深处一棵小树屏幕占比几十个像素结果美术给了一张2048×2048的RGBA纹理。GPU采样时如果Mipmap没开纹理单元就会把整张高分辨率纹理反复搬到缓存里只为了取几个像素的颜色。这就像为了拿冰箱上层的一瓶奶非要整个屋子来回跑。所以我在检查项目时一定会先看两件事纹理是否开了Mipmap纹理格式是不是压缩格式。大多数发烫项目这两项至少有一项不合格。Mipmap的意义是让远处的纹理采样直接命中低分辨率层级减少搬运纹理压缩的意义是让每个像素的数据体积变小直接从源头降低搬运总量。2.2 纹理压缩花小钱办大事的健将级优化纹理压缩是移动端优化里投入产出比最高的操作之一没有“之一”也差不多了。以一张2048×2048的RGBA纹理为例原始大小是16MB左右如果转成ASTC 4x4大约降到4MB转成ASTC 6x6可以降到1.8MB上下。缩小的不只是包体更是运行时GPU读纹理时每帧要搬运的数据体积。当然压缩格式的选择不能一刀切。ETC2和ASTC是移动端最常见的两个阵营ETC2兼容性好几乎所有支持OpenGL ES 3.0的设备都能跑但有个麻烦——RGB下是4bpp带Alpha通道的RGBA会变成8bpp无形中翻倍。ASTC灵活度更高按块压缩4x4、6x6、8x8都可以选画质和体积之间能灵活调节但老旧设备驱动偶尔会有兼容性问题。我个人的默认配置是这样纯三维场景纹理统一用ASTC 6x6带Alpha的关键特效纹理用ASTC 4x4保边缘清晰度法线贴图不要直接套用普通颜色纹理的压缩设置否则压缩后向量方向会严重失真常见的做法是单独压一套专门的法线纹理格式或者把XY分量存到更紧凑的通道里。压缩格式还有一个容易忽略的副作用压缩纹理在采样时会增加一小部分额外计算。不过相比它节省下来的带宽这点计算成本非常划算。遇到发热问题先别急着删贴图很多时候把几十张关键纹理压一下效果比砍掉整棵场景树还明显。2.3 Mipmap、图集还有来自 OpenMVS 的贴图Mipmap最容易被人误解成“让远处贴图变模糊的功能”实际它对带宽的贡献远比“视觉清晰度”重要。没有Mipmap时远处物体的纹素密度下降但GPU并不会聪明地少读数据它依然把全分辨率贴图读进来再被缩放着用开了Mipmap后GPU可以直接选择合适的小层级让缓存命中率大幅提高搬运量骤降。所以我的原则是除了UI部分因为要保证像素级锐利可以酌情关闭或减小Mipmap三维场景里几乎一律开Mipmap。甚至一些粒子特效的贴图如果尺寸偏大也值得开一个带LOD偏移的Mipmap不然粒子全屏爆发时纹理带宽会瞬间吃满。纹理图集Texture Atlas是另一个被低估但有效的办法。它把许多小纹理拼到一张大纹理里既减少DrawCall也减少纹理对象的切换开销。不过图集有个讲究要做足边缘Padding否则采样时会出现旁边纹理“渗出”的接缝色块尤其是做半透特效时特别明显。这里顺便提一句OpenMVS这类开源算法生成的纹理贴图。现在不少游戏项目会用到摄影测量资产比如实景扫描的物体模型跑一遍OpenMVS输出来的纹理贴图经常是8K、16K分辨率的高清大图而且通常还是没压缩的。直接把这种素材塞进移动端项目那就是给GPU“投毒”。我处理这类资产时会把纹理重新拓扑到更合理的UV降低到2K或4K再转成ASTC并对接缝区域做膨胀填充和采样偏移否则贴图在远景和近景切换时很容易闪缝。3. 后处理一场全屏数据搬家如果说纹理问题是“单个数据源反复读太多”那后处理问题就是“整张画面反复抄来抄去”。你可能永远不会想到一个普普通通的Bloom加景深每帧搬运的数据量比整个场景的模型顶点数据还大。3.1 全屏Pass的搬运账后处理的基本思路是先把场景渲染到一张离屏渲染目标RenderTexture上然后把这张图当作纹理逐像素做滤镜计算再输出到另一张图或屏幕。这期间每执行一个Pass就要对全屏尺寸做一次“读取加写入”。以1080p分辨率、R11G11B10半浮点HDR渲染目标为例一帧画面大约8.3MB。一个全屏Pass读一次8.3MB写一次8.3MB合计约16.6MB。假设后处理链有Bloom两次降采样加三次模糊、景深两档、色调映射和色差合并粗算下来六七次全屏Pass很正常一帧就要搬100MB左右的数据。60帧时就是每秒6GB这仅仅是后处理一个环节还没算纹理采样。更麻烦的是后处理是“固定开销”。场景里就算什么都没有只要后处理挂在那里它照样吃满这么多带宽。很多游戏在复杂场景发热时大家习惯归咎于模型面数多真用Profile工具一看后处理可能才是压死带宽的最后一根稻草。3.2 给后处理瘦身的常规套路后处理优化的核心思路其实非常简单减少Pass数量、降低Pass分辨率、合并可以合并的计算。这三个方向做到位能省下的带宽非常可观。先说降低分辨率。很多后处理效果对分辨率并不敏感最典型的就是Bloom。Bloom本身是模糊高亮区域再叠加回去在1/4分辨率下做视觉效果几乎无损但带宽直接降到原来的1/16。景深和色差这类效果半分辨率做也问题不大。只有最后的色调映射和UI合成才需要全分辨率。再说合并Pass。例如传统的泛光和景深如果分太多中间步骤每多一次中间纹理读写都是白花花的带宽。我在工程里常用一个方式把“Bloom采样到主画面”和“最终色调映射”合并到一个Shader里完成中间少出一张临时纹理。看似省了一次读写但全屏尺度下一次就是16MB的搬运量再乘帧率相当可观。动态分辨率也是后处理发热的常用兜底。平时让画面按0.8左右的比例渲染后处理照常跑一旦监控到帧率或温度超标就自动把渲染比例再往下调。这样做不会明显影响画面整体感但温度能很快降下来。为了在代码里好控制我习惯给不同的质量档位预留不同的分辨率偏移参数而不是临时在代码里到处写分支。3.3 顺带厘清“后处理”三个行业同名不同坑这里插一句因为我最初带新人时发现大家一搜“后处理流程”经常被带到完全不同的领域里。渲染里的后处理是本文说的全屏特效解决的是“画面观感”。计算机视觉里的后处理则完全是另一回事比如YOLO模型推理结束后还要对锚框做解码、置信度过滤和NMS非极大值抑制这些也被叫“后处理”它们消耗的是GPU/CPU计算资源和访存带宽而不是渲染管线里的全屏读写。还有CAM数控加工里的“后处理”比如HyperMILL和UG里把刀具路径转成机床能识别的G代码以及处理四轴变换时Z轴回零的判断逻辑那更是截然不同的概念。所以如果在一篇渲染技术文章里看到“后处理”先确认它指的是哪种“后处理”否则很容易干半天牛头不对马嘴。移动游戏发烫优化里的目标通常是第一种也就是渲染管线末端的全屏效果链。4. 实操给两个惯犯上手铐的完整步骤理论说完了接下来是真正能落地执行的环节。我给项目做发烫优化时基本会按“纹理盘查 → 后处理改造 → 实测验证”三步走顺序也重要先解决搬运量最大的数据源再看固定开销最后用数据验收。4.1 纹理盘查先把所有“巨型纹理”揪出来第一步是盘点。我习惯写一个小的编辑器扫描工具把项目里所有纹理的尺寸、格式、Mipmap状态、是否勾选sRGB、被哪些Prefab引用一次性导成表格。人工去翻几百上千张纹理是不可能的工具扫一遍才靠谱。在Unity工程里这段扫描逻辑其实不复杂大概就是遍历AssetDatabase找到所有Texture2D然后读出导入设置var guids AssetDatabase.FindAssets(t:Texture2D, new[] { Assets/GameRes }); foreach (var guid in guids) { var path AssetDatabase.GUIDToAssetPath(guid); var importer AssetImporter.GetAtPath(path) as TextureImporter; if (importer null) continue; var size Mathf.Max(importer.maxTextureSize, GetActualTextureSize(path)); Debug.Log(${path} | {size}x{size} | {importer.textureCompression} | mipmap{importer.mipmapEnabled}); }扫出来之后我会按规则挨个处理大于1024且不是UI的纹理优先压缩如果没有特殊理由都不应保持RGBA32。UI纹理根据用途决定压缩精度常用九宫格切片或细线条类UI尽量用ETC2或ASTC低档位至少要比RGBA32省一半。三维资源纹理直接统一到目标平台压缩格式不建议在Android上继续用RGBA16或RGB24。大尺寸纯色渐变类纹理果断把最大尺寸降下来这类纹理压缩后画质损失肉眼不可见。如果是图片是法线贴图注意不要用ETC2的普通压缩否则边缘的锐利细节会糊掉我的建议是用法线专用的格式或者干脆把采样精度调低后用Shader重建法线再压缩。4.2 后处理通道的“减法”与“降频”后处理改造我一般先拉一张效果清单凡是场景里肉眼判断不太出来的效果全部标记为可关。比如低端机上开超高强度泛光基本就是给GPU增加全屏搬运量收益却不明显。我会把后处理链拆成几个模块每个模块单独指定分辨率系数效果模块高端档中端档低端档Bloom1/2分辨率 3次迭代1/4分辨率 2次迭代1/4分辨率 1次模糊景深/焦散半分辨率启用半分辨率近景才启用关闭色差/暗角全分辨率合并到最后半分辨率合并到最后关闭或合并到LUT动态分辨率区间0.91.00.750.90.60.8实际操作时我建议把“分辨率系数”做成可运行时调整的参数不要写死。后处理发热和场景复杂度是叠加关系难保哪次更新后某张新地图突然变得很重这时候动态把Bloom分辨率降一档温度可能立刻就好转。还有一个容易忽视的点临时渲染目标的分配和释放。后处理每帧都会申请几张临时纹理如果代码里频繁GetTemporary再Release除了带宽消耗还可能引发显存碎片和不必要的拷贝。我习惯在后处理链开始前一次性申请好所需RenderTexture链结束后统一释放尽量不每一帧反复开票。4.3 实测对比同样的场景优化前后谁更“冷静”优化做得再好不上真机测都等于白做。我通常会在一个固定场景跑三分钟固定路径用同一台测试机、同一屏幕亮度、同一帧率目标分别记录优化前和优化后的帧率曲线与温度曲线。举个例子有一次我给一个AR类项目做优化场景里有一张从摄影测量流程出来的6300万像素级别的大纹理后处理链又挂了一整套写实光晕。优化前实测55帧上下机身温度在环境温度下跑到72度左右纹理压成ASTC 6x6并补上Mipmap后处理把泛光降到1/4分辨率、合并色调映射后帧率稳定到58帧附近温度降到65度左右。这组数据对比最直观的体现是我还是那张场景、那台手机改动的是“搬运量”而不是“计算内容”。所以之后我在任何项目验收时都要求上报两个指标平均帧率/最低帧率以及测试结束后的机身温度曲线。只有这两个都稳才能说优化是真的有效。5. 我踩过的坑和一套能直接用的排查清单优化做得多了就会遇到一些反常规、特别好玩的坑。有些问题如果不先有心理预期排查时真的会把整个人绕进去。我把这几年跟纹理和后处理搏斗的经验整理了一个速查表希望能帮你少走弯路。5.1 常见问题速查表现象可能原因对策远处贴图闪闪烁烁Mipmap未开或LOD选层不稳定开启Mipmap必要时调高LOD Bias纹理边缘“渗色”图集Padding不足边缘填充扩到8像素以上压缩后金属边缘有彩色噪点ASTC压得太狠或格式不当换ASTC 4x4或专门优化贴图通道开启后处理瞬间掉帧明显后处理全分辨率Pass太多先降分辨率再合并Pass低端机发热/帧率崩高端机没事后处理链固定开销吃不住按质量档位动态关闭高开销效果半透明特效脏脏的纹理带Alpha被压缩失真使用支持Alpha的高质量纹理格式并重压缩温度降了但画面发糊动态分辨率调太低限制下限模糊交给后处理的局部处理不要全幅降采样这些坑里最常见也最隐蔽的就是“纹理闪烁”。有段时间我一直以为是机型兼容问题后来开帧调试器才发现某张贴图在场景里被缩小到非常小时由于没开Mipmap纹理单元只能尽力朝大图里取采样点导致每帧采样的纹素都不一样画面自然就跳了。把Mipmap一开问题当场消失。5.2 工程上值得留下的工具与习惯工具方面Unity的Frame Debugger可以看每一帧的渲染目标和后处理Pass分配RenderDoc在移动端抓帧看纹理读写也很方便Xcode的Metal调试器能直接分析带宽用量。Android侧则可以用系统自带的GPU Inspector类工具看带宽占比和GPU频率曲线这些比什么大而全的性能面板都来得直接。还有一个我自己固定的习惯每次做发烫优化一定会用有温度传感器的设备实时记录温度曲线并配合固定的“三分钟巡逻路径”。不同机型、不同场景时别指望“手摸一下感觉热不热”手感的误差太大了。用数据说话优化才有后续迭代的空间。工作流上我会在性能测试阶段把“纹理压缩规范化检查”和“后处理开销预算”绑进合入流程。凡是新提交的纹理没有走指定压缩渠道或者后处理配置超过了预算阈值直接标红打回。做完这些之后后续项目再出现发烫的概率会低非常多因为问题在源头就被截住了。最后再分享一个小技巧当你把纹理压缩和后处理的坑都填完后别忘了检查一遍渲染目标格式。很多人优化完纹理后处理却还在用每像素16字节的高精度格式那全屏Pass的带宽立马翻四倍。把后处理中间纹理的格式换成R11G11B10或R16G16B16A16画质差异几乎看不出来但搬运量少了一大截。这算是我这几年里最常被忽略、收益却最高的一处细节了。