Any3DAny3D
·Any3D Team

纹理,那个吃掉你显存的大胃王

3d-compressiontexture-compressionwebglwebgpu

上一篇我们把顶点砍了一半,模型小了点,但没瘦成一道闪电——因为真正的体积大户还在那儿:纹理。一个 PBR 模型,纹理通常占 80% 以上体积,而且这部分在显存里膨胀得最凶。

这一篇专治纹理的「显存大胃王」问题。讲三件事:为什么 PNG/JPG 在 GPU 眼里有原罪;GPU 自己的纹理格式长什么样、为什么不能直接用;以及 Basis Universal + KTX2 这套组合是怎么把三者打通的。

先复习一遍:为什么 JPG 让显存爆炸

上一篇给过一个公式:

显存占用 = 宽度 * 高度 * 4字节(RGBA) * 1.333(含mipmap)

一张 4096×4096 的纹理,无论它在磁盘上是 1.5MB 的 JPG 还是 8MB 的 PNG,进了显存都是 约 87MB。原因只有一个:GPU 不认识 JPG/PNG

GPU 的纹理采样单元(texture sampler)只懂一件事——给定一个 UV 坐标,从一个固定大小的像素块里读出颜色。它要求纹理在显存里是「铺平的原始像素」。所以浏览器在把 JPG 上传到 GPU 之前,必须先用 CPU 把它完全解压成 RGBA 像素,再整块塞进显存。

这个过程有三重麻烦:

  1. 显存爆炸:解压后的原始像素占用巨大,87MB 不是夸张,是公式算出来的。
  2. 上传阻塞:大块像素从 CPU 内存搬到 GPU 显存是个慢操作,会卡住首帧渲染。
  3. CPU 解压开销:大图解压本身就费时,移动端尤其明显。

延续上一篇「压缩海绵」的比喻:PNG/JPG 就是捏扁了的海绵,方便运输;一到 GPU 上,海绵吸水膨胀成原样。下载是快了,显存一点没省。

GPU 自己的纹理格式:天生就在显存里压着

既然 GPU 不接受压缩好的 PNG,那能不能让纹理在显存里也保持压缩状态?GPU 采样时实时解码单个像素块,几乎无开销。

这就是 GPU 原生纹理格式(GPU-native texture format)做的事。代表家族:

格式家族全称主要平台特点
BC1-7Block Compression桌面(PC、Mac)老牌,每代 4×4 像素块压缩
ETC1/2Ericsson Texture Compression移动(Android/iOS 老设备)移动端老标准
ASTCAdaptive Scalable Texture Compression移动/VR(新设备)灵活,质量最好,逐块可调
PVRTCPowerVR老 iOS已逐步被 ASTC 取代

这些格式的共同点:纹理按 4×4 像素的小块(block)压缩存储,GPU 在采样时按需解码这一小块,解出来的不是单个像素而是一块。好处是显存占用直接按固定比例缩水,无论内容是什么。

对比一下:

PNG/JPG(传统)GPU 原生格式
磁盘大小小(JPG 尤其小)中(按块压缩,固定码率)
显存占用大(解压回原始像素)小(块状压缩,常驻)
上传到 GPU慢(CPU 解压 + 大块传输)快(直接搬,无需解压)
采样速度快(已是原始像素)快(硬件实时解码)

看起来 GPU 格式是完美方案。那为什么不能直接用它们?

问题来了:不同设备认不同格式

这正是 GPU 纹理格式最大的坑——碎片化

  • 桌面 PC 认 BC1-7,不认 ASTC
  • 安卓手机认 ETC2/ASTC,多数不认 BC
  • iOS(A7 之后)认 ASTC,老机型认 PVRTC
  • WebGPU/WebGL 走的还是设备背后的同一套硬件能力

一张纹理,如果你想让它「在所有设备上都以 GPU 原生格式存在」,就得为每个平台分别准备一份。一个产品要发桌面 + 安卓 + iOS,同一张贴图要做 BC + ETC2/ASTC 三套版本。包体翻三倍,工程量翻三倍。

更糟的是,Web 端你根本不知道用户拿什么设备打开页面。预生成所有格式不现实,运行时检测又来不及。

Basis Universal:一次编码,到处转码

Basis Universal(简称 Basis)就是为解决这个碎片化而生的。它的思路一句话:

先把纹理编码成一种「中间格式」,运行时再根据当前设备的 GPU 能力,转码(transcode)成对应的原生格式。

转码流程(示意图):

源纹理(PNG/JPG)
      │  一次性离线编码(慢,只做一次)
      ▼
Basis 中间格式(ETC1S 或 UASTC)
      │  打包进 KTX2 容器
      ▼
发布到 Web ──┬── 桌面 GPU ──→ 运行时转码 → BC1/3/7
            ├── Android ───→ 运行时转码 → ETC2
            └── iOS/VR ────→ 运行时转码 → ASTC

