Skip to content
← Back to Blog guide

KHR_materials_variants in GLB: runtime skin swaps without draw calls

By BitSoul3D4 min read95 views

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.

KHR_materials_variants in GLB: runtime skin swaps without draw calls

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:

// 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.

What KHR_materials_variants actually is — illustrated

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:

  1. Create one material slot per variant (e.g. Armor_Default, Armor_Worn, Armor_Gold).
  2. In the GLTF export panel, enable Material Variants and assign each slot to a named variant.
  3. 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:

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.

Activating variants in Godot 4 — illustrated

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:

// 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

ApproachDraw callsVRAMSwap costUse when
Separate GLBsN × baseN × baseLoad from diskVariants differ in geometry
KHR_materials_variants1× base1× geometry + N× textures~0 CPU, 0 GPUSame mesh, different materials
Runtime material swap (no extension)1× basesameSmall1–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:

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.


Need drop-in assets for this workflow? Grab the 94-model Animal Bundle on the BitSoul marketplace and drop them straight into your project.

Tags: animals

Skip the modeling — download it instead

A free BitSoul3D account gets you 2 GLB downloads every month for personal use plus 25 one-time AI Engine credits, no card required. PBR-textured GLB downloads with a full 3D preview before you buy, for Unreal, Unity, Godot or Blender — OBJ and 3D-printable STL come with any purchase or paid plan.

Browse 1,051 models — from $4.99 → or start free (2 downloads a month)