KTX2 in Practice: The Right Way to Use Texture Compression
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:
| ETC1S | UASTC | |
|---|---|---|
| Compression ratio | Very high (like JPEG) | Medium (like high‑quality PNG) |
| Quality | Good enough for color maps | Near‑original quality |
| Memory (after decode) | Typically 4bpp (~1/8 of original) | Typically 8bpp (~1/4 of original) |
| Encoding speed | Slow (adjustable quality) | Fast |
| Best for | Albedo/diffuse, emissive | Normal, metalness‑roughness, data maps |
| Not for | Normal, precision‑sensitive maps | Color 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):
| Format | Disk size | GPU memory (with mipmaps) | Upload to GPU | Cross‑platform |
|---|---|---|---|---|
| PNG | ~5 MB | ~22 MB | Slow | ✅ |
| WebP | ~1 MB | ~22 MB | Slow | ✅ |
| KTX2 (ETC1S) | ~0.5–0.8 MB | ~2.8 MB | Fast | ✅ |
| KTX2 (UASTC) | ~3–4 MB | ~5.6 MB | Fast | ✅ |
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
--bcmpenables ETC1S mode (the base mode of Basis Universal), while--uastc 1enables UASTC mode. They are mutually exclusive.
2. gltf-transform (easiest, highly recommended)
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?
| qlevel | Size | Quality | Encoding time | Use case |
|---|---|---|---|---|
| 128 (default) | Small | Good enough | Medium | Most cases |
| 200-255 | Larger | Near‑lossless | Long (several ×) | High‑quality requirements |
| 60-100 | Very small | Visible blockiness | Fast | Distant / 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:
- Start with default parameters, check size and quality
- Not satisfied? → Adjust
--qlevel(ETC1S) or add--zcmp(UASTC) - Normal map looks blurry? → Confirm you’re using UASTC, not ETC1S
- 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.generateMipmapsis not enabled (in Three.js, KTX2 defaults to using the file’s mipmaps, but the material’sminFilterstill 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.