
如果你也是做移动端渲染优化的一定对这种画面不陌生手机跑了一会儿后盖先热起来帧率像坐过山车电池百分比肉眼可见地往下掉。打开 Profiler 一看GPU 占用率高高挂起再往下翻十次里有七八次都是两个老熟人在带头搞事——纹理采样和后处理。这期发烫优化系列已经写到第 4 篇前面聊了不少从系统调度、帧率策略、功耗监控入手的思路今天我把镜头对准这两个“搬运量”最大的惯犯它们看起来不像复杂的逻辑代码也不像密集的 CPU 计算但每帧搬的数据量足以把移动端 GPU 的带宽烧穿。这篇不是教你把画质无脑调低恰恰相反我要说的是在视觉效果基本不变的前提下怎么从纹理格式、采样方式、后处理 Pass 的组织方式上把 GPU 每帧需要搬的数据量砍下来发热和功耗自然跟着下来。不管你是做 Unity、UE还是直接写 Metal 或 Vulkan 底层这套思路都通用。适合哪些人就是正在被项目发热、掉帧、功耗问题反复折磨的客户端渲染工程师、游戏性能优化工程师尤其是移动端项目刚起步就踩了温控坑的团队。看完这篇你至少知道该从哪里下手能省多少东西以及哪些优化优先级最高。1. 先给“搬运量”算笔账为什么纹理和后处理最容易烧电CPU 快不快看主频和核心数但 GPU 发热不发热其实很大程度看它一秒钟内部搬了多少数据。移动端 GPU 做得再强功耗墙就摆在那里内存带宽也有限。这里说的“搬运”主要是三种数据纹理数据从内存/显存里搬到纹理单元渲染目标在帧缓冲里读进写出以及顶点数据一遍遍过管线。纹理和后处理恰好是最容易被指数级放大的两个环节。1.1 GPU 的搬运量到底是什么你可以把 GPU 想象成一个快递中转站一堆纹理贴图就是仓库里的货物后处理 Pass 就是一条条需要反复上料、卸料的流水线。CPU 算得再快如果仓库里取货、搬货的动作太多整个站的吞吐能力还是会被卡住。而且这个“搬运”本身是实实在在耗电的数据总线翻转、缓存填充、内存读写每一步都在消耗能量。搬同样的货搬的次数越多、单次越重功耗就越高。画面上更直白一点一个 1080p 的全屏特效假设渲染目标是 RGBA16F每像素 8 字节。读一次再写一次就是 1920×1080×8×2约等于 33MB。如果一秒 60 帧就是每秒 2GB 左右的搬运量。这还只是一个全屏 Pass。后处理链一多动辄就是五六个 Pass每帧几个 GB 的数据在 GPU 内部来回倒腾。带宽一旦跑满GPU 核心只能干等数据过来功耗降不下来发热就成了必然结果。1.2 纹理采样和后处理分别在“烧”什么纹理侧的消耗主要来自三块纹理采样和过滤、纹理缓存命中率、各向异性过滤的额外采样。很多场景角色身上的漫反射贴图动不动就是 2048特效贴图一堆 1024每个像素采样四五层叠加起来带宽直接爆。更隐蔽的是缓存命中率——如果同一块纹理在帧内被反复以随机位置采样缓存基本形同虚设每帧都从主存里重新拉数据发热就很明显。后处理侧的消耗核心就一句话渲染目标的读加写。一个 Bloom 效果可能要先做高光提取再做 3 到 5 次降采样模糊最后再升采样合并加起来就是 10 来个全屏 Pass。每个 Pass 都相当于把一整幅画搬进仓库、搬出仓库、再搬进去。PC 端因为带宽大、散热强经常能掩盖这些问题移动端没这个资本所以中招的永远是手机。1.3 换个视角其他领域也在处理“纹理”和“后处理”有意思的是很多不搞实时渲染的领域也在天天和这两个词打交道而且遇到的问题本质是相通的。遥感图像处理里有个概念叫“提取纹理特征”像 ENVI 这类软件会通过均值、方差、对比度、GLCM 等统计量把地物类别分出来。纹理在那里代表的是信息量纹理越碎越密信息量越大需要的处理资源也越多。这跟渲染里“高频纹理会带来更大采样开销”是同一个逻辑。再比如三维重建领域OpenMVS 这类开源算法在做纹理贴图时会专门做纹理块打包和重叠优化目的就是让生成出来的贴图尽量少、尽量紧凑否则模型一出炉就是几十张 2K 贴图加载慢、渲染卡。而工业 CAM 领域里的“后处理”比如 Hypermill 和 UG 的后处理制作是把刀路转成机床能认的 NC 程序涉及轴变换、循环、条件判断像“4 轴变化时 Z 轴回零”这种安全逻辑本质上就是一种带条件的指令生成。AI 推理里的 YOLO 后处理流程也是置信度过滤、NMS 合并、坐标解码全是在整理和筛选数据。这些跨界例子放到一起就能看出一个规律凡是叫“后处理”的环节都容易成为性能瓶颈因为它们在承担整理、筛选、转换的任务时往往要把大量数据从头到尾过一遍。图形渲染里的后处理链也不过是这一规律的又一个现场罢了。2. 纹理优化实战从源头把搬运量打下来直接进入操作。纹理这块优化属于典型的“改一行、省全部”收益立竿见影只要路径对了基本不需要犹豫。2.1 纹理压缩最划算的一刀选对格式就成功一半首先要明确一点GPU 纹理压缩和我们平时用 ZIP 压文件完全是两回事。纹理压缩格式的设计目的是让 GPU 在采样时不用先解压整张图而是能直接按块读取。正因为如此格式选对选错对渲染效率的影响极大。移动端现在的主流选择是 ASTCAndroid 和 iOS 都支持可以按 block 大小控制压缩率从 4x4 到 12x12越高压缩率画质损失越大。我给个比较稳的参数参考普通颜色纹理sRGB 图用 ASTC 6x6 或 8x8 通常肉眼不太看得出区别法线纹理这种对精度敏感的最低建议到 ASTC 5x5压得再狠就容易出现光照断层Mask、数据类纹理尽量少压用 4x4 或者单独定制会更稳。老安卓机型如果担心兼容性ETC2 是 OpenGL ES 3.0 的标配可以作为回退方案。PC 和主机平台则是 BC 系列的天下BC3、BC5、BC7 各管一摊具体看平台和纹理用途。为什么要这么在意压缩格式看一组数字就懂了。一张 1024×1024 的 RGBA8888 纹理不压缩是 4MB转成 ASTC 6x6 后大约是 0.44MB差了快 9 倍。项目里一百张贴图光内存就能省出一大块。内存少了缓存命中率上去了纹理带宽下降发热自然跟着降。这还不算显存带宽的压力后者才是最直接的发热来源。注意压缩格式一定要确认目标平台真正加载的是压缩格式而不是回退成 RGBA。有些引擎在平台不支持指定格式时宁可自动转 RGBA也不报错表现就是内存爆涨、GPU 烫得离谱但你还以为是代码问题。还有一个容易被忽略但很实用的思路就是控制贴图的场景内占比。一张 2048 的特效贴图如果最终在屏幕上只占到 1/4 区域那 1024 完全够用。把“贴图尺寸跟着屏幕占比走”当成一个基本纪律比事后发现烫了再降更高效。2.2 mipmap 和各向异性过滤不只是画质选项更是省电开关mipmap 这个功能很多项目在导入设置里开了就算完没细想过它到底在干嘛。mipmap 会预生成一系列逐级减半的纹理比如 1024 生成 512、256、128……一直到 1×1。这样做有两个核心好处一是避免远处物体采样时出现摩尔纹和闪烁二是大幅节省纹理带宽。当物体离相机远时GPU 会自动选择更小的 mip 级别单次采样覆盖更多屏幕像素相当于“一次搬小货”搬运量自然小得多。代价也很明确内存约增加 1/3因为 1 1/4 1/16 … 约等于 1.33。但绝大多数场景里这点内存换来的带宽收益远远超过成本。真正不适合开 mipmap 的是 UI 贴图和图集。尤其是图集一张图集里塞了几十个不同大小的小图它们的 mip 链会互相污染远处缩小采样时很容易出现奇怪的边缘渗透问题宁可不开或者单独处理。各向异性过滤则是另一个双刃剑。它能提升斜向观察纹理时的清晰度但代价是额外的采样次数。16x AF 在某些移动端 GPU 上开销并不小。我的建议是移动端 4x 足够PC 端可以开到 8x 或 16x但如果项目本身发热敏感优先降 AF。很多工程师一上来就喜欢把所有纹理选项拉到最高实际上在 5 到 6 英寸的屏幕上4x 和 16x 的视觉差异远没有想象中明显。2.3 怎样识别纹理瓶颈给贴图做一次“特征画像”先把思路从 ENVI 提取纹理特征上借过来。在遥感影像分类里纹理特征是一种统计量它帮你判断一片区域是高频复杂纹理还是低频平坦区域。渲染优化也一样我们要找的是场景里那些“高频高复杂度”的贴图纹素密度高、细节多、在屏幕上被频繁采样而且往往还加了 AF 的贴图。它们才是真正的带宽杀手。具体怎么找Unity 里可以用 Profiler 的 Rendering 模块看 texture streaming 和 GPU upload 的变化用 Frame Debugger 看当前 drawcall 用了哪张纹理、采样了几层Android 平台用 Snapdragon Profiler 或 Android GPU InspectorAGI看带宽模块里 texture fetch 的字节数iOS 上用 Xcode 的 GPU Frame Capture 查纹理资源和 bandwidth usage。关键不是看尺寸而是看这张贴图在屏幕上“最终画出来占多大”。去 RenderDoc 或 Xcode 里看每个 draw 的 target size如果一张 2048 贴图最后只画在屏幕 1/8 大小的区域那它要么是细节冗余要么就是因为没开 mipmap 导致的过度采样。这里分享一个我自己常用的方法把场景里所有纹理按“占用 GPU 带宽估算”排序公式简化为纹理尺寸 × 采样次数 × 在屏幕上的影响范围。先砍排名前五的基本就够忙活一阵了。越是项目后期越要养成这种给贴图做画像的习惯否则等到发热了再全场检索效率极低。3. 后处理优化实战少搬一趟就是赚到后处理是全场景渲染里最后一道工序也是最能藏画面质感的地方。Bloom 一开整体氛围立刻不一样DOF 一上镜头语言马上高级。但这份漂亮是有代价的而且代价往往被严重低估。3.1 每一次全屏 Pass都是一趟全量搬运先算笔账。一次全屏 Pass 的成本大约是渲染目标宽 × 高 × 每像素字节数 × 2读一次、写一次。1080p 用 RGBA16F每像素 8 字节跑一次全屏 Pass 的搬运量就是 1920×1080×8×2≈33MB。如果一个后处理链里有 Bloom 的 4 个 Pass、Tone Mapping 1 个 Pass、DOF 1 个 Pass再加上 Color Grading光 RT 搬运就上百 MB。60 帧每秒就是 6GB 以上的搬运量——这还没算 Bloom 本身空间滤波的采样开销。把后处理链想象成快递中转站里反复搬运包裹的传送带每多一个全屏 Pass就把一整堆包裹从 A 仓搬到 B 仓再搬回来。传送带的数量越多中转次数越密功耗自然指数上升。这也是为什么单纯数 Pass 数量没有意义真正要看的是每一趟搬运的“货物重量”和“趟数”。3.2 轻量化三板斧降采样、合并 Pass、换目标格式降采样是后处理优化里性价比最高的动作。Bloom 的经典做法是先把亮度阈值提取到半分辨率后面所有模糊都在 1/4 分辨率甚至更低做最后再合成回全分辨率。DOF 和 Motion Blur 也可以在半分辨率下做再配合深度引导或双边滤波把边缘补回来。移动端项目里把所有后处理 RT 从全分辨率降到 1/2 分辨率视觉差异往往很小但带宽直接砍到原来的四分之一。合并 Pass 也是老生常谈但永远有效的优化。Tone Mapping 和 Color Grading 完全可以写在同一个 shader 里Bloom 的最终合成步骤也可以和 Tone Mapping 合并在一起省掉一次全屏 RT 读写。能合就合渲染目标切换全屏 Pass 是 GPU 上最贵的操作之一。每个 Pass 之间的 RT ping-pong看起来只是切换一下实际上一轮读写下来几百 MB 就没了。换目标格式则是最容易被新人忽略的一点。不是所有后处理都需要 RGBA16F。移动端如果不追求 HDR 效果主 RT 用 LDR 或者 R11G11B10F 都可以法线、深度这类单值纹理按数据范围选 R8、R16 或 R32不要一律 RGBA。每像素字节数小一截带宽就等比省一截而视觉差异在大多数情况下根本看不出来。3.3 给后处理加“判定逻辑”不该干活的地方别干活工业 CAM 后处理里有个很经典的操作叫“判断 4 轴变化、Z 轴回零”——意思是后处理程序会预先检查刀路里是否有旋转轴变化如果条件满足才插入特定指令避免每一次输出都做重复操作。这个“条件触发”的思维放到图形后处理里思路完全一致让每个后处理 Pass 在适合的时候跑在不该跑的时候立刻退出或降档。实现方式就是写一个后处理管理器动态决定每个 Pass 跑不跑、跑多少分辨率。举个例子镜头快速旋转时动态模糊有存在感但玩家站在原地聊天、镜头几乎不动时动态模糊对画面的贡献几乎为零这时直接关掉是最省电的决策。Bloom 也是UI 界面全屏弹出或者处于战斗结算画面时根本不需要全分辨率高光提取降到最低档甚至关闭也无妨。DOF 可以加一个距离判断主体物体在景深内时不需要额外算散景。再加上设备温度和帧率作为兜底信号发热到阈值就直接切换到只有一个 Tone Mapping 的极简模式。用一段很短的伪代码就能说清楚这个思路int GetPostFxLevel(float frameMs, float batteryTempC, bool isUIShowing) { if (isUIShowing) return 0; // UI 界面极简模式 if (frameMs 20f || batteryTempC 42f) return 0; // 高温/重负载极简模式 if (frameMs 16f || batteryTempC 38f) return 1; // 中低档半分辨率 Bloom无 DOF return 2; // 高档完整特效 }这段逻辑本身没有任何玄学但能帮你省掉大量无效的 GPU 工作。关键是用户几乎感知不到变化但温度和功耗是实打实的改变。3.4 借 YOLO 后处理的思路过滤、合并、提前退出再往深一层说后处理优化和 YOLO 这类目标检测模型的推理后处理其实遵循的是同一套原则。YOLO 后处理流程里会先对模型输出的大量候选框做置信度过滤再做类别筛选最后通过 NMS 合并重叠框。核心思想就是把计算量大的运算放到数据量小的时候再做把无效数据尽早剔除。图形后处理里完全可以照搬这套过滤机制。Bloom 之前先估算整帧平均亮度低于阈值就跳过整个高光提取流程因为提了也没东西可提SSAO 或 DOF 类深度相关效果先用深度纹理生成一张重要性图在平坦区域直接不做采样UI 界面出现时整个后处理链降为最低档。这种“过滤 合并 提前退出”的模式比单纯优化单个 Pass 的 shader 更釜底抽薪。你省的不是一个 Pass 的 0.1ms而是整条流水线一秒钟几 GB 的搬运量。4. 一套能直接抄的优化流程与复盘案例讲了这么多原理下面给一套我自己项目里验证过的排查和优化流程。直接对着做就行。4.1 抓现场用 Profiler 黄金三件套定位热源第一步永远是先抓现场别猜。猜纹理问题还是后处理问题最后大概率会白忙一场。我通常先用引擎内置 Profiler 看整体 GPU 时间占比和帧率分布确认是不是 GPU bound。如果 GPU 耗时占总帧时间很大再开平台级工具做二次定位。Android 上我常用 Snapdragon Profiler 或 Android GPU InspectorAGI重点看带宽模块里的 texture fetch 字节数和 render target 读写字节数。iOS 上用 Xcode 的 Metal System Trace 和 GPU Frame Capture同样能看到带宽和每个 Pass 的开销。Unity 工程还可以叠加 Frame Debugger直接看每一帧的 drawcall 和 RT 状态。怎么判断主犯“纹理”问题通常表现为 texture fetch 相关数值明显高或者单个 drawcall 绑定了巨大的贴图“后处理”问题则表现为大量的全屏 Pass、RT 读写带宽高以及 Pass 间隔里有频繁的 RT 切换。两者经常会同时出现但总有一个是主要贡献者。先抓主犯再处理从犯优化效率会高很多。4.2 纹理侧优化 SOP按这个顺序走基本不会漏先建立贴图清单把所有纹理导出成一张表标出尺寸、格式、mipmap 状态、被哪些材质引用。按“世界空间占用率”排序在场景里实际显示尺寸远小于贴图尺寸的统一降尺寸别怕降视觉几乎无变化。统一压缩格式并确认生效移动端优先 ASTC兼容性要求高时回退 ETC2确认目标平台加载的确实是压缩格式不是 RGBA。检查 mipmap 状态除 UI 和图集外全部开启图集要么拆开处理要么单独定义 mip 链。控制各向异性过滤等级移动端 4x 足够PC 端视发热情况取 8x不要无条件 16x。复查一次实际内存占用和带宽占用做前后对比确认每一项改动都产生了预期收益。4.3 后处理侧优化 SOP后处理侧按这个顺序执行打开 Frame Debugger 或 RenderDoc列出整条后处理链的所有全屏 Pass。给每个 Pass 分类必要型Tone Mapping、抗锯齿和氛围型Bloom、DOF、Motion Blur、Vignette。优先降分辨率再考虑砍数量把氛围型 Pass 的 RT 分辨率统一降到 1/2Patch 光照相关效果可以用 1/4。合并能合并的 Pass目标是把后处理链控制在 3 个全屏 Pass 以内。写动态档位管理器按帧耗时、设备温度、场景状态进行切档。记得锁帧率。如果功耗预算充足60 帧就是 60 帧别让 GPU 偷偷跑满 120 帧还觉得自己“性能很好”对发热毫无好处。4.4 复盘一个典型项目从 82% 降到 48%前阵子一个 3D 卡牌对战项目战斗场景 GPU 占用率 82%手机烫得根本不敢碰帧率一路从 60 掉到 30 上下。我们按上面的流程抓现场Snapdragon Profiler 显示 texture fetch 带宽很高RT 读写量也不低Frame Debugger 里一拉后处理链整整有 6 个全屏 Pass。纹理侧做了三件事角色立绘从 2048 降到 1024压缩成 ASTC 6x6场景装饰贴图从 1024/2048 统一降到 512/1024并且全部开启 mipmap法线纹理从 RGBA8888 转成 ASTC 5x5精度损失在可接受范围内。后处理侧也做了三件事Bloom 的高光提取降到半分辨率模糊在 1/4 分辨率下做 3 次Tone Mapping 和 Bloom 合成合并成一个 shader待机状态下只保留 Tone Mapping 和一个轻量版 Bloom。优化后对比如下项目优化前优化后GPU 占用率82%48%后处理全屏 Pass 数63主场景纹理内存占用约 420MB约 180MB整机功耗下降-约 1.2W机身温度感受烫手温热画质上普通玩家完全感觉不到区别只有盯着对比截图抠细节的测试同事才能勉强发现远景某些高光稍微弱了一点点。这个案例给我最大的触动就是所谓发热优化很多时候不是做多高深的算法优化而是把不该搬的数据量省下来。5. 常见排查问题与避坑实录最后整理一些我在实际项目里经常遇到的问题都是一些很容易踩进去的坑。5.1 高频问题速查表现象可能原因排查与解决压缩纹理后 GPU 还是烫压缩格式没生效回退成 RGBA用材质查看工具或抓帧工具确认实际加载的格式ASTC 压完画面发糊压缩率选得过高颜色图降到 6x6 或 8x8法线图至少 5x5Bloom 降采样后依然慢高光提取还是全分辨率模糊迭代次数过多高光提取也在半分辨率做迭代次数随 RT 分辨率调整mipmap 开了之后贴图边缘发黑图集开了 mipmap图集单独处理或拆开UI 贴图不要开 mipmap后处理 Pass 减少了还是烫RT 格式太大或反复切换检查 RT 格式合并 Pass减少 ping-pong 切换动态档位切换时画面闪烁切换时机不对在切场景或 UI 弹出的瞬间切换不要在半屏渲染中途切换远处物体依然在闪mipmap 没开或 LOD 偏移过大开启 mipmap调整各向异性过滤等级5.2 几条用真金白银换来的经验第一不要迷信“全屏 Pass 数量少就快”这句话。真正吃带宽的是 RT 的格式和切换频率不是 Pass 数量本身。把两个 Pass 合并成一个如果不小心把目标从 R11G11B10F 换成了 RGBA16F那等于白忙一场。第二法线贴图压缩要留足精度。ASTC 压到 6x6 以后在动态光照下很容易出现明显的断层感尤其是角色皮肤和金属材质。这块省出来的内存不大但视觉风险很高建议法线至少 5x5或者干脆用 BC5 在 PC 端保底。第三Bloom 的迭代次数要跟着 RT 分辨率走。RT 分辨率越小每一级模糊的有效半径就会显得越小这时候反而可以适当增加迭代次数来弥补视觉上的精度损失。反过来全分辨率下疯狂迭代高斯模糊除了把 GPU 烧穿不会有任何人注意到差别。第四动态档位切换时选一个画面不太敏感的“安全时刻”再切。比如切场景的瞬间、UI 全屏弹出的瞬间那些时候画面本身就在变化观众很难注意到后处理降了一档。千万别在镜头缓慢推进时突然关掉 DOF整屏都会看起来明显“抖”一下。这些年我踩过最深的坑就是一开始总觉得纹理多几 MB、后处理多一两个 Pass 不是什么大事结果真机上发热了才回头优化项目节奏被拖得很被动。如果你正好在做一个新项目我的建议是项目启动时就给纹理预算和后处理 Pass 数量设一个上限把“搬运量”当成一种资源去管理而不是等设备烫了再到处抓惯犯。真到发烫再回头总归是亡羊补牢。