Three.js From Zero · Article s6-03
S6-03 KTX2 & Basis Textures
KTX2 & Basis — GPU-native compressed textures
PNG/JPG decompress to raw RGBA before the GPU can use them. 4K texture → 64MB VRAM. KTX2+Basis stays compressed ON the GPU. 6-8× smaller VRAM AND smaller download.
1. PNG/JPG aren't GPU formats
When you load a PNG, the browser decodes it to raw RGBA. A 4K PNG that's 2MB on disk becomes 64MB (4096 × 4096 × 4) in VRAM. A dozen such textures = 768MB. Phones cap at ~1GB total GPU memory. Game over.
2. Block-compressed texture formats
- ETC1S / ETC2: 4× ratio, Mali/Adreno mobile GPUs.
- BC1/BC3/BC7: 4-8× ratio, desktop GPUs.
- ASTC: 4-36× ratio, modern mobile.
- PVRTC: older iOS.
Every GPU speaks some subset. None speaks all.
3. Enter Basis + KTX2
Basis: a meta-format. Authored once. Transcoded at load time to whatever the current GPU supports.
KTX2: the container (like .glb for textures). Stores Basis-encoded data + mipmaps + metadata.
4. The pipeline
# Author: PNG → KTX2 with Basis encoding
npx @gltf-transform/cli uastc in.glb out.glb # higher quality
# or
npx @gltf-transform/cli etc1s in.glb out.glb # smaller
# Or standalone
basisu texture.png -output_file texture.ktx2 -uastc
5. Three.js consumer
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2 = new KTX2Loader()
.setTranscoderPath('https://unpkg.com/[email protected]/examples/jsm/libs/basis/')
.detectSupport(renderer); // picks the right block format per GPU
const loader = new GLTFLoader().setKTX2Loader(ktx2);
loader.load('model.glb', g => scene.add(g.scene));
6. ETC1S vs UASTC
| Codec | Quality | Size | Use |
|---|---|---|---|
| ETC1S | Medium | Very small | Albedo, roughness (low-frequency) |
| UASTC | High | Larger | Normal maps, detail masks |
Rule: anything where banding shows (normal maps, photographic detail) → UASTC. Else ETC1S.
7. VRAM wins
Same 4K texture:
- PNG loaded → 64MB VRAM
- JPG loaded → 64MB VRAM (same, JPG is CPU-only compression)
- KTX2 ETC1S → 8MB VRAM
- KTX2 UASTC → 16MB VRAM
Plus the download is 500KB-2MB instead of 2-8MB. Two wins.
8. Mipmaps included
KTX2 stores pre-generated mipmap levels in the file. GPU uploads them all directly. No CPU mipmap generation at load time — another big win.
9. Demo — sphere with KTX2 texture
Loads a Three.js sample model that uses KTX2 textures and verifies all the pipeline steps.
10. Gotchas
- Transcoder path: the Basis transcoder is a WASM blob. Self-host the
examples/jsm/libs/basis/folder. - Normal maps: must use UASTC or visible banding.
- sRGB vs linear: authored textures are sRGB. Three.js KTX2Loader handles the flag correctly if the KTX2 metadata is set.
- Non-multiple-of-4 sizes: block-compressed formats want multiples of 4. Pad or resize.
11. Takeaways
- KTX2 = compressed-on-GPU textures. 6-8× VRAM + smaller download.
- Basis = meta-format, transcoded per-GPU at load.
- ETC1S for albedo/roughness, UASTC for normals.
- Three.js KTX2Loader handles it all. Self-host the transcoder.
- First texture format to be "ship by default."