@genex-ai/cli-demo 0.12.1 → 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.
- package/README.md +1 -0
- package/dist/index.js +203 -4
- package/package.json +7 -2
- package/templates/controllers/NOTICE.md +65 -0
- package/templates/controllers/assets/animation-library.glb +0 -0
- package/templates/controllers/assets/character.glb +0 -0
- package/templates/controllers/assets/default-avatar.vrm +0 -0
- package/templates/controllers/character/character-animations.ts +682 -0
- package/templates/controllers/character/character-controller.ts +1636 -0
- package/templates/controllers/character/follow-camera.ts +644 -0
- package/templates/controllers/character/keyboard-input.ts +277 -0
- package/templates/controllers/character/presets.ts +176 -0
- package/templates/controllers/character/touch-joystick.ts +387 -0
- package/templates/controllers/character/vrm/capsule-fit.ts +52 -0
- package/templates/controllers/character/vrm/foot-ik.ts +341 -0
- package/templates/controllers/character/vrm/vrm-loader.ts +44 -0
- package/templates/controllers/character/vrm/vrm-retarget.ts +195 -0
- package/templates/controllers/drone/drone-controller.ts +1073 -0
- package/templates/controllers/drone/presets.ts +225 -0
- package/templates/controllers/interact/enter-exit.ts +502 -0
- package/templates/controllers/shared/colliders.ts +456 -0
- package/templates/controllers/shared/math.ts +230 -0
- package/templates/controllers/shared/physics-world.ts +622 -0
- package/templates/controllers/vehicle/presets.ts +297 -0
- package/templates/controllers/vehicle/vehicle-controller.ts +615 -0
- package/templates/controllers/vehicle/wheel.ts +1200 -0
- package/templates/skills/genex-getting-started/SKILL.md +5 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +205 -0
- package/templates/skills/genex-threejs-character-controller/references/animations.md +235 -0
- package/templates/skills/genex-threejs-character-controller/references/tuning-and-presets.md +102 -0
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +198 -0
- package/templates/skills/genex-threejs-physics-rapier/SKILL.md +128 -0
- package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +202 -0
- package/templates/skills/genex-threejs-physics-rapier/references/physics-setup.md +207 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +3 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +15 -7
- package/templates/skills/genex-threejs-vehicle-controllers/SKILL.md +110 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/car.md +162 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +150 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +199 -0
|
@@ -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.
|
|
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
|
-
|
|
27
|
+
4. Add camera direction when framing, controls, transitions, or scale perception
|
|
23
28
|
affect play.
|
|
24
|
-
|
|
29
|
+
5. Add procedural animation when object motion needs authored phases,
|
|
25
30
|
convergence, looping, or deterministic timelines.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Car: VehicleController + ShapeCastWheel
|
|
2
|
+
|
|
3
|
+
The car is three cooperating pieces:
|
|
4
|
+
|
|
5
|
+
- **`VehicleController`** (`vehicle/vehicle-controller.ts`) — the brain. It
|
|
6
|
+
creates a dynamic Rapier body **without colliders**, owns the drivetrain
|
|
7
|
+
(engine torque curve, gear ratios, RPM-threshold auto shift), routes
|
|
8
|
+
drive/brake/steer demands to the wheels, and applies their suspension +
|
|
9
|
+
friction impulses to the body.
|
|
10
|
+
- **`ShapeCastWheel`** (`vehicle/wheel.ts`) — one per corner, created via
|
|
11
|
+
`car.addWheel()`. Each wheel sweeps a cylinder at the ground, computes a
|
|
12
|
+
suspension spring/damper impulse and slip-curve tire friction, and drives
|
|
13
|
+
its own visual groups (steer, suspension bounce, spin).
|
|
14
|
+
- **`vehiclePresets`** (`vehicle/presets.ts`) — complete car recipes:
|
|
15
|
+
`carConfig`, chassis collider specs, shared wheel options, and four wheel
|
|
16
|
+
slots (order FL, FR, RL, RR).
|
|
17
|
+
|
|
18
|
+
Conventions: **+Z is the car's forward axis**, +X is left, and positive steer
|
|
19
|
+
input turns LEFT. Wheel `position` is the axle mount point in chassis-local
|
|
20
|
+
space.
|
|
21
|
+
|
|
22
|
+
## Wiring (matches the shipped preset shapes)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import * as THREE from "three";
|
|
26
|
+
import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
|
|
27
|
+
import { cuboidCollider } from "./controllers/shared/colliders.ts";
|
|
28
|
+
import { VehicleController } from "./controllers/vehicle/vehicle-controller.ts";
|
|
29
|
+
import { vehiclePresets } from "./controllers/vehicle/presets.ts";
|
|
30
|
+
import { KeyboardInput } from "./controllers/character/keyboard-input.ts";
|
|
31
|
+
|
|
32
|
+
const physics = await PhysicsWorld.create();
|
|
33
|
+
const world = physics.world;
|
|
34
|
+
|
|
35
|
+
// 1. Controller (creates the dynamic body — no colliders yet).
|
|
36
|
+
const preset = vehiclePresets["arcade-kart"];
|
|
37
|
+
const car = new VehicleController({
|
|
38
|
+
world,
|
|
39
|
+
position: new THREE.Vector3(12, 1.4, 4),
|
|
40
|
+
carConfig: preset.carConfig,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 2. Chassis colliders — YOU attach them to car.body from the preset recipe.
|
|
44
|
+
for (const c of preset.chassisColliders) {
|
|
45
|
+
cuboidCollider(world, car.body, [c.halfExtents.x, c.halfExtents.y, c.halfExtents.z], {
|
|
46
|
+
position: [c.offset.x, c.offset.y, c.offset.z],
|
|
47
|
+
density: c.density,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Wheels — shared options + per-slot role flags. Register all four
|
|
52
|
+
// before the first update() so the torque split is stable.
|
|
53
|
+
const wheelGeom = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 20);
|
|
54
|
+
wheelGeom.rotateZ(Math.PI / 2); // the wheel model spins around LOCAL X
|
|
55
|
+
for (const slot of preset.wheelSlots) {
|
|
56
|
+
const wheel = car.addWheel({
|
|
57
|
+
...preset.wheelShared,
|
|
58
|
+
...slot,
|
|
59
|
+
position: new THREE.Vector3(slot.position.x, slot.position.y, slot.position.z),
|
|
60
|
+
});
|
|
61
|
+
const mesh = new THREE.Mesh(wheelGeom, wheelMat);
|
|
62
|
+
wheel.modelObject.add(mesh); // wheelGroup steers, suspensionGroup bounces, modelObject spins
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Scene graph: chassis mesh under chassisObject, chassisObject in the scene.
|
|
66
|
+
car.chassisObject.add(carBodyMesh); // your visual chassis (a genex model GLB works)
|
|
67
|
+
scene.add(car.chassisObject);
|
|
68
|
+
physics.registerBody(car.body, car.chassisObject); // interpolated render sync
|
|
69
|
+
|
|
70
|
+
// 5. Input + fixed-step update (BEFORE world.step(), via onBeforeStep).
|
|
71
|
+
const kb = new KeyboardInput(); // WASD/arrows drive+steer, Space = brake
|
|
72
|
+
physics.onBeforeStep(() => {
|
|
73
|
+
car.setMovement(kb.getCarMovement());
|
|
74
|
+
car.update();
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`setMovement` merges field-wise — send the complete `CarMovementIntent`
|
|
79
|
+
every frame (a stale partial leaves old `true`s behind), which
|
|
80
|
+
`kb.getCarMovement()` already does. Touch input: pass
|
|
81
|
+
`{ joystickL: { x, y } }` from `TouchJoystick` (pushing right steers right).
|
|
82
|
+
|
|
83
|
+
If an on-foot character shares the world, create its body with
|
|
84
|
+
`userData: { controller: { excludeVehicleRay: true } }` so the wheels never
|
|
85
|
+
treat the player as drivable ground.
|
|
86
|
+
|
|
87
|
+
## Presets and provenance
|
|
88
|
+
|
|
89
|
+
| Preset | Drivetrain | Suspension | Notes |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `arcade-kart` | 600 HP, AWD, front steer, single gear | stiff (springK 38000) | upstream demo "Vehicle 1", verbatim. The safe default. |
|
|
92
|
+
| `muscle-drift` | 600 HP, AWD with rear `driveTorqueWeight: 2` | soft (springK 25000) | upstream demo "Vehicle 2", verbatim. Power-oversteers into drifts under throttle. |
|
|
93
|
+
| `offroad-bouncy` | 450 HP, AWD, wider steering | soft, long travel (rayLength 0.8), bigger wheels | Genex-authored starting point derived from arcade-kart. |
|
|
94
|
+
| `race-grip` | 800 HP, RWD, 4-speed gearbox | very stiff (springK 42000) | Genex-authored, tuned in the testbed (see below). |
|
|
95
|
+
|
|
96
|
+
**Suspension scales with mass.** `springK`/`dampingC` in `wheelShared` were
|
|
97
|
+
tuned against each preset's `assumedChassisDensity` (200 for all four). If
|
|
98
|
+
your chassis is materially heavier or lighter, rescale `springK`
|
|
99
|
+
proportionally and keep `dampingC` below `2*sqrt(springK * massPerWheel)` —
|
|
100
|
+
too little spring bottoms out, too much damping locks the suspension solid.
|
|
101
|
+
|
|
102
|
+
## Gearbox and RPM
|
|
103
|
+
|
|
104
|
+
- Peak engine torque derives as `engineHorsepower * 7022 / engineMaxRPM`
|
|
105
|
+
(N·m); "car feels slow" → raise `engineHorsepower`.
|
|
106
|
+
- A single-entry `gearRatios` (arcade-kart, muscle-drift) disables shifting.
|
|
107
|
+
Multiple entries enable the auto shift: up above `shiftUpRPM`, down below
|
|
108
|
+
`shiftDownRPM`, with `shiftCooldown` seconds between shifts.
|
|
109
|
+
`transmissionMode: "manual"` shifts only via `car.setGear(index)`.
|
|
110
|
+
- Live telemetry for HUDs: `car.engineRPM` (drive-weighted average wheel RPM
|
|
111
|
+
× drive ratio), `car.gearIndex` (0-based — display `gearIndex + 1`),
|
|
112
|
+
`car.currLinVel.length() * 3.6` for km/h.
|
|
113
|
+
- **Why race-grip's gearing works** (and what to check if you author your
|
|
114
|
+
own gearbox): rolling resistance grows with wheel speed, so each gear has
|
|
115
|
+
a drag-limited equilibrium RPM it can never exceed on flat ground
|
|
116
|
+
(measured in the testbed: ~5500 / ~4900 / ~4100 for race-grip's gears
|
|
117
|
+
1–3). `shiftUpRPM` must sit BELOW the current gear's equilibrium or the
|
|
118
|
+
shift point is never reached and the car sits at redline forever.
|
|
119
|
+
race-grip uses `shiftUpRPM: 4300`, giving clean upshifts with post-shift
|
|
120
|
+
RPM ~2800–3000 — safely above `shiftDownRPM: 2400`, so no gear hunting.
|
|
121
|
+
Lower `rollingResistanceCoef` (race-grip: 0.004 vs default 0.007) raises
|
|
122
|
+
every gear's ceiling and buys the shifter headroom.
|
|
123
|
+
|
|
124
|
+
## Drift tuning: tire grip vs rollover
|
|
125
|
+
|
|
126
|
+
The knobs, in the order to reach for them:
|
|
127
|
+
|
|
128
|
+
- `tireGripFactor` (wheel option, default 1.5) — averaged with the ground
|
|
129
|
+
collider's friction: effective grip = `(surfaceFriction + tireGripFactor) / 2`.
|
|
130
|
+
Lower = more slide everywhere.
|
|
131
|
+
- `latFrictionEllipseScale` — scales ONLY the cornering half of the friction
|
|
132
|
+
ellipse. The direct drift knob: below 1 the car slides sideways sooner
|
|
133
|
+
while braking/accelerating stay strong.
|
|
134
|
+
- `driveTorqueWeight` on the rear slots (muscle-drift uses 2) — rear-biased
|
|
135
|
+
torque makes throttle break the rear loose.
|
|
136
|
+
- Brake-and-turn (Space is the brake) initiates a slide with any preset.
|
|
137
|
+
|
|
138
|
+
**The rollover trap** — measured in the testbed while retuning `race-grip`:
|
|
139
|
+
the lateral slip curve keeps ~90% grip even in a full slide, so peak lateral
|
|
140
|
+
acceleration is about `(surfaceFriction + tireGripFactor) / 2 *
|
|
141
|
+
latFrictionEllipseScale` in g. If that exceeds the chassis's static rollover
|
|
142
|
+
threshold (`halfTrack / comHeight` — ~1.0 g for the race-grip chassis), a
|
|
143
|
+
hard slide TRIPS THE CAR OVER instead of drifting. The race-grip retune
|
|
144
|
+
fixed exactly this, two-sided:
|
|
145
|
+
|
|
146
|
+
1. capped lateral grip with `latFrictionEllipseScale: 0.8` (and
|
|
147
|
+
`lngFrictionEllipseScale: 1.15` so braking/launch stay strong), and
|
|
148
|
+
2. lowered the center of mass — a light cabin collider (density 60) over a
|
|
149
|
+
low-slung main mass (offset y −0.15) keeps the CoM near axle height so
|
|
150
|
+
hard cornering leans instead of tipping.
|
|
151
|
+
|
|
152
|
+
If your car flips in corners, do the same: lower the grip-side product or
|
|
153
|
+
lower the CoM. Raising `springK` alone does not fix rollover.
|
|
154
|
+
|
|
155
|
+
## Telemetry for effects
|
|
156
|
+
|
|
157
|
+
Per wheel (from `car.wheels`, a `ReadonlyMap<string, ShapeCastWheel>`):
|
|
158
|
+
`wheel.slipStrength` (0..1, max of longitudinal/lateral slip — the skid
|
|
159
|
+
smoke/screech trigger), `wheel.rayHitPos` + `wheel.rayHitNormal` (where to
|
|
160
|
+
spawn marks), `wheel.wheelLinVel` (surface speed), `wheel.rayHit` (null when
|
|
161
|
+
airborne). Vector getters are live internal instances — `.copy()` them,
|
|
162
|
+
never mutate. Value getters are one-frame-stale snapshots by design.
|