Loading your first model
Loading a 3D model into Three.js follows a consistent pattern: create a loader, point it at a file, and handle the result in a callback. The GLTFLoader is the loader you'll use most often, since glTF/GLB is the standard format for web 3D.
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'/models/my-model.glb',
// onLoad: called when the model is fully loaded
(gltf) => {
scene.add(gltf.scene);
},
// onProgress: called during loading (optional)
(progress) => {
const percent = (progress.loaded / progress.total) * 100;
console.log(percent.toFixed(0) + '% loaded');
},
// onError: called if something goes wrong (optional)
(error) => {
console.error('Failed to load model:', error);
}
);The load method takes a URL and up to three callbacks: success, progress, and error. The success callback receives a gltf object containing everything the file held: the scene graph, animations, cameras, and metadata.
An async alternative
If you prefer async/await over callbacks, GLTFLoader provides a loadAsync method. This is cleaner when you need to load multiple assets in sequence or when your loading logic lives inside an async function.
const loader = new GLTFLoader();
async function loadModel() {
try {
const gltf = await loader.loadAsync('/models/my-model.glb');
scene.add(gltf.scene);
} catch (error) {
console.error('Failed to load model:', error);
}
}
// Loading multiple models in parallel
async function loadAllModels() {
const [robot, tree, building] = await Promise.all([
loader.loadAsync('/models/robot.glb'),
loader.loadAsync('/models/tree.glb'),
loader.loadAsync('/models/building.glb')
]);
scene.add(robot.scene);
scene.add(tree.scene);
scene.add(building.scene);
}The loadAsync approach loses the progress callback, so use the callback version if you need a loading bar. For everything else, loadAsync is simpler to read and reason about.
Loading with Draco compression
If your glTF file was compressed with Draco, the standard GLTFLoader won't decode it on its own. You need to attach a DRACOLoader that provides the decoder. The decoder files ship with Three.js and need to be served as static assets.
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
// Set up the Draco decoder
const dracoLoader = new DRACOLoader();
// Point to the directory containing the decoder files:
// draco_decoder.js, draco_decoder.wasm, draco_wasm_wrapper.js
dracoLoader.setDecoderPath('/draco/');
// Optional: use the JS decoder instead of WASM
// dracoLoader.setDecoderConfig({ type: 'js' });
// Attach to GLTFLoader
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
// Now load compressed models normally
loader.load('/models/compressed.glb', (gltf) => {
scene.add(gltf.scene);
});
// When you're done loading all models, free the decoder
// dracoLoader.dispose();The Draco decoder files are found in the Three.js package at node_modules/three/examples/jsm/libs/draco/. Copy that directory to your public/static folder so the loader can fetch them at runtime. In a Vite or Nuxt project, place them in the public/draco/ directory.
Loading progress
When loading large models or multiple assets, you'll want a loading screen. The THREE.LoadingManager tracks the progress of all loaders attached to it and fires callbacks as items load.
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {
console.log('Started loading:', url);
console.log(loaded + ' of ' + total + ' items loaded');
};
manager.onProgress = (url, loaded, total) => {
const percent = (loaded / total) * 100;
// Update your loading bar here
document.querySelector('.progress-bar').style.width =
percent + '%';
};
manager.onLoad = () => {
// Everything is loaded, so hide loading screen
document.querySelector('.loading-screen').style.display =
'none';
};
manager.onError = (url) => {
console.error('Failed to load:', url);
};
// Pass the manager to your loaders
const gltfLoader = new GLTFLoader(manager);
const textureLoader = new THREE.TextureLoader(manager);
// All loads through these loaders are tracked by the manager
gltfLoader.load('/models/scene.glb', (gltf) => {
scene.add(gltf.scene);
});
textureLoader.load('/textures/ground.jpg', (texture) => {
// use texture
});The loading manager counts items, not bytes. If you load one 50MB model and five 1KB textures, the progress bar will jump from 0% to ~17% when the first item loads, regardless of file size. For byte-level progress on individual files, use the progress callback on the loader's load method directly.
Loading textures separately
Not everything comes bundled in a glTF file. Sometimes you need to load textures independently, for a custom terrain, a skybox, or to apply a texture to procedural geometry. The TextureLoader handles standard image formats (PNG, JPG, WebP).
const textureLoader = new THREE.TextureLoader();
// Basic texture load
const colorMap = textureLoader.load('/textures/brick_color.jpg');
const normalMap = textureLoader.load('/textures/brick_normal.jpg');
const roughnessMap = textureLoader.load('/textures/brick_roughness.jpg');
const material = new THREE.MeshStandardMaterial({
map: colorMap,
normalMap: normalMap,
roughnessMap: roughnessMap
});
// Set texture properties
colorMap.colorSpace = THREE.SRGBColorSpace; // color textures need this
colorMap.wrapS = THREE.RepeatWrapping;
colorMap.wrapT = THREE.RepeatWrapping;
colorMap.repeat.set(4, 4); // tile 4x in each direction
// Normal and roughness maps stay in linear space (the default)
// Don't set SRGBColorSpace on non-color data mapsA frequent mistake people make is forgetting to set colorSpace = THREE.SRGBColorSpace on color textures (diffuse/albedo maps). Without it, colors appear washed out because Three.js assumes linear color space by default. Data textures like normal maps and roughness maps should stay in linear space.