@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.
- 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-embed-auth/SKILL.md +126 -54
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +17 -11
- 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,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.
|
|
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.
|