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 的做法類似,但更自動——KHR_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、小程式,每種場景到底該用哪種組合。

贊助支持