Swapping a character's outfit, a weapon's finish, or a vehicle's livery at runtime is a staple of almost every modern game — and the naive solution (load a separate GLB per variant) blows your draw call budget before the level even loads. KHR_materials_variants is the GLTF extension that solves this problem natively: multiple material sets baked into a single GLB, switchable in one API call with zero extra geometry.
What KHR_materials_variants actually is
`KHR_materials_variants` is a ratified Khronos GLTF 2.0 extension that lets you define a named list of variants at the asset root, then map each mesh primitive to a different material per variant. The file ships with one baseline material set (the default GLTF material), plus N alternate sets stored in the extension block. At load time only the default renders; switching variants is a metadata operation — no geometry reload, no additional GPU buffers.
The extension lives in two places in the GLB JSON. At the root it declares the variant names; on each mesh primitive it maps variant index to material index:
```json
// Root-level (extensionsUsed + asset root extensions):
"KHR_materials_variants": { "variants": [{"name":"default"},{"name":"worn"},{"name":"gold"}] }
// Per-primitive extensions — each mapping is { variants: [idx], material: idx }:
"KHR_materials_variants": {
"mappings": [
{"variants": [0], "material": 0},
{"variants": [1], "material": 2},
{"variants": [2], "material": 4}
]
}
```
Each primitive can map to a completely different material (different albedo, roughness, metallic, emissive). Variants share geometry: the same vertex buffer, same UVs, same rig — only the material bindings differ.
![]()
Authoring variants in Blender
Blender's built-in GLTF exporter (4.2+) supports `KHR_materials_variants` via the Material Variants panel in the GLTF export sidebar. Workflow:
- Create one material slot per variant (e.g. `Armor_Default`, `Armor_Worn`, `Armor_Gold`).
- In the GLTF export panel, enable Material Variants and assign each slot to a named variant.
- Export as GLB. Confirm the file contains `KHR_materials_variants` in `extensionsUsed`.
Keep variant count reasonable: 3–8 variants per asset is typical. Each variant adds one extra material entry to the GLB but no geometry — file size grows linearly with texture count, not mesh complexity.
Activating variants in Godot 4
Godot 4's GLTF importer recognises `KHR_materials_variants` as of 4.1. After import, each variant is accessible via `GLTFState`. Populate `gltf_state` at load time via `GLTFDocument.append_from_file()`, then switch variants by index:
```gdscript
func apply_variant(variant_name: String) -> void:
# gltf_state populated at load time via GLTFDocument.append_from_file()
var variants := gltf_state.get_additional_data("KHR_materials_variants_names") as Array
var idx := variants.find(variant_name)
if idx == -1:
push_warning("Variant not found: " + variant_name)
return
for mesh in get_children_recursive(model, MeshInstance3D):
var im := mesh.mesh as ImporterMesh
if im:
im.set_surface_material(idx, gltf_state.materials[idx])
```
For production use, the cleaner pattern is to bake materials into `MeshInstance3D` override slots at import time using an `EditorImportPlugin` — that way variant switching is a direct `set_surface_override_material()` call with no GLTF state retained at runtime.
![]()
Using variants in Three.js and web-based engines
Three.js ships a `KHRMaterialsVariantsExtension` plugin in `three/addons`. Register it on the loader, then call `selectVariant()` after the GLB loads — the call traverses the scene graph and applies cached material references per primitive synchronously, in one frame, when textures are already GPU-resident:
```javascript
// Import GLTFLoader and KHRMaterialsVariantsExtension from 'three/addons'
const loader = new GLTFLoader();
loader.register(parser => new KHRMaterialsVariantsExtension(parser));
loader.load('armor.glb', (gltf) => {
scene.add(gltf.scene);
// Switch to any named variant — zero extra draw calls, no geometry reload
gltf.functions.selectVariant(gltf.scene, 'gold');
});
```
`selectVariant` returns a Promise, but all material swaps happen within a single frame when textures are already loaded. List available variants via `gltf.userData.gltfExtensions['KHR_materials_variants'].variants.map(v => v.name)`.
Performance profile and when NOT to use it
| Approach | Draw calls | VRAM | Swap cost | Use when |
|---|---|---|---|---|
| Separate GLBs | N × base | N × base | Load from disk | Variants differ in geometry |
| KHR_materials_variants | 1× base | 1× geometry + N× textures | ~0 CPU, 0 GPU | Same mesh, different materials |
| Runtime material swap (no extension) | 1× base | same | Small | 1–2 variants, no reuse |
`KHR_materials_variants` wins when you have 3 or more surface variants on the same mesh, the asset is instanced across the scene, or you need deterministic VRAM cost — load once, switch freely. It's overkill for single-use props with one alternate skin; use a plain material swap there.
Texture packing for variants
All variant materials share the same UV layout — critical for packing efficiency. Use a shared AO/roughness/metallic ORM texture where channels don't change between variants, and only swap the albedo. This halves per-variant VRAM overhead:
- `armor_ao_rough_metal.png` — shared across all variants (R=AO, G=roughness, B=metallic)
- `armor_albedo_default.png`, `armor_albedo_worn.png`, `armor_albedo_gold.png` — variant-specific
With this approach, a 3-variant asset at 2K textures uses 3 albedos + 1 ORM = 4 textures instead of 9.
Where to find assets ready for variant authoring
The BitSoul 3D marketplace ships 747 GLB assets — props, characters, vehicles, and environment kits — in clean, single-UV layouts ideal for adding material variants post-download. Grab a weapon or vehicle asset, add 3 Blender materials, export with `KHR_materials_variants` enabled, and you have a production-ready multi-skin asset in under an hour.
A free BitSoul account covers prototyping this variant pipeline; commercial use of the assets in a shipped game is included with paid memberships. Browse the full library at bitsoulhosting.com/marketplace.
---
*This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.*