Three.js — application & game scale
SkillDocs & knowledgeApplication/game-scale three.js: ES modules, GLTF pipeline (DRACO/KTX2/meshopt), AnimationMixer, physics (rapier/cannon-es), react-three-fiber, and performance at scale (InstancedMesh, LOD, draw calls). Triggers on: three.js, GLTFLoader, r3f, game loop, WebGL memory leak, boids.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Three.js — application & game scale skill
What this skill tells your AI
The instructions your AI receives, as published by 0xdarkmatter/claude-mods in skills/threejs-ops/SKILL.md and read by ahel’s review.
Patterns for building three.js applications and games: module setup, asset pipelines, animation, simulation loops, physics, the React ecosystem, and staying fast (and leak-free) as scene complexity grows.
Scope split with sibling skills — do not duplicate them:
| Concern | Owner |
|---|---|
| Creative/generative three.js — scene scaffolding, GLSL shaders, particles, post-processing | genart-ops |
three.js inside a Mapbox GL custom layer (CustomLayerInterface, threebox) | mapbox-ops |
| App/game-scale three.js — modules, assets, animation, loops, physics, R3F, scale | this skill |
1. ES-module reality (read this before writing any <script> tag)
three.js is ES-modules only. The legacy patterns are dead and will 404 or silently break on any modern release:
| Dead pattern | Removed | Use instead |
|---|---|---|
examples/js/* non-module loaders (js/loaders/GLTFLoader.js, THREE.OrbitControls globals) | r148 | three/addons/ module imports |
build/three.js + build/three.min.js UMD builds (<script src=…> + global THREE) | r160 | build/three.module.js via import map or bundler |
Versioning: npm publishes 0.<release>.<patch> — three@0.185.1 is r185.
Releases land monthly; pin exact versions.
No-bundler setup (import map)
Copy assets/importmap-starter.html — a complete,
runnable starter (import map + addons + resize + setAnimationLoop). The core:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
</script>
Gotchas: both entries MUST pin the same version (mixed versions =
instanceof failures across module copies); the three/addons/ key needs the
trailing slash; import maps must appear before the first type="module"
script.
Bundler vs no-bundler
| Situation | Choice |
|---|---|
| Real app/game, npm deps, physics WASM, R3F | Vite (npm create vite@latest) — default answer |
| Demo, CodePen, teaching, drop-in page on an existing site | Import map — zero build |
@dimforge/rapier3d (WASM-bindgen) | Needs a bundler; use rapier3d-compat without one (§5) |
With a bundler, import { X } from 'three/addons/…' resolves via the package's
exports map — same specifier both worlds.
WebGPU (know it exists; default to WebGL)
Since r167 three ships a parallel build: import { WebGPURenderer } from 'three/webgpu' plus the TSL node-shader language from three/tsl (import-map
users: point the "three" key at build/three.webgpu.js instead of
three.module.js — it re-exports core; never load both). WebGPURenderer
falls back to WebGL2 automatically and initializes async (await renderer.init(), or let setAnimationLoop defer for you). Default for
app/game work remains WebGLRenderer — reach for WebGPU when you need compute
(GPU crowds, particle sims) or TSL materials. Shader-level TSL/GLSL work is
genart-ops territory.
2. GLTF asset pipeline
glTF (.glb) is the format. Wire all three decoders once at startup so any
optimized asset loads; full setup, optimization CLI recipes (gltf-transform,
gltfpack), and CC0 model sources (Quaternius, Kenney, Poly Pizza) in
references/asset-pipeline.md.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const draco = new DRACOLoader().setDecoderPath(
'https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
const ktx2 = new KTX2Loader()
.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/libs/basis/')
.detectSupport(renderer); // MUST pass the live renderer
const loader = new GLTFLoader()
.setDRACOLoader(draco).setKTX2Loader(ktx2).setMeshoptDecoder(MeshoptDecoder);
const { scene: model, animations } = await loader.loadAsync('hero.glb');
Cloning a skinned character for multiple instances needs
SkeletonUtils.clone() — a plain .clone() shares (and corrupts) the skeleton.
3. Animation system
AnimationMixer drives everything (skeletal + morph targets). One mixer per
model root; mixer.update(dt) every frame. Crossfades, blending weights,
one-shot clips, additive layers, and morph-target patterns in
references/animation.md.
const mixer = new THREE.AnimationMixer(model);
const idle = mixer.clipAction(THREE.AnimationClip.findByName(animations, 'Idle'));
const run = mixer.clipAction(THREE.AnimationClip.findByName(animations, 'Run'));
idle.play();
// smooth transition — never .stop() + .play()
run.reset().play();
idle.crossFadeTo(run, 0.3, /*warp*/ true);
4. Game loops
Render on rAF; simulate on a fixed timestep when gameplay/physics needs
determinism. Full accumulator pattern (with render interpolation), dt clamping,
tab-hidden handling, input sampling, and frame-rate-independent damping
(MathUtils.damp, follow cameras) in
references/game-loop.md.
const FIXED = 1 / 60;
let acc = 0, last = performance.now();
renderer.setAnimationLoop((now) => {
acc += Math.min((now - last) / 1000, 0.1); // clamp: tab-switch dt spike
last = now;
while (acc >= FIXED) { simulate(FIXED); acc -= FIXED; }
render(acc / FIXED); // interpolation alpha
});
Rules: use renderer.setAnimationLoop (not raw rAF — required for WebXR, and
it pauses cleanly); clamp dt or the first frame after a background tab
teleports everything; pause simulation on document.hidden
(visibilitychange); determinism = fixed step + seeded RNG + no per-frame
Math.random() in sim code.
5. Physics
Default: @dimforge/rapier3d (Rust/WASM — fast, deterministic, actively
developed, built-in kinematic character controller). cannon-es remains the
pure-JS fallback for zero-WASM constraints. Decision table, init patterns,
body/mesh sync, and character controllers in
references/physics.md.
The package-name trap:
| Package | When |
|---|---|
@dimforge/rapier3d | Bundler with WASM support (Vite: works out of the box) |
@dimforge/rapier3d-compat | No bundler / import map — WASM inlined as base64; await RAPIER.init() first |
import RAPIER from '@dimforge/rapier3d-compat';
await RAPIER.init();
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
// step inside the FIXED loop (§4), then copy body → mesh:
world.timestep = FIXED; world.step();
mesh.position.copy(body.translation());
mesh.quaternion.copy(body.rotation());
6. react-three-fiber + drei
R3F v9 (React 19) renders the three.js scene graph declaratively;
drei is its helper library. When: React
app, UI-heavy, want the pmndrs ecosystem (@react-three/rapier,
postprocessing). When not: no React on the page, engine-style tight control,
minimal bundle. Hooks, pitfalls (per-frame setState kills you — mutate refs in
useFrame), and the drei shortlist in
references/r3f-drei.md.
7. Scale patterns — staying fast, staying leak-free
Draw calls are the budget; GPU memory leaks are the debt. renderer.info is the
meter for both. InstancedMesh at count, LOD, culling, and the full disposal
discipline in references/scale-and-disposal.md.
Quick rules:
- Same mesh × N ≥ ~50 →
InstancedMesh(one draw call). Set matrices viasetMatrixAt(i, m), theninstanceMatrix.needsUpdate = true. - Watch
renderer.info.render.callsper frame — hundreds is fine, thousands is the problem.renderer.info.memory.{geometries,textures}must return to baseline after teardown. - Removing from the scene frees nothing. GPU memory needs explicit
geometry.dispose(),material.dispose(), every texture.dispose(), andrenderer.dispose()on full teardown (SPA route change = the classic leak).
function disposeObject(root) {
root.traverse((o) => {
o.geometry?.dispose();
for (const m of Array.isArray(o.material) ? o.material : [o.material]) {
if (!m) continue;
for (const v of Object.values(m)) v?.isTexture && v.dispose();
m.dispose();
}
});
root.removeFromParent();
}
8. Ambient life — boids, steering, waypoints
Crowd/wildlife/NPC motion is steering forces + a spatial hash, driven from the fixed loop and rendered via InstancedMesh. Seek/arrive/wander, the three boids rules, waypoint drivers, and library options in references/actors-and-steering.md. For structured game-state building blocks (actor motion controllers, waypoint drivers, camera rigs) see xt4d/GameBlocks — agent-oriented, self-explanatory reference implementations.
Bundled resources
| Resource | Load when |
|---|---|
| references/asset-pipeline.md | Loading/optimizing GLTF, decoder wiring, sourcing CC0 models |
| references/animation.md | Crossfades, blend weights, one-shots, morph targets |
| references/game-loop.md | Fixed timestep, interpolation, pause/visibility, input sampling, damping/follow-cam, determinism |
| references/physics.md | rapier vs cannon-es, init, sync, character controllers |
| references/r3f-drei.md | React Three Fiber apps, drei helpers, R3F pitfalls |
| references/scale-and-disposal.md | Instancing, LOD, culling, draw-call budgets, disposal/leaks |
| references/actors-and-steering.md | Boids, steering, waypoint actors, ambient life |
| assets/importmap-starter.html | Starting a no-bundler project — copy and edit |
| assets/three-facts.json | Canonical version gates + package facts (verifier input) |
Staleness verifier — this skill encodes fast-moving facts (three version gates, npm package names/versions). Check internal consistency (CI) or live drift against the npm registry:
python3 skills/threejs-ops/scripts/check-three-facts.py --offline # structural, no network — PR CI
python3 skills/threejs-ops/scripts/check-three-facts.py --live # npm registry probe — exit 10 drift, 7 unreachable
python3 skills/threejs-ops/scripts/check-three-facts.py --offline --json | jq '.data[] | select(.status!="ok")'
Signals
- GitHub stars
- 36
- Forks
- 5
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
threejs-ops- Source
- github.com/0xdarkmatter/claude-mods