Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/gamedev-skills-threejs-gltf-loading/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 34% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 33% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 58% | 0% |
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.
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.
jsimport { 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) );
jsimport * 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); });
jsconst 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); } }
jsimport { 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));
jsnew 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.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | pass→pass | 18,966 | 17,291 | -9% | 1 | 1 | 0% | 2,899 | 4,579 | +58% | 0 | 0 | — |
case-01 | fail→pass | 18,150 | 13,338 | -27% | 1 | 1 | 0% | 2,680 | 3,590 | +34% | 0 | 0 | — |
case-02 | fail→pass | 14,072 | 12,622 | -10% | 1 | 1 | 0% | 2,618 | 3,476 | +33% | 0 | 0 | — |
case-04 | pass→pass | 18,136 | 20,375 | +12% | 1 | 1 | 0% | 3,320 | 5,253 | +58% | 0 | 0 | — |
case-05 | fail→pass | 20,436 | 13,663 | -33% | 1 | 1 | 0% | 2,383 | 4,281 | +80% | 0 | 0 | — |
case-06 | pass→pass | 9,032 | 10,354 | +15% | 1 | 1 | 0% | 1,829 | 2,900 | +59% | 0 | 0 | — |
case-07 | fail→fail | 10,609 | 13,795 | +30% | 1 | 1 | 0% | 1,962 | 3,761 | +92% | 0 | 0 | — |
case-08 | pass→pass | 14,372 | 10,124 | -30% | 1 | 1 | 0% | 2,422 | 3,811 | +57% | 0 | 0 | — |
case-09 | pass→pass | 5,795 | 4,546 | -22% | 1 | 1 | 0% | 1,074 | 2,805 | +161% | 0 | 0 | — |
case-10 | pass→pass | 6,771 | 9,038 | +33% | 1 | 1 | 0% | 1,292 | 2,672 | +107% | 0 | 0 | — |
case-11 | pass→pass | 14,603 | 5,575 | -62% | 1 | 1 | 0% | 1,801 | 3,030 | +68% | 0 | 0 | — |
case-12 | pass→pass | 15,551 | 10,566 | -32% | 1 | 1 | 0% | 1,814 | 3,943 | +117% | 0 | 0 | — |
case-13 | pass→pass | 9,125 | 5,899 | -35% | 1 | 1 | 0% | 1,457 | 2,949 | +102% | 0 | 0 | — |
case-14 | pass→pass | 5,267 | 4,479 | -15% | 1 | 1 | 0% | 901 | 2,600 | +189% | 0 | 0 | — |
case-15 | pass→pass | 16,637 | 23,474 | +41% | 1 | 1 | 0% | 3,040 | 6,126 | +102% | 0 | 0 | — |
case-16 | pass→pass | 11,844 | 8,761 | -26% | 1 | 1 | 0% | 2,195 | 3,638 | +66% | 0 | 0 | — |
case-17 | pass→pass | 4,638 | 3,409 | -26% | 1 | 1 | 0% | 829 | 2,604 | +214% | 0 | 0 | — |
case-18 | pass→pass | 10,631 | 5,043 | -53% | 1 | 1 | 0% | 1,912 | 2,864 | +50% | 0 | 0 | — |
case-19 | fail→pass | 10,399 | 6,001 | -42% | 1 | 1 | 0% | 1,836 | 3,027 | +65% | 0 | 0 | — |
case-20 | pass→pass | 11,583 | 10,765 | -7% | 1 | 1 | 0% | 1,362 | 2,858 | +110% | 0 | 0 | — |
case-21 | pass→pass | 13,350 | 10,337 | -23% | 1 | 1 | 0% | 2,471 | 3,991 | +62% | 0 | 0 | — |
case-22 | pass→pass | 5,571 | 4,370 | -22% | 1 | 1 | 0% | 1,023 | 2,673 | +161% | 0 | 0 | — |
case-23 | pass→pass | 9,727 | 6,339 | -35% | 1 | 1 | 0% | 1,811 | 3,093 | +71% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.