纹理数据格式:压缩纹理原理与缓存优化

纹理格式这个话题,说实话是很多图形工程师容易忽略的角落。我早年做移动端项目时,就因为在纹理格式上踩了坑,导致帧率直接腰斩。今天咱们就把这块彻底讲透。

一、为什么需要纹理压缩?

先问个问题:一张 1024×1024 的 RGBA8 纹理,占多少显存?

答案是 4MB。如果场景里有 100 张这样的纹理,就是 400MB。显存带宽更是噩梦——每次采样都要读取 4 个字节,GPU 的显存带宽很快就会被吃光。

纹理压缩就是为了解决这个问题。它不像 JPEG 那样有损到肉眼可见,而是用一种「块压缩」的思路,在视觉质量和带宽占用之间找到平衡。

二、主流压缩格式详解

1. BC/DXT 系列(桌面端王者)

BC 系列是 DirectX 的亲儿子。我最早接触的是 DXT1,那时候还在做《魔兽世界》风格的 MMORPG。

格式压缩比特点适用场景
BC1 (DXT1)6:1不支持 Alpha,4×4 块压缩不透明贴图、颜色纹理
BC2 (DXT2/3)4:14 位 Alpha,精度较低简单透明度
BC3 (DXT4/5)4:18 位 Alpha,插值计算法线贴图、复杂透明度
BC54:1双通道压缩,存储法线法线贴图专用
BC6H6:1HDR 纹理,浮点数据HDR 环境贴图
BC73:1高质量 RGBA,支持 PBR高质量纹理

我个人习惯:桌面端无脑选 BC7。虽然压缩比不如 BC1,但视觉质量几乎无损。我在做 3A 级场景时,所有漫反射贴图都用 BC7,法线贴图用 BC5。

核心原理:每个 4×4 像素块被压缩成 128 位或 64 位数据。块内只存储两个端点颜色,中间颜色通过线性插值得到。说白了,就是用「渐变」来近似「细节」。

2. ETC 系列(移动端老兵)

ETC1 是 Android 的标配。但有个坑——它不支持 Alpha 通道。我记得第一次在 Android 上做半透明粒子效果时,发现纹理全是黑的,查了半天才发现是 ETC1 不支持 Alpha。

ETC2 解决了这个问题,它是 OpenGL ES 3.0 的强制要求。现在新设备基本都支持了。

格式压缩比Alpha 支持质量
ETC16:1不支持中等
ETC2 RGB6:1不支持较好
ETC2 RGBA4:1支持(8 位)较好

避坑指南:我曾经在项目中用了 ETC1 做 UI 纹理,结果按钮的阴影全没了。后来才意识到,UI 纹理经常需要 Alpha 通道做圆角和阴影。移动端 UI 纹理建议用 ETC2 RGBA 或者干脆用未压缩的 RGBA8888。

3. ASTC(移动端新贵)

ASTC 是 ARM 推出的格式,现在已经是移动端的首选了。它的灵活性让我特别喜欢——块大小可以从 4×4 到 12×12 自由选择。

块大小压缩比质量适用场景
4×43.56:1极高UI、重要纹理
6×68:1一般场景纹理
8×814.22:1中等背景、地形
12×1232:1极简纹理

我建议:移动端项目直接用 ASTC 6×6 作为默认格式。如果纹理很重要(比如角色皮肤),降到 4×4。背景贴图可以升到 8×8。这个平衡点是我在多个项目中试出来的。

三、Mipmap 链与各向异性过滤

Mipmap 链:不只是抗锯齿

很多人以为 Mipmap 只是为了消除远处纹理的闪烁。其实它更大的作用是——提升缓存命中率。

你想想看:当远处物体只占屏幕几个像素时,如果还去采样 1024×1024 的原图,那缓存里全是无用数据。Mipmap 链会自动切换到合适的分辨率,让采样区域变小,缓存利用率大幅提升。

关键点:Mipmap 每级缩小一半,总共 log2(N) + 1 级。比如 1024 纹理有 11 级(1024→512→256→...→1)。

