Kris

KTX2 in Practice: The Right Way to Use Texture Compression

3D CompressionTexture CompressionKTX2Basis UniversalglTF

The previous article explained the relationship between GPU texture formats, Basis Universal, and KTX2. Now that you understand the theory, this one is all about hands‑on work: how to choose between ETC1S and UASTC, which tools to use, how to write the commands, and how to load them in engines.

You can copy the commands and follow along.

First, the most important choice: ETC1S or UASTC

Basis Universal offers two intermediate encodings. Choosing the wrong one isn’t just “not good enough” – normal maps will look completely muddy. Start by memorizing this table:

ETC1SUASTC
Compression ratioVery high (like JPEG)Medium (like high‑quality PNG)
QualityGood enough for color mapsNear‑original quality
Memory (after decode)Typically 4bpp (~1/8 of original)Typically 8bpp (~1/4 of original)
Encoding speedSlow (adjustable quality)Fast
Best forAlbedo/diffuse, emissiveNormal, metalness‑roughness, data maps
Not forNormal, precision‑sensitive mapsColor maps (overkill, larger size)

Why can’t normal maps use ETC1S? Because normal maps store direction vectors, and the RGB channels of each pixel are constrained (vector length ≈ 1). ETC1S is a block compression designed for “colors that look right” – it’s insensitive to per‑channel precision. After compression, the vector directions drift, causing lighting to look immediately wrong, especially in specular highlights and high‑frequency details. UASTC preserves numeric values much better and can handle that precision requirement.

Practical rule of thumb:

  • Color maps (albedo, emissive) → ETC1S
  • Data maps (normal, roughness, metallic, AO, thickness) → UASTC
  • Not sure and want to save space → try ETC1S first; if it looks blurry in close‑ups, switch to UASTC

Same image, four formats compared

Using a 2048×2048 albedo texture as a baseline (values are community averages for reference):

FormatDisk sizeGPU memory (with mipmaps)Upload to GPUCross‑platform
PNG~5 MB~22 MBSlow
WebP~1 MB~22 MBSlow
KTX2 (ETC1S)~0.5–0.8 MB~2.8 MBFast
KTX2 (UASTC)~3–4 MB~5.6 MBFast

Note that WebP’s GPU memory usage is the same as PNG – it’s only smaller on disk; once loaded into VRAM it’s decompressed to raw pixels. KTX2 with ETC1S keeps both disk and memory very low, which is where it really shines.

Toolchain: three paths that all work

There are several tools to compress KTX2. They’re listed here by convenience:

1. toktx (official, most powerful)

The Khronos official tool, with the most parameters. Great for processing individual textures.

# Convert PNG to ETC1S encoded KTX2
toktx --bcmp --uastc 0 albedo.ktx2 albedo.png

# Convert PNG to UASTC encoded KTX2
toktx --uastc 1 normal.ktx2 normal.png

Common parameters:

# ETC1S + quality level (1-255, default 128, higher = better quality & larger size)
toktx --bcmp --uastc 0 --qlevel 200 albedo.ktx2 albedo.png

# UASTC + super compression (Zstandard, further reduces disk size)
toktx --uastc 1 --zcmp 19 normal.ktx2 normal.png

# Auto‑generate mipmaps (strongly recommended)
toktx --bcmp --genmipmap albedo.ktx2 albedo.png

# Specify sRGB color space (must for color maps)
toktx --bcmp --srgb albedo.ktx2 albedo.png

--bcmp enables ETC1S mode (the base mode of Basis Universal), while --uastc 1 enables UASTC mode. They are mutually exclusive.

If you have a whole glTF/GLB model, use gltf-transform to convert all textures inside it to KTX2 in one command, automatically choosing ETC1S or UASTC based on the map’s intended use.

# Install
npm install -g @gltf-transform/cli

# Compress the whole model in one go
gltf-transform optimize model.glb model-optimized.glb \
  --texture-compress basisu

Internally, it detects the texture usage: color maps → ETC1S, data maps → UASTC, and automatically writes the KHR_texture_basisu extension. This is the best choice for 90% of use cases – no need to run toktx manually for each texture.

3. Online tools (fastest to get started)

