@genex-ai/cli-demo 0.11.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +203 -4
  3. package/package.json +7 -2
  4. package/templates/controllers/NOTICE.md +65 -0
  5. package/templates/controllers/assets/animation-library.glb +0 -0
  6. package/templates/controllers/assets/character.glb +0 -0
  7. package/templates/controllers/assets/default-avatar.vrm +0 -0
  8. package/templates/controllers/character/character-animations.ts +682 -0
  9. package/templates/controllers/character/character-controller.ts +1636 -0
  10. package/templates/controllers/character/follow-camera.ts +644 -0
  11. package/templates/controllers/character/keyboard-input.ts +277 -0
  12. package/templates/controllers/character/presets.ts +176 -0
  13. package/templates/controllers/character/touch-joystick.ts +387 -0
  14. package/templates/controllers/character/vrm/capsule-fit.ts +52 -0
  15. package/templates/controllers/character/vrm/foot-ik.ts +341 -0
  16. package/templates/controllers/character/vrm/vrm-loader.ts +44 -0
  17. package/templates/controllers/character/vrm/vrm-retarget.ts +195 -0
  18. package/templates/controllers/drone/drone-controller.ts +1073 -0
  19. package/templates/controllers/drone/presets.ts +225 -0
  20. package/templates/controllers/interact/enter-exit.ts +502 -0
  21. package/templates/controllers/shared/colliders.ts +456 -0
  22. package/templates/controllers/shared/math.ts +230 -0
  23. package/templates/controllers/shared/physics-world.ts +622 -0
  24. package/templates/controllers/vehicle/presets.ts +297 -0
  25. package/templates/controllers/vehicle/vehicle-controller.ts +615 -0
  26. package/templates/controllers/vehicle/wheel.ts +1200 -0
  27. package/templates/skills/genex-getting-started/SKILL.md +5 -0
  28. package/templates/skills/genex-threejs-character-controller/SKILL.md +205 -0
  29. package/templates/skills/genex-threejs-character-controller/references/animations.md +235 -0
  30. package/templates/skills/genex-threejs-character-controller/references/tuning-and-presets.md +102 -0
  31. package/templates/skills/genex-threejs-character-controller/references/wiring.md +198 -0
  32. package/templates/skills/genex-threejs-embed-auth/SKILL.md +126 -54
  33. package/templates/skills/genex-threejs-multiplayer/SKILL.md +17 -11
  34. package/templates/skills/genex-threejs-physics-rapier/SKILL.md +128 -0
  35. package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +202 -0
  36. package/templates/skills/genex-threejs-physics-rapier/references/physics-setup.md +207 -0
  37. package/templates/skills/genex-threejs-skill-router/SKILL.md +3 -0
  38. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +15 -7
  39. package/templates/skills/genex-threejs-vehicle-controllers/SKILL.md +110 -0
  40. package/templates/skills/genex-threejs-vehicle-controllers/references/car.md +162 -0
  41. package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +150 -0
  42. package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +199 -0