关键点:

  • 离线编码只做一次,得到一个紧凑的中间表示
  • 运行时转码极快(纯计算、几毫秒级),而且转的是块格式,不需要逐像素解压
  • 转码后送进显存的就是真正的 GPU 原生格式,显存占用按块压缩算,和 GPU 原生格式一致

Basis 提供两种中间编码模式,下一篇会展开讲,这里先记住名字:

  • ETC1S:压缩率极高,适合漫反射/albedo 等颜色贴图
  • UASTC:质量更高,适合法线等对精度敏感的贴图

KTX2:装 GPU 纹理的标准容器

到这里还有一个工程问题:编码后的 Basis 数据放哪、怎么标记、怎么和 glTF 关联?答案是 KTX2

KTX2(Khronos Texture 2)不是又一种图片格式,而是一个容器格式——就像 .zip 不关心里面装的是文档还是图片,KTX2 只负责把 GPU 纹理数据(包括 Basis 编码的)按标准结构打包,并附上元信息(格式、mipmap 层级、色彩空间等)。

在 glTF 里,KTX2 通过扩展 KHR_texture_basisu 接入:纹理不再是 PNG 文件,而是一个 KTX2 文件,里面是 Basis 编码。加载时引擎检测设备能力,转码成对应的 BC/ETC/ASTC。

三者关系理一下,别混了:

名字角色类比
Basis Universal编码方案(怎么把纹理压成中间格式)一种「压缩算法」
KTX2容器格式(怎么把编码后的数据打包)一个「包装盒」
KHR_texture_basisuglTF 扩展(告诉引擎这是 Basis 纹理)一个「标签」

一个 KTX2 文件,内部可以是 Basis 编码(跨平台),也可以是某种原生格式(比如直接装 BC7)。Web 上 99% 的情况是装 Basis,因为我们要的就是「一次编码、到处转码」。

VRAM 实例:4096 纹理的对比

把前面的公式和 GPU 格式叠在一起,看一张 4096×4096 RGBA 纹理在不同方案下的真实占用:

方案磁盘大小显存占用(含 mipmap)上传速度跨平台
PNG~8MB~87MB慢(需解压)
JPG~1.5MB~87MB慢(需解压)
WebP~2MB~87MB慢(需解压)
KTX2 (ETC1S)~2-3MB~11-14MB✅(转码)
KTX2 (UASTC)~6-8MB~22MB✅(转码)

显存数字怎么来的:GPU 块压缩通常按 4bpp(每像素 4 位)或 8bpp 算。4096×4096 在 4bpp 下约 8MB,含 mipmap 再乘 1.333 ≈ 11MB。UASTC 多数转码到 8bpp,所以约 22MB。

重点不是某一行的精确数字,而是这两条:

  1. 传统格式(PNG/JPG/WebP)显存占用几乎一样——都是解压后的原始像素,87MB。磁盘再小,显存不省。
  2. KTX2 显存直接降到 1/4 到 1/8,且磁盘大小也并不吃亏。

这也是为什么 VR、移动端 Web 几乎必上 KTX2——87MB 一张贴图在 2GB 显存的手机上能放几张?而 11MB 能放 7 张。

平台支持矩阵:哪些 GPU 认哪些格式

虽然 Basis 帮我们屏蔽了细节,但了解底层映射有助于排障。这是当前主流设备对原生格式的支持情况:

平台 / 设备BC1-7ETC2ASTCPVRTC
桌面 PC(D3D11/12、Vulkan、WebGPU)部分(新 GPU)
macOS(Metal)✅(新机型)
Android(主流)
iOS(A8+)✅(老机型)
WebGL 2取决于扩展部分
WebGPU✅(桌面)✅(视设备)

Basis 在运行时会探测这些能力,把同一份中间编码转成最合适的那一个。这也是为什么 Basis 这一层在 Web 上几乎不可替代——你没法在发布前预知用户设备。

上传流程对比:传统 vs GPU 格式

最后用一张流程图把差别固化下来。

传统 PNG/JPG:

PNG 文件 ──下载──> CPU 内存 ──CPU 解压(慢)──> RGBA 像素块 ──上传(大,慢)──> 显存(87MB)

KTX2 + Basis:

KTX2 文件 ──下载──> CPU 内存 ──运行时转码(快)──> GPU 块格式 ──上传(小,快)──> 显存(11MB)

后者少了「CPU 逐像素解压」这一大块,上传的数据量也小一个数量级。首帧渲染快、显存省,是这套方案的核心价值。

下一步

道理讲完了,下一篇动手。我们用 toktxgltf-transform 把纹理实际压成 KTX2,在 Three.js / Babylon.js 里加载出来,再聊聊 ETC1S 和 UASTC 怎么选、压缩参数怎么调。

赞助支持