ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

纹理与后处理:移动设备发烫的隐藏元凶

2026/9/14 19:39:02 拓冰建站 浏览量
纹理与后处理:移动设备发烫的隐藏元凶 设备发烫的排查跟蹲点抓惯犯很像。前面几篇我们把CPU满载、Shader复杂、状态切换这些嫌疑人都过了一遍帧数确实涨了结果机器拿在手里还是烫。这时候你打开帧率曲线FPS看着挺正常GPU占用率也不算满但机身温度和握着一杯热奶茶差不多。如果你的项目也卡在这个怪圈里我很确定不是算不动是搬不动。今天要聊的就是所有发烫优化问题里搬运量最大、也最容易被放过的两个惯犯——纹理Texture和后处理Post-Processing。这篇是发烫优化系列的第4篇主要面向实时渲染和游戏性能优化方向做AR、做小游戏、做App特效的都可以参考。只要你需要让设备凉下来、帧率稳下来这两块就绕不过去。1. 功耗的大头不在计算而在“搬数据”很多刚入行的同学有个直觉GPU发热是因为它在拼命做计算。这句话对了一半。GPU里的ALU确实在算但真正让移动SoC温度飙升的往往不是“算”本身而是为了算而发生的“数据搬运”。1.1 从仓库到工位每一趟都有成本把GPU想象成一个工厂DRAM是仓库L2 Cache是工位旁的临时货架计算单元是流水线上的工人。工人干活很快但每次要拿原料都得先从仓库搬到临时货架再从临时货架搬到手上。在集成电路的世界里“从DRAM读一个cache line回来”的能耗比“执行一次浮点乘加”高出几个数量级。更麻烦的是移动SoC的DRAM带宽是共享的。CPU、GPU、NPU、ISP全都挤在同一条内存通道上GPU一个人在那边拼命搬不光自己发热还会把整机的带宽占满造成其他模块卡顿。所以你会看到一种很典型的现象GPU占用率看起来不高但整机功耗很高温度也很高。1.2 发热问题本质是带宽问题我在做帧率优化时习惯先看三样东西GPU时间、CPU时间、内存带宽。很多时候前两项都还健康唯独带宽被顶到接近上限温度自然就上去了。那数据到底被谁搬走的绝大多数项目里排名靠前的一定有这两个纹理采样以及后处理链路的全屏Pass。纹理采样是“读大户”每个像素着色时要按UV去取纹素采样完了可能还要过滤、混合。一张场景里几十上百张贴图每个对象每个像素都取好几遍累加起来非常恐怖。后处理是“读写双大户”Bloom、景深、抗锯齿、Color Grading每一项都是整屏数据读进来、写完再存出去。屏幕分辨率不变数据量只跟分辨率和帧率挂钩。一个后处理效果就是一次全屏“大扫除”效果叠得越多数据搬运量线性上涨。这两个惯犯的共同点是什么它们都不像Shader复杂度那样可以一眼从“GPU占用率”上看出来但它们都在拼命消耗带宽然后把热量留在芯片里。所以下面咱们一个一个抓。2. 头号惯犯纹理你的每一口采样都在花钱纹理采样的开销被严重低估主要因为它不是一个显眼的数字。单个纹理fetch看起来微不足道但一帧画面里可能有几百万个片元Fragment在跑每个片元可能采样好几张贴图每个采样都要经过Texture Fetch单元从内存搬数据、做过滤。账算到最后它就是当之无愧的搬运王。2.1 贴图格式没压带宽直接翻好几倍先说最基础的你项目里的贴图到底用了什么格式。很多项目从美术手里拿到的素材是PS导出的TGA/PNG或者干脆是直接从网上下载的JPG然后被引擎默认转成了RGBA8888未压缩格式。RGBA8888意味着每个像素占4个字节一张2048×2048的贴图存在内存里就是16MB。16MB听起来不算大但你要知道这张贴图在被采样的每一帧里数据都是要经过内存通道反复搬运的。如果场景里有30张这种贴图同时在视野内光纹理这部分每帧就要处理几百MB的数据。要是跑到60fps每秒就是几十GB直接把你手机的总线带宽干穿。正确做法是用GPU可以直接采样的压缩格式也就是纹理压缩。注意纹理压缩和JPG/PNG这类传统图片压缩完全是两回事。JPG压缩完CPU还要解压成完整像素才能用纹理压缩则让GPU硬件在采样时直接解压采哪里解哪里不需要整张图回内存。移动端现在最常用的是ASTCiOS和现代Android GPU基本都支持。同样一张2048×2048贴图换成ASTC 4x4后体积从16MB降到大约2MB用ASTC 6x6会进一步降到0.9MB左右ASTC 8x8大约0.5MB。压缩之后每帧搬运的数据量直接变成原来的八分之一到三十二分之一发热量自然跟着掉。我见过不少团队前脚刚把Shader优化完后脚发现项目的UI贴图和场景贴图还是原始的RGBA8888立刻前功尽弃。所以这里第一条军规所有贴图在进入游戏包之前必须强制过一遍纹理压缩。如果你用了OpenMVS这类开源重建流程生成纹理贴图更要警惕。离线重建出来的高精纹理贴图尺寸特别夸张动不动就是8K、16K直接丢进引擎不仅包体爆炸运行时带宽也是灾难。正确做法是在纹理生成端就做好压缩和尺寸限制绝不能让高分辨率图裸奔到运行时。选压缩档位也有讲究。ASTC 4x4质量最接近原图适合角色脸部、UI、高品质场景ASTC 6x6在体积和画质之间平衡普通场景贴图常用ASTC 8x8体积最小但细节容易糊法线贴图慎用。法线贴图一旦压缩过度光照细节全没了还不如直接砍分辨率来得干净。2.2 Mipmap看起来费内存实际是省带宽的杠杆“Mipmap会多占三分之一内存所以我不开。”这句话我听过太多次。多占三分之一不假但为了这点内存付出的带宽代价是几倍甚至十几倍。没有Mipmap时GPU采样一个距离很远的物体屏幕上几个像素可能对应纹素里一大片区域。GPU没法直接拿一整块区域算颜色它只能按比例从纹理的多个位置反复取样本导致缓存命中率骤降几乎每一次采样都要打到DRAM。远处的草、远处的砖墙、整张贴图都在视野深处的时候这种开销会非常夸张。开了Mipmap之后GPU会根据LOD选一张合适分辨率的层去采样。远处的物体直接采样小图局部性大大提升缓存命中率明显改善。我做过一次实测某开放场景在开启Mipmap后纹理相关带宽下降了差不多60%。作为交换内存只多了30%左右这笔买卖非常划算。另外一个容易被忽略的参数是各向异性过滤Anisotropic Filtering。很多引擎默认给到8x甚至有人直接拉满16x。各向异性过滤是为了让斜着看的表面不变糊但代价是更多的采样次数。在移动端我一般建议默认4x关键场景或大面积地形再考虑8x16x通常没有肉眼可见的收益纯粹给GPU添堵。2.3 合并纹理少切换少搬运纹理搬运量大不仅仅来自单张贴图的体积还来自“切换”带来的开销。GPU在渲染一个物体时要绑定纹理、绑定采样器。如果一帧里频繁切换纹理状态采样单元就反复进入冷启动状态缓存里的数据全部作废下次采样只能重新从内存里拉。业界最常用的方案是纹理图集Texture Atlas把很多张小图拼到一张大图里。这样原本需要切换十几张纹理的Draw Call可能只需要绑定一张大图。常见的UI、小物件、特效序列帧都可以做图集合并。图集有个细节必须处理Mipmap的“漏边”问题。图集在生成Mipmap时如果直接对整张大图做降采样小图边界部分会把隔壁小图的纹素混进来导致采样时出现“串色”或边缘发虚。解决办法是在每张小图周围留出一定像素的padding并在低Mip层级把颜色向边缘延展这个步骤如果资源管线里没做好后面会非常被动。2.4 尺寸和采样次数都要“够用就好”另一个常见问题是贴图精度严重超出实际显示需求。一张4K贴图如果模型在实际游戏里离相机最远也就三四米屏幕上最大占用面积可能只有几百像素那这4K就是纯粹给你手机添热。我建议团队在资产规范里直接定“纹素密度”标准也就是每平方米的模型表面最多允许多少像素的贴图精度。美术在出图时根据模型的实际大小和可能出现的最远/最近距离去选贴图分辨率。能用1024的没必要上2048能用512的别硬上1024。同时采样次数也需要控制。一张画面上如果所有Shader都是“BaseMap NormalMap RoughnessMap MaskMap”四层采样每个像素就至少有四次纹理fetch。如果用了多层混合、视差映射、体积贴图次数还会更多。对于移动端能合并到一张通道图里的就不要拆开能用单通道灰度图表示的信息不要硬上RGBA。3. 二号惯犯后处理一个Pass就是一轮全屏搬运如果说纹理是“读大户”那后处理就是“读写双料大户”。游戏画面渲染完要经过一堆全屏效果才能最终输出到屏幕。每增加一个PassGPU就得把上一趟结果读回来写完再送出去。屏幕分辨率越高、效果越多、帧率越高搬运量涨得越离谱。3.1 算一笔账一个Bloom吃掉多少带宽我们来做一个简单的算术。1080p分辨率大约是1920×1080207万像素。如果用RGBA16F的浮点纹理作为后处理中间RT每个像素占8字节那么一整张画面就是大约16.6MB数据。一个典型的后处理Pass至少要把这个RT读一遍、再写一遍也就是33MB的搬移量。一个标准的Bloom效果拆细了大概是这样一个流程从主画面提取亮部降采样到半分辨率两个方向分别模糊再上采样回全分辨率最后和主画面合成。整个链路常会做到5到7个Pass。就算按6个Pass算每帧的搬运量大约是6×33198MB。按60fps跑就是接近12GB/s的DDR流量。这个数字有多可怕很多中端手机的GPU能分配到的有效带宽也就十几GB/s。你什么都没干一个Bloom就把带宽吃掉大半剩下的场景纹理、帧缓冲、CPU读写全都在等内存通道喘气。所以后处理不是“掉不掉帧”的问题它是直接把设备体温往上拉的关键因素。尤其是一些竖屏二次元游戏UI上堆五六个后处理效果背后GPU就是在拿整块屏幕当哑铃来回举。3.2 移动GPU的TBDR特性让中间RT更贵移动GPU普遍是Tile-Based Deferred Rendering架构跟PC上的Immediate Mode GPU有本质区别。Tile架构会把屏幕分成一个个小块在片元里完成光照、混合等操作尽量让数据留在GPU内部的Tile Memory里不用每步都写回DRAM。这是它的优势所在。但问题是后处理链路往往需要“前一个Pass的输出、成为下一个Pass的输入”。在中间结果不能留在Tile里时GPU必须把它写回DRAM下一个Pass再读回来。一次中间RT的切换就是一次完整的DRAM往返。另外要小心后处理的RenderTarget格式。如果引擎默认用RGBA16F做中间RT精度是高但带宽也高。很多后处理其实不需要16F精度R11G11B10F、RGBA8也能达到视觉要求。换一个格式带宽立刻对半砍。在Vulkan里可以尝试用Subpass把多个后处理合并到同一个RenderPass里利用Subpass之间不需要写回DRAM的特性。部分引擎支持FrameBuffer Fetch直接读取上一个阶段的像素输出也能减少中间RT的落盘。如果你的引擎支持这套玩法后处理链路的带宽能省不少。3.3 实操取舍降分辨率、合并Pass、砍抗锯齿具体到项目里我建议从三个方向下手。第一半分辨率是个好东西。Bloom的模糊、SSAO、体积光、部分景深这类效果本质是低频信息完全不需要全分辨率计算。先把画面降成一半分辨率做完这些效果再上采样合并视觉上几乎看不出区别带宽却直接减半。再狠一点用四分之一分辨率也可以接受。我的习惯是所有模糊类后处理强制半分辨率起步。第二合并Pass。很多后处理链路可以优化掉多余的往返。比如亮部提取和第一次降采样可以在同一个Pass里完成上采样的同时做Color Grading这样原本独立的两个Pass变成了一个。再比如“径向模糊色调映射”这种不冲突的效果放到同一个Pass里等于白送省一趟全屏搬运。第三抗锯齿选择要冷静。MSAA在移动端Tile架构里如果不开resolve其实很便宜但一旦后面接了一堆后处理MSAA的resolve结果还是要写回DRAM再被后处理读走成本立刻就上来了。FXAA虽然画质一般但只是一个全屏Pass非常轻量适合移动端大批量项目。TAA画质最好但要存历史帧、要做运动向量、要做重投影带宽开销是三者里最高的。做之前先问自己这个项目的画风真的需要TAA吗很多卡通渲染项目用FXAA加合理MSAA效果已经完全够用了。3.4 “后处理流程”不只有渲染同一套逻辑到处适用搜索“后处理”的时候你会发现YOLO后处理流程、HyperMILL五轴后处理、UG后处理等等一堆不同领域的同名概念。它们和渲染后处理其实是同一件事主流程跑完之后对结果做的后续加工和整理。YOLO目标检测的后处理要做NMS、要解码边界框、要把特征图数据反复整理这些都是内存搬运。模型推理本身可能只占一半时间剩下的一半都花在后处理上和渲染后处理一样是隐藏的带宽大户。所以这篇文章讲的思想放在别的领域也一样成立后处理这环节一旦复杂起来搬运量就会失控。4. 抓现行从帧里定位这两个惯犯的方法说了这么多理论怎么确定你项目里的“惯犯”就是它们不能靠猜要靠证据。我的习惯是开Profiler把每一帧的带宽账拉出来看哪一项占比大就抓哪一项。4.1 用硬件计数器看带宽不要只看帧率帧率只是最终结果它不会告诉你瓶颈在哪。我常用的工具各平台不太一样高通平台有Snapdragon Profiler新的叫Snapdragon Perf Profiler或者直接在Android Studio的Profiler里看Mali平台有Arm Mobile StudioApple这边用Xcode自带的Metal System Trace。老牌的PerfDog看温度和功耗也很好用。打开之后优先看这几项GPU Busy、Bus Read/Write、Memory Bandwidth、Shader ALU Utilization。如果你的GPU Busy并不高但Memory Bandwidth接近满载那基本可以断定是搬运问题而不是计算问题。CPU和GPU时间都看起来很健康但温度很高大概率也是带宽在背后捣鬼。4.2 纹理搬运过量的证据长什么样Texture Cache Miss率异常高是最典型的纹理问题信号。此外采样器停滞Sampler Stall明显、GPU的Texture Fetch单元忙到不行也是证据。真正定位到具体贴图时我建议做两个小实验来交叉验证。第一个实验把场景里所有纹理临时换成低两个级LOD的低分辨率版本。如果带宽和温度有明显下降说明纹理体积超了。第二个实验把Mipmap Bias临时调高比如整体2如果温度和带宽都降了说明采样缓存命中率太低Mipmap或后续的LOD策略有问题。想更细一点可以用RenderDoc抓一帧逐Draw Call看纹理绑定和采样器状态。凡是采样了超大尺寸贴图但实际屏幕覆盖面积很小的全是优化对象。4.3 后处理搬运过量的证据长什么样后处理问题的证据更加肉眼可见。在RenderDoc里抓帧数一下有多少个全屏Pass看每个Pass的RenderTarget格式再看它的Load/Store操作写的是Load还是Clear。如果一连串Pass都是LoadStore数据就在里面来回倒腾。另一个特征是在Profiler里关掉所有后处理帧时间和带宽立刻大幅下降只开某一个又能看到对应上升。想量化的话把后处理链路的每个Pass单独打开比较带宽曲线就能知道哪个Pass最“烧”。我测过很多项目最后排名靠前的几乎都是Bloom和景深这两个效果也都跟实际视觉重要度不成正比。4.4 A/B测试设计先控制变量再谈结论优化完成后一定要用同一台设备、同一条相机路径、同样的场景和时长做对照这才有说服力。我会设一个固定镜头脚本绕场景完整走一圈记录15分钟再把优化项单独打开或关闭重复测试。有一个关键教训不要同时改两个变量。比如你既换了纹理格式又砍了后处理Pass最后温度下来了你根本分不清是哪个起了作用也没办法判断哪个方向值得继续深挖。我之前栽过这个跟头浪费了两个测试周期。另外温度是滞后指标。不要优化完马上摸手机跑十分钟让温度稳定下来再对比否则误差很大。对比时记录的数据建议包含平均/最大帧时间、GPU Busy、Read/Write带宽、电池端功耗、机身最高温度。5. 从源头立规矩别老靠优化季救命前面说的都是“事后查”但真正成熟的项目应该在立项和管线设计之初就把纹理和后处理这两个惯犯关进笼子里。5.1 在资产管线里强制纹理压缩不要指望每个美术和开发都会主动压缩贴图这必须靠流程把关。U3D里可以用TextureImporter设置平台默认格式UE里可以改纹理组设置再进一步可以直接在素材上传阶段通过自动构建工具强制转成ASTC。普通颜色贴图用ASTC 6x6角色脸部和UI用ASTC 4x4法线和Mask类贴图单独定标准。凡是超过项目尺寸上限的贴图构建系统直接报错或者自动降级。5.2 给后处理写进性能预算我建议每个项目在开发初期就定一张后处理预算表明确写上每晚最多一个全屏Pass、一个半分辨率Pass允许使用Bloom但不允许再加独立景深非剧情演出不允许开TAA。如果引擎里有RenderPass统计工具就设定软件层面的超预算告警让任何人都不能轻易往场景里堆效果。其实我见过太多项目前三个月疯狂堆后处理最后一个月疯狂删。与其这样不如在管线里直接卡上限把违规操作杜绝在源头。5.3 最后再分享一个小技巧如果你在后处理链路里必须保留中间RT留意一下RT的Load/Store动作。很多引擎默认会把RT前一帧的内容保留下来这会产生额外的Load带宽。如果这个RT在每帧开始时不需要保留旧内容就把它的清屏动作设成DontCare也就是告诉GPU“旧数据不要了别读”Mali和Adreno都能省下不少可见的带宽。这个细节几乎所有框架文档里都有但没几个团队真正去调省下来的数据量却相当可观。