gamedev-skills/threejs-gltf-loading
> 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.
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill threejs-gltf-loading
Load .gltf/.glb models and play their animations in three.js, including
compressed geometry (DRACO/Meshopt) and textures (KTX2). Patterns target
r165+, verified against r184.
play baked/skinned animation clips with an AnimationMixer.
.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.
animations are ready to render with minimal parsing. Prefer it over OBJ (no scene
graph, no animation) and FBX (heavy) for the web.
GLTFLoader. loader.load(url, onLoad, onProgress, onError). Theresult gltf has gltf.scene (the Object3D root), gltf.animations
(AnimationClip[]), gltf.cameras, and gltf.asset.
gltf.scene to your scene and frame it. Inspect the hierarchy withtraverse / getObjectByName to find the parts you'll control.
AnimationMixer. One mixer per animated root;mixer.clipAction(clip).play(); advance with mixer.update(delta) every frame.
DRACOLoader (and/or KTX2Loader +Meshopt) so DRACO meshes and KTX2 textures load; point the decoders at their
files.
gltf.animations, and confirmthe model is visible (right scale, lit) and the clip actually plays.
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)
);
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);
});
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);
}
}
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/[email protected]/examples/jsm/libs/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));
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);
});
});
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/refsoutside and assign them in the callback, or use await loader.loadAsync(url).
mixer.update(delta) each frame, or youpassed milliseconds instead of seconds (use clock.getDelta()), or you forgot
action.play().
mismatched. setDecoderPath/setTranscoderPath must point at files matching your
three.js version.
AnimationMixer per animated root andcreate all actions from it; don't make a new mixer per clip.
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.
Object3D to give it a cleanpivot rather than fighting baked offsets.
loadAsync+ a LoadingManager progress bar, reusing models with SkeletonUtils.clone, and
exporter guidance (apply transforms, one clean root), read
references/loaders-and-animation.md.
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.Take gamedev-skills/threejs-gltf-loading from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.