KTX2 实战:纹理压缩的正确打开方式
上一篇把 GPU 纹理格式、Basis Universal、KTX2 的关系讲清楚了。道理懂了,这篇全是动手:ETC1S 和 UASTC 怎么选、用什么工具、命令怎么敲、引擎里怎么加载。
可以照着抄,边看边做。
先解决最重要的一个选择:ETC1S 还是 UASTC
Basis 提供两种中间编码,选错了不是「不够好」的问题,而是法线贴图直接糊掉。先把这张表记住:
| ETC1S | UASTC | |
|---|---|---|
| 压缩率 | 极高(类似 JPG) | 中等(类似高质量 PNG) |
| 画质 | 颜色类贴图够用 | 接近原始质量 |
| 显存(转码后) | 通常 4bpp(约 1/8 原始) | 通常 8bpp(约 1/4 原始) |
| 编码速度 | 慢(可调级别) | 较快 |
| 适用 | albedo/漫反射、emissive | normal、metalness-roughness、数据贴图 |
| 不适用 | normal、需要精确数值的图 | 颜色贴图(杀鸡用牛刀,体积偏大) |
为什么法线贴图不能用 ETC1S?因为法线贴图存的是方向向量,每个像素的 RGB 三个通道互相约束(向量长度 ≈ 1)。ETC1S 是为「颜色看起来对」设计的块压缩,它对单通道精度不敏感,压完之后向量方向会偏,光照立刻显得不对——尤其是高光位置和高频细节。UASTC 对数值保留得更好,能扛住这种精度要求。
实用规则:
- 颜色贴图(albedo、emissive)→ ETC1S
- 数据贴图(normal、roughness、metallic、AO、thickness)→ UASTC
- 拿不准、又想省体积 → 先试 ETC1S,特写下糊了再换 UASTC
同一张图,四种格式比一比
用一张 2048×2048 的 albedo 贴图做基准(数据为社区典型值,仅供参考):
| 格式 | 磁盘大小 | 显存占用(含 mipmap) | 上传到 GPU | 跨平台 |
|---|---|---|---|---|
| PNG | ~5MB | ~22MB | 慢 | ✅ |
| WebP | ~1MB | ~22MB | 慢 | ✅ |
| KTX2 (ETC1S) | ~0.5-0.8MB | ~2.8MB | 快 | ✅ |
| KTX2 (UASTC) | ~3-4MB | ~5.6MB | 快 | ✅ |
注意 WebP 的显存占用和 PNG 一样——它只是磁盘小,进了显存照样解压成原始像素。KTX2 的 ETC1S 把磁盘和显存同时打到很低,这就是它值钱的地方。
工具链:三条路都通
压缩 KTX2 的工具不止一个,按「顺手程度」排:
1. toktx(官方、最强大)
Khronos 官方工具,参数最全,适合单独处理纹理。
# 把 PNG 转成 ETC1S 编码的 KTX2
toktx --bcmp --uastc 0 albedo.ktx2 albedo.png
# 把 PNG 转成 UASTC 编码的 KTX2
toktx --uastc 1 normal.ktx2 normal.png
常用参数:
# ETC1S + 质量档位(1-255,默认 128,越高画质越好体积越大)
toktx --bcmp --uastc 0 --qlevel 200 albedo.ktx2 albedo.png
# UASTC + 超级压缩(Zstandard,进一步缩小磁盘体积)
toktx --uastc 1 --zcmp 19 normal.ktx2 normal.png
# 自动生成 mipmap(强烈建议加)
toktx --bcmp --genmipmap albedo.ktx2 albedo.png
# 指定 sRGB 色彩空间(颜色贴图必须)
toktx --bcmp --srgb albedo.ktx2 albedo.png
--bcmp是 ETC1S 模式的开关(Basis Universal 的基础模式),--uastc 1是 UASTC 模式。两者互斥。
2. gltf-transform(最省事,强烈推荐)
如果你手上是整个 glTF/GLB 模型,用 gltf-transform 一条命令把里面所有纹理换成 KTX2,按贴图用途自动选 ETC1S/UASTC。
# 安装
npm install -g @gltf-transform/cli
# 一键压缩整个模型
gltf-transform optimize model.glb model-optimized.glb \
--texture-compress basisu
它内部会判断贴图用途:颜色类用 ETC1S,数据类用 UASTC,并自动写好 KHR_texture_basisu 扩展。这是 90% 场景的最佳选择,不用一个个手动敲 toktx。
3. 在线工具(最快上手)
不想装环境时,直接用浏览器工具:gltf.report(在线版 gltf-transform)、KTX2 Converter 等。上传,下载,完事。适合早期试验或一次性任务。
完整 pipeline:从源文件到上线
整理一下标准流程(流程图):
源文件(PNG/JPG/PSD/TGA)
│
├── [整模型] gltf-transform optimize model.glb → 自动识别贴图类型
│ └─ 颜色贴图 → ETC1S
│ └─ 数据贴图 → UASTC
│ └─ 写入 KHR_texture_basisu 扩展
│
└── [单张贴图] toktx → 手动指定 ETC1S/UASTC + 色彩空间 + mipmap
│
▼
压缩后的 KTX2 / GLB
│
▼
引擎加载(Three.js / Babylon.js)── 运行时转码 → GPU 原生格式
Three.js 里加载 KTX2
Three.js 从 r129 起原生支持 KTX2,但需要提供 KTX2Loader 并配置转码器(basis transcoder wasm)。
import * as THREE from "three";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
// 1. 初始化 KTX2 转码器,探测当前 GPU 支持哪种原生格式
const ktx2Loader = new KTX2Loader()
.setTranscoderPath("/basis/") // basis transcoder wasm 所在目录
.detectSupport(renderer); // 必须传入 renderer 探测能力
// 2. 配置 GLTFLoader,把 KTX2/Draco/MeshOpt 都挂上
const gltfLoader = new GLTFLoader();
gltfLoader.setKTX2Loader(ktx2Loader);
gltfLoader.setDRACOLoader(
new DRACOLoader().setDecoderPath("/draco/")
);
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
// 3. 加载模型,纹理会自动转码
gltfLoader.load("/models/model-optimized.glb", (gltf) => {
scene.add(gltf.scene);
});
几个关键点:
detectSupport(renderer)必须调,它决定了运行时转码成哪种原生格式setTranscoderPath指向 basis transcoder 的 wasm 文件(从 basis_transcoder release 复制到 public 目录)- 如果模型同时用了 Draco,记得把 DRACOLoader 也挂上;用了 MeshOpt 就挂 MeshoptDecoder
单独加载一张 KTX2 贴图:
const texture = await ktx2Loader.loadAsync("/textures/albedo.ktx2");
texture.colorSpace = THREE.SRGBColorSpace; // 颜色贴图设 sRGB
material.map = texture;
最常见的坑就在这一步:颜色贴图忘了设
colorSpace = SRGBColorSpace,整张图发灰发暗。数据贴图(normal/roughness)则保持NoColorSpace(线性),别搞反。
Babylon.js 里加载 KTX2
Babylon 的做法类似,但更自动——KHRAudioExtension、KHR_texture_basisu 在 GLTFFileLoader 里默认开启,只要保证 transcoder 文件能被找到。
import { Scene } from "@babylonjs/core/scene";
import { Engine } from "@babylonjs/core/Engines/engine";
import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
import { GLTFFileLoader } from "@babylonjs/loaders/glTF/glTFFileLoader";
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_texture_basisu";
const engine = new Engine(canvas);
const scene = new Scene(engine);
SceneLoader.ImportMesh(
"",
"/models/",
"model-optimized.glb",
scene,
(meshes) => {
// 模型加载完成,KTX2 已自动转码
}
);
Babylon 会自动从 CDN 拉取 basis transcoder,离线/内网环境需要手动配置 BASISFileLoader.TranscoderModule。
压缩参数调优:质量与体积的平衡
ETC1S 的核心参数是 --qlevel(1-255)。它怎么影响结果:
| qlevel | 体积 | 画质 | 编码耗时 | 适用 |
|---|---|---|---|---|
| 128(默认) | 小 | 够用 | 中 | 多数情况 |
| 200-255 | 偏大 | 接近无损 | 长(数倍) | 高质量要求 |
| 60-100 | 很小 | 有可见块状 | 快 | 远景/小贴图 |
UASTC 体积相对固定,主要靠 --zcmp(Zstandard 超级压缩)调磁盘大小,不影响显存(解压后还是 8bpp)。
调优顺序建议:
- 先用默认参数压一遍,看体积和画质
- 不满意 → 调
--qlevel(ETC1S)或加--zcmp(UASTC) - 法线贴图糊了 → 确认用的是 UASTC,不是 ETC1S
- 颜色偏暗 → 检查色彩空间设置(sRGB 标志、引擎里的 colorSpace)
常见问题排查
转码失败 / 加载报错
- 检查 transcoder wasm 路径是否正确(Three.js 要
setTranscoderPath) - 检查引擎版本是否支持当前 KTX2 版本(旧版 basis 编码不被新版 transcoder 支持)
- 控制台通常会有具体错误,按关键词搜
颜色偏暗 / 偏亮
- 颜色贴图(albedo)没设 sRGB,或设反了
- toktx 时漏了
--srgb(颜色贴图要加) - 数据贴图(normal)误加了 sRGB
mipmap 缺失,远处闪烁
- 压缩时没加
--genmipmap - 引擎里
texture.generateMipmaps没开(Three.js 里 KTX2 默认随文件,但仍需材质minFilter用 mipmap 模式)
特写下法线方向不对
- 法线贴图用了 ETC1S,换 UASTC
- 确认法线贴图是 OpenGL 风格(绿色通道朝上),DirectX 风格在某些引擎里要翻转 G 通道
文件反而变大了
- 小尺寸贴图(< 128×128)用 KTX2 不划算,块压缩有固定开销,会占满整个块
- 纯色贴图别压 KTX2,直接用材质色值更省
下一步
纹理压缩这块到这就基本通关了。但「会用工具」不等于「会用对工具」——下一篇把前 4 篇的所有知识汇总成一个选型框架:桌面、移动、VR、小程序,每种场景到底该用哪种组合。