Browse Source

feat(skills): threejs-ops review pass — WebGPU note, input/damping, cross-links

- SKILL.md: WebGPU positioning subsection (three/webgpu + three/tsl
  exports since r167, webgpu-build import-map swap, async init, WebGL2
  fallback, when to reach for it) — the r167 gate joins three-facts.json
  and the verifier now iterates ALL committed gates generically
- game-loop.md: input-sampling pattern (state object, e.code, blur
  clear, fixed-tick sampling for determinism) and frame-rate-independent
  damping (MathUtils.damp, follow-camera snippet)
- Fix ghost link: scope table pointed at
  mapbox-ops/references/three-custom-layer.md, which exists only in the
  installed ~/.claude copy, not the repo — repointed to mapbox-ops
  SKILL.md (repo sync flagged separately)
- Reciprocal cross-links: genart-ops and mapbox-ops related-skills now
  list threejs-ops; genart-ops carries a scope note deferring app/game
  three.js here

All gates re-verified: 29/29 suite, 37/37 mapbox suite, verifier
offline clean (3 gates), validate.sh 107/0, doc-drift clean,
description 932/1024 chars, body 273/500 lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 1 week ago
parent
commit
d9dfd4108e

+ 3 - 1
skills/genart-ops/SKILL.md

@@ -5,7 +5,7 @@ license: MIT
 allowed-tools: "Read Write Bash"
 metadata:
   author: claude-mods
-  related-skills: color-ops, javascript-ops, typescript-ops, mapbox-ops
+  related-skills: color-ops, javascript-ops, typescript-ops, mapbox-ops, threejs-ops
 ---
 
 # Generative Art Operations
@@ -13,6 +13,8 @@ metadata:
 Practical patterns for creative coding and generative art. Covers three.js, p5.js, SVG generation, GLSL shaders, procedural algorithms, and color theory for computational aesthetics.
 
 > Color-ops handles CSS color, accessibility, and design tokens. This skill focuses on generative/procedural color techniques (palette algorithms, shader color, gradient interpolation in perceptual space).
+>
+> Application/game-scale three.js — GLTF asset pipelines, AnimationMixer, fixed-timestep game loops, physics (rapier/cannon-es), react-three-fiber, instancing/disposal at scale — is [threejs-ops](../threejs-ops/SKILL.md). This skill owns the creative/shader side of three.js.
 
 ---
 

+ 1 - 1
skills/mapbox-ops/SKILL.md

@@ -21,7 +21,7 @@ license: MIT
 compatibility: "Web mapbox-gl-js v3.x. screenshot_map.py needs Python 3.10+ and Playwright (chromium); check-mapbox-facts.py is stdlib-only Python 3.10+."
 metadata:
   author: claude-mods
-  related-skills: "color-ops, screenshot, genart-ops"
+  related-skills: "color-ops, screenshot, genart-ops, threejs-ops"
 ---
 
 # Mapbox GL JS — advanced web toolkit (v3)

+ 18 - 4
skills/threejs-ops/SKILL.md

@@ -1,6 +1,6 @@
 ---
 name: threejs-ops
-description: "Application/game-scale three.js — ES modules + import maps (UMD builds are dead), GLTF asset pipeline (DRACO/KTX2/meshopt, gltf-transform, gltfpack, CC0 sources), AnimationMixer crossfading, game loops (fixed timestep, dt clamp), physics (@dimforge/rapier3d vs cannon-es), react-three-fiber + drei, and scale (InstancedMesh, LOD, draw-call budgets, disposal, boids/steering). Use for: three.js app, three.js game, import map, three.min.js, UMD, GLTFLoader, DRACOLoader, KTX2Loader, meshopt, gltf-transform, gltfpack, glb optimize, CC0 models, Quaternius, Kenney, Poly Pizza, AnimationMixer, crossfade, animation blending, skeletal animation, morph targets, game loop, fixed timestep, rapier, rapier3d, cannon-es, character controller, kinematic body, react-three-fiber, r3f, drei, InstancedMesh, LOD, frustum culling, draw calls, dispose geometry, WebGL memory leak, boids, steering, waypoint, NPC ambient life."
+description: "Application/game-scale three.js — ES modules + import maps (UMD builds are dead), GLTF asset pipeline (DRACO/KTX2/meshopt, gltf-transform, gltfpack, CC0 sources), AnimationMixer crossfading, game loops (fixed timestep, dt clamp), physics (@dimforge/rapier3d vs cannon-es), react-three-fiber + drei, and scale (InstancedMesh, LOD, draw-call budgets, disposal, boids/steering). Use for: three.js app, three.js game, import map, three.min.js, UMD, GLTFLoader, DRACOLoader, KTX2Loader, meshopt, gltf-transform, gltfpack, glb optimize, CC0 models, Quaternius, Kenney, Poly Pizza, AnimationMixer, crossfade, animation blending, skeletal animation, morph targets, game loop, fixed timestep, rapier, rapier3d, cannon-es, character controller, kinematic body, react-three-fiber, r3f, drei, InstancedMesh, LOD, frustum culling, draw calls, dispose geometry, WebGL memory leak, WebGPURenderer, TSL, boids, steering, waypoint, NPC ambient life."
 license: MIT
 compatibility: "Web three.js r150+ (ES modules only — UMD builds removed in r160). check-three-facts.py is stdlib-only Python 3.10+."
 metadata:
