@woosh/meep-engine 2.145.0 → 2.146.0
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/package.json +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
- package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -352
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallJump.js +11 -3
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +12 -0
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
- package/src/engine/control/first-person/collision/KinematicMover.js +634 -592
- package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
- package/src/engine/physics/PLAN.md +943 -809
- package/src/engine/physics/body/BodyStorage.d.ts +9 -0
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +23 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
- package/src/engine/physics/ccd/linear_sweep.js +238 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +185 -183
- package/src/engine/simulation/Ticker.d.ts +14 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +136 -1
|
@@ -1,352 +1,365 @@
|
|
|
1
|
-
# Collision handling — ground-up construction plan
|
|
2
|
-
|
|
3
|
-
Companion to DESIGN.md (base controller) and TODO.md.
|
|
4
|
-
|
|
5
|
-
**Premise.** The control layer is excellent and stays. The collision
|
|
6
|
-
layer is being **replaced wholesale** — the current
|
|
7
|
-
`_moveAndSlide` + `_integrateVerticalAndResolveGround` + scalar
|
|
8
|
-
ground-resolver regime is not a foundation to build on; it's scaffolding
|
|
9
|
-
to delete once the replacement reaches parity. We keep relying on
|
|
10
|
-
`PhysicsSystem` for all the heavy lifting (`shapeCast`, `raycast`,
|
|
11
|
-
`overlap`, `compute_penetration`) — this plan does *more* with it, not
|
|
12
|
-
less.
|
|
13
|
-
|
|
14
|
-
Every phase is **guard-test-first**, and ordered so the controller stays
|
|
15
|
-
shippable and green after each one. The new mover is built alongside,
|
|
16
|
-
proven against ported + new specs, switched over, and only then is the
|
|
17
|
-
old code deleted.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## 1. The seam: control produces motion, collision resolves it
|
|
22
|
-
|
|
23
|
-
The clean split that makes a ground-up rebuild safe — the control layer
|
|
24
|
-
already does its half today:
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
CONTROL (sacred, unchanged) COLLISION (rebuilt)
|
|
28
|
-
────────────────────────── ──────────────────
|
|
29
|
-
intent → desired velocity move(pose, shape, velocity, dt)
|
|
30
|
-
accel curves, jump FSM, ──v──▶ → resolved position
|
|
31
|
-
gravity/impulse, abilities, → corrected velocity
|
|
32
|
-
mastery, posture → ground state {grounded, normal}
|
|
33
|
-
▲ │
|
|
34
|
-
└────────── grounded, groundNormal ◀─────────┘
|
|
35
|
-
(read next tick by jump FSM,
|
|
36
|
-
gravity gating, slope logic)
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
The control layer's output each tick is a **desired velocity** (it
|
|
40
|
-
already computes this — intent+accel horizontally, gravity+jump
|
|
41
|
-
vertically). The collision layer's *only* job is: given that velocity,
|
|
42
|
-
the capsule pose, and the world, produce the actual resulting position,
|
|
43
|
-
the velocity corrected for what it hit, and the ground state. It never
|
|
44
|
-
invents motion; it only constrains it.
|
|
45
|
-
|
|
46
|
-
One cleanup this forces (Phase 4): **gravity + jump-impulse application
|
|
47
|
-
move out of the deleted collision method into a small motor helper**
|
|
48
|
-
the base loop and abilities call. The solver stays purely about
|
|
49
|
-
collision.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## 2. What we keep / replace / delete
|
|
54
|
-
|
|
55
|
-
**Keep (untouched):** the whole control stack — intent, mono-exp accel,
|
|
56
|
-
jump FSM, posture→collider sizing, abilities (Slide/WallRun/Mantle/
|
|
57
|
-
LedgeGrab), mastery, camera composition, sensors.
|
|
58
|
-
|
|
59
|
-
**Replace:** the regime that *consumes* velocity —
|
|
60
|
-
`_moveAndSlide` (horizontal-only swept slide), the direct vertical
|
|
61
|
-
position add, and the downward-ray scalar-Y ground snap.
|
|
62
|
-
|
|
63
|
-
**Delete (Phase 4, after parity):** `_moveAndSlide`,
|
|
64
|
-
`_integrateVerticalAndResolveGround`'s collision body, the
|
|
65
|
-
`CAST_STEP_HEIGHT` implicit-step hack, and the scalar `groundResolver`
|
|
66
|
-
*as the grounding authority* (it survives only as a no-physics fallback
|
|
67
|
-
inside the new ground probe).
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## 3. Physics capabilities we build on
|
|
72
|
-
|
|
73
|
-
The P1–P6 narrowphase/query work is the enabling foundation — the
|
|
74
|
-
rebuild is only feasible because these are now narrowphase-exact.
|
|
75
|
-
|
|
76
|
-
| Capability | Status | Source |
|
|
77
|
-
|---|---|---|
|
|
78
|
-
| `raycast` exact surface **normal** (sphere/box/capsule/mesh/heightmap) | ✅ | refine_ray_hit P1–P3 (`6f931b4`…`4c2292c`) |
|
|
79
|
-
| `shapeCast` true contact **normal at TOI** (MPR, not broadphase −dir) | ✅ | `shape_cast.js` normal-recovery |
|
|
80
|
-
| `shapeCast` **start-in-contact** → `t=0` + valid normal | ✅ | `shape_cast.js:195`, `:350` |
|
|
81
|
-
| `shapeCast` analytic slab-narrowing (long-sweep accuracy) | ✅ | `shape_cast.js:203` |
|
|
82
|
-
| `compute_penetration(out, A,pa,ra, B,pb,rb) → depth`, `out`=unit B→A | ✅ **public** | `compute_penetration.js:138` |
|
|
83
|
-
| `overlap(shape,pos,rot,out,off,filter)` → body-id list | ✅ | `overlap_shape.js` |
|
|
84
|
-
| EPA adaptive tol / MPR fallback / box-box edge / capsule-tri manifold / GJK cache | ✅ | P2.2, P3.1, P3.2, P1.1c, P2.1 |
|
|
85
|
-
|
|
86
|
-
Contracts the plan honours:
|
|
87
|
-
- `shapeCast` `result.t` is a **world distance** when direction is
|
|
88
|
-
unit-length and `tMax = sweepLength`. New casts keep that.
|
|
89
|
-
- `compute_penetration` accepts the convex player capsule vs any
|
|
90
|
-
shape (only concave-vs-concave throws); `out_direction` is the unit
|
|
91
|
-
**B→A** separation, depth is the scalar return. Push the capsule (A)
|
|
92
|
-
out along `+out_direction · depth`.
|
|
93
|
-
- `shapeCast` `result.position` is the swept **centre**, not the
|
|
94
|
-
contact point — we derive ground-surface Y from the cast `t` + the
|
|
95
|
-
capsule's bottom offset, so we never need a contact point.
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## 4. Target architecture — a dedicated `KinematicMover`
|
|
100
|
-
|
|
101
|
-
Extract the collision solver into its own module (name placeholder —
|
|
102
|
-
`KinematicMover` / `CharacterMover`), testable in isolation against a
|
|
103
|
-
real `PhysicsSystem`, not buried in the 1500-line system file. Matches
|
|
104
|
-
the repo's extraction taste (Spring, EyeOffsetStack, FirstPersonSensors).
|
|
105
|
-
|
|
106
|
-
It knows nothing about the controller — just a capsule, a velocity, the
|
|
107
|
-
world, and an `up` axis. Single entry point:
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
move(transform, shape, velocity, dt, filter) → MoveResult
|
|
111
|
-
// mutates transform.position; returns:
|
|
112
|
-
// { velocityX/Y/Z (corrected), grounded, groundNormal, hit }
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Modelled on modern kinematic character controllers (Jolt
|
|
116
|
-
`CharacterVirtual`, Source `TryPlayerMove`+`CategorizePosition`), the
|
|
117
|
-
move is an explicit sequence — **recover → slide → (stairs) → ground →
|
|
118
|
-
settle**:
|
|
119
|
-
|
|
120
|
-
1. **Recover (depenetration).** Before moving, get clear. `overlap()`
|
|
121
|
-
the capsule; for each body, `compute_penetration` → push the capsule
|
|
122
|
-
out by `depth · dir`; iterate a few times (deepest-first) to
|
|
123
|
-
convergence. Handles spawned-in-geometry, world-crushed-into-us, and
|
|
124
|
-
the start-solid case a pure sweep *cannot*. This is now a **core
|
|
125
|
-
step**, available from Phase 1 — not a bolt-on — because
|
|
126
|
-
`compute_penetration` is public.
|
|
127
|
-
|
|
128
|
-
2. **Sweep-and-slide (unified 3D).** Sweep the *full* velocity·dt
|
|
129
|
-
(gravity folded in — no H/V split) via `shapeCast`; stop at TOI,
|
|
130
|
-
clip velocity onto the contact tangent, project the residual, repeat
|
|
131
|
-
up to N iterations. **Crease-aware**: a second plane re-violating the
|
|
132
|
-
first → project onto the seam `normalize(n₁×n₂)`; a third plane in
|
|
133
|
-
one move → dead-stop. Walls, floors, ceilings are all just contact
|
|
134
|
-
planes, so anti-tunnelling (H and V) falls out of one loop.
|
|
135
|
-
|
|
136
|
-
3. **Stairs (explicit, optional per move).** When grounded and blocked
|
|
137
|
-
by a low step: sweep up by `stepHeight`, sweep the residual forward,
|
|
138
|
-
sweep back down — the Jolt/Source step pattern. Replaces the 5 cm
|
|
139
|
-
implicit-cast hack with real stair climbing, gated so walls taller
|
|
140
|
-
than `stepHeight` still block.
|
|
141
|
-
|
|
142
|
-
4. **Ground categorize + stick.** Short downward `shapeCast` (capsule
|
|
143
|
-
swept down a `SKIN + band`); if it hits within the band with
|
|
144
|
-
`normal.y ≥ MIN_WALK_NORMAL` (~0.7), set `grounded`, capture the
|
|
145
|
-
normal, snap to the surface, and kill the into-ground velocity
|
|
146
|
-
component. Band-test + active snap (not a strict `y ≤ testY`) is what
|
|
147
|
-
structurally kills the landing **bounce**. Too-steep normal →
|
|
148
|
-
not grounded → the slope was already a slide plane in step 2, so the
|
|
149
|
-
player slides down it.
|
|
150
|
-
|
|
151
|
-
5. **Settle.** One final `compute_penetration` recover pass guarantees
|
|
152
|
-
the tick ends penetration-free (resting-contact float drift
|
|
153
|
-
insurance; usually a no-op).
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
the
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
the
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
the
|
|
245
|
-
|
|
246
|
-
the
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1
|
+
# Collision handling — ground-up construction plan
|
|
2
|
+
|
|
3
|
+
Companion to DESIGN.md (base controller) and TODO.md.
|
|
4
|
+
|
|
5
|
+
**Premise.** The control layer is excellent and stays. The collision
|
|
6
|
+
layer is being **replaced wholesale** — the current
|
|
7
|
+
`_moveAndSlide` + `_integrateVerticalAndResolveGround` + scalar
|
|
8
|
+
ground-resolver regime is not a foundation to build on; it's scaffolding
|
|
9
|
+
to delete once the replacement reaches parity. We keep relying on
|
|
10
|
+
`PhysicsSystem` for all the heavy lifting (`shapeCast`, `raycast`,
|
|
11
|
+
`overlap`, `compute_penetration`) — this plan does *more* with it, not
|
|
12
|
+
less.
|
|
13
|
+
|
|
14
|
+
Every phase is **guard-test-first**, and ordered so the controller stays
|
|
15
|
+
shippable and green after each one. The new mover is built alongside,
|
|
16
|
+
proven against ported + new specs, switched over, and only then is the
|
|
17
|
+
old code deleted.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. The seam: control produces motion, collision resolves it
|
|
22
|
+
|
|
23
|
+
The clean split that makes a ground-up rebuild safe — the control layer
|
|
24
|
+
already does its half today:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
CONTROL (sacred, unchanged) COLLISION (rebuilt)
|
|
28
|
+
────────────────────────── ──────────────────
|
|
29
|
+
intent → desired velocity move(pose, shape, velocity, dt)
|
|
30
|
+
accel curves, jump FSM, ──v──▶ → resolved position
|
|
31
|
+
gravity/impulse, abilities, → corrected velocity
|
|
32
|
+
mastery, posture → ground state {grounded, normal}
|
|
33
|
+
▲ │
|
|
34
|
+
└────────── grounded, groundNormal ◀─────────┘
|
|
35
|
+
(read next tick by jump FSM,
|
|
36
|
+
gravity gating, slope logic)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The control layer's output each tick is a **desired velocity** (it
|
|
40
|
+
already computes this — intent+accel horizontally, gravity+jump
|
|
41
|
+
vertically). The collision layer's *only* job is: given that velocity,
|
|
42
|
+
the capsule pose, and the world, produce the actual resulting position,
|
|
43
|
+
the velocity corrected for what it hit, and the ground state. It never
|
|
44
|
+
invents motion; it only constrains it.
|
|
45
|
+
|
|
46
|
+
One cleanup this forces (Phase 4): **gravity + jump-impulse application
|
|
47
|
+
move out of the deleted collision method into a small motor helper**
|
|
48
|
+
the base loop and abilities call. The solver stays purely about
|
|
49
|
+
collision.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. What we keep / replace / delete
|
|
54
|
+
|
|
55
|
+
**Keep (untouched):** the whole control stack — intent, mono-exp accel,
|
|
56
|
+
jump FSM, posture→collider sizing, abilities (Slide/WallRun/Mantle/
|
|
57
|
+
LedgeGrab), mastery, camera composition, sensors.
|
|
58
|
+
|
|
59
|
+
**Replace:** the regime that *consumes* velocity —
|
|
60
|
+
`_moveAndSlide` (horizontal-only swept slide), the direct vertical
|
|
61
|
+
position add, and the downward-ray scalar-Y ground snap.
|
|
62
|
+
|
|
63
|
+
**Delete (Phase 4, after parity):** `_moveAndSlide`,
|
|
64
|
+
`_integrateVerticalAndResolveGround`'s collision body, the
|
|
65
|
+
`CAST_STEP_HEIGHT` implicit-step hack, and the scalar `groundResolver`
|
|
66
|
+
*as the grounding authority* (it survives only as a no-physics fallback
|
|
67
|
+
inside the new ground probe).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Physics capabilities we build on
|
|
72
|
+
|
|
73
|
+
The P1–P6 narrowphase/query work is the enabling foundation — the
|
|
74
|
+
rebuild is only feasible because these are now narrowphase-exact.
|
|
75
|
+
|
|
76
|
+
| Capability | Status | Source |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `raycast` exact surface **normal** (sphere/box/capsule/mesh/heightmap) | ✅ | refine_ray_hit P1–P3 (`6f931b4`…`4c2292c`) |
|
|
79
|
+
| `shapeCast` true contact **normal at TOI** (MPR, not broadphase −dir) | ✅ | `shape_cast.js` normal-recovery |
|
|
80
|
+
| `shapeCast` **start-in-contact** → `t=0` + valid normal | ✅ | `shape_cast.js:195`, `:350` |
|
|
81
|
+
| `shapeCast` analytic slab-narrowing (long-sweep accuracy) | ✅ | `shape_cast.js:203` |
|
|
82
|
+
| `compute_penetration(out, A,pa,ra, B,pb,rb) → depth`, `out`=unit B→A | ✅ **public** | `compute_penetration.js:138` |
|
|
83
|
+
| `overlap(shape,pos,rot,out,off,filter)` → body-id list | ✅ | `overlap_shape.js` |
|
|
84
|
+
| EPA adaptive tol / MPR fallback / box-box edge / capsule-tri manifold / GJK cache | ✅ | P2.2, P3.1, P3.2, P1.1c, P2.1 |
|
|
85
|
+
|
|
86
|
+
Contracts the plan honours:
|
|
87
|
+
- `shapeCast` `result.t` is a **world distance** when direction is
|
|
88
|
+
unit-length and `tMax = sweepLength`. New casts keep that.
|
|
89
|
+
- `compute_penetration` accepts the convex player capsule vs any
|
|
90
|
+
shape (only concave-vs-concave throws); `out_direction` is the unit
|
|
91
|
+
**B→A** separation, depth is the scalar return. Push the capsule (A)
|
|
92
|
+
out along `+out_direction · depth`.
|
|
93
|
+
- `shapeCast` `result.position` is the swept **centre**, not the
|
|
94
|
+
contact point — we derive ground-surface Y from the cast `t` + the
|
|
95
|
+
capsule's bottom offset, so we never need a contact point.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 4. Target architecture — a dedicated `KinematicMover`
|
|
100
|
+
|
|
101
|
+
Extract the collision solver into its own module (name placeholder —
|
|
102
|
+
`KinematicMover` / `CharacterMover`), testable in isolation against a
|
|
103
|
+
real `PhysicsSystem`, not buried in the 1500-line system file. Matches
|
|
104
|
+
the repo's extraction taste (Spring, EyeOffsetStack, FirstPersonSensors).
|
|
105
|
+
|
|
106
|
+
It knows nothing about the controller — just a capsule, a velocity, the
|
|
107
|
+
world, and an `up` axis. Single entry point:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
move(transform, shape, velocity, dt, filter) → MoveResult
|
|
111
|
+
// mutates transform.position; returns:
|
|
112
|
+
// { velocityX/Y/Z (corrected), grounded, groundNormal, hit }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Modelled on modern kinematic character controllers (Jolt
|
|
116
|
+
`CharacterVirtual`, Source `TryPlayerMove`+`CategorizePosition`), the
|
|
117
|
+
move is an explicit sequence — **recover → slide → (stairs) → ground →
|
|
118
|
+
settle**:
|
|
119
|
+
|
|
120
|
+
1. **Recover (depenetration).** Before moving, get clear. `overlap()`
|
|
121
|
+
the capsule; for each body, `compute_penetration` → push the capsule
|
|
122
|
+
out by `depth · dir`; iterate a few times (deepest-first) to
|
|
123
|
+
convergence. Handles spawned-in-geometry, world-crushed-into-us, and
|
|
124
|
+
the start-solid case a pure sweep *cannot*. This is now a **core
|
|
125
|
+
step**, available from Phase 1 — not a bolt-on — because
|
|
126
|
+
`compute_penetration` is public.
|
|
127
|
+
|
|
128
|
+
2. **Sweep-and-slide (unified 3D).** Sweep the *full* velocity·dt
|
|
129
|
+
(gravity folded in — no H/V split) via `shapeCast`; stop at TOI,
|
|
130
|
+
clip velocity onto the contact tangent, project the residual, repeat
|
|
131
|
+
up to N iterations. **Crease-aware**: a second plane re-violating the
|
|
132
|
+
first → project onto the seam `normalize(n₁×n₂)`; a third plane in
|
|
133
|
+
one move → dead-stop. Walls, floors, ceilings are all just contact
|
|
134
|
+
planes, so anti-tunnelling (H and V) falls out of one loop.
|
|
135
|
+
|
|
136
|
+
3. **Stairs (explicit, optional per move).** When grounded and blocked
|
|
137
|
+
by a low step: sweep up by `stepHeight`, sweep the residual forward,
|
|
138
|
+
sweep back down — the Jolt/Source step pattern. Replaces the 5 cm
|
|
139
|
+
implicit-cast hack with real stair climbing, gated so walls taller
|
|
140
|
+
than `stepHeight` still block.
|
|
141
|
+
|
|
142
|
+
4. **Ground categorize + stick.** Short downward `shapeCast` (capsule
|
|
143
|
+
swept down a `SKIN + band`); if it hits within the band with
|
|
144
|
+
`normal.y ≥ MIN_WALK_NORMAL` (~0.7), set `grounded`, capture the
|
|
145
|
+
normal, snap to the surface, and kill the into-ground velocity
|
|
146
|
+
component. Band-test + active snap (not a strict `y ≤ testY`) is what
|
|
147
|
+
structurally kills the landing **bounce**. Too-steep normal →
|
|
148
|
+
not grounded → the slope was already a slide plane in step 2, so the
|
|
149
|
+
player slides down it.
|
|
150
|
+
|
|
151
|
+
5. **Settle.** One final `compute_penetration` recover pass guarantees
|
|
152
|
+
the tick ends penetration-free (resting-contact float drift
|
|
153
|
+
insurance; usually a no-op).
|
|
154
|
+
|
|
155
|
+
**No-slip ⇔ walkable (one gate, not two).** Between recover and slide,
|
|
156
|
+
when the feet rest on walkable ground (`_groundedAt` — a walkable normal
|
|
157
|
+
within the stick band, the *same* `MIN_WALK_NORMAL` as step 4), the
|
|
158
|
+
downward **gravity** velocity is dropped before the slide. Otherwise
|
|
159
|
+
gravity projects onto a ramp's tangent as a downhill **drift** and a
|
|
160
|
+
standing player creeps down every slope it can walk. A biped grips any
|
|
161
|
+
slope it could walk up; the instant its feet slip it could not have
|
|
162
|
+
climbed either — so *"doesn't slide down"* and *"can be walked up"* are
|
|
163
|
+
the same threshold, not two independent tunables. A too-steep face fails
|
|
164
|
+
the gate, keeps its gravity, and slides (step 4) — purchase and no-slip
|
|
165
|
+
appear and vanish together as the one knob moves. A jump (`v.y > 0`) is
|
|
166
|
+
exempt so the launch is never cancelled.
|
|
167
|
+
|
|
168
|
+
Slope handling, multi-body resting contact, and unstick all fall out of
|
|
169
|
+
steps 1/2/4 — no special cases. Abilities get correct collision for free
|
|
170
|
+
by routing their motion through the same `move()`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 5. Phased plan (rebuild, not patch)
|
|
175
|
+
|
|
176
|
+
### Phase 1 — `KinematicMover`: recover + unified sweep-and-slide ✅ **landed**
|
|
177
|
+
Stand up the module. Implement **recover** (overlap +
|
|
178
|
+
compute_penetration) and **unified 3D sweep-and-slide** (crease-aware).
|
|
179
|
+
The system calls `move()` for the combined velocity·dt, replacing both
|
|
180
|
+
`_moveAndSlide` and the direct vertical add. Grounding stays on the old
|
|
181
|
+
resolver as a short-lived interim shim (removed in Phase 2) so this
|
|
182
|
+
phase isolates "does the new mover move correctly."
|
|
183
|
+
|
|
184
|
+
**Guard tests** (`KinematicMover.spec.js`, against a real PhysicsSystem):
|
|
185
|
+
spawn straddling a box → recovered outside within a tick; wall stop;
|
|
186
|
+
slide-along axis-aligned + oblique wall; **vertical** anti-tunnel (drop
|
|
187
|
+
through a thin slab → lands on top); jump into ceiling → stops; corner
|
|
188
|
+
crease → clean stop, no leak/chatter. Port the existing
|
|
189
|
+
`MoveAndSlide.spec.js` scenarios through the new path.
|
|
190
|
+
|
|
191
|
+
**Risk:** medium. New module, but isolated; old grounding still in place.
|
|
192
|
+
|
|
193
|
+
### Phase 2 — Ground categorize + slope + stick (inside the mover) ✅ **landed**
|
|
194
|
+
Add steps 4 (categorize/stick) to the mover. Down-`shapeCast` →
|
|
195
|
+
`{grounded, normal}`; `MIN_WALK_NORMAL` gate; snap + kill into-ground
|
|
196
|
+
velocity; reproject grounded velocity onto the slope. **Remove the
|
|
197
|
+
interim grounding shim and the scalar resolver authority** (resolver
|
|
198
|
+
demoted to no-physics fallback *inside* the probe, behind one function —
|
|
199
|
+
uniform flow). Feed `grounded`/`groundNormal` back to control.
|
|
200
|
+
|
|
201
|
+
**Guard tests** (`GroundSlope.spec.js`): walk up a 30° ramp keeps speed,
|
|
202
|
+
stays grounded, correct normal; stationary-on-floor stays at y≈0 across
|
|
203
|
+
120 ticks (the no-bounce repro, now with nothing masking it); 60° face →
|
|
204
|
+
not grounded, slides down, not glued.
|
|
205
|
+
|
|
206
|
+
**Risk:** medium. Changes grounding semantics; the bounce reconciliation
|
|
207
|
+
lives here.
|
|
208
|
+
|
|
209
|
+
### Phase 3 — Stairs ✅ **landed**
|
|
210
|
+
Real stair climbing, gated by `stepHeight` (default 0.3 m, just under
|
|
211
|
+
the capsule radius). Three cooperating pieces — it took iterating to
|
|
212
|
+
find that all three are needed:
|
|
213
|
+
|
|
214
|
+
1. **Explicit step-up** (`_tryStepUp`, Source/Jolt up-forward-down).
|
|
215
|
+
When a grounded move is blocked, decide step-vs-wall with a **thin
|
|
216
|
+
horizontal ray at the step-height plane** (`sy + stepHeight + skin`),
|
|
217
|
+
then — only if clear — lift by stepHeight, advance the residual
|
|
218
|
+
forward, drop onto the step, commit when it gained ground within
|
|
219
|
+
stepHeight, and **restore the horizontal velocity**. The velocity
|
|
220
|
+
restore is the crux for REALISTIC stairs: the capsule's round bottom
|
|
221
|
+
also rolls up low risers, but only with sustained forward momentum,
|
|
222
|
+
and a controller that reads back the slide-clipped velocity loses
|
|
223
|
+
that momentum at the riser and stalls. The step-up climbs without
|
|
224
|
+
depending on momentum and hands the velocity back so the player keeps
|
|
225
|
+
moving up the flight.
|
|
226
|
+
|
|
227
|
+
The step-vs-wall ray is what stops the round bottom climbing a
|
|
228
|
+
too-tall wall — and the reason it's a THIN ray, not the obvious
|
|
229
|
+
"lift the capsule and sweep it forward" clearance cast: lifted so its
|
|
230
|
+
tip sits at stepHeight, the capsule's rounded bottom narrows through
|
|
231
|
+
the band `(stepHeight, stepHeight+radius)`, so a wall whose top lands
|
|
232
|
+
in that band is never reached by the swept shape — it reads "clear"
|
|
233
|
+
and the player climbs it, *faster speed reaching further into the
|
|
234
|
+
round-off* (the gray-box bug: jitter at a walk, clean climb at a
|
|
235
|
+
sprint). A thin ray has no round-off: it reads the obstacle's true
|
|
236
|
+
height, so a step (top below the plane) is passed over and a wall
|
|
237
|
+
(top above) is struck on its front face, at any approach speed.
|
|
238
|
+
|
|
239
|
+
The ray is cast along the **blocked direction** (the horizontal
|
|
240
|
+
velocity the slide removed = the obstacle's inward normal), from the
|
|
241
|
+
**pre-slide** centre, reaching the swept distance plus a forward
|
|
242
|
+
extent. Both matter for OBLIQUE approaches: at 45° the capsule meets a
|
|
243
|
+
wall with its normal-direction extent, so a ray along *travel* stops
|
|
244
|
+
short of the face and reads a wall as clear (climbing it); and
|
|
245
|
+
rounding a convex corner the slide carries the *post*-slide centre
|
|
246
|
+
just past the corner, so a ray from there shoots past the obstacle —
|
|
247
|
+
the pre-slide centre was still in front of what blocked it. The reach
|
|
248
|
+
tracking the sweep isn't the banned speed coupling: the climb-or-block
|
|
249
|
+
decision is the step-height plane alone; the reach only governs how
|
|
250
|
+
far ahead to look for what the slide already hit.
|
|
251
|
+
|
|
252
|
+
A single ray still can't catch a convex *point*: grazing a corner a
|
|
253
|
+
few degrees off its bisector, the blocked normal runs nearly parallel
|
|
254
|
+
to a face, so the ray slips past the corner just outside the footprint
|
|
255
|
+
and reads clear. The backstop is a post-hoc OVERLAP query — after
|
|
256
|
+
up-forward-down, if the destination overlaps geometry the climb landed
|
|
257
|
+
the round body ON a wall corner, not on a step (a real step top is
|
|
258
|
+
rested on `skin` above, never overlapping), so it's rejected. Probe for
|
|
259
|
+
the common case, overlap-test to catch what a ray threads past.
|
|
260
|
+
|
|
261
|
+
The same round-bottom perch is why the slide keeps every contact's true
|
|
262
|
+
normal (no "flatten steep contacts to vertical" — that would also rob
|
|
263
|
+
a too-steep *slope* of its downhill slide) and why the footprint
|
|
264
|
+
mount in (2) is gated on height above the **centre surface**, not the
|
|
265
|
+
feet (gating on the feet lets a capsule that rode up a hair mount the
|
|
266
|
+
next sliver and ratchet up a wall).
|
|
267
|
+
2. **Two-probe ground categorize** for honest grounded-ness on a step
|
|
268
|
+
edge — *walkability* from a centre raycast (ignores a step's convex
|
|
269
|
+
top edge and a wall's side face, so a steep *slope* is correctly
|
|
270
|
+
not-grounded) and *rest height* from a footprint shapecast (mounts
|
|
271
|
+
a step the leading edge overhangs). A single probe can't tell a
|
|
272
|
+
climbable step edge from a steep slope; the split can.
|
|
273
|
+
3. **Step-DOWN** = the ground-stick reach is `stepHeight`: walking off
|
|
274
|
+
a drop ≤ stepHeight snaps onto the lower surface (stays grounded);
|
|
275
|
+
a larger drop goes airborne.
|
|
276
|
+
|
|
277
|
+
Footgun caught in the prototype: my first stair test used *deep
|
|
278
|
+
overlapping* steps, where the round-bottom roll alone climbs and the
|
|
279
|
+
explicit step-up looked unnecessary. Realistic *thin* treads (shallower
|
|
280
|
+
than the capsule footprint) need the step-up — see the thin-tread tests.
|
|
281
|
+
|
|
282
|
+
**Guard tests** (`collision/Stairs.spec.js` + `KinematicMoverIntegration.spec.js`,
|
|
283
|
+
all green): climb a 5-step staircase; climb a thin-tread staircase
|
|
284
|
+
(treads < footprint) both at the mover level and through the full
|
|
285
|
+
controller; clear a single curb; a 0.5 m riser (> stepHeight) blocks;
|
|
286
|
+
a 0.5 m riser is a **clean wall — no edge ride-up, and clearing is
|
|
287
|
+
speed-independent** (walk and sprint both stop dead, the gray-box guard);
|
|
288
|
+
descend a staircase grounded the whole way.
|
|
289
|
+
|
|
290
|
+
### Phase 4 — Motor seam + delete old code ✅ **landed**
|
|
291
|
+
The mover is now the **only** collision path when a `PhysicsSystem` is
|
|
292
|
+
present — the opt-in flag is gone; dispatch is on physics presence, not
|
|
293
|
+
a flag.
|
|
294
|
+
|
|
295
|
+
What shipped:
|
|
296
|
+
- `_integrateVerticalAndResolveGround` is now a thin dispatcher:
|
|
297
|
+
`_applyGravity` (the motor) → `_moveViaMover` (physics) **or**
|
|
298
|
+
`_moveFlatGround` (no physics) → `_detectJumpApex`.
|
|
299
|
+
- **Gravity** extracted to `_applyGravity`; **land / leave-ground**
|
|
300
|
+
extracted to `_onLand` / `_onLeaveGround`, shared by both move paths
|
|
301
|
+
(the duplicated scaffolding from Phase 1's wiring is gone).
|
|
302
|
+
- **Deleted** `_moveAndSlide` (+ its `CAST_STEP_HEIGHT` / `SKIN`
|
|
303
|
+
constants and the `slideRay` / `slideHit` scratch), the legacy ground-
|
|
304
|
+
resolution body, and the `useKinematicMover` flag.
|
|
305
|
+
- `MoveAndSlide.spec.js` deleted — it tested `_moveAndSlide`; its
|
|
306
|
+
scenarios (wall-stop, axis + oblique slide, anti-tunnel) are covered
|
|
307
|
+
by `collision/KinematicMover.spec.js`.
|
|
308
|
+
|
|
309
|
+
What stayed (deliberately):
|
|
310
|
+
- `useBuiltInFlatGround` / `groundY` / `groundResolver` remain — they're
|
|
311
|
+
the **no-physics** fallback (`_moveFlatGround`) for headless and
|
|
312
|
+
control-layer unit tests, and the resolver is still spec-covered. With
|
|
313
|
+
physics present they're unused (the mover probes the world).
|
|
314
|
+
- **WallRun still self-integrates** its reduced-gravity model rather than
|
|
315
|
+
routing through `move()`. Slide already routes through the mover (it
|
|
316
|
+
calls `_integrateVerticalAndResolveGround`); WallRun's custom model is
|
|
317
|
+
left as a future enhancement, not required for the legacy deletion.
|
|
318
|
+
|
|
319
|
+
**Result:** 29 suites / 236 tests green; the entire ability + jump +
|
|
320
|
+
momentum + posture suite passes through the consolidated path.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 6. Physics requirements — status
|
|
325
|
+
|
|
326
|
+
**Resolved.** My one HIGH ask from the review — a public penetration
|
|
327
|
+
query — is **satisfied**: `compute_penetration` is public and is exactly
|
|
328
|
+
the right shape (unit B→A direction + scalar depth, capsule-vs-anything).
|
|
329
|
+
It graduates from "deferred safety net" to the **core recover/settle
|
|
330
|
+
primitive** the whole mover leans on (steps 1 & 5). Confirmed it works:
|
|
331
|
+
yes, this is precisely what's needed — thank you for keeping it decoupled
|
|
332
|
+
and public.
|
|
333
|
+
|
|
334
|
+
**No remaining blocking requirements.** Phases 1–4 need nothing further
|
|
335
|
+
from the physics engine. The two low-priority niceties from the review
|
|
336
|
+
turned out not to bite:
|
|
337
|
+
- *Contact point from `shapeCast`* — not needed; ground-surface Y is
|
|
338
|
+
derivable from the cast `t` + the capsule bottom offset.
|
|
339
|
+
- *Multi-contact manifold from one query* — covered for resting contact
|
|
340
|
+
by iterating `compute_penetration` over **every** `overlap()` body in
|
|
341
|
+
the recover pass; the single-sweep corner case is handled by the slide
|
|
342
|
+
iteration instead. So no native multi-contact query is required.
|
|
343
|
+
|
|
344
|
+
If anything *would* be a future nicety (not needed now): a swept query
|
|
345
|
+
returning the *set* of TOI-simultaneous contacts (for one-pass corner
|
|
346
|
+
creases instead of iterating). Strictly an optimisation; the iterative
|
|
347
|
+
slide is correct without it.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 7. Constants (ours ← reference lineage)
|
|
352
|
+
|
|
353
|
+
| Constant | Plan value | Reference | Phase |
|
|
354
|
+
|---|---|---|---|
|
|
355
|
+
| slide iterations | 4 | Quake `numbumps` 4 | 1 |
|
|
356
|
+
| `SKIN` | 0.005 m | Fauerby `veryCloseDistance` ~0.005 | 1 |
|
|
357
|
+
| recover max iters | ~4 | — (convergence cap) | 1 |
|
|
358
|
+
| `MIN_WALK_NORMAL` | ~0.7 (≈45°) | Quake3 0.7 / Source `normal.z ≥ 0.7` | 2 |
|
|
359
|
+
| ground-probe band | ~0.06 m | Source `StayOnGround` ~2 u | 2 |
|
|
360
|
+
| crease dead-stop | zero on 3rd plane | Quake `SV_FlyMove` | 1 |
|
|
361
|
+
| `stepHeight` | ~0.3 m (tunable) | Source `sv_stepsize` 18 u (~0.34 m) | 3 |
|
|
362
|
+
|
|
363
|
+
Clip stays at effective overbounce `1.0` gated on into-wall (`v·n < 0`),
|
|
364
|
+
relying on `SKIN` for clearance — equivalent in practice to Quake3's
|
|
365
|
+
`OVERCLIP 1.001` nudge; no change planned.
|