Three.js — application & game scale

SkillDocs & knowledge

Application/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.

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:

ConcernOwner
Creative/generative three.js — scene scaffolding, GLSL shaders, particles, post-processinggenart-ops
three.js inside a Mapbox GL custom layer (CustomLayerInterface, threebox)mapbox-ops
App/game-scale three.js — modules, assets, animation, loops, physics, R3F, scalethis 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 patternRemovedUse instead
examples/js/* non-module loaders (js/loaders/GLTFLoader.js, THREE.OrbitControls globals)r148three/addons/ module imports
build/three.js + build/three.min.js UMD builds (<script src=…> + global THREE)r160build/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

SituationChoice
Real app/game, npm deps, physics WASM, R3FVite (npm create vite@latest) — default answer
Demo, CodePen, teaching, drop-in page on an existing siteImport 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:

PackageWhen
@dimforge/rapier3dBundler with WASM support (Vite: works out of the box)
@dimforge/rapier3d-compatNo 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 via setMatrixAt(i, m), then instanceMatrix.needsUpdate = true.
  • Watch renderer.info.render.calls per 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(), and renderer.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

ResourceLoad when
references/asset-pipeline.mdLoading/optimizing GLTF, decoder wiring, sourcing CC0 models
references/animation.mdCrossfades, blend weights, one-shots, morph targets
references/game-loop.mdFixed timestep, interpolation, pause/visibility, input sampling, damping/follow-cam, determinism
references/physics.mdrapier vs cannon-es, init, sync, character controllers
references/r3f-drei.mdReact Three Fiber apps, drei helpers, R3F pitfalls
references/scale-and-disposal.mdInstancing, LOD, culling, draw-call budgets, disposal/leaks
references/actors-and-steering.mdBoids, steering, waypoint actors, ambient life
assets/importmap-starter.htmlStarting a no-bundler project — copy and edit
assets/three-facts.jsonCanonical 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