When you don’t want to install anything, use browser‑based tools: gltf.report (online version of gltf-transform), KTX2 Converter, etc. Upload, download, done. Perfect for early experiments or one‑off tasks.

Complete pipeline: from source to production

Here’s the standard workflow (flow chart):

Source files (PNG/JPG/PSD/TGA)
        │
        ├── [Whole model] gltf-transform optimize model.glb → auto‑detect map type
        │            └─ Color map → ETC1S
        │            └─ Data map → UASTC
        │            └─ Write KHR_texture_basisu extension
        │
        └── [Single texture] toktx → manually specify ETC1S/UASTC + color space + mipmaps
        │
        ▼
Compressed KTX2 / GLB
        │
        ▼
Engine loading (Three.js / Babylon.js) ── runtime transcode → GPU native format

Loading KTX2 in Three.js

Three.js has native support for KTX2 since r129: attach KTX2Loader (specify the path to the basis transcoder wasm, detect GPU capabilities) to GLTFLoader, then any glTF containing KTX2 textures will be automatically transcoded during loading.

const ktx2Loader = new KTX2Loader().setTranscoderPath("/basis/").detectSupport(renderer);
gltfLoader.setKTX2Loader(ktx2Loader); // If the model also uses Draco/MeshOpt, remember to attach DRACOLoader / MeshoptDecoder as well

detectSupport(renderer) is mandatory – it decides which native format to transcode to at runtime. Also, for color maps, set texture.colorSpace = THREE.SRGBColorSpace. Forgetting this is the classic “everything looks gray” trap (data maps like normal/roughness should stay linear).

Loading KTX2 in Babylon.js

Babylon.js is even simpler – GLTFFileLoader enables KHR_texture_basisu by default, and automatically fetches the basis transcoder from CDN. Loading a glTF with KTX2 textures works out of the box. For offline / intranet environments, manually configure BASISFileLoader.TranscoderModule.

Compression parameter tuning: balancing quality and size

The core parameter for ETC1S is --qlevel (1-255). How does it affect the result?

qlevelSizeQualityEncoding timeUse case
128 (default)SmallGood enoughMediumMost cases
200-255LargerNear‑losslessLong (several ×)High‑quality requirements
60-100Very smallVisible blockinessFastDistant / small textures

UASTC’s size is relatively fixed; the main way to tweak disk size is via --zcmp (Zstandard super compression), which does not affect GPU memory (still 8bpp after decompression).

Suggested tuning order:

  1. Start with default parameters, check size and quality
  2. Not satisfied? → Adjust --qlevel (ETC1S) or add --zcmp (UASTC)
  3. Normal map looks blurry? → Confirm you’re using UASTC, not ETC1S
  4. Colors look too dark? → Check color space settings (sRGB flag, engine’s colorSpace)

Common troubleshooting

Transcode failure / load errors

  • Verify the transcoder wasm path is correct (Three.js needs setTranscoderPath)
  • Check if the engine version supports the current KTX2 version (old basis encoding may not be supported by the new transcoder)
  • The console usually shows a specific error message; search by keywords

Colors too dark / too bright

  • Color map (albedo) is not set to sRGB, or set incorrectly
  • toktx command missing --srgb (must be added for color maps)
  • Data map (normal) mistakenly had sRGB applied

Missing mipmaps, flickering in the distance

  • Compression didn’t include --genmipmap
  • Engine’s texture.generateMipmaps is not enabled (in Three.js, KTX2 defaults to using the file’s mipmaps, but the material’s minFilter still needs to use a mipmap mode)

Normal direction wrong in close‑ups

  • Normal map used ETC1S – switch to UASTC
  • Confirm the normal map is OpenGL‑style (green channel points up); DirectX style may need to flip the G channel in some engines

File size unexpectedly larger

  • Small textures (< 128×128) don’t benefit from KTX2 – block compression has a fixed overhead that fills the entire block
  • Don’t compress solid‑color textures; use the material’s color value directly – it’s cheaper

What’s next

You’ve now basically mastered texture compression. But “knowing the tools” isn’t the same as “using the right tools” – the next article will consolidate all the knowledge from the previous four articles into a selection framework: desktop, mobile, VR, mini‑apps – which combination to use for each scenario.

Support Us