Three.js From Zero · Article s6-03

S6-03 KTX2 & Basis Textures

Season 6 · Article 03

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

CodecQualitySizeUse
ETC1SMediumVery smallAlbedo, roughness (low-frequency)
UASTCHighLargerNormal 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."