From workflows
Load glTF/GLB models in three.js with GLTFLoader, play skinned animations with AnimationMixer, and handle DRACO/Meshopt-compressed meshes and KTX2 textures.
How this skill is triggered — by the user, by Claude, or both
Slash command
/workflows:threejs-gltf-loadingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Load `.gltf`/`.glb` models and play their animations in three.js, including
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.
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.
GLTFLoader. loader.load(url, onLoad, onProgress, onError). The
result 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 with
traverse / 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 confirm
the 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);
});
});
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).mixer.update(delta) each frame, or you
passed milliseconds instead of seconds (use clock.getDelta()), or you forgot
action.play().setDecoderPath/setTranscoderPath must point at files matching your
three.js version.AnimationMixer per animated root and
create all actions from it; don't make a new mixer per clip.Object3D to give it a clean
pivot rather than fighting baked offsets.loadAsync
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.npx claudepluginhub gamedev-skills/awesome-gamedev-agent-skills --plugin gamedevLoads GLTF models, textures, images, HDR environments in Three.js. Coordinates async asset loading and progress tracking with LoadingManager.
Provides Three.js API references, best practices, and code examples for scene setup, geometry, materials, lighting, textures, animation, loaders, shaders, postprocessing, and interaction in 3D web apps.
Builds interactive 3D web scenes with Three.js using WebGL/WebGPU. Guides on scenes, cameras, renderers, geometries, materials, meshes, lights, animations, and OrbitControls.