入门指南

一张纹理进入 GPU 之后发生了什么

一张 300 KB 的 PNG,可能悄悄占掉 5 MB 显存。这篇指南会讲清楚原因、GPU 如何用块压缩把纹理变小,以及该怎么选格式。每个概念都配有可以动手试的演示。

约 10 分钟5 个交互演示
开始阅读

第 1 章

文件大小不等于显存大小

PNG 和 JPEG 是为存储和传输图片设计的。GPU 在绘制纹理之前,要先把图片解开成原始纹素,通常每个纹素 4 字节。

GPU 每帧要对这些纹素采样数百万次,所以它们必须随时可读。因此显存占用只取决于分辨率和像素格式,和文件压得多小无关。再加上 mipmap(给远处表面用的逐级缩小副本),还要多出大约三分之一。

动手试试显存计算器调整分辨率和纹理数量,看看同样的纹理在不同格式下要占多少显存。
分辨率
  • RGBA8
    每纹素 32 位
    213 MiB
    基准
    每张 21.3 MiB
  • BC7 · ASTC 4×4
    每纹素 8 位
    53.3 MiB
    小 4 倍
    每张 5.3 MiB
  • BC1 · ETC2 RGB
    每纹素 4 位
    26.7 MiB
    小 8 倍
    每张 2.7 MiB
  • ASTC 6×6
    每纹素 3.56 位
    23.8 MiB
    小 9 倍
    每张 2.4 MiB
  • ASTC 8×8
    每纹素 2 位
    13.3 MiB
    小 16 倍
    每张 1.3 MiB
  • ASTC 12×12
    每纹素 0.89 位
    6 MiB
    小 35.7 倍
    每张 612 KiB

块格式在每一级 mipmap 都会向上取整到完整的块,因此这里的数字与 GPU 实际分配的一致。

第 2 章

为什么 GPU 不能直接用 PNG

着色器采样纹理时,可能按任意顺序读取任意一个纹素,硬件必须立刻找到它。

PNG 和 JPEG 使用变长编码:想读中间某个像素,必须先把它前面的数据全部解码。GPU 格式则是定长的,每个 4×4 块占用完全相同的字节数,所以任意纹素的地址只需一次乘法就能算出来。纹理在显存里始终保持压缩状态,由纹理单元在读取时实时解码。

动手试试找到一个纹素点击这张 32×32 纹理中的任意纹素。块格式可以直接算出它存在哪里,PNG 做不到。

可以用方向键移动选中位置。

选中的纹素
x 13,y 18
所在的块
第 3 列,第 4 行
在 BC1 中的地址
(4 × 8 + 3) × 8 字节 = 第 280 字节

64 个块在内存中的排列

0511

同一个纹素在 PNG 中

必须先解压第 0 到第 18 行(32 行中的 19 行)

第 3 章

拆开一个 4×4 块

BC1 是最早被广泛使用的格式,最能直观说明原理:它只用 8 字节存下 16 个纹素。

编码器先选出两个端点颜色。解码器把它们混合成一条直线上的 4 个颜色,每个纹素只记一个 2 位编号,指向离自己最近的那个颜色。颜色变化平滑的块能保留得很好;混有多种无关颜色的块就会失真。

动手试试BC1 块浏览器在示例纹理中选一个 4×4 块,看看 BC1 究竟是怎么存它的。
块 12, 4

第 4 章

格式很多,取舍各不相同

较新的格式比 BC1 更会花每一位:更多端点、一个块内多条颜色线、更好的透明度,或者可选的块大小。

BC7 和 ASTC 4×4 每纹素 8 位,效果接近原图。ASTC 通过块大小在质量和体积之间取舍:每个块固定 128 位,所以 12×12 的块平均每纹素不到 1 位。Basis Universal 走的是另一条路:只发布一个紧凑文件,到设备上再转码成该 GPU 支持的格式。

