Support KHR_materials_unlit in Three.js GLTFLoader

This article explains how to parse and support the KHR_materials_unlit extension when loading 3D models using GLTFLoader in Three.js. It covers how Three.js handles unlit materials automatically, provides a code implementation, and explains how to verify that your unlit materials are rendering correctly without light sources.

Automatic Support in GLTFLoader

In Three.js, GLTFLoader supports the KHR_materials_unlit extension automatically out of the box. You do not need to install additional plugins or manually register the extension.

When GLTFLoader parses a glTF file containing the KHR_materials_unlit extension, it automatically bypasses the default physically-based rendering (PBR) material (MeshStandardMaterial) and maps the affected materials to MeshBasicMaterial. Because MeshBasicMaterial does not react to light sources, it renders using only its flat color or texture map, satisfying the “unlit” requirement.

Loading a Model with Unlit Materials

To load a model that utilizes the KHR_materials_unlit extension, use the standard GLTFLoader implementation.

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

// Create scene, camera, and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Initialize GLTFLoader
const loader = new GLTFLoader();

// Load the glTF model
loader.load(
    'path/to/unlit_model.gltf',
    (gltf) => {
        scene.add(gltf.scene);
        console.log('Model loaded successfully!');
    },
    (xhr) => {
        console.log((xhr.loaded / xhr.total * 100) + '% loaded');
    },
    (error) => {
        console.error('An error occurred while loading the model:', error);
    }
);

Because the materials are unlit, they will be visible in the scene even if you do not add any light sources (such as AmbientLight or DirectionalLight) to the scene.

Verifying Unlit Materials Programmatically

If you need to verify that the loader has correctly parsed the extension and applied MeshBasicMaterial, you can traverse the loaded scene graph and inspect the materials of the meshes.

loader.load('path/to/unlit_model.gltf', (gltf) => {
    gltf.scene.traverse((child) => {
        if (child.isMesh) {
            // Check if the material is an instance of MeshBasicMaterial
            if (child.material.isMeshBasicMaterial) {
                console.log(`Mesh "${child.name}" correctly uses an unlit material.`);
            } else {
                console.log(`Mesh "${child.name}" uses a standard lit material.`);
            }
        }
    });
    scene.add(gltf.scene);
});

Creating Custom Fallbacks or Manual Handling

If you are writing custom shader materials or need to manually parse the extension from the raw glTF data, you can access the parser’s association data.

loader.load('path/to/unlit_model.gltf', (gltf) => {
    const parser = gltf.parser;
    
    gltf.scene.traverse((child) => {
        if (child.isMesh) {
            const materialIndex = parser.associations.get(child.material)?.materials;
            if (materialIndex !== undefined) {
                const gltfMaterialDef = parser.json.materials[materialIndex];
                
                // Check if the glTF material definition uses the unlit extension
                if (gltfMaterialDef.extensions && gltfMaterialDef.extensions.KHR_materials_unlit) {
                    // Apply custom behavior or shaders for unlit meshes here
                }
            }
        }
    });
});