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---5
6# three.js glTF Loading
7
8Load `.gltf`/`.glb` models and play their animations in three.js, including
9compressed geometry (DRACO/Meshopt) and textures (KTX2). Patterns target
10**r184**; preserve an existing project's pinned release unless migration is requested.
11
12## When to use
13
14- Use to import a 3D model, add it to the scene, inspect its node hierarchy, and
15 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/...`.
18
19**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 out
22of scope; prefer glTF over OBJ/FBX for runtime.
23
24## Core workflow
25
261. **Why glTF.** It's a transmission format: binary vertex data, PBR materials, and
27 animations are ready to render with minimal parsing. Prefer it over OBJ (no scene
28 graph, no animation) and FBX (heavy) for the web.
292. **Load with `GLTFLoader`.** `loader.load(url, onLoad, onProgress, onError)`. The
30 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 with
33 `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 their
38 files.
396. **Verify what loaded** — log the scene graph and `gltf.animations`, and confirm
40 the model is visible (right scale, lit) and the clip actually plays.
41
42## Patterns
43
44### 1. Load a model and frame it
45
46```js
47import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
48
49const 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```
62
63### 2. Play a skinned animation with AnimationMixer
64
65```js
66import * as THREE from 'three';
67import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
68
69let mixer; // declare outside so the loop can see it
70const clock = new THREE.Clock();
71
72new GLTFLoader().load('assets/character.glb', (gltf) => {
73 scene.add(gltf.scene);
74 mixer = new THREE.AnimationMixer(gltf.scene); // one mixer per animated root
75 const clip = THREE.AnimationClip.findByName(gltf.animations, 'Run')
76 ?? gltf.animations[0];
77 mixer.clipAction(clip).play();
78});
79
80renderer.setAnimationLoop(() => {
81 const dt = clock.getDelta();
82 if (mixer) mixer.update(dt); // advance the animation by real seconds
83 renderer.render(scene, camera);
84});
85```
86
87### 3. Cross-fade between two clips
88
89```js
90const actions = {};
91mixer = new THREE.AnimationMixer(gltf.scene);
92for (const clip of gltf.animations) {
93 actions[clip.name] = mixer.clipAction(clip);
94}
95actions['Idle'].play();
96
97function 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```
105
106### 4. DRACO-compressed geometry
107
108```js
109import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
110import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
111
112const 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/');
115
116const loader = new GLTFLoader();
117loader.setDRACOLoader(draco);
118loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));
119```
120
121### 5. Find and animate a named part
122
123```js
124new 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```
137
138## Pitfalls
139
140- **Model loads but is invisible** → it has lit (PBR) materials and the scene has no
141 light or environment. Add a light or `scene.environment` (see
142 `threejs-materials-lighting`), and check scale — glTF is in metres, so a 0.01-scaled
143 asset is tiny.
144- **`load` is async** → `gltf` only exists inside the callback; declare `mixer`/refs
145 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 you
147 passed milliseconds instead of seconds (use `clock.getDelta()`), or you forgot
148 `action.play()`.
149- **DRACO/KTX2 model fails** → the decoder/transcoder path is wrong or version-
150 mismatched. `setDecoderPath`/`setTranscoderPath` must point at files matching your
151 three.js version.
152- **Multiple mixers fighting** → use **one** `AnimationMixer` per animated root and
153 create all actions from it; don't make a new mixer per clip.
154- **Baked-in transforms surprise you** → exporters sometimes bake scale/rotation onto
155 child nodes. Dump the hierarchy (names + position/rotation/scale) before relying on
156 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 clean
158 pivot rather than fighting baked offsets.
159
160## References
161
162- For the full decode/transcode setup (DRACO + Meshopt + KTX2 together), `loadAsync`
163 + a `LoadingManager` progress bar, reusing models with `SkeletonUtils.clone`, and
164 exporter guidance (apply transforms, one clean root), read
165 `references/loaders-and-animation.md`.
166
167## Related skills
168
169- `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.