常见 GPU 纹理格式
格式每纹素位数透明度常见平台适合
BC141 位桌面、主机显存紧张时的不透明颜色
BC38支持桌面、主机旧内容中带平滑透明度的颜色
BC44数据通道桌面、主机单通道:遮罩、粗糙度、高度
BC58数据通道桌面、主机法线贴图(X 和 Y)
BC6H8不支持桌面、主机天空盒、光照贴图等 HDR 颜色
BC78支持桌面、主机高质量颜色,有无透明度均可
ETC14不支持几乎所有 Android非常老的设备上的不透明颜色
ETC24–8支持Android、iOS支持广泛的移动端兜底格式
ASTC0.89–8支持较新的 Android 和 iOS、Apple Silicon移动端可调节的质量
PVRTC2–4支持较老的 iPhone 和 iPad仅限较老的 Apple 设备
Basis ETC1S取决于转码目标支持任意 GPU(通过 KTX2)下载体积小,任意 GPU 可用
Basis UASTC取决于转码目标支持任意 GPU(通过 KTX2)质量高,任意 GPU 可用
动手试试运行真实编码器用本站工具同款的 WebAssembly 编码器压缩这张 512×512 示例图,再放大对比局部。
全部在浏览器中运行,不会上传任何内容。
放大区域
原图
RGBA8
显存 1 MiB
BC1
BC1
每纹素 4 位
显存 128 KiB
BC7
BC7
每纹素 8 位
显存 256 KiB
ASTC 6×6
ASTC 6×6
每纹素 3.56 位
显存 116 KiB
ASTC 12×12
ASTC 12×12
每纹素 0.89 位
显存 29 KiB

点击“运行编码器”压缩示例图。第一次运行时会下载编码器。

第 5 章

如何选择格式

两个问题就能很快缩小范围:哪些 GPU 要加载这张纹理?它里面存的是什么数据?

颜色纹理、法线贴图和单通道遮罩对压缩的反应各不相同;桌面、移动和 Web 平台支持的格式家族也不一样。先选出你的场景得到一个合理的起点,再用自己的纹理检查效果。

动手试试格式选择器选择纹理运行的平台和它包含的内容。
运行在哪里?
纹理里是什么?

建议起点

ASTC 6×6

也可以考虑: ASTC 4×4 · ETC2 RGB

较新的 Android 和 iOS GPU 都支持 ASTC。6×6 是较好的平衡点;重点资源用 4×4,老设备用 ETC2。

打开 ASTC 编码器

第 6 章

实用建议

养成几个习惯,就能避开大多数纹理压缩问题。

  1. 01

    颜色标记为 sRGB,数据保持线性

    反照率和 UI 纹理属于 sRGB。法线、粗糙度等数据贴图必须保持线性,否则光照会出错。

  2. 02

    给法线贴图单独的格式

    使用 BC5 或 EAC RG11 这类双通道格式,在着色器里重建 Z。用颜色格式既浪费位数,又会出现明显的凹凸瑕疵。

  3. 03

    生成 mipmap

    大约多占 33% 显存,但能避免远处表面闪烁,还能让采样更快。

  4. 04

    尺寸要对齐块大小

    大多数格式以 4×4 块为单位,尺寸能被 4 整除就不需要填充。PVRTC 等旧格式还要求边长是 2 的幂且为正方形。

  5. 05

    用真实内容在真实设备上检查

    放大到原始尺寸对比解码结果和原图,并确认目标 GPU 支持所选格式。

常见问题

GPU 纹理压缩是无损的吗?

不是。BC、ETC、ASTC 和 PVRTC 都是每块固定大小的有损格式,损失多少取决于格式、块大小和图像内容。

为什么不直接用 PNG 或 JPEG 当纹理?

它们下载更快,但 GPU 无法直接采样,所以会被解码成原始纹素,占用完整的显存。GPU 格式在显存里同样很小。

哪种纹理格式最好?

桌面端 BC7 是稳妥的默认选择,较新的移动设备用 ASTC;如果一个文件要在很多种 GPU 上运行(比如 Web),用 KTX2 + Basis Universal。

“每纹素位数”是什么意思?

它表示平均每个纹素占用的存储。原始 RGBA8 是 32 位,BC1 是 4 位,BC7 是 8 位,ASTC 随块大小从 8 位一直降到约 0.89 位。

用你自己的纹理试试

所有编码器都在你的浏览器本地运行。上传 PNG 或 JPEG,选择格式,然后对比结果。