做过移动端性能优化的朋友,应该能感同身受:你费尽心思优化了 DrawCall、改好了合批、压完了网格体量,结果一进游戏跑五分钟,机身照样烫得能煎鸡蛋。这时候你才猛然意识到,真正把芯片热量拉起来的,往往是那些不起眼的“搬运动作”——数据在内存和GPU之间的传输。而在所有传输数据里,纹理和后处理是搬运量最大的两个惯犯,这一篇我们就专门来收拾它们。
“发烫优化系列”前面几篇讲了 CPU 侧的耗时点、渲染管线的常规瓶颈,这次我们换一个视角,从 GPU 的带宽和功耗角度来分析。你手里的设备之所以发热、降频、卡顿,很大程度上不是计算量爆了,而是“搬了太多不该搬的东西”。纹理和后处理恰恰是这条搬运河上的两艘超载货轮。这篇会把它俩的搬运量怎么算、怎么砍、怎么在保证画质的前提下把温度和功耗降下来,全部拆开讲清楚,适合正在做 Unity、UE 移动端项目,或者对渲染性能有优化需求的开发者参考。
1. 先搞清楚“搬运量”是什么,为什么它是发热的元凶
1.1 芯片发热和“搬运”之间的关系
我们通常觉得手机发烫是因为 CPU 或者 GPU 在疯狂计算,频率拉得高,所以烫。这个说法对,但不全对。真正让芯片温度失控的,除了高负载计算,还有大量的内存读写操作。你可以把芯片想象成一个中央厨房:计算单元是大厨,纹理数据和各种中间结果是食材,内存是仓库。大厨再厉害,如果连续不断有人从仓库搬食材进厨房,再搬半成品出去,这个“搬运”过程本身就会消耗大量体力——也就是功耗。
在 GPU 里,这个“搬运”消耗的功耗非常可观。移动端 SoC 的内存带宽是有限的,LPDDR5 之类的内存虽然频率高,但带宽依然是稀缺资源,而且每次内存访问都要付出能量代价。同一个 GPU 核心,做一次浮点运算和读一次纹理数据,后者的能耗可能是前者的好多倍。所以当你的游戏里到处是高分辨率纹理、动不动就做全屏后处理采样,GPU 的功耗会迅速拉满,温度上来之后触发降频,帧率就跟着崩了。
这也是为什么很多项目在 PC 上跑得飞快,一上手机就原形毕露。PC 内存带宽宽裕,显卡功耗大一点无所谓,散热也扛得住。但手机不行,芯片面积小、被动散热,带宽就那么几条车道,搬运量一大,立刻发烫。所以我们做移动端优化,最核心的思路就是压缩“搬运量”,让 GPU 少搬数据,这比单纯压计算量见效更快。
1.2 为什么偏偏是纹理和后处理成了“惯犯”
可能有朋友要问,网格、顶点数据、骨骼动画也有搬运量,为什么不抓它们?原因很简单:纹理和后处理在“单位时间内搬运的数据量”和“搬运频率”上,都远远超过其他数据。
先看纹理。游戏场景里纹理从开机到关机全程都在被采样,每一次渲染都要从显存/内存里读取纹素,过绘制率高的时候一个像素可能采样好几遍。而且纹理数量多,场景少说几十张、多则几百张,加起来的体积轻松突破几百 MB。当 GPU 需要随机读取这些数据时,带宽压力非常大。
再看后处理。每做一个全屏特效,都意味着把整张屏幕的内容当成数据读出来,处理完再写回去。一个 Bloom 光是降采样、模糊、合成就要跑好几遍全屏读写,搬运量是几何级数上涨。再加上抗锯齿、调色、景深、运动模糊,随便一叠,带宽占用直接爆表。
更麻烦的是,纹理和后处理还会互相放大问题:纹理分辨率越高,后处理采样时读的数据就越多;后处理 RT 分辨率越高,处理纹理信息时搬运量越大。这两个惯犯通常结伴作案。所以把它们放在一起讲,是因为优化思路是相通的——都是想办法减少像素数据的搬运次数和搬运体量。
2. 纹理:一张贴图是怎么把带宽吃光的
2.1 纹理的搬运量怎么算:从不压缩到压缩的数量级差距
很多人觉得“纹理大就是内存占用高”,其实纹理真正要命的是带宽占用。一张纹理只要在渲染时被采样,它的数据就会在内存和 GPU 缓存之间反复搬运。我们来算一笔账。
一张 1024x1024 的 RGBA32 未压缩纹理,单张大小是 1024 x 1024 x 4 = 4MB。听起来不夸张是吧?但 GPU 一个帧里会对它采样多少次?一个角色模型可能有 20000 个三角形,每个像素都会采样基础贴图、法线贴图、高光贴图,屏幕上一帧就要采样几十万次甚至更多。而每个采样点都可能要从 4MB 的数据里读取信息,如果纹理没有做压缩,带宽消耗是灾难性的。
同样一张 1024x1024 纹理,如果用 ETC2 压缩(4bpp,也就是每像素 4 比特),大小降到 512KB;如果用 ASTC 8x8 块(约 2bpp),更是压到 256KB。你想想,同样的贴图,GPU 要搬运的数据量相差 8 到 16 倍,发热量的差距能不大吗?所以评论区里我经常强调:移动端纹理压缩不是可选项,是标配,谁不做谁就等着烤手。
这里顺手提一个很多团队踩过的坑:开发期在 PC 上用 BC7 格式,跑得没问题,打到手机包忘记转换,结果 Android 上一堆 BC 格式纹理根本没法用,或者内存暴涨。Unity 里 Android 默认纹理格式如果没有明确指定,有时会出这种问题。做打包之前一定检查每个平台的 Texture Override 设置,该上 ASTC 就上 ASTC,别让 PC 的选项把移动端坑了。
2.2 纹理压缩选型:ETC2 和 ASTC 怎么选
移动端现在主流纹理压缩格式就两个方向:ETC2 和 ASTC。ETC2 是 OpenGL ES 3.0 之后所有移动 GPU 都支持的格式,兼容性非常好,Unity 和 UE 在 Android 上默认也推荐它。它的压缩率是 4:1,RGBA 支持,硬件解码开销很小。缺点是质量上限一般,特别是大压缩比需求时不太够用。
ASTC 是新一代压缩格式,支持从 4x4 到 12x12 的块大小选择,压缩比从 8:1 到 20:1 都能灵活控制。好处是质量可控,同样体积下视觉损失比 ETC2 小;坏处是它要求 GPU 支持 ASTC,目前主流移动 GPU 基本都支持,但一些低端老设备可能需要适配回退。
我个人的经验是:如果项目最低适配设备比较新,直接用 ASTC,选 6x6 或 8x8 块大小作为默认,视觉几乎无损,体积和带宽都很友好。如果还要兼容两年前的旧设备,那就 ETC2 兜底,但尽量把大尺寸纹理做成 ASTC 加一张 ETC2 备用。
这里有个细节容易忽略:ASTC 的块大小选择不是“越压缩越好”。块越大,压缩比越高,带宽越低,但质量也越差。尤其对法线贴图,压缩块太大会出现明显的带状瑕疵。实践里我习惯法线贴图用 ASTC 6x6,颜色贴图用 ASTC 8x8,粗糙度、金属度这类低频信息用 10x10 也没问题。你可以按类型区分配置,别一张配置打全场。
2.3 Mipmap:不是“画质优化”,而是“发热优化”
很多刚入门的朋友以为 Mipmap 是为了让远处纹理更清晰或者减少闪烁,其实它最大的价值是降低带宽消耗。Mipmap 会为一张纹理生成一连串预过滤的小尺寸纹理,当物体离相机远时,GPU 会自动选择更小的层级采样,读的数据量就少很多。
举个例子:一面墙离相机 30 米远,如果没有 Mipmap,GPU 仍然会去读最高分辨率的纹理,然后做缩小滤波。它读的还是那 4MB 的数据,但最终屏幕上这个物体可能只占了 100 个像素,等于为了 100 个像素搬了 4MB 的数据,浪费得离谱。开了 Mipmap 之后,GPU 会选择大约 64x64 的层级,只需要读几 KB,搬运量直接少了几个数量级。
所以我经常说,Mipmap 是移动端“性价比最高”的发热优化手段,没有之一。它能同时减少显存带宽、缓存压力、功耗,而且几乎不损失最终画质。代价只是纹理内存增加约三分之一(因为要存各个层级),这点开销跟省下的带宽相比完全可以忽略。
设置上要注意两点:一是在导入纹理时勾选 Generate Mip Maps;二是设置好 Mipmap 偏移和最大级别,避免远处物体还在采样过大的层级。Unity 里可以通过 TextureImporter 设置 mipmapBias 或者在 Shader 里通过 textureLod 控制,想精细管理时可以按材质分类调整。
2.4 容易被忽略的纹理细节:尺寸上限、格式、采样器
纹理优化并不只是选个压缩格式那么简单,还有几个细节经常被忽略。
第一是纹理最大尺寸(Max Size)。很多美术同学习惯把资源从外包拿过来直接用,原始尺寸 4096 甚至 8192。在移动端这个尺寸绝大多数情况完全没有必要。UI 可以压到 512 或 1024,角色贴图 2048 顶天了,场景大纹理 2048 足够,除非做盖亚那种超大环境才需要 4096。你压一半尺寸,带宽直接降到四分之一,这是肉眼可见的收益。
第二是纹理格式。除了压缩格式,还要注意是否用了高精度格式。HDR 相关纹理用 RGBA16F 可以理解,普通 Diffuse 贴图如果用了 RGBA16F 或 RGBA32F 就属于资源浪费了。法线贴图尽量用 BC5/ASTC 专用格式,不要用 RGBA32 硬存,不然搬运量翻倍。
第三是采样模式。Clamp 和 Repeat 的选择会影响纹理缓存命中率。纹理采样时 GPU 有颜色缓存和纹理缓存,如果采样模式合适,能够提高命中率,减少实际内存读取。这个属于锦上添花,但积少成多。
还有一个容易踩的坑:纹理图集(Texture Atlas)太大。图集能减少 DrawCall,这是对的,但超大图集意味着当你需要一个很小的区域时,GPU 也可能把整个大页读进缓存。做 UI 图集时不要无脑合并成 4096,尽量拆成多个 1024 或 2048 的图集,按界面用途分块,对缓存友好得多。
3. 后处理:一次全屏特效到底搬了多少数据
3.1 后处理链路里的 RT 搬运
后处理是另一个“搬运量”大户,这里很多人没有概念,觉得 Bloom 不就是一遍模糊吗?错。一个标准的 Bloom 流程最少是这样的:先把主相机画面拷贝到 RT1(读主图像、写 RT1),然后对 RT1 做降采样生成不同 Mip 的亮部 RT(每次降采样都是一次全屏读写),再做横、纵两个方向的高斯模糊(又是多次全屏读写),最后把所有亮部模糊结果叠加回主画面(又一次全屏读写)。
你数一下,这一套流程跑下来,一个普通分辨率下,光是全屏读写的次数就能上两位数。每次全屏读写代表什么?以 1080p 为例,一张 RGBA16F 的 RT,大小是 1920x1080x8 ≈ 16.6MB。一次读写(读 + 写)就是大约 33MB 的数据搬运。如果 Bloom 跑了 10 次全屏操作,那就是 330MB 的搬运量。这还只是 Bloom 一个特效,再加 AO、抗锯齿、色调映射、暗角,一次后处理管线轻松搬运超过 1GB 数据。
可以这么说:每多一个全屏特效,你就在给手机加热一分。移动端优化后处理的核心,就是把“每帧被全屏读写处理的数据量”压下来,能半分辨率就不全分辨率,能合并 pass 就不拆开跑,能不用的特效直接关掉。
3.2 半分辨率处理与 pass 合并
半分辨率处理是当前移动端后处理优化的黄金策略。很多后处理效果对分辨率并不敏感,比如 Bloom、景深、运动模糊,它们本质是低频信息,在半分辨率下做和全分辨率下做视觉差异极小。所以做法很简单:先把主画面降采样成半分辨率,然后在半分辨率上做所有重特效,最后再升采样回全分辨率合成。
这个策略能让后处理纹理的带宽消耗直接降到四分之一。举例来说,Bloom 在 1080p 下如果全分辨率做,每次全屏读写 33MB;半分辨率则是 960x540,RT 大小变成约 4.1MB,一次读写约 8.2MB,四倍差距非常直观。
pass 合并同样重要。比如 Bloom 的横纵模糊,很多引擎是拆成两个 pass 跑的:先横向模糊写一张 RT,再纵向模糊读出来。这两个 pass 中间还涉及一张中间 RT 的分配和释放。如果某个平台支持硬件双线性过滤,你可以把横纵合并成一个 pass,用 4 次纹理采样实现 9 个像素的模糊,减少一次全屏读写。别小看省掉的这一次,一个 bloom 流程省一次全屏读写就是几十 MB 的带宽收益。
另外要注意:后处理 RT 的分配要复用,不要每帧动态创建。频繁分配、释放 RT 会造成内存碎片和带宽开销。Unity 里可以用临时 RT 池,或者直接用 RenderTexture 的全局复用方案。我自己在项目里会维护一个按分辨率分级的 RT 池,后处理需要的中间 RT 都从池里取,用完归还,实测内存抖动和 GC 都明显改善。
3.3 移动端后处理的取舍方案
在移动端做后处理,我一直主张“按需裁剪”。不是所有 PC 端的后处理效果都能搬到手机上,必须按设备分级配置。
以 Bloom 为例。完整版 Bloom 效果好但贵,移动端可以用两个优化替代方案。第一个是“模拟 Bloom”,用一个预模糊的亮部贴图去叠加,只要一张低分辨率 RT,跑两个 pass 就完事,效果虽然没那么细腻,但胜在便宜。第二个是拿 Bloom 的关键参数打折:把迭代层数从 5 层减到 3 层,降采样倍数从 2x 改成 4x,视觉损失不大,带宽省一半以上。
另一方面,抗锯齿在移动端也是个搬运大户。MSAA 在移动端虽然硬件支持,但更高采样倍数的 MSAA 带宽消耗惊人。我推荐移动端优先用 FXAA 或 TAA 的轻量版,或者直接用引擎自带的移动端抗锯齿方案。如果你上了 MSAA 4x,再加上 HDR 后处理,带宽会呈指数级上涨,发热几乎压不住。
还有一个经验:很多游戏的后处理不是实时算出来的,是“贴图预烘焙”的。比如调色、暗角这些比较静态的效果,直接用一张 LUT 贴图和一张 Mask 贴图叠加,跑一次全屏 shader 就够。后处理里凡是能用贴图预计算的,就不要实时计算,这是移动端优化绕不开的思路。
4. 实操流程:我是怎么定位和压掉这两个惯犯的
4.1 先量化再动手:抓取带宽数据
优化之前先要量化,不然就是瞎猜。移动端抓 GPU 带宽有几个常用工具:Unity 的 Frame Debugger 和 Profiler 能看渲染状态和大致耗时;Xcode 的 Instruments 里 GPU 计数器可以抓 AGX GPU 的带宽数据;Android 上用 Snapdragon Profiler 或者 Mali Offline Compiler 能看具体硬件的带宽指标和指令数。
我的习惯是先用 Profiler 看 RT 的分配数量、纹理的采样数量,手动算一遍每个 pass 的带宽估算,圈定嫌疑对象。然后在真机上跑一遍,看 GPU 频率和温度曲线,确认发热瓶颈。这一步能避免你做无用功——比如你花大力气压缩了纹理,结果发现瓶颈在 CPU 侧的 draw submission,那优化方向就错了。
这里要注意:不要在编辑器里看性能数据。编辑器里咱们的显卡是台式机级别,带宽宽裕得很,跑出的画面流畅不代表手机流畅。所有性能数据必须以真机为准,而且一定要覆盖中低端设备。高端机可能跑起来根本不烫,但中端机 5 分钟就降频卡顿,你的优化目标是保住低端机体验。
4.2 纹理优化清单:从大到小逐项排查
我理了一个纹理优化的排查清单,按优先级排序:
- 第一步,检查全局纹理导入设置。把所有非 UI 纹理统一设置为 ASTC 6x6 或 8x8,UI 纹理按屏幕尺寸设置为合适的图集大小,关闭不必要的 2048 以上纹理。
- 第二步,检查纹理的 Max Size。逐张过一遍场景纹理,凡是远景可见度不高的大纹理,把 Max Size 降到 1024;角色、武器等关注度高的资源才保留 2048。
- 第三步,确认所有低频、背景、地表纹理都开启了 Mipmap,并且正确设置了 Mipmap 偏移。同时检查 Shader 里有没有关闭 Mipmap 的写法,比如某些自定义 Unlit Shader 用了 tex2Dbias,会导致 mipmap 失效。
- 第四步,检查纹理格式是否有误用。法线贴图不要存成 RGBA32,金属度、粗糙度贴图不要用 RGBA16F,能用 R8 的就不要用 RGBA8。Unity 里可以给单通道纹理设置合适的格式,省内存又省带宽。
- 第五步,检查运行时有没有动态创建大纹理。比如某些 UI 框架会动态生成模糊背景,如果每次打开界面都创建一张全屏 RT,这是隐藏的带宽炸弹,建议改成预烘焙或复用缓存。
- 第六步,用 Profiler 对比优化前后的帧耗时和温度。这一步比较关键,记录下来方便后续排查回归。
4.3 后处理优化清单:砍 pass、降分辨率、预烘焙
后处理的优化我按这个思路执行:
- 第一步,梳理整个后处理管线,把所有特效列出来,标出必须保留、可以降级、可以直接删除三类。移动端不是每个特效都要上,优先保留色彩分级和抗锯齿,其他见机行事。
- 第二步,把 Bloom、DOF、AO 这类重特效全部切成半分辨率执行。这里有个技巧:做降采样时不要直接粗暴缩一半,要先用一个轻量平均 shader 做一次 2x2 降采样,或者在降采样时保留阈值信息,避免亮部细节丢失。
- 第三步,合并 pass。能在一个 shader 里完成的效果不要拆开,能用 Blit 完成的操作不要让 CPU 参与。尽量让后处理链路的 RT 数量最小化,比如 Bloom 的中间 RT 可以直接复用。
- 第四步,考虑用引擎的现成后处理方案做裁剪。Unity 的 URP 后处理栈里每个 feature 都有质量等级,Bloom 有 Low/Medium/High 三档,低档下迭代层数和降采样倍数都会自动降低,非常省心。UE 的话可以把 Post Process Volume 的 Bloom 降到最低 Setting 级。
- 第五步,真机验证。看一眼优化后的画面有没有明显劣化,同时测量温度。如果温度还是高,继续压,直到找到画质和发热的平衡点。
5. 常见问题与排查技巧实录
5.1 优化完画面变糊
这是最常见的问题。通常是因为纹理 Max Size 压得太过,或者后处理半分辨率没有正确处理。纹理方面,角色脸部、UI、关键道具这类视觉敏感资源不要压得太狠,留 2048;场景远处模糊一点通常玩家感知不强。后处理方面,如果 Bloom 半分辨率后亮部虚成一片,检查是不是降采样 shader 没有做阈值提取,处理好亮部阈值就能保住光晕轮廓。
5.2 压缩后出现色块或边缘毛刺
ASTC 压缩块太大时容易出现明显的色块和边缘毛刺,尤其是法线贴图和 UI 上带细字体的纹理。遇到这种情况,把该纹理单独设置为 ASTC 4x4 或 6x6,牺牲一点压缩比换取质量。UI 纹理建议单独配置为 ASTC 4x4 甚至 ETC2,因为 UI 上文字锐利度很重要,8x8 块会糊。
5.3 后处理在真机和编辑器表现不一致
我在项目里碰到过几次:编辑器里 Bloom 效果很好,真机上要么没效果要么过爆。排查后发现是 HDR 设置和颜色空间不一致导致的。编辑器默认可能打开了 HDR,真机上没开,或者 gamma 和 linear 空间不同导致效果差异。后处理链路要注意 RT 格式和相机设置的统一,特别是 HDR 开关要跟编辑器保持一致。另一个坑是部分移动 GPU 对半精度浮点支持不完整,RGBA16F 在后处理链路上精度不够,真机会有色阶断层,这时要么升级 RT 格式,要么优化算法避免极端颜色。
5.4 说一点关于“后处理”这个概念的题外话
搜索“后处理”的时候,你会发现这个词在好几个行业都有完全不同的意思。比如数控加工里有“后处理”是指生成机床代码的配置,AI 目标检测里也有非极大值抑制之类的“后处理”流程,甚至三维重建的纹理贴图也有自己的“后处理”阶段。我这里说的“后处理”特指游戏渲染管线里的画面后处理(Post-Processing),也就是对渲染好的画面做滤镜、模糊、合成等操作。大家在网上搜资料的时候注意区分,别找到完全不相干的方向去。
这个系列的内容核心是“发烫优化”,所以我们会一直围绕实时渲染、手机 GPU、带宽、功耗这条主线。至于 AI 后处理、数控后处理那些,属于另一个完全不同的领域,这里就不展开了。
5.5 一份快速自检的避坑清单
最后给大家一份我在项目里常用的避坑清单,照着查能省很多时间:
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 纹理压缩格式 | Android 用 ASTC,iOS 用 ASTC 或 PVRTC | 沿用 PC 的 BC7 导致移动端异常 |
| 纹理最大尺寸 | 按资源类型分别限制 512/1024/2048 | 全场景统一用 4096 |
| Mipmap | 低频、远景、大纹理必须开 | 为了省内存关闭 Mipmap |
| 法线贴图格式 | 用 ASTC/BC5 专用格式 | 用 RGBA32 存法线 |
| 后处理分辨率 | 重特效半分辨率执行 | 全分辨率跑 Bloom/DOF |
| RT 复用 | 使用 RT 池复用中间 RT | 每帧创建销毁 RT |
| 后处理 pass 数 | 能合并就合并 | 一个 Bloom 拆十几个 pass |
| UI 图集 | 按界面拆分,控制在 1024/2048 | 无脑合并成 4096 大图 |
这份清单不是万能药,但可以帮你快速覆盖 80% 的纹理和后处理发热问题。剩下 20% 就要结合具体项目的渲染管线和设备适配情况去定位了。
6. 最后聊一点我的实际体会
优化纹理和后处理这件事,我踩过最大的坑就是“一上来就动方案”。之前有个项目,美术同学辛苦做的次世代风格场景,我上来就把所有纹理压成 ASTC 8x8,后处理全关了,结果画面肉眼可见的“降级”,美术同事差点没把我拉黑。后来我才意识到,优化的核心不是把特效全砍掉,而是在保证画面表现的前提下,把每个效果的“成本”压到最低。纹理可以分主次,角色和场景关键物件保持高质量,远处和低频的物件压一压;后处理可以分级,Bloom 半分辨率跑,DOF 在特定镜头才开,效果上观众根本察觉不到。
我自己的习惯是先把 Profiler 跑起来,用一个固定场景固定镜头,记录优化前的帧耗时、温度和功耗基线,然后每次只改一个维度,比如先压缩纹理格式,再开 Mipmap,再降后处理分辨率,每步都记录变化。这样你能清楚地知道哪个操作带来了多大的收益,后续回归也好定位问题。别一次把所有优化都堆上去,到时候出了问题你根本不知道是哪一步引起的。
再补充一个容易被忽略的小点:纹理和后处理的优化效果,在设备温度高的时候体现得最明显。手机在低负载状态下功耗差异不大,但只要负载拉升到降频临界点,你做没做带宽优化,直接决定了设备是稳定 60 帧还是 30 帧上下跳。所以测试的时候别只测刚开机那几分钟,最好拿一台用了几小时、已经有点热的设备跑一段战斗场景,看看优化后的帧率曲线稳不稳。稳得住,说明你的搬运量压到位了;还在跳,那就继续查纹理采样和 RT 读写,惯犯通常藏不住。