Explorar o código

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 hai 2 semanas
pai
achega
d9dfd4108e

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

@@ -5,7 +5,7 @@ license: MIT
 allowed-tools: "Read Write Bash"
 allowed-tools: "Read Write Bash"
 metadata:
 metadata:
   author: claude-mods
   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
 # 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.
 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).
 > 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+."
 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:
 metadata:
   author: claude-mods
   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)
 # Mapbox GL JS — advanced web toolkit (v3)

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

@@ -1,6 +1,6 @@
 ---
 ---
 name: threejs-ops
 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
 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+."
 compatibility: "Web three.js r150+ (ES modules only — UMD builds removed in r160). check-three-facts.py is stdlib-only Python 3.10+."
 metadata:
 metadata:
@@ -19,7 +19,7 @@ fast (and leak-free) as scene complexity grows.
 | Concern | Owner |
 | Concern | Owner |
 |---|---|
 |---|---|
 | Creative/generative three.js — scene scaffolding, GLSL shaders, particles, post-processing | [genart-ops](../genart-ops/SKILL.md) |
 | 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** |
 | 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
 With a bundler, `import { X } from 'three/addons/…'` resolves via the package's
 `exports` map — same specifier both worlds.
 `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
 ## 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
 Render on rAF; simulate on a **fixed timestep** when gameplay/physics needs
 determinism. Full accumulator pattern (with render interpolation), dt clamping,
 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
 ```javascript
 const FIXED = 1 / 60;
 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/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/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/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/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 |
 | [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": {
   "version_gates": {
     "examples_js_removed": "r148",
     "examples_js_removed": "r148",
     "umd_builds_removed": "r160",
     "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": {
   "npm_scheme": {
     "package": "three",
     "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
 **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
 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 —
 Facing: agents look along velocity —
 `mesh.quaternion.setFromRotationMatrix(m.lookAt(ZERO, velocity, UP))`, slerped
 `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`.
   and suspend `AudioContext`.
 - `THREE.Timer.connect(document)` implements exactly this for its delta.
 - `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
 Fixed timestep is necessary but not sufficient. For replays, lockstep
 networking, or reproducible tests:
 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
   construction, and the same WASM build — one of the reasons it's the default
   ([physics.md](physics.md)).
   ([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
 At 60 fps the whole frame is **16.6 ms**; browsers need ~2 ms, leaving ~14 ms
 for sim + render. Measure before optimizing:
 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",
         findings.append(Finding("facts-meta", "fail",
                                 f"schema={facts.get('schema')!r} as_of={facts.get('as_of')!r}"))
                                 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", {})
     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"))
             findings.append(Finding(f"gate:{key}", "fail", f"malformed gate {gate!r} in facts"))
         elif re.search(rf"\*\*{gate}\*\*|\b{gate}\b", skill_md):
         elif re.search(rf"\*\*{gate}\*\*|\b{gate}\b", skill_md):
             findings.append(Finding(f"gate:{key}", "ok", f"{gate} stated in SKILL.md"))
             findings.append(Finding(f"gate:{key}", "ok", f"{gate} stated in SKILL.md"))