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-loading
3description: >
4 Load glTF/GLB models in three.js with GLTFLoader and play their skinned
5 animations with AnimationMixer, including DRACO/Meshopt-compressed meshes and
6 KTX2 textures. Use when importing 3D models into three.js — when the user
7 mentions glTF, GLB, GLTFLoader, AnimationMixer, animation clips, DRACOLoader, or
8 "load a 3D model". For scene/camera/renderer setup use threejs-scene-setup; for
9 materials and lights use threejs-materials-lighting.
10---
11
12# three.js glTF Loading
13
14Load `.gltf`/`.glb` models and play their animations in three.js, including
15compressed geometry (DRACO/Meshopt) and textures (KTX2). Patterns target
16**r184**; preserve an existing project's pinned release unless migration is requested.
17
18## When to use
19
20- Use to import a 3D model, add it to the scene, inspect its node hierarchy, and
21 play baked/skinned animation clips with an `AnimationMixer`.
22- Use when files are `.gltf`/`.glb`, or code imports `GLTFLoader` /
23 `DRACOLoader` / `KTX2Loader` from `three/addons/loaders/...`.
24
25**When *not* to use:** creating the renderer/camera/loop → `threejs-scene-setup`.
26Tuning surface look, lights, or shadows on the loaded model →
27`threejs-materials-lighting`. Authoring/exporting the model itself (Blender) is out
28of scope; prefer glTF over OBJ/FBX for runtime.
29
30## Core workflow
31
321. **Why glTF.** It's a transmission format: binary vertex data, PBR materials, and
33 animations are ready to render with minimal parsing. Prefer it over OBJ (no scene
34 graph, no animation) and FBX (heavy) for the web.
352. **Load with `GLTFLoader`.** `loader.load(url, onLoad, onProgress, onError)`. The
36 result `gltf` has `gltf.scene` (the `Object3D` root), `gltf.animations`
37 (`AnimationClip[]`), `gltf.cameras`, and `gltf.asset`.
383. **Add `gltf.scene` to your scene** and frame it. Inspect the hierarchy with
39 `traverse` / `getObjectByName` to find the parts you'll control.
404. **Play animations with an `AnimationMixer`.** One mixer per animated root;
41 `mixer.clipAction(clip).play()`; advance with `mixer.update(delta)` every frame.
425. **Decode compressed assets.** Attach a `DRACOLoader` (and/or `KTX2Loader` +
43 Meshopt) so DRACO meshes and KTX2 textures load; point the decoders at their
44 files.
456. **Verify what loaded** — log the scene graph and `gltf.animations`, and confirm
46 the model is visible (right scale, lit) and the clip actually plays.
47
48## Patterns
49
50### 1. Load a model and frame it
51
52```js
53import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
54
55const loader = new GLTFLoader();
56loader.load(
57 'assets/robot.glb',
58 (gltf) => {
59 const root = gltf.scene;
60 scene.add(root);
61 // Inspect: gltf.animations is an array of AnimationClip.
62 console.log('clips:', gltf.animations.map((c) => c.name));
63 },
64 (event) => console.log(`${(event.loaded / event.total) * 100}% loaded`),
65 (error) => console.error('glTF load failed:', error)
66);
67```
68
69### 2. Play a skinned animation with AnimationMixer
70
71```js
72import * as THREE from 'three';
73import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
74
75let mixer; // declare outside so the loop can see it
76const clock = new THREE.Clock();
77
78new GLTFLoader().load('assets/character.glb', (gltf) => {
79 scene.add(gltf.scene);
80 mixer = new THREE.AnimationMixer(gltf.scene); // one mixer per animated root
81 const clip = THREE.AnimationClip.findByName(gltf.animations, 'Run')
82 ?? gltf.animations[0];
83 mixer.clipAction(clip).play();
84});
85
86renderer.setAnimationLoop(() => {
87 const dt = clock.getDelta();
88 if (mixer) mixer.update(dt); // advance the animation by real seconds
89 renderer.render(scene, camera);
90});
91```
92
93### 3. Cross-fade between two clips
94
95```js
96const actions = {};
97mixer = new THREE.AnimationMixer(gltf.scene);
98for (const clip of gltf.animations) {
99 actions[clip.name] = mixer.clipAction(clip);
100}
101actions['Idle'].play();
102
103function transitionTo(name, duration = 0.3) {
104 const next = actions[name];
105 next.reset().play();
106 for (const [n, action] of Object.entries(actions)) {
107 if (n !== name) action.crossFadeTo(next, duration, false);
108 }
109}
110```
111
112### 4. DRACO-compressed geometry
113
114```js
115import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
116import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
117
118const draco = new DRACOLoader();
119// Point at the decoder files you ship (or a pinned CDN copy of the same version).
120draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/libs/draco/');
121
122const loader = new GLTFLoader();
123loader.setDRACOLoader(draco);
124loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));
125```
126
127### 5. Find and animate a named part
128
129```js
130new GLTFLoader().load('assets/car.glb', (gltf) => {
131 scene.add(gltf.scene);
132 const wheels = [];
133 gltf.scene.traverse((node) => {
134 if (node.name.startsWith('Wheel')) wheels.push(node);
135 });
136 renderer.setAnimationLoop(() => {
137 const dt = clock.getDelta();
138 for (const w of wheels) w.rotation.x += dt * 4;
139 renderer.render(scene, camera);
140 });
141});
142```
143
144## Pitfalls
145
146- **Model loads but is invisible** → it has lit (PBR) materials and the scene has no
147 light or environment. Add a light or `scene.environment` (see
148 `threejs-materials-lighting`), and check scale — glTF is in metres, so a 0.01-scaled
149 asset is tiny.
150- **`load` is async** → `gltf` only exists inside the callback; declare `mixer`/refs
151 outside and assign them in the callback, or use `await loader.loadAsync(url)`.
152- **Animation never moves** → you didn't call `mixer.update(delta)` each frame, or you
153 passed milliseconds instead of seconds (use `clock.getDelta()`), or you forgot
154 `action.play()`.
155- **DRACO/KTX2 model fails** → the decoder/transcoder path is wrong or version-
156 mismatched. `setDecoderPath`/`setTranscoderPath` must point at files matching your
157 three.js version.
158- **Multiple mixers fighting** → use **one** `AnimationMixer` per animated root and
159 create all actions from it; don't make a new mixer per clip.
160- **Baked-in transforms surprise you** → exporters sometimes bake scale/rotation onto
161 child nodes. Dump the hierarchy (names + position/rotation/scale) before relying on
162 a node's local transform; re-export from the source if the rig is unusable.
163- **Origins are off** → re-parent a part under a fresh `Object3D` to give it a clean
164 pivot rather than fighting baked offsets.
165
166## References
167
168- For the full decode/transcode setup (DRACO + Meshopt + KTX2 together), `loadAsync`
169 + a `LoadingManager` progress bar, reusing models with `SkeletonUtils.clone`, and
170 exporter guidance (apply transforms, one clean root), read
171 `references/loaders-and-animation.md`.
172
173## Related skills
174
175- `threejs-scene-setup` — the renderer, camera, and loop this model renders into.
176- `threejs-materials-lighting` — lighting/environment so PBR models look right.
177- `fps-shooter` — a 3D genre that composes three.js skills.