我在项目中遇到过一个问题:角色贴图用了 Mipmap,但远处的角色突然变模糊。后来发现是 Mipmap 的 LOD 偏移没调好。解决方案是手动设置 LOD 偏移值,让过渡更平滑。

各向异性过滤:斜着看也不怕

各向异性过滤(AF)解决的是「斜视角」问题。当纹理表面与视线成锐角时,常规的 Mipmap 采样会模糊得一塌糊涂。

AF 的原理是:沿着视线方向进行多次采样,然后加权平均。采样次数就是 AF 的等级——2x、4x、8x、16x。

AF 等级采样次数性能开销视觉提升
1x (关闭)1斜视角模糊
2x2略有改善
4x4中等明显改善
8x8较高很好
16x16极好

我个人习惯:桌面端开 8x,移动端开 4x。16x 的提升已经微乎其微了,但性能开销翻倍。不值得。

小技巧:各向异性过滤对地面、墙壁这类平面纹理效果最明显。对球体、角色这类曲面纹理,效果就没那么显著了。所以如果性能紧张,可以只对地面纹理开高 AF。

四、纹理尺寸与对齐规则

尺寸必须是 2 的幂?

老显卡确实要求纹理尺寸必须是 2 的幂(POT)。但现在的新硬件已经支持非 2 的幂(NPOT)了。

不过,我建议还是尽量用 POT。为什么?

  • Mipmap 链需要 2 的幂才能完美生成
  • 纹理压缩(BC/ETC/ASTC)在 POT 上效率更高
  • GPU 的纹理缓存对 POT 有优化
  • 各向异性过滤在 POT 上效果更好

如果实在要用 NPOT,记住两个规则:

  1. 纹理尺寸不能超过 GPU 的最大限制(一般是 16384 或 8192)
  2. NPOT 纹理不能生成完整的 Mipmap 链

对齐规则:缓存命中的关键

纹理数据在显存中是以「行」为单位存储的。每行的起始地址需要对齐到缓存行大小(一般是 128 字节或 256 字节)。

我踩过的一个坑:有一张 1025×1024 的纹理,因为宽度不是 4 的倍数,导致每行末尾有 3 个像素的浪费。更糟糕的是,这破坏了缓存对齐,采样时频繁触发缓存缺失。

对齐规则总结:

  • BC/DXT 格式:宽度和高度必须是 4 的倍数
  • ETC 格式:宽度和高度必须是 4 的倍数
  • ASTC 格式:宽度和高度必须是块大小的倍数(如 6×6 块要求 6 的倍数)
  • 未压缩格式:每行字节数建议对齐到 128 字节

实际项目中的尺寸选择

我一般这样选纹理尺寸:

用途推荐尺寸说明
角色漫反射1024×1024主角用 2048,NPC 用 512
角色法线1024×1024和漫反射保持一致
环境贴图2048×2048立方体贴图,6 张
地形4096×4096大场景用,配合 Mipmap
UI 图标128×128NPOT 也可以,但建议 POT
粒子纹理256×256小尺寸,多张

注意:纹理尺寸不是越大越好。2048×2048 的纹理比 1024×1024 大 4 倍,但视觉提升可能只有 10%。我见过有人把 UI 图标做成 1024×1024,纯粹是浪费显存。

五、实战建议总结

说了这么多,最后给几个可以直接用的建议:

  1. 桌面端:BC7 是首选,法线贴图用 BC5,HDR 用 BC6H
  2. 移动端:ASTC 6×6 是默认,重要纹理用 4×4,背景用 8×8
  3. Mipmap:永远开启,除非纹理只在固定距离使用
  4. 各向异性过滤:桌面 8x,移动 4x,地面纹理可以更高
  5. 尺寸对齐:POT 优先,压缩纹理必须 4 的倍数

嗯,这些就是我这些年摸爬滚打总结出来的经验。纹理格式这块,看似简单,但每个选择都会影响最终的性能和画质。希望你能少走一些我走过的弯路。