@@ -0,0 +1,202 @@
1
+ # Colliders from assets
2
+
3
+ Everything here documents `src/controllers/shared/colliders.ts` as installed
4
+ by `npx genex controller`. All helpers assume `PhysicsWorld.create()` has
5
+ already resolved — constructing a collider before WASM init throws.
6
+
7
+ ## Contents
8
+
9
+ - Decision table: which collider for which asset
10
+ - Explicit primitive helpers
11
+ - Auto-colliders from a GLB scene
12
+ - Hull vs trimesh
13
+ - Scale pitfalls
14
+ - Collider options
15
+ - Sensors
16
+ - End-to-end: a genex model prop
17
+
18
+ ## Decision table: which collider for which asset
19
+
20
+ | Asset | Collider | Why |
21
+ | --- | --- | --- |
22
+ | ground, walls, crates, platforms (box-shaped) | `cuboidCollider` | cheapest, exact for boxes |
23
+ | dynamic prop from `npx genex model` (barrel, rock, chair) | `convexHullColliderFromMesh` (or `collidersFromObject(..., "hull")` for multi-mesh GLBs) | tight fit, still convex/fast/solid |
24
+ | static level GLB (terrain piece, building interior, track) | `trimeshColliderFromMesh` per mesh | exact triangles; safe because it never moves |
25
+ | ball-shaped things | `ballCollider` | exact sphere |
26
+ | tall dynamic props, posts | `capsuleCollider` / `cylinderCollider` | stable standing shapes |
27
+ | pickups, triggers, zones | any shape + `{ sensor: true, mass: 0 }` | overlap events, no contact forces |
28
+ | the player, cars, drones | none of the above — `npx genex controller` | controllers own their collider recipes |
29
+
30
+ ## Explicit primitive helpers
31
+
32
+ ```ts
33
+ import {
34
+ cuboidCollider,
35
+ ballCollider,
36
+ capsuleCollider,
37
+ cylinderCollider,
38
+ } from "./controllers/shared/colliders.ts";
39
+
40
+ cuboidCollider(world, body, [1, 0.4, 2.4], { friction: 1 }); // HALF extents: 2 x 0.8 x 4.8 box
41
+ ballCollider(world, body, 0.5);
42
+ capsuleCollider(world, body, 0.3, 0.3); // (halfHeight, radius)
43
+ cylinderCollider(world, body, 0.15, 3); // (halfHeight, radius)
44
+ ```
45
+
46
+ Two classic mistakes, called out because they are silent:
47
+
48
+ - **Cuboid takes HALF extents.** A `[1, 0.4, 2.4]` collider is a 2 × 0.8 × 4.8
49
+ box. Do not halve twice.
50
+ - **Capsule/cylinder arg order is `(halfHeight, radius)`** — the REVERSE of
51
+ `THREE.CapsuleGeometry(radius, length)` — and the capsule's `halfHeight`
52
+ covers the cylindrical section only: total height is
53
+ `2 * halfHeight + 2 * radius`, so `(0.3, 0.3)` is 1.2 units tall.
54
+
55
+ All helpers return the created `RAPIER.Collider` and accept a `ColliderOptions`
56
+ last argument (below), including `position`/`rotation` relative to the body.
57
+
58
+ ## Auto-colliders from a GLB scene
59
+
60
+ `collidersFromObject` walks every visible mesh under an object and creates one
61
+ collider per mesh — the batteries-included path for GLBs:
62
+
63
+ ```ts
64
+ import { collidersFromObject } from "./controllers/shared/colliders.ts";
65
+
66
+ const gltf = await loader.loadAsync("./assets/models/weathered-wooden-barrel.glb");
67
+ const model = gltf.scene;
68
+ scene.add(model);
69
+
70
+ const body = physics.createBody({ type: "dynamic", position: [0, 2, 0] }, model);
71
+ collidersFromObject(world, body, model, "hull", { friction: 0.7, density: 400 });
72
+ ```
73
+
74
+ - Shapes: `"cuboid"` (bounding box per mesh — cheapest), `"ball"` (bounding
75
+ sphere), `"hull"` (convex hull per mesh — the default choice for `genex
76
+ model` props), `"trimesh"` (exact triangles — static geometry only).
77
+ - `object3d` must be the SAME object registered to the body (their frames must
78
+ coincide); call it right after `createBody`, before the first step.
79
+ - Hidden meshes are skipped unless you pass `includeInvisible: true` in the
80
+ options.
81
+ - It returns the array of created colliders, so you can attach events or
82
+ retune later.
83
+
84
+ For a single mesh you can target directly, the explicit forms skip the
85
+ traversal:
86
+
87
+ ```ts
88
+ import {
89
+ convexHullColliderFromMesh,
90
+ trimeshColliderFromMesh,
91
+ } from "./controllers/shared/colliders.ts";
92
+
93
+ convexHullColliderFromMesh(world, dynamicBody, propMesh, { density: 400 });
94
+ trimeshColliderFromMesh(world, fixedBody, levelMesh, { friction: 1 });
95
+ ```
96
+
97
+ Both bake the mesh's current world transform relative to the body, so the
98
+ collider lands exactly where the mesh renders.
99
+
100
+ ## Hull vs trimesh
101
+
102
+ | | Convex hull | Trimesh |
103
+ | --- | --- | --- |
104
+ | fit | shrink-wraps the mesh; concavities filled in (a mug loses its opening) | exact triangles, concavities preserved |
105
+ | solidity | solid volume | **hollow shell** — a body starting inside it is stuck or falls through |
106
+ | cost | cheap contacts | expensive contacts; scales with triangle count |
107
+ | dynamic bodies | yes — the default for props | **never on fast dynamic bodies** — thin hollow triangles are the classic tunneling recipe |
108
+ | static level geometry | fine but approximate | the right tool |
109
+
110
+ When one hull is too crude for a concave prop, prefer either of these before
111
+ reaching for a dynamic trimesh:
112
+
113
+ - **Per-mesh hulls:** most multi-part GLBs already split into several meshes,
114
+ and `collidersFromObject(world, body, model, "hull")` gives one hull per
115
+ part — a compound of convex pieces approximates concavity well.
116
+ - **A few hand-placed primitives:** two or three `cuboidCollider`s with
117
+ `position` offsets often beat any auto shape (this is how the bundled
118
+ vehicle chassis recipes work).
119
+
120
+ `"hull"` can also fail outright on degenerate/coplanar geometry — the helper
121
+ throws with the mesh name; fall back to `"cuboid"` for such meshes.
122
+
123
+ ## Scale pitfalls
124
+
125
+ Colliders copy geometry **at creation time**; they do not track later
126
+ transforms.
127
+
128
+ - **Scale the model BEFORE creating colliders.** Normalize the GLB (e.g.
129
+ `model.scale.setScalar(targetHeight / bboxHeight)`) first, then create the
130
+ body and colliders. Changing `scale` afterwards resizes the render mesh
131
+ only — the collider keeps the old size.
132
+ - **Put scale on the mesh, not on the registered root.** The auto-collider
133
+ path faithfully replicates an upstream quirk: the registered root's world
134
+ scale is double-counted in collider sizing, so a root scaled 2× produces
135
+ colliders 2× larger than the rendered meshes. Scaling child meshes (or the
136
+ GLB scene before registering a plain unscaled root) avoids the quirk
137
+ entirely.
138
+ - **`"ball"` uses `scale.x` only** (same upstream quirk, replicated not
139
+ fixed) — a non-uniformly scaled sphere gets the wrong radius. Use `"hull"`
140
+ for squashed spheres.
141
+ - **Verify visually.** `physics.enableDebug(scene)` draws every collider as a
142
+ wireframe — one glance catches every scale/offset mistake in this section.
143
+
144
+ ## Collider options
145
+
146
+ `ColliderOptions`, accepted by every helper (and applied via
147
+ `applyColliderOptions(collider, options)` if you retune at runtime):
148
+
149
+ | Option | Notes |
150
+ | --- | --- |
151
+ | `friction` | negative values are legal and load-bearing — the bundled character capsule ships with `-0.5` because its traction is synthetic; do not clamp |
152
+ | `restitution` | bounciness 0–1 |
153
+ | `density` / `mass` | **mutually exclusive — picking both throws.** `density` is kg/m³ (water ≈ 1000, wood ≈ 400–700); `mass` is absolute kg |
154
+ | `sensor` | overlap detection, no contact forces |
155
+ | `position` / `rotation` | collider pose relative to its body (euler XYZ radians) |
156
+ | `collisionGroups` / `solverGroups` | raw Rapier bitmasks |
157
+ | `contactSkin` | extra contact thickness — trades a visual gap for less jitter |
158
+ | `frictionCombineRule` / `restitutionCombineRule`, `activeCollisionTypes` | raw Rapier passthroughs |
159
+
160
+ Density matters more than it looks: impulse-driven controllers push bodies by
161
+ force, so a crate with the default density reacts very differently from one at
162
+ `density: 100`. Give props deliberate densities.
163
+
164
+ ## Sensors
165
+
166
+ ```ts
167
+ const zone = cuboidCollider(world, zoneBody, [2, 1, 2], { sensor: true, mass: 0 });
168
+ physics.setColliderEvents(zone, {
169
+ onIntersectionEnter: ({ other }) => { if (other.object3d === playerRoot) enterZone(); },
170
+ onIntersectionExit: ({ other }) => { if (other.object3d === playerRoot) leaveZone(); },
171
+ });
172
+ ```
173
+
174
+ Always give sensors `mass: 0` when they hang off a dynamic body (e.g. a
175
+ vehicle's boarding sensor) so they add no mass. Remember exit events fire both
176
+ handler families — keep exit logic idempotent (see the physics-setup
177
+ reference).
178
+
179
+ ## End-to-end: a genex model prop
180
+
181
+ ```ts
182
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
183
+ import { collidersFromObject } from "./controllers/shared/colliders.ts";
184
+
185
+ const loader = new GLTFLoader();
186
+ const gltf = await loader.loadAsync("./assets/models/weathered-wooden-barrel.glb");
187
+ const barrel = gltf.scene;
188
+
189
+ // 1. Normalize scale FIRST (target: 1.1 units tall).
190
+ const bbox = new THREE.Box3().setFromObject(barrel);
191
+ barrel.scale.setScalar(1.1 / (bbox.max.y - bbox.min.y));
192
+
193
+ // 2. Wrap in an unscaled root, add to the scene, register, then colliders.
194
+ const root = new THREE.Object3D();
195
+ root.add(barrel);
196
+ scene.add(root);
197
+ const body = physics.createBody({ type: "dynamic", position: [3, 2, 0] }, root);
198
+ collidersFromObject(world, body, root, "hull", { friction: 0.7, density: 500 });
199
+ ```
200
+
201
+ Generating the GLB itself (`npx genex model "..."`) is covered by
202
+ `$genex-ai-model`.
@@ -0,0 +1,207 @@
1
+ # Physics world setup
2
+
3
+ Everything here documents `src/controllers/shared/physics-world.ts` as
4
+ installed by `npx genex controller`. The class names, methods, and defaults
5
+ below are the real exports — use them as written.
6
+
7
+ ## Contents
8
+
9
+ - Creating the world
10
+ - Rigid bodies and the mesh registry
11
+ - Kinematic platforms
12
+ - Collision and sensor events
13
+ - Sleeping bodies
14
+ - Tunneling and CCD
15
+ - Pause, slow motion, and per-step gating
16
+ - Debug rendering and teardown
17
+ - Ground-query userData flags
18
+ - The benign boot warning
19
+
20
+ ## Creating the world
21
+
22
+ ```ts
23
+ import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
24
+
25
+ const physics = await PhysicsWorld.create(); // Earth defaults
26
+ const moon = await PhysicsWorld.create({ gravity: [0, -1.62, 0] });
27
+ ```
28
+
29
+ `PhysicsWorld.create()` awaits `RAPIER.init()` — the WASM is embedded in
30
+ `@dimforge/rapier3d-compat`, so no bundler configuration is needed, and
31
+ multiple `create()` calls share a single init. **Nothing may construct any
32
+ `RAPIER.*` object before this promise resolves**; collider helpers assume it
33
+ already has.
34
+
35
+ Options you actually touch (`PhysicsWorldOptions`, defaults in parentheses):
36
+
37
+ | Option | Default | Meaning |
38
+ | --- | --- | --- |
39
+ | `gravity` | `[0, -9.81, 0]` | world gravity, m/s² |
40
+ | `timeStep` | `1/60` | fixed simulation step in seconds; exposed as `physics.timeStep` and mirrored into `world.timestep` — the ONLY dt physics code may use |
41
+ | `maxDelta` | `1/30` | per-frame wall-clock clamp (spiral-of-death guard: at most 2 substeps per frame at the default step); raise it to let physics catch up after long hitches |
42
+ | `interpolate` | `true` | lerp rendered poses between fixed steps — leave on |
43
+
44
+ The remaining options (`numSolverIterations` 4, `numInternalPgsIterations` 1,
45
+ `allowedLinearError` 0.001, `predictionDistance` 0.002, `minIslandSize` 128,
46
+ `maxCcdSubsteps` 1, `contactNaturalFrequency` 30, `lengthUnit` 1) mirror the
47
+ solver defaults the controllers were tuned against — change them only with a
48
+ measured reason.
49
+
50
+ ## Rigid bodies and the mesh registry
51
+
52
+ ```ts
53
+ const body = physics.createBody(
54
+ {
55
+ type: "dynamic", // "dynamic" | "fixed" | "kinematicPosition" | "kinematicVelocity"
56
+ position: [0, 3, 0],
57
+ rotation: [0, Math.PI / 2, 0], // euler XYZ radians, or a THREE.Quaternion
58
+ linearDamping: 0.1,
59
+ ccd: true, // fast small bodies only
60
+ userData: { controller: { excludeVehicleRay: true } },
61
+ },
62
+ mesh // optional: registers mesh to follow the body
63
+ );
64
+ ```
65
+
66
+ Passing an `Object3D` as the second argument calls
67
+ `physics.registerBody(body, object3d)` for you: every `physics.step`, the
68
+ object's position/quaternion are driven from the body pose, interpolated
69
+ between fixed steps. Registry rules:
70
+
71
+ - **Register scene-root-level groups and add them to the scene FIRST.** The
72
+ parent's inverse world matrix and the object's world scale are captured at
73
+ registration time and never refreshed — if the registered object's parent
74
+ later moves or scales, the sync silently desyncs.
75
+ - `physics.unregisterBody(body)` stops the sync; `physics.removeBody(body)`
76
+ also drops collider event handlers and removes the body (and its colliders)
77
+ from the world. `physics.getObject3d(body)` returns the registered object.
78
+ - The registered object's transform is OWNED by physics. To teleport, move the
79
+ body (`body.setTranslation({ x, y, z }, true)` — the `true` wakes it), never
80
+ the mesh.
81
+
82
+ Colliders are attached separately — see
83
+ `references/colliders-from-assets.md` in this skill.
84
+
85
+ ## Kinematic platforms
86
+
87
+ Moving/rotating platforms are `kinematicPosition` bodies whose next pose is set
88
+ inside `onBeforeStep`, so Rapier derives their velocities and carries riders:
89
+
90
+ ```ts
91
+ const platBody = physics.createBody(
92
+ { type: "kinematicPosition", position: [-2, 0.6, 8] },
93
+ platformMesh
94
+ );
95
+ cuboidCollider(world, platBody, [1.5, 0.15, 1.5], { friction: 1 });
96
+
97
+ let simTime = 0;
98
+ physics.onBeforeStep(() => {
99
+ simTime += physics.timeStep;
100
+ platBody.setNextKinematicTranslation({ x: -2 + 3 * Math.sin(simTime * 0.5), y: 0.6, z: 8 });
101
+ // rotating: rotBody.setNextKinematicRotation(quaternion)
102
+ });
103
+ ```
104
+
105
+ Advance your own `simTime` by `physics.timeStep` (as above), never by the
106
+ render delta. Set platform poses **before** controller `update()` calls in the
107
+ same callback so characters standing on them read fresh platform velocity.
108
+
109
+ ## Collision and sensor events
110
+
111
+ Per-collider handlers — `setColliderEvents` also enables
112
+ `ActiveEvents.COLLISION_EVENTS` on the collider (without that flag Rapier never
113
+ reports the pair):
114
+
115
+ ```ts
116
+ physics.setColliderEvents(pickupCollider, {
117
+ onIntersectionEnter: ({ other }) => {
118
+ // other: { collider, rigidBody, object3d } — object3d is the registered
119
+ // mesh of the other body, or null
120
+ if (other.object3d === playerRoot) collect();
121
+ },
122
+ });
123
+ ```
124
+
125
+ - Solid contacts fire `onCollisionEnter`; sensor overlaps fire
126
+ `onIntersectionEnter`. **Exit events fire BOTH** `onCollisionExit` and
127
+ `onIntersectionExit` — intentional, do not "fix" it; make exit handlers
128
+ idempotent.
129
+ - `physics.clearColliderEvents(collider)` removes handlers
130
+ (`removeBody` does it automatically).
131
+ - `physics.onCollisionEvent((h1, h2, started) => ...)` is a raw tap over every
132
+ drained event; its one intended consumer is the enter/exit system:
133
+ `physics.onCollisionEvent((h1, h2, s) => mgr.handleIntersectionEvent(h1, h2, s))`
134
+ (see `$genex-threejs-vehicle-controllers`). Both hooks return an unsubscribe
135
+ function.
136
+
137
+ ## Sleeping bodies
138
+
139
+ Dynamic bodies sleep when at rest (`canSleep` default `true`) — good: sleeping
140
+ islands cost nothing, and the mesh sync skips sleeping bodies so their meshes
141
+ simply hold still. What to know:
142
+
143
+ - Waking is automatic on contact and impulse. When you move a body directly,
144
+ pass the wake flag: `body.setTranslation(pos, true)`, `body.setLinvel(v, true)`.
145
+ - A body you nudge by writing tiny velocities every few frames may keep
146
+ falling asleep between nudges — create it with `canSleep: false` instead of
147
+ fighting the sleep threshold.
148
+ - If a stack "freezes" mid-air after you deleted its support, you removed the
149
+ body without waking neighbors — `physics.removeBody` handles the common case;
150
+ exotic cases can call `body.wakeUp()` on neighbors.
151
+
152
+ ## Tunneling and CCD
153
+
154
+ A fast small body can pass through a thin collider entirely between two fixed
155
+ steps. In order of preference:
156
+
157
+ 1. Make static geometry **thick** (the ground in the setup snippet is a 0.5
158
+ thick box, not a plane).
159
+ 2. Enable CCD on the fast body: `ccd: true` in `createBody` (projectiles,
160
+ thrown props). `maxCcdSubsteps` stays at 1 unless you measure misses.
161
+ 3. Never fix tunneling with a trimesh on the moving body — trimeshes are
162
+ hollow and make it worse (see the colliders reference).
163
+ 4. Shrinking `timeStep` is a last resort: it changes tuning for every
164
+ controller in the scene.
165
+
166
+ ## Pause, slow motion, and per-step gating
167
+
168
+ - `physics.paused = true` freezes simulation without banking time — unpausing
169
+ never replays the gap.
170
+ - `physics.timeScale = 0.5` is half-speed slow-mo (1 = realtime).
171
+ - `physics.stepsLastFrame` is the number of fixed substeps the latest `step()`
172
+ ran (can be 0 on a fast frame). Use it to gate once-per-physics-step work
173
+ done outside the physics loop, e.g. applying a platform's turn to the camera
174
+ only on frames where a substep actually ran.
175
+
176
+ ## Debug rendering and teardown
177
+
178
+ ```ts
179
+ physics.enableDebug(scene); // wireframe of every collider — tuning only
180
+ physics.disableDebug(); // remove + dispose the lines
181
+ physics.dispose(); // free the Rapier world, event queue, registries
182
+ ```
183
+
184
+ Debug lines cost CPU/GPU every frame — never leave them on in a published
185
+ game. `physics.debugEnabled` reports the current state.
186
+
187
+ ## Ground-query userData flags
188
+
189
+ Controllers probe the world for "ground" (character ray, wheel shapecasts).
190
+ Bodies opt out via `userData` at creation (`ControllerUserData` shape):
191
+
192
+ ```ts
193
+ { controller: { excludeRay: true } } // ignored by ALL ground queries
194
+ { controller: { excludeCharacterRay: true } } // ignored by the character's ground ray only
195
+ { controller: { excludeVehicleRay: true } } // ignored by wheel shapecasts only
196
+ ```
197
+
198
+ The character body itself should carry `excludeVehicleRay: true` so car wheels
199
+ never treat the on-foot player as drivable ground — the character skill's
200
+ wiring does this.
201
+
202
+ ## The benign boot warning
203
+
204
+ `@dimforge/rapier3d-compat` 0.19.3 prints one console warning at init —
205
+ `using deprecated parameters for the initialization function; pass a single object instead` —
206
+ from its own embedded WASM loader. It is not caused by game code, cannot be
207
+ silenced from game code, and affects nothing. Leave it alone.
@@ -16,6 +16,9 @@ map, execution order, and acceptance gate.
16
16
  | Work needed | Load |
17
17
  | --- | --- |
18
18
  | shot composition, chase/side/orbit rigs, camera handoffs, projection ownership, pointer look, floating origins | `$genex-threejs-camera-direction` |
19
+ | on-foot player movement: walk/run/jump, third-person character, slopes, stairs, moving platforms, animation binding | `$genex-threejs-character-controller` |
20
+ | the player drives or flies something: cars, drones, vehicle physics, gearbox, enter/exit between character and vehicle | `$genex-threejs-vehicle-controllers` |
21
+ | anything falls, collides, gets pushed, or needs physics: Rapier world setup, colliders for meshes and GLBs, collision events | `$genex-threejs-physics-rapier` |
19
22
  | launch and docking timelines, procedural transform phases, springs, staging, rotating-frame alignment, debris motion | `$genex-threejs-procedural-animation` |
20
23
  | reusable scalar/vector fields, domain warping, causal masks, procedural normals | `$genex-threejs-procedural-fields` |
21
24
  | atlas-filtered blocks, planetary surfaces, terrain wetness, lava/emissive procedural surfaces, authored frame PBR, specular AA | `$genex-threejs-procedural-materials` |
@@ -17,16 +17,21 @@ Three.js release or branch, and do not blindly copy demo architecture.
17
17
 
18
18
  1. Define the game contract: player verb, win/interaction loop, target device,
19
19
  camera distance, scene scale, motion, and frame budget.
20
- 2. Select the minimum scene-generation skills: geometry, materials, vegetation,
20
+ 2. Wire the gameplay layer for the player verb: the physics world via
21
+ `$genex-threejs-physics-rapier` when anything falls, collides, or gets
22
+ pushed; on-foot movement via `$genex-threejs-character-controller`;
23
+ driving/flying and character↔vehicle enter/exit via
24
+ `$genex-threejs-vehicle-controllers`.
25
+ 3. Select the minimum scene-generation skills: geometry, materials, vegetation,
21
26
  architecture, planets, water, precipitation, clouds, or VFX.
22
- 3. Add camera direction when framing, controls, transitions, or scale perception
27
+ 4. Add camera direction when framing, controls, transitions, or scale perception
23
28
  affect play.
24
- 4. Add procedural animation when object motion needs authored phases,
29
+ 5. Add procedural animation when object motion needs authored phases,
25
30
  convergence, looping, or deterministic timelines.
26
- 5. Add shared fields before writing multiple independent noise layers.
27
- 6. Add lighting, atmosphere, and shadows only after the no-post baseline reads.
28
- 7. Add image-pipeline, bloom, exposure, grading, or AO last.
29
- 8. Validate in a real browser with fixed seeds, captures, interaction checks,
31
+ 6. Add shared fields before writing multiple independent noise layers.
32
+ 7. Add lighting, atmosphere, and shadows only after the no-post baseline reads.
33
+ 8. Add image-pipeline, bloom, exposure, grading, or AO last.
34
+ 9. Validate in a real browser with fixed seeds, captures, interaction checks,
30
35
  and performance evidence.
31
36
 
32
37
  ## Acceptance gate
@@ -39,6 +44,9 @@ A routed Genex scene is incomplete until it exposes:
39
44
  - debug views for generated fields, masks, or passes;
40
45
  - a no-post baseline that still communicates the subject;
41
46
  - a clear quality tier or render-budget knob when the effect is expensive;
47
+ - when physics or controllers are in play, a fixed-timestep loop: per-frame
48
+ work (platforms, enter/exit, controller updates) runs inside the physics
49
+ world's before-step hook, then the world steps — never in the render loop;
42
50
  - browser evidence showing the canvas renders, moves, and responds to input.
43
51
 
44
52
  ## Publish and multiplayer awareness
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: genex-threejs-vehicle-controllers
3
+ description: Add a drivable car or a flyable drone to a Genex Three.js game with `npx genex controller car` / `npx genex controller drone` — shapecast-wheel vehicle physics with a gearbox, PD quadcopter flight, tuned presets, character enter/exit with animation and camera handoff, all over Rapier physics. Use whenever the player drives or flies something.
4
+ ---
5
+
6
+ # Genex Three.js Vehicle Controllers
7
+
8
+ Real vehicle physics for plain Three.js games: a car built from a dynamic
9
+ chassis body plus one shapecast wheel per corner (suspension spring/damper,
10
+ slip-curve tire model, speed-sensitive steering, engine torque curve with an
11
+ RPM-threshold automatic gearbox), and a quadcopter drone flown by a PD
12
+ attitude controller mixing four thrust propellers. Both are vendored
13
+ TypeScript classes copied INTO the game — not an npm dependency — so the code
14
+ is yours to read and tune.
15
+
16
+ ## Install
17
+
18
+ Run inside the game project (where `genex init` ran):
19
+
20
+ ```bash
21
+ npx genex controller car # VehicleController + ShapeCastWheel + presets
22
+ npx genex controller drone # DroneController + presets
23
+ npm i @dimforge/rapier3d-compat
24
+ ```
25
+
26
+ Files land in `src/controllers/` (shared physics glue, the vehicle or drone
27
+ module, `interact/enter-exit.ts`, and the input + follow-camera modules).
28
+ Existing files are never overwritten (use `--force` to refresh). For the
29
+ on-foot character that enters these vehicles, also run
30
+ `npx genex controller character` and load `$genex-threejs-character-controller`.
31
+
32
+ ## The loop contract (get this right first)
33
+
34
+ Every controller exposes `update(dt?)` that must run once per **fixed physics
35
+ substep, BEFORE `world.step()`**. The `dt` argument is ignored — all internal
36
+ math uses `world.timestep`. Wire it through `PhysicsWorld` (installed with
37
+ every controller kind):
38
+
39
+ ```ts
40
+ const physics = await PhysicsWorld.create();
41
+
42
+ physics.onBeforeStep(() => {
43
+ const dt = physics.timeStep;
44
+ // 1. kinematic platforms (setNextKinematicTranslation/Rotation)
45
+ mgr.update(dt); // 2. enter/exit manager
46
+ if (!character.isParked) character.update(dt); // 3. controller brains
47
+ car.update(dt);
48
+ drone.update(dt);
49
+ }); // 4. world.step() runs after
50
+
51
+ renderer.setAnimationLoop(() => {
52
+ physics.step(clock.getDelta()); // fixed substeps + interpolated mesh sync
53
+ // camera + render here — render-delta code never goes inside onBeforeStep
54
+ });
55
+ ```
56
+
57
+ Never call a controller's `update()` from the render loop, never step the
58
+ world yourself, and never feed the render delta into physics code.
59
+
60
+ ## Wiring references
61
+
62
+ - [references/car.md](references/car.md) — chassis body + colliders, wheels,
63
+ presets with provenance, gearbox and RPM, drift vs rollover tuning.
64
+ - [references/drone.md](references/drone.md) — body + propeller mounts,
65
+ VELOCITY vs POSITION control modes, the PD gain mass-scaling rule, presets.
66
+ - [references/enter-exit.md](references/enter-exit.md) — `EnterExitManager`,
67
+ park/unpark, sensor tuning, seat animations (`Sitting_Enter` /
68
+ `Driving_Loop` / `Sitting_Exit`), follow-camera handoff.
69
+
70
+ ## Presets at a glance
71
+
72
+ Spread a preset into the controller and build colliders/wheels/propellers
73
+ from its recipe (exact code in the references).
74
+
75
+ | Preset | Kind | Feel | Provenance |
76
+ | --- | --- | --- | --- |
77
+ | `arcade-kart` | car | AWD, stiff, forgiving — the safe default | upstream demo, verbatim |
78
+ | `muscle-drift` | car | soft, rear-biased torque, power-oversteers | upstream demo, verbatim |
79
+ | `offroad-bouncy` | car | long-travel springs, big wheels, low grip | Genex-authored |
80
+ | `race-grip` | car | stiff RWD, 4-speed auto gearbox, flat controllable drift | Genex-authored, testbed-tuned |
81
+ | `camera-drone` | drone | slow, heavily damped filming platform (~2 kg) | Genex-authored |
82
+ | `racing-drone` | drone | agile FPV-style racer (~5 kg) | Genex-authored |
83
+ | `heavy-lifter` | drone | ~298 kg cargo platform, POSITION gains pre-scaled | upstream demo, verbatim |
84
+
85
+ ## Multiplayer: vehicle occupancy is shared state
86
+
87
+ If the game is multiplayer, **who is in what vehicle must be synced** — a
88
+ remote player vanishing into an apparently empty car reads as a bug. Load
89
+ `$genex-threejs-multiplayer` before writing any networking code, then:
90
+
91
+ - Publish occupancy with the player's state (e.g. `driving: "car" | null`)
92
+ or as a shared key — and the occupied vehicle's pose with it.
93
+ - Same authority rule as the character: the **local player simulates the
94
+ physics of whatever they occupy**; remote vehicles are interpolated
95
+ visuals only. Never run `VehicleController`/`DroneController` for a
96
+ remote player — drive a plain mesh from their synced pose.
97
+ - An unoccupied vehicle needs one owner too: elect one client (e.g. the
98
+ first in the room) to simulate it and publish its pose as shared state.
99
+
100
+ ## Boundaries and troubleshooting
101
+
102
+ - Physics world setup, collider strategy for `genex model` GLBs, and general
103
+ Rapier pitfalls live in `$genex-threejs-physics-rapier`.
104
+ - Camera systems beyond the bundled follow camera: `$genex-threejs-camera-direction`.
105
+ - Skid smoke, dust, rotor wash: `$genex-threejs-procedural-vfx`, driven by
106
+ the telemetry getters listed in the references (`wheel.slipStrength`,
107
+ `drone.propellersInfo`).
108
+ - **One-time console warning at boot** ("using deprecated parameters for the
109
+ initialization function...") comes from the Rapier WASM loader itself —
110
+ harmless, not fixable from game code. Do not chase it.