three.js glTF Loading
Load .gltf/.glb models and play their animations in three.js, including
compressed geometry (DRACO/Meshopt) and textures (KTX2). Patterns target
r184; preserve an existing project's pinned release unless migration is requested.
When to use
- Use to import a 3D model, add it to the scene, inspect its node hierarchy, and
play baked/skinned animation clips with an
AnimationMixer.
- Use when files are
.gltf/.glb, or code imports GLTFLoader /
DRACOLoader / KTX2Loader from three/addons/loaders/....
When not to use: creating the renderer/camera/loop → threejs-scene-setup.
Tuning surface look, lights, or shadows on the loaded model →
threejs-materials-lighting. Authoring/exporting the model itself (Blender) is out
of scope; prefer glTF over OBJ/FBX for runtime.
Core workflow
- Why glTF. It's a transmission format: binary vertex data, PBR materials, and
animations are ready to render with minimal parsing. Prefer it over OBJ (no scene
graph, no animation) and FBX (heavy) for the web.
- Load with
GLTFLoader. loader.load(url, onLoad, onProgress, onError). The
result gltf has gltf.scene (the Object3D root), gltf.animations
(AnimationClip[]), gltf.cameras, and gltf.asset.
- Add
gltf.scene to your scene and frame it. Inspect the hierarchy with
traverse / getObjectByName to find the parts you'll control.
- Play animations with an
AnimationMixer. One mixer per animated root;
mixer.clipAction(clip).play(); advance with mixer.update(delta) every frame.
- Decode compressed assets. Attach a
DRACOLoader (and/or KTX2Loader +
Meshopt) so DRACO meshes and KTX2 textures load; point the decoders at their
files.
- Verify what loaded — log the scene graph and
gltf.animations, and confirm
the model is visible (right scale, lit) and the clip actually plays.
Patterns
1. Load a model and frame it
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'assets/robot.glb',
(gltf) => {
const root = gltf.scene;
scene.add(root);
// Inspect: gltf.animations is an array of AnimationClip.
console.log('clips:', gltf.animations.map((c) => c.name));
},
(event) => console.log(`${(event.loaded / event.total) * 100}% loaded`),
(error) => console.error('glTF load failed:', error)
);
2. Play a skinned animation with AnimationMixer
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
let mixer; // declare outside so the loop can see it
const clock = new THREE.Clock();
new GLTFLoader().load('assets/character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene); // one mixer per animated root
const clip = THREE.AnimationClip.findByName(gltf.animations, 'Run')
?? gltf.animations[0];
mixer.clipAction(clip).play();
});
renderer.setAnimationLoop(() => {
const dt = clock.getDelta();
if (mixer) mixer.update(dt); // advance the animation by real seconds
renderer.render(scene, camera);
});
3. Cross-fade between two clips
const actions = {};
mixer = new THREE.AnimationMixer(gltf.scene);
for (const clip of gltf.animations) {
actions[clip.name] = mixer.clipAction(clip);
}
actions['Idle'].play();
function transitionTo(name, duration = 0.3) {
const next = actions[name];
next.reset().play();
for (const [n, action] of Object.entries(actions)) {
if (n !== name) action.crossFadeTo(next, duration, false);
}
}
4. DRACO-compressed geometry
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
// Point at the decoder files you ship (or a pinned CDN copy of the same version).
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/libs/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));
5. Find and animate a named part
new GLTFLoader().load('assets/car.glb', (gltf) => {
scene.add(gltf.scene);
const wheels = [];
gltf.scene.traverse((node) => {
if (node.name.startsWith('Wheel')) wheels.push(node);
});
renderer.setAnimationLoop(() => {
const dt = clock.getDelta();
for (const w of wheels) w.rotation.x += dt * 4;
renderer.render(scene, camera);
});
});
Pitfalls
- Model loads but is invisible → it has lit (PBR) materials and the scene has no
light or environment. Add a light or
scene.environment (see
threejs-materials-lighting), and check scale — glTF is in metres, so a 0.01-scaled
asset is tiny.
load is async → gltf only exists inside the callback; declare mixer/refs
outside and assign them in the callback, or use await loader.loadAsync(url).
- Animation never moves → you didn't call
mixer.update(delta) each frame, or you
passed milliseconds instead of seconds (use clock.getDelta()), or you forgot
action.play().
- DRACO/KTX2 model fails → the decoder/transcoder path is wrong or version-
mismatched.
setDecoderPath/setTranscoderPath must point at files matching your
three.js version.
- Multiple mixers fighting → use one
AnimationMixer per animated root and
create all actions from it; don't make a new mixer per clip.
- Baked-in transforms surprise you → exporters sometimes bake scale/rotation onto
child nodes. Dump the hierarchy (names + position/rotation/scale) before relying on
a node's local transform; re-export from the source if the rig is unusable.
- Origins are off → re-parent a part under a fresh
Object3D to give it a clean
pivot rather than fighting baked offsets.
References
- For the full decode/transcode setup (DRACO + Meshopt + KTX2 together),
loadAsync
- a
LoadingManager progress bar, reusing models with SkeletonUtils.clone, and
exporter guidance (apply transforms, one clean root), read
references/loaders-and-animation.md.
Related skills
threejs-scene-setup — the renderer, camera, and loop this model renders into.
threejs-materials-lighting — lighting/environment so PBR models look right.
fps-shooter — a 3D genre that composes three.js skills.
1---2name: threejs-gltf-loading3description: Load glTF/GLB models in three.js with GLTFLoader and play their skinned animations with AnimationMixer, including DRACO/Meshopt-compressed meshes and KTX2 textures. Use when importing 3D models into three.js — when the user mentions glTF, GLB, GLTFLoader, AnimationMixer, animation clips, DRACOLoader, or "load a 3D model". For scene/camera/renderer setup use threejs-scene-setup; for materials and lights use threejs-materials-lighting.4---56# three.js glTF Loading78Load `.gltf`/`.glb` models and play their animations in three.js, including9compressed geometry (DRACO/Meshopt) and textures (KTX2). Patterns target10**r184**; preserve an existing project's pinned release unless migration is requested.1112## When to use1314- Use to import a 3D model, add it to the scene, inspect its node hierarchy, and15 play baked/skinned animation clips with an `AnimationMixer`.16- Use when files are `.gltf`/`.glb`, or code imports `GLTFLoader` /17 `DRACOLoader` / `KTX2Loader` from `three/addons/loaders/...`.1819**When *not* to use:** creating the renderer/camera/loop → `threejs-scene-setup`.20Tuning surface look, lights, or shadows on the loaded model →21`threejs-materials-lighting`. Authoring/exporting the model itself (Blender) is out22of scope; prefer glTF over OBJ/FBX for runtime.2324## Core workflow25261. **Why glTF.** It's a transmission format: binary vertex data, PBR materials, and27 animations are ready to render with minimal parsing. Prefer it over OBJ (no scene28 graph, no animation) and FBX (heavy) for the web.292. **Load with `GLTFLoader`.** `loader.load(url, onLoad, onProgress, onError)`. The30 result `gltf` has `gltf.scene` (the `Object3D` root), `gltf.animations`31 (`AnimationClip[]`), `gltf.cameras`, and `gltf.asset`.323. **Add `gltf.scene` to your scene** and frame it. Inspect the hierarchy with33 `traverse` / `getObjectByName` to find the parts you'll control.344. **Play animations with an `AnimationMixer`.** One mixer per animated root;35 `mixer.clipAction(clip).play()`; advance with `mixer.update(delta)` every frame.365. **Decode compressed assets.** Attach a `DRACOLoader` (and/or `KTX2Loader` +37 Meshopt) so DRACO meshes and KTX2 textures load; point the decoders at their38 files.396. **Verify what loaded** — log the scene graph and `gltf.animations`, and confirm40 the model is visible (right scale, lit) and the clip actually plays.4142## Patterns4344### 1. Load a model and frame it4546```js47import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';4849const loader = new GLTFLoader();50loader.load(51 'assets/robot.glb',52 (gltf) => {53 const root = gltf.scene;54 scene.add(root);55 // Inspect: gltf.animations is an array of AnimationClip.56 console.log('clips:', gltf.animations.map((c) => c.name));57 },58 (event) => console.log(`${(event.loaded / event.total) * 100}% loaded`),59 (error) => console.error('glTF load failed:', error)60);61```6263### 2. Play a skinned animation with AnimationMixer6465```js66import * as THREE from 'three';67import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';6869let mixer; // declare outside so the loop can see it70const clock = new THREE.Clock();7172new GLTFLoader().load('assets/character.glb', (gltf) => {73 scene.add(gltf.scene);74 mixer = new THREE.AnimationMixer(gltf.scene); // one mixer per animated root75 const clip = THREE.AnimationClip.findByName(gltf.animations, 'Run')76 ?? gltf.animations[0];77 mixer.clipAction(clip).play();78});7980renderer.setAnimationLoop(() => {81 const dt = clock.getDelta();82 if (mixer) mixer.update(dt); // advance the animation by real seconds83 renderer.render(scene, camera);84});85```8687### 3. Cross-fade between two clips8889```js90const actions = {};91mixer = new THREE.AnimationMixer(gltf.scene);92for (const clip of gltf.animations) {93 actions[clip.name] = mixer.clipAction(clip);94}95actions['Idle'].play();9697function transitionTo(name, duration = 0.3) {98 const next = actions[name];99 next.reset().play();100 for (const [n, action] of Object.entries(actions)) {101 if (n !== name) action.crossFadeTo(next, duration, false);102 }103}104```105106### 4. DRACO-compressed geometry107108```js109import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';110import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';111112const draco = new DRACOLoader();113// Point at the decoder files you ship (or a pinned CDN copy of the same version).114draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/libs/draco/');115116const loader = new GLTFLoader();117loader.setDRACOLoader(draco);118loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));119```120121### 5. Find and animate a named part122123```js124new GLTFLoader().load('assets/car.glb', (gltf) => {125 scene.add(gltf.scene);126 const wheels = [];127 gltf.scene.traverse((node) => {128 if (node.name.startsWith('Wheel')) wheels.push(node);129 });130 renderer.setAnimationLoop(() => {131 const dt = clock.getDelta();132 for (const w of wheels) w.rotation.x += dt * 4;133 renderer.render(scene, camera);134 });135});136```137138## Pitfalls139140- **Model loads but is invisible** → it has lit (PBR) materials and the scene has no141 light or environment. Add a light or `scene.environment` (see142 `threejs-materials-lighting`), and check scale — glTF is in metres, so a 0.01-scaled143 asset is tiny.144- **`load` is async** → `gltf` only exists inside the callback; declare `mixer`/refs145 outside and assign them in the callback, or use `await loader.loadAsync(url)`.146- **Animation never moves** → you didn't call `mixer.update(delta)` each frame, or you147 passed milliseconds instead of seconds (use `clock.getDelta()`), or you forgot148 `action.play()`.149- **DRACO/KTX2 model fails** → the decoder/transcoder path is wrong or version-150 mismatched. `setDecoderPath`/`setTranscoderPath` must point at files matching your151 three.js version.152- **Multiple mixers fighting** → use **one** `AnimationMixer` per animated root and153 create all actions from it; don't make a new mixer per clip.154- **Baked-in transforms surprise you** → exporters sometimes bake scale/rotation onto155 child nodes. Dump the hierarchy (names + position/rotation/scale) before relying on156 a node's local transform; re-export from the source if the rig is unusable.157- **Origins are off** → re-parent a part under a fresh `Object3D` to give it a clean158 pivot rather than fighting baked offsets.159160## References161162- For the full decode/transcode setup (DRACO + Meshopt + KTX2 together), `loadAsync`163 + a `LoadingManager` progress bar, reusing models with `SkeletonUtils.clone`, and164 exporter guidance (apply transforms, one clean root), read165 `references/loaders-and-animation.md`.166167## Related skills168169- `threejs-scene-setup` — the renderer, camera, and loop this model renders into.170- `threejs-materials-lighting` — lighting/environment so PBR models look right.171- `fps-shooter` — a 3D genre that composes three.js skills.