@@ -19,7 +19,7 @@ fast (and leak-free) as scene complexity grows.
 | Concern | Owner |
 |---|---|
 | Creative/generative three.js — scene scaffolding, GLSL shaders, particles, post-processing | [genart-ops](../genart-ops/SKILL.md) |
-| three.js inside a Mapbox GL custom layer | [mapbox-ops](../mapbox-ops/references/three-custom-layer.md) |
+| three.js inside a Mapbox GL custom layer (`CustomLayerInterface`, threebox) | [mapbox-ops](../mapbox-ops/SKILL.md) |
 | App/game-scale three.js — modules, assets, animation, loops, physics, R3F, scale | **this skill** |
 
 ---
@@ -73,6 +73,18 @@ script.
 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](../genart-ops/SKILL.md) territory.
+
 ---
 
 ## 2. GLTF asset pipeline
@@ -128,7 +140,9 @@ idle.crossFadeTo(run, 0.3, /*warp*/ true);
 
 Render on rAF; simulate on a **fixed timestep** when gameplay/physics needs
 determinism. Full accumulator pattern (with render interpolation), dt clamping,
-and tab-hidden handling in [references/game-loop.md](references/game-loop.md).
+tab-hidden handling, input sampling, and frame-rate-independent damping
+(`MathUtils.damp`, follow cameras) in
+[references/game-loop.md](references/game-loop.md).
 
 ```javascript
 const FIXED = 1 / 60;
@@ -240,7 +254,7 @@ self-explanatory reference implementations.
 |---|---|
 | [references/asset-pipeline.md](references/asset-pipeline.md) | Loading/optimizing GLTF, decoder wiring, sourcing CC0 models |
 | [references/animation.md](references/animation.md) | Crossfades, blend weights, one-shots, morph targets |
-| [references/game-loop.md](references/game-loop.md) | Fixed timestep, interpolation, pause/visibility, determinism |
+| [references/game-loop.md](references/game-loop.md) | Fixed timestep, interpolation, pause/visibility, input sampling, damping/follow-cam, determinism |
 | [references/physics.md](references/physics.md) | rapier vs cannon-es, init, sync, character controllers |
 | [references/r3f-drei.md](references/r3f-drei.md) | React Three Fiber apps, drei helpers, R3F pitfalls |
 | [references/scale-and-disposal.md](references/scale-and-disposal.md) | Instancing, LOD, culling, draw-call budgets, disposal/leaks |

+ 2 - 1
skills/threejs-ops/assets/three-facts.json

@@ -5,7 +5,8 @@
   "version_gates": {
     "examples_js_removed": "r148",
     "umd_builds_removed": "r160",
-    "_comment": "r148 (Dec 2022) dropped examples/js non-module loaders; r160 (Dec 2023) dropped build/three.js + build/three.min.js UMD builds. The skill assumes r150+ and ES modules only."
+    "webgpu_build_added": "r167",
+    "_comment": "r148 (Dec 2022) dropped examples/js non-module loaders; r160 (Dec 2023) dropped build/three.js + build/three.min.js UMD builds; r167 (Jul 2024) added the three.webgpu.js build + three/webgpu + three/tsl exports. The skill assumes r150+ and ES modules only."
   },
   "npm_scheme": {
     "package": "three",

+ 1 - 1
skills/threejs-ops/references/actors-and-steering.md

@@ -51,7 +51,7 @@ class Agent {
 
 **Wander** (idle meandering): project a point ahead of the agent, jitter a
 target around a small circle there, `seek` it. Deterministic if the jitter uses
-the seeded RNG ([game-loop.md](game-loop.md) §5).
+the seeded RNG ([game-loop.md](game-loop.md) §7).
 
 Facing: agents look along velocity —
 `mesh.quaternion.setFromRotationMatrix(m.lookAt(ZERO, velocity, UP))`, slerped

+ 49 - 2
skills/threejs-ops/references/game-loop.md

@@ -99,7 +99,54 @@ document.addEventListener('visibilitychange', () => {
   and suspend `AudioContext`.
 - `THREE.Timer.connect(document)` implements exactly this for its delta.
 
-## 5. Determinism
+## 5. Input sampling
+
+DOM input events fire asynchronously, between frames — never mutate game state
+in the handler. Handlers write to a state object; the loop reads it:
+
+```javascript
+const input = { keys: new Set(), pointer: new THREE.Vector2(), pointerDown: false };
+addEventListener('keydown', (e) => { if (!e.repeat) input.keys.add(e.code); });
+addEventListener('keyup',   (e) => input.keys.delete(e.code));
+addEventListener('blur',    () => input.keys.clear());   // alt-tab = stuck keys otherwise
+
+// inside the fixed tick:
+const forward = input.keys.has('KeyW') ? 1 : 0;
+```
+
+- Use `e.code` (physical key — `KeyW` works on AZERTY), not `e.key`.
+- The `blur` clear matters: without it, releasing a key while the window is
+  unfocused leaves it "held" forever.
+- One-shot presses (jump) need edge detection: track `justPressed` by diffing
+  against the previous tick's set, or consume the key on read.
+- Mouse-look wants the Pointer Lock API — drei's `<PointerLockControls>` or
+  three's addon of the same name wrap it.
+- Sampling input in the **fixed tick** (not per rAF) keeps replays/determinism
+  intact (§7): the same tick always sees the same input snapshot.
+
+## 6. Smoothing — frame-rate-independent damping
+
+`value += (target - value) * 0.1` per frame is the classic bug: the smoothing
+speed changes with the frame rate (2.4× stiffer at 144 Hz). The correct,
+dt-aware form is exponential decay — built in as `THREE.MathUtils.damp`:
+
+```javascript
+// λ ≈ 1/seconds-to-close-63%-of-the-gap; bigger = snappier
+camera.position.x = THREE.MathUtils.damp(camera.position.x, target.x, 4, dt);
+```
+
+The third-person follow camera in four lines:
+
+```javascript
+const behind = player.localToWorld(new THREE.Vector3(0, 2.5, -5)); // offset in player space
+camera.position.lerp(behind, 1 - Math.exp(-4 * dt));               // same math as damp
+camera.lookAt(player.position);
+```
+
+Use damping for cameras, UI-ish motion, and audio params; never for physics
+bodies (the solver owns those — see [physics.md](physics.md)).
+
+## 7. Determinism
 
 Fixed timestep is necessary but not sufficient. For replays, lockstep
 networking, or reproducible tests:
@@ -117,7 +164,7 @@ networking, or reproducible tests:
   construction, and the same WASM build — one of the reasons it's the default
   ([physics.md](physics.md)).
 
-## 6. Frame budget quick reference
+## 8. Frame budget quick reference
 
 At 60 fps the whole frame is **16.6 ms**; browsers need ~2 ms, leaving ~14 ms
 for sim + render. Measure before optimizing:

+ 6 - 4
skills/threejs-ops/scripts/check-three-facts.py

@@ -112,11 +112,13 @@ def run_offline(findings: list[Finding]) -> None:
         findings.append(Finding("facts-meta", "fail",
                                 f"schema={facts.get('schema')!r} as_of={facts.get('as_of')!r}"))
 
-    # O2 — version gates stated in SKILL.md exactly as the facts commit to them.
+    # O2 — every version gate the facts commit to is stated in SKILL.md.
     gates = facts.get("version_gates", {})
-    for key, gate in (("examples_js_removed", gates.get("examples_js_removed")),
-                      ("umd_builds_removed", gates.get("umd_builds_removed"))):
-        if not gate or not re.match(r"r\d{3}$", gate):
+    gate_items = [(k, v) for k, v in gates.items() if k != "_comment"]
+    if len(gate_items) < 2:
+        findings.append(Finding("gates", "fail", f"expected >= 2 version gates, got {len(gate_items)}"))
+    for key, gate in gate_items:
+        if not gate or not re.match(r"r\d{3}$", str(gate)):
             findings.append(Finding(f"gate:{key}", "fail", f"malformed gate {gate!r} in facts"))
         elif re.search(rf"\*\*{gate}\*\*|\b{gate}\b", skill_md):
             findings.append(Finding(f"gate:{key}", "ok", f"{gate} stated in SKILL.md"))