Any3DAny3D
·Any3D Team

KTX2 实战:纹理压缩的正确打开方式

3d-compressiontexture-compressionktx2basis-universalgltf

上一篇把 GPU 纹理格式、Basis Universal、KTX2 的关系讲清楚了。道理懂了,这篇全是动手:ETC1S 和 UASTC 怎么选、用什么工具、命令怎么敲、引擎里怎么加载。

可以照着抄,边看边做。

先解决最重要的一个选择:ETC1S 还是 UASTC

Basis 提供两种中间编码,选错了不是「不够好」的问题,而是法线贴图直接糊掉。先把这张表记住:

ETC1SUASTC
压缩率极高(类似 JPG)中等(类似高质量 PNG)
画质颜色类贴图够用接近原始质量
显存(转码后)通常 4bpp(约 1/8 原始)通常 8bpp(约 1/4 原始)
编码速度慢(可调级别)较快
适用albedo/漫反射、emissivenormal、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 的做法类似,但更自动——KHRAudioExtensionKHR_texture_basisuGLTFFileLoader 里默认开启,只要保证 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)。

调优顺序建议:

  1. 先用默认参数压一遍,看体积和画质
  2. 不满意 → 调 --qlevel(ETC1S)或加 --zcmp(UASTC)
  3. 法线贴图糊了 → 确认用的是 UASTC,不是 ETC1S
  4. 颜色偏暗 → 检查色彩空间设置(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、小程序,每种场景到底该用哪种组合。

赞助支持