@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,809 +1,943 @@
|
|
|
1
|
-
# Physics engine — state of play
|
|
2
|
-
|
|
3
|
-
Tracker for what's built, what's pending, and what's deferred.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Context
|
|
8
|
-
|
|
9
|
-
Deterministic JS rigid-body physics engine for the meep ECS. Target: game
|
|
10
|
-
scenarios with up to millions of mostly-sleeping bodies, deterministic replays
|
|
11
|
-
for netcode and reproducible debugging, broad shape coverage for common game
|
|
12
|
-
collisions. Pure JS — no WASM, no SIMD, no worker threads.
|
|
13
|
-
|
|
14
|
-
Architectural references for design choices:
|
|
15
|
-
- **Jolt** — pre-allocated body pool, active-list iteration, two-tree
|
|
16
|
-
broadphase (static + dynamic).
|
|
17
|
-
- **Bullet** — `btPersistentManifold` cache layout with up to 4 points.
|
|
18
|
-
- **Box2D / Catto** — sequential impulse with warm-starting, Sutherland-Hodgman
|
|
19
|
-
face clipping for box-box.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Done
|
|
24
|
-
|
|
25
|
-
### Foundations
|
|
26
|
-
- `RigidBody`, `Collider`, `BodyKind`, `RigidBodyFlags`, `ColliderFlags`,
|
|
27
|
-
`SleepState`, `PhysicsEvents`.
|
|
28
|
-
- `BodyStorage`: SoA pool, generation-tracked stable IDs, dense awake list,
|
|
29
|
-
min-heap free for deterministic ID reuse.
|
|
30
|
-
- `PhysicsSystem`: full public API surface (gravity, force/impulse with and
|
|
31
|
-
without application point, torque, velocity setter, wake/sleep, contact
|
|
32
|
-
filter callback).
|
|
33
|
-
- Binary serialization adapters for `RigidBody` and `Collider` (transient
|
|
34
|
-
runtime state deliberately excluded).
|
|
35
|
-
- `PairUint32Map`: open-addressed Robin Hood + Fibonacci hash for the
|
|
36
|
-
pair → manifold-slot index (the one new collection added to `core/collection/`).
|
|
37
|
-
|
|
38
|
-
### Pipeline (`PhysicsSystem.fixedUpdate`)
|
|
39
|
-
1. Velocity integration (semi-implicit Euler, linear + angular, gravity,
|
|
40
|
-
damping, world-frame inverse-inertia for torque)
|
|
41
|
-
2. Per-collider broadphase refit with fat AABB (Box2D-style velocity-padded
|
|
42
|
-
slack)
|
|
43
|
-
3. Pair generation: per-leaf query against both BVHs (static + dynamic),
|
|
44
|
-
canonical `(min, max)` pairs, dedup via manifold touched flag
|
|
45
|
-
4. Wake propagation for sleeping bodies in the pair list
|
|
46
|
-
5. Narrowphase cross-product over collider lists
|
|
47
|
-
6. Sequential-impulse solver (Catto-style, warm-start, friction, Baumgarte)
|
|
48
|
-
7. Position integration (linear + quaternion)
|
|
49
|
-
8. Sleep test (per-body velocity² below threshold for ≥ 0.5 s)
|
|
50
|
-
9. Manifold diff → `ContactBegin` / `Stay` / `End` event dispatch
|
|
51
|
-
10. `manifolds.advance_frame()` — roll touched bits, evict grace-expired slots
|
|
52
|
-
|
|
53
|
-
### Shape coverage
|
|
54
|
-
| Pair | Path | Manifold |
|
|
55
|
-
|---|---|---|
|
|
56
|
-
| sphere-sphere | closed-form | 1 point |
|
|
57
|
-
| sphere-box | closed-form (handles centre-inside-box) | 1 point |
|
|
58
|
-
| capsule-sphere | point-on-segment closed-form | 1 point |
|
|
59
|
-
| capsule-capsule | segment-segment closest pair | 1 point |
|
|
60
|
-
| capsule-box | iterative segment-vs-OBB (primary) + cap-centre sphere-vs-OBB at each endpoint | up to 3 |
|
|
61
|
-
| box-box face-face | SAT + Sutherland-Hodgman clipping | up to 4 |
|
|
62
|
-
| box-box edge-edge | SAT + segment-segment closest-pair | 1 point |
|
|
63
|
-
| sphere / box / capsule × concave (heightmap, mesh) | closed-form `*_triangle_contact` per triangle via decomposition dispatcher | up to a few points per triangle (deepest wins) |
|
|
64
|
-
| other convex × concave | per-triangle GJK + EPA via decomposition dispatcher | 1 point per triangle |
|
|
65
|
-
| anything else | GJK + EPA, MPR fallback on EPA non-convergence | 1 point |
|
|
66
|
-
|
|
67
|
-
### Non-convex shapes
|
|
68
|
-
- **`is_convex` flag** on `AbstractShape3D.prototype` (default `true`).
|
|
69
|
-
Overridden to `false` on `HeightMapShape3D`, `MeshShape3D`, `UnionShape3D`.
|
|
70
|
-
`TransformedShape3D` inherits via getter that reads the wrapped subject.
|
|
71
|
-
- **`HeightMapShape3D`** — orientation-vector + `Sampler2D`-backed terrain
|
|
72
|
-
shape. Heights sampled via `sampleChannelCatmullRomUV` (matching the
|
|
73
|
-
terrain system's geometry construction). Compute_bounding_box,
|
|
74
|
-
contains_point, signed_distance, nearest_point_on_surface all
|
|
75
|
-
implemented; `support` throws (non-convex by construction).
|
|
76
|
-
- **`Triangle3D`** — buffer-flyweight convex shape. `bind(buffer, offset)`
|
|
77
|
-
repoints at 9 consecutive floats in an external Float64Array. Zero
|
|
78
|
-
allocation per emission; used by the decomposition path.
|
|
79
|
-
- **Triangle decomposition machinery** under
|
|
80
|
-
`engine/physics/narrowphase/decomposition/`:
|
|
81
|
-
- `TRIANGLE_FLOAT_STRIDE = 10` per triangle (`vA.xyz`, `vB.xyz`,
|
|
82
|
-
`vC.xyz`, `feature_id`).
|
|
83
|
-
- `heightmap_enumerate_triangles(out, offset, shape, ...aabb)` —
|
|
84
|
-
Arvo-projects the convex's AABB into heightmap-local, intersects
|
|
85
|
-
with the footprint to derive a cell range, emits 2 triangles per
|
|
86
|
-
cell with stable feature_ids.
|
|
87
|
-
- `mesh_enumerate_triangles(out, offset, shape, ...aabb)` — linear
|
|
88
|
-
O(N) scan over `MeshShape3D.indices` with tight per-triangle AABB
|
|
89
|
-
filtering. feature_id = triangle index.
|
|
90
|
-
- `aabb_world_to_local(out, world_aabb, pos, rot)` — 8-corner
|
|
91
|
-
projection of a world AABB into a body's local frame.
|
|
92
|
-
- `decompose_to_triangles(...)` — dispatcher switching on shape
|
|
93
|
-
type marker.
|
|
94
|
-
- **Narrowphase concave dispatch** in `narrowphase_step.js`: detects
|
|
95
|
-
`is_convex === false`, computes convex's world AABB, projects to
|
|
96
|
-
concave's local frame, decomposes, per-triangle GJK + EPA with
|
|
97
|
-
one-sided face-normal rejection and contact-normal dedup. Concave-vs-
|
|
98
|
-
concave dynamic pairs are explicitly refused.
|
|
99
|
-
|
|
100
|
-
### Solver
|
|
101
|
-
- Sequential impulse with warm-starting (10 velocity iterations by default).
|
|
102
|
-
- Coulomb friction with disk-clamped tangent impulses.
|
|
103
|
-
- Baumgarte position correction folded into the velocity solve.
|
|
104
|
-
- Full angular Jacobian (`I_w⁻¹ = R · diag · R^T`) and angular impulse
|
|
105
|
-
application.
|
|
106
|
-
- Public force/impulse-at-point API (`applyForceAt`, `applyImpulseAt`,
|
|
107
|
-
`applyTorque`).
|
|
108
|
-
|
|
109
|
-
### Sleep + events
|
|
110
|
-
- Per-island **atomic sleep**: an island sleeps when `max(|v|² + |ω|²)`
|
|
111
|
-
across all members stays below the threshold long enough; the whole
|
|
112
|
-
island sleeps in the same frame. Replaces the per-body chatter on
|
|
113
|
-
weakly-connected piles.
|
|
114
|
-
- **Atomic wake**: members of a sleeping island are threaded into a
|
|
115
|
-
circular doubly-linked list (`sleep_group_next` / `sleep_group_prev`);
|
|
116
|
-
waking any one member walks the chain and wakes the rest in the same
|
|
117
|
-
call. A 100-block stack hit at the base wakes top-down in one frame
|
|
118
|
-
rather than over 100 frames of broadphase propagation.
|
|
119
|
-
- `DisableSleep` on any island member exempts the whole island.
|
|
120
|
-
- ContactBegin / Stay / End buffer + dispatch through both
|
|
121
|
-
`PhysicsSystem.onContactBegin/Stay/End` Signals and the per-entity
|
|
122
|
-
`entity.sendEvent(PhysicsEvents.ContactBegin, ...)` channel (when a
|
|
123
|
-
dataset is attached).
|
|
124
|
-
|
|
125
|
-
### Islands
|
|
126
|
-
- **Union-find** with path halving + union by min-index over the awake-body
|
|
127
|
-
+ touched-contact graph (`engine/physics/island/union_find.js`).
|
|
128
|
-
- **`IslandBuilder`** produces deterministic CSR-style output: bodies and
|
|
129
|
-
manifold slots grouped by island, sorted ascending within and across
|
|
130
|
-
islands. Static / kinematic bodies are constraint anchors only — they
|
|
131
|
-
don't merge islands, so disjoint piles on the same floor are separate
|
|
132
|
-
islands.
|
|
133
|
-
- **
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- `
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
the
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
concave
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
few
|
|
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
|
-
per
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
- [x]
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
constraints
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
position
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
`
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
- [x] **
|
|
540
|
-
`IslandBuilder` Pass 1b
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
- [x] **
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
the
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
**
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
###
|
|
791
|
-
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
-
|
|
1
|
+
# Physics engine — state of play
|
|
2
|
+
|
|
3
|
+
Tracker for what's built, what's pending, and what's deferred.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Deterministic JS rigid-body physics engine for the meep ECS. Target: game
|
|
10
|
+
scenarios with up to millions of mostly-sleeping bodies, deterministic replays
|
|
11
|
+
for netcode and reproducible debugging, broad shape coverage for common game
|
|
12
|
+
collisions. Pure JS — no WASM, no SIMD, no worker threads.
|
|
13
|
+
|
|
14
|
+
Architectural references for design choices:
|
|
15
|
+
- **Jolt** — pre-allocated body pool, active-list iteration, two-tree
|
|
16
|
+
broadphase (static + dynamic).
|
|
17
|
+
- **Bullet** — `btPersistentManifold` cache layout with up to 4 points.
|
|
18
|
+
- **Box2D / Catto** — sequential impulse with warm-starting, Sutherland-Hodgman
|
|
19
|
+
face clipping for box-box.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Done
|
|
24
|
+
|
|
25
|
+
### Foundations
|
|
26
|
+
- `RigidBody`, `Collider`, `BodyKind`, `RigidBodyFlags`, `ColliderFlags`,
|
|
27
|
+
`SleepState`, `PhysicsEvents`.
|
|
28
|
+
- `BodyStorage`: SoA pool, generation-tracked stable IDs, dense awake list,
|
|
29
|
+
min-heap free for deterministic ID reuse.
|
|
30
|
+
- `PhysicsSystem`: full public API surface (gravity, force/impulse with and
|
|
31
|
+
without application point, torque, velocity setter, wake/sleep, contact
|
|
32
|
+
filter callback).
|
|
33
|
+
- Binary serialization adapters for `RigidBody` and `Collider` (transient
|
|
34
|
+
runtime state deliberately excluded).
|
|
35
|
+
- `PairUint32Map`: open-addressed Robin Hood + Fibonacci hash for the
|
|
36
|
+
pair → manifold-slot index (the one new collection added to `core/collection/`).
|
|
37
|
+
|
|
38
|
+
### Pipeline (`PhysicsSystem.fixedUpdate`)
|
|
39
|
+
1. Velocity integration (semi-implicit Euler, linear + angular, gravity,
|
|
40
|
+
damping, world-frame inverse-inertia for torque)
|
|
41
|
+
2. Per-collider broadphase refit with fat AABB (Box2D-style velocity-padded
|
|
42
|
+
slack)
|
|
43
|
+
3. Pair generation: per-leaf query against both BVHs (static + dynamic),
|
|
44
|
+
canonical `(min, max)` pairs, dedup via manifold touched flag
|
|
45
|
+
4. Wake propagation for sleeping bodies in the pair list
|
|
46
|
+
5. Narrowphase cross-product over collider lists
|
|
47
|
+
6. Sequential-impulse solver (Catto-style, warm-start, friction, Baumgarte)
|
|
48
|
+
7. Position integration (linear + quaternion)
|
|
49
|
+
8. Sleep test (per-body velocity² below threshold for ≥ 0.5 s)
|
|
50
|
+
9. Manifold diff → `ContactBegin` / `Stay` / `End` event dispatch
|
|
51
|
+
10. `manifolds.advance_frame()` — roll touched bits, evict grace-expired slots
|
|
52
|
+
|
|
53
|
+
### Shape coverage
|
|
54
|
+
| Pair | Path | Manifold |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| sphere-sphere | closed-form | 1 point |
|
|
57
|
+
| sphere-box | closed-form (handles centre-inside-box) | 1 point |
|
|
58
|
+
| capsule-sphere | point-on-segment closed-form | 1 point |
|
|
59
|
+
| capsule-capsule | segment-segment closest pair | 1 point |
|
|
60
|
+
| capsule-box | iterative segment-vs-OBB (primary) + cap-centre sphere-vs-OBB at each endpoint | up to 3 |
|
|
61
|
+
| box-box face-face | SAT + Sutherland-Hodgman clipping | up to 4 |
|
|
62
|
+
| box-box edge-edge | SAT + segment-segment closest-pair | 1 point |
|
|
63
|
+
| sphere / box / capsule × concave (heightmap, mesh) | closed-form `*_triangle_contact` per triangle via decomposition dispatcher | up to a few points per triangle (deepest wins) |
|
|
64
|
+
| other convex × concave | per-triangle GJK + EPA via decomposition dispatcher | 1 point per triangle |
|
|
65
|
+
| anything else | GJK + EPA, MPR fallback on EPA non-convergence | 1 point |
|
|
66
|
+
|
|
67
|
+
### Non-convex shapes
|
|
68
|
+
- **`is_convex` flag** on `AbstractShape3D.prototype` (default `true`).
|
|
69
|
+
Overridden to `false` on `HeightMapShape3D`, `MeshShape3D`, `UnionShape3D`.
|
|
70
|
+
`TransformedShape3D` inherits via getter that reads the wrapped subject.
|
|
71
|
+
- **`HeightMapShape3D`** — orientation-vector + `Sampler2D`-backed terrain
|
|
72
|
+
shape. Heights sampled via `sampleChannelCatmullRomUV` (matching the
|
|
73
|
+
terrain system's geometry construction). Compute_bounding_box,
|
|
74
|
+
contains_point, signed_distance, nearest_point_on_surface all
|
|
75
|
+
implemented; `support` throws (non-convex by construction).
|
|
76
|
+
- **`Triangle3D`** — buffer-flyweight convex shape. `bind(buffer, offset)`
|
|
77
|
+
repoints at 9 consecutive floats in an external Float64Array. Zero
|
|
78
|
+
allocation per emission; used by the decomposition path.
|
|
79
|
+
- **Triangle decomposition machinery** under
|
|
80
|
+
`engine/physics/narrowphase/decomposition/`:
|
|
81
|
+
- `TRIANGLE_FLOAT_STRIDE = 10` per triangle (`vA.xyz`, `vB.xyz`,
|
|
82
|
+
`vC.xyz`, `feature_id`).
|
|
83
|
+
- `heightmap_enumerate_triangles(out, offset, shape, ...aabb)` —
|
|
84
|
+
Arvo-projects the convex's AABB into heightmap-local, intersects
|
|
85
|
+
with the footprint to derive a cell range, emits 2 triangles per
|
|
86
|
+
cell with stable feature_ids.
|
|
87
|
+
- `mesh_enumerate_triangles(out, offset, shape, ...aabb)` — linear
|
|
88
|
+
O(N) scan over `MeshShape3D.indices` with tight per-triangle AABB
|
|
89
|
+
filtering. feature_id = triangle index.
|
|
90
|
+
- `aabb_world_to_local(out, world_aabb, pos, rot)` — 8-corner
|
|
91
|
+
projection of a world AABB into a body's local frame.
|
|
92
|
+
- `decompose_to_triangles(...)` — dispatcher switching on shape
|
|
93
|
+
type marker.
|
|
94
|
+
- **Narrowphase concave dispatch** in `narrowphase_step.js`: detects
|
|
95
|
+
`is_convex === false`, computes convex's world AABB, projects to
|
|
96
|
+
concave's local frame, decomposes, per-triangle GJK + EPA with
|
|
97
|
+
one-sided face-normal rejection and contact-normal dedup. Concave-vs-
|
|
98
|
+
concave dynamic pairs are explicitly refused.
|
|
99
|
+
|
|
100
|
+
### Solver
|
|
101
|
+
- Sequential impulse with warm-starting (10 velocity iterations by default).
|
|
102
|
+
- Coulomb friction with disk-clamped tangent impulses.
|
|
103
|
+
- Baumgarte position correction folded into the velocity solve.
|
|
104
|
+
- Full angular Jacobian (`I_w⁻¹ = R · diag · R^T`) and angular impulse
|
|
105
|
+
application.
|
|
106
|
+
- Public force/impulse-at-point API (`applyForceAt`, `applyImpulseAt`,
|
|
107
|
+
`applyTorque`).
|
|
108
|
+
|
|
109
|
+
### Sleep + events
|
|
110
|
+
- Per-island **atomic sleep**: an island sleeps when `max(|v|² + |ω|²)`
|
|
111
|
+
across all members stays below the threshold long enough; the whole
|
|
112
|
+
island sleeps in the same frame. Replaces the per-body chatter on
|
|
113
|
+
weakly-connected piles.
|
|
114
|
+
- **Atomic wake**: members of a sleeping island are threaded into a
|
|
115
|
+
circular doubly-linked list (`sleep_group_next` / `sleep_group_prev`);
|
|
116
|
+
waking any one member walks the chain and wakes the rest in the same
|
|
117
|
+
call. A 100-block stack hit at the base wakes top-down in one frame
|
|
118
|
+
rather than over 100 frames of broadphase propagation.
|
|
119
|
+
- `DisableSleep` on any island member exempts the whole island.
|
|
120
|
+
- ContactBegin / Stay / End buffer + dispatch through both
|
|
121
|
+
`PhysicsSystem.onContactBegin/Stay/End` Signals and the per-entity
|
|
122
|
+
`entity.sendEvent(PhysicsEvents.ContactBegin, ...)` channel (when a
|
|
123
|
+
dataset is attached).
|
|
124
|
+
|
|
125
|
+
### Islands
|
|
126
|
+
- **Union-find** with path halving + union by min-index over the awake-body
|
|
127
|
+
+ touched-contact graph (`engine/physics/island/union_find.js`).
|
|
128
|
+
- **`IslandBuilder`** produces deterministic CSR-style output: bodies and
|
|
129
|
+
manifold slots grouped by island, sorted ascending within and across
|
|
130
|
+
islands. Static / kinematic bodies are constraint anchors only — they
|
|
131
|
+
don't merge islands, so disjoint piles on the same floor are separate
|
|
132
|
+
islands.
|
|
133
|
+
- **Islands feed the sleep test + grouping, not a per-island solver loop.**
|
|
134
|
+
The TGS contact solver flattens every island's contacts into one
|
|
135
|
+
Gauss-Seidel sweep (`solver/solve_contacts.js`) — islands share no bodies,
|
|
136
|
+
so a single flat sweep is identical to per-island sweeps. The partition is
|
|
137
|
+
still rebuilt every step and consumed by the atomic-island sleep test and the
|
|
138
|
+
joint/contact island grouping; it is also the natural unit for a future
|
|
139
|
+
worker-based parallel solve (see Performance / Scale). (Earlier revisions ran
|
|
140
|
+
the solver per island; the TGS rewrite flattened it — same result, simpler
|
|
141
|
+
loop.)
|
|
142
|
+
|
|
143
|
+
### Compound bodies
|
|
144
|
+
- A body has 0..N attached colliders. Each collider has its own world
|
|
145
|
+
transform and its own BVH leaf.
|
|
146
|
+
- Same-entity colliders, child-entity colliders (via `ParentEntity`), or
|
|
147
|
+
hybrids all supported.
|
|
148
|
+
- `ColliderObserverSystem` auto-attaches colliders via the dataset when
|
|
149
|
+
paired with `PhysicsSystem` in an EntityManager.
|
|
150
|
+
- Narrowphase runs the cross-product over both bodies' collider lists per
|
|
151
|
+
body-pair, accumulates candidates, reduces to ≤4 contacts by
|
|
152
|
+
depth + spread.
|
|
153
|
+
|
|
154
|
+
### Public queries
|
|
155
|
+
- `raycast(origin, dir, max_dist, filter?)` — nearest hit across both trees,
|
|
156
|
+
**refined to the true shape surface** (narrowphase). `result.t` /
|
|
157
|
+
`result.normal` are exact for sphere / box / capsule / mesh / heightmap
|
|
158
|
+
colliders (per-leaf analytic ray tests + triangle Möller–Trumbore for
|
|
159
|
+
concave); composite convex shapes fall back to the broadphase AABB hit. A ray
|
|
160
|
+
crossing a fat leaf AABB but missing the true shape is correctly a miss.
|
|
161
|
+
- `shapeCast(ray, shape, rotation, result, filter?)` — broadphase swept
|
|
162
|
+
AABB against both BVHs; per-candidate AABB-slab interval narrowing,
|
|
163
|
+
coarse step over the narrowed window, GJK bisection to time-of-impact.
|
|
164
|
+
Output normal is the true contact-surface normal at the kiss point,
|
|
165
|
+
recovered by re-running GJK + EPA at `best_t` on the winning candidate.
|
|
166
|
+
Falls back to `-ray.direction` only on EPA degeneracies (NaN / zero
|
|
167
|
+
depth). Tests cover axis-aligned, off-axis, and oblique cube-vs-cube;
|
|
168
|
+
sphere-vs-smooth-shape near-tangent has documented angular tolerance
|
|
169
|
+
bands inherited from EPA on smooth supports.
|
|
170
|
+
- `overlap(shape, position, rotation, output, output_offset, filter?)`
|
|
171
|
+
— broadphase + per-candidate GJK overlap detection. Writes body_ids
|
|
172
|
+
into a caller-sized buffer; returns count. Convex query shapes only
|
|
173
|
+
(concave throws). Concave candidates routed through the per-triangle
|
|
174
|
+
decomposition path. Designed for speculative kinematic queries on
|
|
175
|
+
kinematic bodies (character controllers, AOE selection).
|
|
176
|
+
|
|
177
|
+
### Standalone narrowphase utilities
|
|
178
|
+
- `deepest_pair_penetration(out_normal, shapeA, posA, rotA, shapeB, posB,
|
|
179
|
+
rotB)` (exported from `narrowphase_step.js`) — runs the **same**
|
|
180
|
+
`dispatch_pair` the contact solver consumes for one posed shape pair and
|
|
181
|
+
returns the DEEPEST contact's depth + world normal (B → A). The single
|
|
182
|
+
source of truth for "minimum-translation between two posed shapes", reused by
|
|
183
|
+
`compute_penetration` (and available to any other query).
|
|
184
|
+
- `compute_penetration(out_direction, shape_a, pos_a, rot_a, shape_b,
|
|
185
|
+
pos_b, rot_b)` — non-system geometry primitive: positive penetration
|
|
186
|
+
depth + outward direction (B → A convention) on overlap, 0 otherwise.
|
|
187
|
+
**Hardened** — delegates to `deepest_pair_penetration`, so it is correct
|
|
188
|
+
(not "correct sometimes") for every shape pair the engine can build:
|
|
189
|
+
- sphere / box / capsule pairs → exact closed-form (box-box via SAT, so a
|
|
190
|
+
small body resting on a large floor reports the few-cm near-face overlap,
|
|
191
|
+
NOT the metres-deep "exit through the far side" that MPR's centroid-seeded
|
|
192
|
+
portal used to return);
|
|
193
|
+
- general convex pairs → GJK + EPA (exact for polytopes; curved shapes never
|
|
194
|
+
reach it — they have closed forms);
|
|
195
|
+
- convex × concave → triangle decomposition + the closed-form per-triangle
|
|
196
|
+
solvers, bounded to each triangle's true 2-D extent (the old closed-mesh
|
|
197
|
+
side-face over-report is gone).
|
|
198
|
+
The previous per-triangle half-space test is retained ONLY as a recovery
|
|
199
|
+
fallback for the one case the one-sided closed forms can't resolve: a convex
|
|
200
|
+
shape that has fully tunnelled to the *inner* side of a concave surface (a
|
|
201
|
+
depenetration query must still push it back out — exact for heightmap terrain,
|
|
202
|
+
a valid outward push for closed meshes). Concave × concave throws (M×N
|
|
203
|
+
triangle pairs out of scope). The spec asserts an "applying out_direction ×
|
|
204
|
+
depth separates the shapes" invariant across every convex+convex pair type and
|
|
205
|
+
convex+concave, plus exact per-type depths and the small-box-on-huge-floor
|
|
206
|
+
regression (3 m → 0.05 m).
|
|
207
|
+
|
|
208
|
+
### Determinism
|
|
209
|
+
- Direct typed-array writes on hot paths (bypassing `Vector3#set`'s observer
|
|
210
|
+
dispatch) — Transform writes still go through `set()` because external
|
|
211
|
+
systems subscribe (TransformAttachment, EntityNode, FogOfWarRevealer,
|
|
212
|
+
ViewportPosition).
|
|
213
|
+
- Active body iteration sorted by body index.
|
|
214
|
+
- Pair canonicalisation `(min, max)`.
|
|
215
|
+
- Min-heap free list for slot reuse.
|
|
216
|
+
- No `Math.random` anywhere in the simulation step.
|
|
217
|
+
- Same-runtime bit-exact determinism by design; cross-runtime is a known
|
|
218
|
+
future seam.
|
|
219
|
+
|
|
220
|
+
### Migration
|
|
221
|
+
- `Motion` / `MotionSystem` / `MotionSerializationAdapter` relocated from
|
|
222
|
+
the meep core (`engine/ecs/`) to the game-domain layer
|
|
223
|
+
(`mir-engine/model/game/ecs/`). meep no longer ships the legacy shim.
|
|
224
|
+
|
|
225
|
+
### Alternative narrowphase: MPR
|
|
226
|
+
- `engine/physics/gjk/mpr.js` — Minkowski Portal Refinement (XenoCollide,
|
|
227
|
+
Snethen GDC 2009). Single-pass overlap test + MTV computation,
|
|
228
|
+
output convention matches EPA so it's drop-in compatible at any
|
|
229
|
+
narrowphase call site. Tends to converge in 5–15 iterations on
|
|
230
|
+
smooth shapes where EPA stalls (the polytope-on-curved-surface
|
|
231
|
+
failure mode the torus-knot reproducer exercised). **Wired as the EPA
|
|
232
|
+
non-convergence fallback** in `narrowphase_step` at both the body-level
|
|
233
|
+
and per-triangle GJK+EPA paths: when EPA returns a non-positive / non-finite
|
|
234
|
+
depth, MPR is tried before giving up. `shape_cast` and `compute_penetration`
|
|
235
|
+
use it for the same reason.
|
|
236
|
+
|
|
237
|
+
### Bonus utilities
|
|
238
|
+
- `core/geom/3d/line/line3_closest_points_segment_segment.js` — generally
|
|
239
|
+
useful 3D segment-segment closest-pair via Ericson §5.1.9.
|
|
240
|
+
- `core/collection/PairUint32Map.js` — non-allocating
|
|
241
|
+
`Map<(u32, u32) → u32>` with Robin Hood + Fibonacci hash.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Limitations / Known caveats
|
|
246
|
+
|
|
247
|
+
- **Multi-collider material precision** — *resolved for contact materials.* The
|
|
248
|
+
narrowphase now combines the specific source-collider pair's friction /
|
|
249
|
+
restitution per contact and stores them in the manifold (CONTACT_STRIDE
|
|
250
|
+
offsets 14/15); the solver reads them per contact, so mixed-material compound
|
|
251
|
+
bodies are accurate (regression test: an asymmetric-friction body yaws when
|
|
252
|
+
shoved). Still primary-collider only: the contact-filter callback's
|
|
253
|
+
`colliderA/B` arguments and the body-level sensor / concave-dispatch flags —
|
|
254
|
+
a smaller follow-up.
|
|
255
|
+
- **EPA on smooth shapes**: degenerates (no flat face to converge on).
|
|
256
|
+
Mitigated by closed-form paths for sphere/cube/capsule pairs and by the
|
|
257
|
+
**MPR fallback** on EPA non-convergence; exotic convex shapes vs spheres can
|
|
258
|
+
still occasionally fail if both EPA and MPR degenerate.
|
|
259
|
+
- **EPA on `Triangle3D`** — *resolved.* The concave dispatch now uses the
|
|
260
|
+
closed-form `sphere_triangle_contact` / `box_triangle_contact` /
|
|
261
|
+
`capsule_triangle_contact` solvers (P1.1a–c) instead of per-triangle GJK+EPA
|
|
262
|
+
for those primitives, so a sphere/box/capsule on a heightmap or mesh decelerates
|
|
263
|
+
and settles correctly; the `narrowphase_concave.spec.js` "drop and settle"
|
|
264
|
+
cases and the mesh torus-knot settle test are **un-skipped**. Per-triangle
|
|
265
|
+
GJK+EPA remains only as the fallback for *other* convex shapes vs triangles.
|
|
266
|
+
(`compute_penetration` now routes through that same dispatch via
|
|
267
|
+
`deepest_pair_penetration` — see *Standalone narrowphase utilities* — instead
|
|
268
|
+
of its old half-space pre-test; the half-space test survives only as a
|
|
269
|
+
tunnel-recovery fallback.)
|
|
270
|
+
- **Box-box edge-edge contact**: a single point at the true closest-pair of the
|
|
271
|
+
two edges (P3.2), not the old body-centre midpoint. This is geometrically
|
|
272
|
+
correct — and an empirical SAT-source sweep confirms the edge-cross branch
|
|
273
|
+
*only* fires for **transverse** edge crossings (inter-edge angle ≈ 83-90°),
|
|
274
|
+
where two skew lines meet at a unique point. Near-parallel edge contacts
|
|
275
|
+
cannot reach this branch (a near-parallel `edgeA × edgeB` never wins the SAT
|
|
276
|
+
minimum) — they resolve through the multi-point face-clipping path. So the
|
|
277
|
+
once-planned "multi-point edge contact for near-parallel edges" refinement is
|
|
278
|
+
**moot**; see the resolved Stability backlog entry.
|
|
279
|
+
- **CCD floor only**: speculative margin via the fattened AABB prevents
|
|
280
|
+
most tunnelling. No per-body swept shape-cast for very fast objects.
|
|
281
|
+
- **Cross-runtime determinism is not guaranteed**: `Math.sin/cos/exp/log`
|
|
282
|
+
are ULP-correct but not bit-exact across V8 / SpiderMonkey / JSC.
|
|
283
|
+
- **Dynamic concave bodies under TGS** — *resolved by per-substep re-detection
|
|
284
|
+
(below); kept here for the rationale.* The substep loop normally re-derives
|
|
285
|
+
contact geometry analytically from the per-triangle contact feature (witness
|
|
286
|
+
anchors + normal) captured once by narrowphase and held fixed for the whole
|
|
287
|
+
outer step. For a convex body the contact feature is stable under the small
|
|
288
|
+
per-step motion, so this is exact; for a *dynamic concave mesh body* (e.g. a
|
|
289
|
+
torus knot rocking on its own lobes) the supporting triangle itself changes
|
|
290
|
+
as the body rocks, so freezing the feature would pump a little energy in and
|
|
291
|
+
the body would rock / slowly sink instead of settling. Note this is NOT a
|
|
292
|
+
contact-precision issue —
|
|
293
|
+
the knot already uses the exact closed-form box-triangle solver (P1.1b);
|
|
294
|
+
the problem is purely that TGS freezes *which* feature is in contact across
|
|
295
|
+
substeps. The common concave case — a convex dynamic body on static concave
|
|
296
|
+
terrain — is unaffected (the convex side's feature is stable), and that is
|
|
297
|
+
the only concave case the engine targets.
|
|
298
|
+
|
|
299
|
+
**Interim fix (implemented): per-substep concave re-detection.** For
|
|
300
|
+
contact pairs involving a concave body, the substep loop re-runs the
|
|
301
|
+
concave narrowphase geometry at the current substep pose (instead of the
|
|
302
|
+
analytic refresh that freezes the feature) and re-prepares those contacts
|
|
303
|
+
from the fresh witness/normal/depth — so the contact normal tracks the
|
|
304
|
+
rocking body and no energy is pumped in. Convex pairs keep the cheap
|
|
305
|
+
analytic refresh. This is ~Nx narrowphase cost on concave-involved pairs
|
|
306
|
+
(acceptable — they're rare), gated by collider convexity. Un-skips the
|
|
307
|
+
torus-knot dynamic-settle test.
|
|
308
|
+
|
|
309
|
+
**Better long-term fix: convex collision proxies (not raw concave).** Every
|
|
310
|
+
major engine (Box2D, Jolt, PhysX, Rapier) requires dynamic bodies to be
|
|
311
|
+
convex or convex-decomposed; raw concave meshes are static-only. The right
|
|
312
|
+
granularity is a *few* convex pieces — NOT the thousands of tets a
|
|
313
|
+
volumetric mesher produces (tet count ≈ collider/BVH-leaf count, which
|
|
314
|
+
explodes the broadphase for an awake body; tet meshing is for a future
|
|
315
|
+
FEM/soft-body subsystem, not rigid collision). See the "Convex collision
|
|
316
|
+
proxies for dynamic concave bodies" backlog item — a 3D convex hull builder
|
|
317
|
+
(single-hull proxy covers most dynamic objects) plus an optional
|
|
318
|
+
few-hull (V-HACD-style) decomposition. Those supersede the interim
|
|
319
|
+
per-substep re-detection once built.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Backlog (planned, in scope)
|
|
324
|
+
|
|
325
|
+
### Solver quality (next major work)
|
|
326
|
+
|
|
327
|
+
These items move the engine from "competent" to "great". TGS is the next
|
|
328
|
+
significant solver-architecture change; joints come after, once the TGS
|
|
329
|
+
scaffolding is in place.
|
|
330
|
+
|
|
331
|
+
- **TGS (Temporal Gauss-Seidel) substepping with split-impulse** — Phases
|
|
332
|
+
1–3 **LANDED**. The solver is now a staged TGS pipeline
|
|
333
|
+
(`solver/solve_contacts.js`: `prepare_contacts` → per substep
|
|
334
|
+
[`refresh_contacts` → `warm_start_contacts` → `solve_velocity` →
|
|
335
|
+
`solve_position`] → `apply_restitution`), driven by the substep loop in
|
|
336
|
+
`PhysicsSystem.fixedUpdate`. Defaults: `substeps = 4`,
|
|
337
|
+
`velocityIterations = 4`, `positionIterations = 1` (all fields on
|
|
338
|
+
`PhysicsSystem`).
|
|
339
|
+
- **Phase 1 — split impulse.** Position correction runs on a per-body
|
|
340
|
+
pseudo-velocity (`__pseudo_velocity`) folded into the pose by
|
|
341
|
+
`integrate_position` and discarded; depth correction never
|
|
342
|
+
contaminates persistent velocity.
|
|
343
|
+
- **Phase 2 — one-shot restitution.** Velocity pass is pure
|
|
344
|
+
non-penetration; restitution is a single post-loop pass driving
|
|
345
|
+
`vn → -e·vn_approach`, gated on a running max normal impulse
|
|
346
|
+
(`maxNormalImpulse`) so transient collisions still bounce under
|
|
347
|
+
per-substep warm-start.
|
|
348
|
+
- **Phase 3 — substep loop.** `substeps` sub-iterations at `h = dt/N`.
|
|
349
|
+
Forces consumed once at full `dt` before the loop; gravity applied
|
|
350
|
+
per substep; **warm-start replayed per substep** (the crux — a
|
|
351
|
+
per-substep impulse balances one substep of gravity, so resting
|
|
352
|
+
stacks hold at zero velocity). Contact geometry is re-derived
|
|
353
|
+
**analytically** each substep from frozen local witness anchors +
|
|
354
|
+
the trusted prepare-time depth (a sign-robust delta), so narrowphase
|
|
355
|
+
runs **once** per outer step — cheaper than the originally-planned
|
|
356
|
+
per-substep match-and-merge refresh, and exact for convex
|
|
357
|
+
primitives whose contact feature is stable under small motion.
|
|
358
|
+
|
|
359
|
+
Results vs the single-step solver: a 100:1 mass ratio now stacks
|
|
360
|
+
instead of crushing through (regression test added); 8-cube stacks
|
|
361
|
+
settle to zero velocity and sleep (were impossible long-term under SI);
|
|
362
|
+
falling-tower bench cost unchanged (~48 ms/1000 active bodies);
|
|
363
|
+
`substeps = 1` reproduces the single-step result bit-for-bit-ish
|
|
364
|
+
(one-frame restitution delay aside).
|
|
365
|
+
|
|
366
|
+
**Hard-won lessons (for REVIEW_002):**
|
|
367
|
+
- Warm-start MUST be per-substep, not once. Replaying a full-frame
|
|
368
|
+
impulse once while gravity arrives per substep over-pushes resting
|
|
369
|
+
contacts and *explodes* deep stacks. Per-substep warm-start +
|
|
370
|
+
per-substep gravity cancel exactly at rest.
|
|
371
|
+
- Restitution must gate on the *running max* normal impulse, not the
|
|
372
|
+
end-of-loop value — per-substep warm-start relaxes a transient
|
|
373
|
+
contact's `j_n` back to ~0 by the end, which would suppress the
|
|
374
|
+
bounce.
|
|
375
|
+
- Analytic separation re-derivation beats per-substep narrowphase
|
|
376
|
+
for convex shapes (cheaper, no manifold-lifecycle churn) but is
|
|
377
|
+
only as good as the frozen normal — see the concave caveat below.
|
|
378
|
+
|
|
379
|
+
Follow-ups since the core landed:
|
|
380
|
+
- [x] **Box-box SAT reference tie-break deadband** — aligned cube
|
|
381
|
+
stacks (4–10 high) now settle to zero velocity and sleep; the
|
|
382
|
+
reference-face flip-flop that creeped/toppled them is gone.
|
|
383
|
+
- [x] **Per-substep contact re-detection for concave pairs** — lifts
|
|
384
|
+
the dynamic-concave-body limitation; the torus-knot dynamic-settle
|
|
385
|
+
test is un-skipped. Concave pairs re-run narrowphase geometry each
|
|
386
|
+
substep (`redetect_concave_contacts`); convex pairs keep the cheap
|
|
387
|
+
analytic refresh.
|
|
388
|
+
|
|
389
|
+
Remaining (Phases 4–6) — now complete:
|
|
390
|
+
- [x] Regression coverage: heavy-on-light pyramid (10× capstone on two
|
|
391
|
+
light cubes settles + sleeps) and a ragdoll-stub (shoulder
|
|
392
|
+
ball-socket + elbow hinge arm hangs, stays articulated, settles).
|
|
393
|
+
- [x] REVIEW_002 retrospective — `engine/physics/REVIEW_002.md`.
|
|
394
|
+
|
|
395
|
+
References: Catto 2018 ("Soft Constraints" GDC talk + the TGS
|
|
396
|
+
follow-up); Box2D v3 source (`b2ApplyRestitution`, the substep solver
|
|
397
|
+
stages); Rapier as the closest architectural sibling.
|
|
398
|
+
|
|
399
|
+
- [x] **Constraints / joints — DONE (phases 1–7 below).** One configurable
|
|
400
|
+
6-DOF joint (lock/free/limit/motor/spring + swing-twist cone-twist) plus
|
|
401
|
+
the raycast vehicle. Covers chains/ropes, ragdolls, vehicles (incl.
|
|
402
|
+
suspension), and the mechanical set (doors, pistons, welds, sliders,
|
|
403
|
+
powered hinges/wheels). The design rationale below is kept as history; the
|
|
404
|
+
phasing checklist records what landed. Solver/joint retrospective in
|
|
405
|
+
`REVIEW_003.md`.
|
|
406
|
+
|
|
407
|
+
Original framing (now satisfied): TGS unblocked it (joint-chain
|
|
408
|
+
convergence is a TGS sweet spot), warm-start + per-substep + island
|
|
409
|
+
machinery was reusable, and the SPOOK compliance dial gave spring
|
|
410
|
+
constraints essentially for free.
|
|
411
|
+
|
|
412
|
+
**Foundational work (do first): generalise the solver to constraint
|
|
413
|
+
rows.** Today `solver/solve_contacts.js` is hard-coded to the
|
|
414
|
+
contact-shape constraint (normal + 2 friction tangents, ≥0 clamp,
|
|
415
|
+
restitution, penetration bias). Joints are equality / inequality
|
|
416
|
+
constraints on relative velocity at anchors, generally bilateral
|
|
417
|
+
(impulse may be ±) with optional limits and motors. The clean shape —
|
|
418
|
+
and what Jolt / Box2D-v3 do — is a **generic constraint row**: a
|
|
419
|
+
Jacobian (linear + angular parts per body), an effective mass, a bias
|
|
420
|
+
(position error × SPOOK gain, or motor target), and impulse bounds
|
|
421
|
+
`[lo, hi]` (`[0,∞)` for a contact/limit, `(−∞,∞)` for an equality,
|
|
422
|
+
`[−maxForce·h, +maxForce·h]` for a motor). Each joint type just fills
|
|
423
|
+
in its rows; the existing per-body impulse-apply primitive
|
|
424
|
+
(`apply_impulse_to_body` + `world_inverse_inertia_apply`), the
|
|
425
|
+
per-substep warm-start, the islands, and the split-impulse / SPOOK
|
|
426
|
+
position handling are all reused. Contacts become *one* constraint
|
|
427
|
+
type among several rather than the hard-coded path.
|
|
428
|
+
|
|
429
|
+
The specific constraint set, its use-case mapping, and per-type
|
|
430
|
+
architecture-fit assessment are under review (see the constraints
|
|
431
|
+
sketch). High level: ball-socket / distance / spring / weld and the
|
|
432
|
+
grab constraint are near drop-ins on the row machinery; hinge /
|
|
433
|
+
prismatic / cone-twist / motors / limits add angular-row + bounded-row
|
|
434
|
+
mechanics (still within the impulse framework); raycast vehicles,
|
|
435
|
+
conveyor surface-velocity, and gear/pulley coupling are higher-level
|
|
436
|
+
systems or contact modifiers that sit *on top of* the primitives
|
|
437
|
+
rather than being generic rows.
|
|
438
|
+
|
|
439
|
+
**Decision: build ONE configurable 6-DOF constraint** (PhysX D6 / Jolt
|
|
440
|
+
SixDOF), implemented mode-by-mode. The `Joint` ECS component carries
|
|
441
|
+
`dofMode[6]` (3 linear, 3 angular) each `{locked|free|limited|spring|
|
|
442
|
+
motor}` + per-DOF limit/spring/motor config + warm-start accumulators.
|
|
443
|
+
Concrete joints are configs, not new code (ball-socket = lock 3 linear;
|
|
444
|
+
hinge = lock 3 linear + 2 angular; weld = lock 6; cone-twist = lock 3
|
|
445
|
+
linear + limit 3 angular; suspension = spring 1 linear + lock rest).
|
|
446
|
+
|
|
447
|
+
Phasing:
|
|
448
|
+
1. [x] Constraint-row solver as a **parallel row set** in the TGS
|
|
449
|
+
substep loop (contacts left untouched, not ported — lower risk).
|
|
450
|
+
`constraint/solve_constraints.js` reuses `world_inverse_inertia`,
|
|
451
|
+
per-substep warm-start, and the SPOOK position bias; `Joint`
|
|
452
|
+
component + `link_joint`/`unlink_joint` in PhysicsSystem;
|
|
453
|
+
`jointIterations` knob. Bodies need no collider.
|
|
454
|
+
2. [x] **LOCKED linear DOFs → ball-socket.** Pendulum (anchor pinned
|
|
455
|
+
to a world pivot, body swings) and a 2-link chain (body↔body,
|
|
456
|
+
joints stay connected, chain hangs) pass. → **chains, ropes,
|
|
457
|
+
pendulums working.**
|
|
458
|
+
3. [x] LOCKED angular + linear DOFs in the frame basis — **weld,
|
|
459
|
+
hinge, prismatic done**. Joint frame bases
|
|
460
|
+
(`localBasisA`/`localBasisB`); BOTH linear and angular rows now
|
|
461
|
+
resolve in frame A's axes (cleared the world-axis linear debt — the
|
|
462
|
+
solver is fully frame-relative). Angular: relative rotation
|
|
463
|
+
`qD = conj(qA)·qB` → small-angle error, ωB−ωA rows + SPOOK bias.
|
|
464
|
+
Linear: `C·axis` error, vA−vB rows. `asWeld()` / `asHinge(axis)` /
|
|
465
|
+
`asPrismatic(axis)` presets. Verified: weld holds pose + orientation
|
|
466
|
+
against an off-centre torque; hinge swings about its free axis only
|
|
467
|
+
(locked axes < 0.02); prismatic slides along its one free axis,
|
|
468
|
+
locked on the others; all LOCKED-mode tests still green after the
|
|
469
|
+
frame-basis rewrite.
|
|
470
|
+
4. [x] LIMITED + MOTOR (bounded rows) → doors, pistons, wheel
|
|
471
|
+
spin/drive, joint ROM. **LIMITED done** (linear + angular):
|
|
472
|
+
`setLinearLimit(axis,lo,hi)` / `setAngularLimit(axis,lo,hi)` set a
|
|
473
|
+
per-DOF travel/ROM range. The whole row set is now **one mode-
|
|
474
|
+
agnostic solve** parameterised by `(bias, clamp range)`: LOCKED is
|
|
475
|
+
the bilateral case (Baumgarte bias, unclamped); LIMITED is a
|
|
476
|
+
**speculative (β=1) one-sided velocity constraint** that removes
|
|
477
|
+
exactly the approach velocity so the DOF *lands on* its stop (no
|
|
478
|
+
penetration, no rebound — an inelastic end-stop) and self-gates when
|
|
479
|
+
far from the bound; only the push-out side of the bias is clamped so
|
|
480
|
+
a teleport is eased out, not yanked. Verified: a vertical slider
|
|
481
|
+
falls freely then stops dead on its lower stop (lands at the bound,
|
|
482
|
+
no overshoot/rebound, locked axes held); a spun hinge stops dead on
|
|
483
|
+
each ±end-stop with no rebound and holds. Angular position is the
|
|
484
|
+
small-angle measure (`2·sin(θ/2)`) — accurate for modest ROM, see
|
|
485
|
+
phase 6 for wide cones. **MOTOR next** (target-velocity row, impulse
|
|
486
|
+
clamped to `±maxForce·h`).
|
|
487
|
+
5. [x] SPRING (SPOOK soft) → suspension, bungees, soft ragdolls.
|
|
488
|
+
`setLinearSpring(axis,k,c)` / `setAngularSpring(axis,k,c)`. A
|
|
489
|
+
compliant (regularised) row in the same unified solve: per substep
|
|
490
|
+
`denom = c + h·k`, compliance `γ = 1/(h·denom)`, restoring bias
|
|
491
|
+
`(k/denom)·C`, softened mass `1/(K+γ)`; the iteration carries one
|
|
492
|
+
extra `+ γ·λ_accum` term (γ = 0 ⇒ the LOCKED/LIMITED/MOTOR rows are
|
|
493
|
+
bit-for-bit unchanged). Verified: a vertical strut settles at exactly
|
|
494
|
+
the m·g/k deflection and a stiffer spring sags less and stays stable;
|
|
495
|
+
an undamped spring oscillates about equilibrium (stores energy) while
|
|
496
|
+
a damped one comes to rest; a torsional spring holds a gravity-loaded
|
|
497
|
+
hinge at its balance angle. Suspension element ready (the simulated-
|
|
498
|
+
wheel option for phase 7); also the soft basis for cone-twist.
|
|
499
|
+
6. [x] Cone-twist / swing-twist angular limits → ragdolls. Opt-in
|
|
500
|
+
`Joint.swingTwist` (or the `asConeTwist(twistLo,twistHi,swingY[,swingZ])`
|
|
501
|
+
preset) switches the angular position measure from the per-axis
|
|
502
|
+
small-angle vector to a swing-twist decomposition: angular X = twist
|
|
503
|
+
about the bone, Y/Z = swing off it, each an **exact** angle. The
|
|
504
|
+
existing LIMITED/SPRING/LOCKED rows are reused unchanged on those
|
|
505
|
+
positions, so a twist/swing limit holds at the true angle at wide
|
|
506
|
+
ROM (a 1.2 rad swing stops at 1.2, where the small-angle proxy
|
|
507
|
+
drifts to ~1.287). Verified: exact swing/twist stops, free-within-
|
|
508
|
+
cone, twist/swing independence; default (small-angle) path untouched.
|
|
509
|
+
**Decision — inlined, not the Quaternion method.** Benchmarked the
|
|
510
|
+
allocation-free inlined `swing_twist_error` against
|
|
511
|
+
`Quaternion.computeSwingAndTwist` (`swing_twist.bench.spec.js`): the
|
|
512
|
+
inline is **~5x** faster than the method with reused out-params and
|
|
513
|
+
**~10x** vs the naive fresh-allocation form (object property access +
|
|
514
|
+
normalize + a quaternion multiply + GC). In the per-substep
|
|
515
|
+
per-joint hot loop that margin is worth the duplicated math, so the
|
|
516
|
+
solver inlines it (the Quaternion method stays for general callers).
|
|
517
|
+
7. [x] Vehicle layer — **raycast-vehicle controller**
|
|
518
|
+
(`vehicle/RaycastVehicle.js`): single chassis body + raycast wheels.
|
|
519
|
+
Per frame (before `fixedUpdate`) each wheel casts its suspension ray,
|
|
520
|
+
applies a spring+damper suspension force along the contact normal
|
|
521
|
+
(`applyForceAt`), and a tyre-friction impulse (`applyImpulseAt`) —
|
|
522
|
+
lateral grip that cancels side-slip plus longitudinal drive/brake,
|
|
523
|
+
clamped together to a friction circle μ·N. `addWheel`, `setSteering`,
|
|
524
|
+
`setDriveForce`, `setBrake`; per-wheel runtime (contact, compression,
|
|
525
|
+
normal, spin) for rendering. A controller on top of the public
|
|
526
|
+
`raycast` + force API, not a new constraint; the 6-DOF spring+motor
|
|
527
|
+
is the simulated-wheel alternative. Verified: hovers on its springs
|
|
528
|
+
(4 contacts, settled), drives/coasts/brakes along its axis, tyre grip
|
|
529
|
+
arrests a sideways shove, steering turns it upright, and it free-falls
|
|
530
|
+
cleanly when airborne. Note: suspension is one dt-force per frame (not
|
|
531
|
+
per-substep), so a resting chassis carries a ~g·h velocity-sample
|
|
532
|
+
artifact (it hovers stably; position is steady to sub-cm). Ray
|
|
533
|
+
accuracy follows `PhysicsSystem.raycast` — now narrowphase-exact for
|
|
534
|
+
sphere / box / capsule / mesh / heightmap ground.
|
|
535
|
+
8. [ ] Extras: pulley, gear, conveyor (contact surface-velocity),
|
|
536
|
+
breakable-joint flag.
|
|
537
|
+
|
|
538
|
+
Foundation gaps — both now closed:
|
|
539
|
+
- [x] **Island integration.** Jointed dynamic-dynamic bodies are
|
|
540
|
+
unioned into one island (`IslandBuilder` Pass 1b), so a chain /
|
|
541
|
+
ragdoll sleeps and wakes as a unit; `__wake_joints` propagates wake
|
|
542
|
+
across a joint when one side is awake and the other asleep
|
|
543
|
+
(e.g. a kinematic/motor driver pulling a sleeping chain). Verified:
|
|
544
|
+
a damped chain settles and both links sleep in one sleep group.
|
|
545
|
+
- [x] **Generation-checked body references.** `solve_joints`,
|
|
546
|
+
`IslandBuilder` Pass 1b and `__wake_joints` all gate on
|
|
547
|
+
`storage.is_valid(packedId)`, so a joint to an unlinked / slot-reused
|
|
548
|
+
body goes inert instead of attaching to the wrong body or crashing.
|
|
549
|
+
Verified: unlinking a jointed body leaves the joint inert and the
|
|
550
|
+
survivor free.
|
|
551
|
+
|
|
552
|
+
References: Catto / Box2D-v3 joint solvers; Jolt's `Constraint` base
|
|
553
|
+
(`SetupVelocityConstraint` / `WarmStartVelocityConstraint` /
|
|
554
|
+
`SolveVelocityConstraint` / `SolvePositionConstraint`); PhysX D6 /
|
|
555
|
+
ODE joint taxonomy.
|
|
556
|
+
|
|
557
|
+
### Stability
|
|
558
|
+
- [x] **Closed-form triangle-vs-primitive solvers** — `sphere_triangle_contact`
|
|
559
|
+
/ `box_triangle_contact` / `capsule_triangle_contact` (P1.1a–c), wired into
|
|
560
|
+
the concave decomposition dispatch in place of per-triangle GJK+EPA for
|
|
561
|
+
those primitives. Un-skipped the `narrowphase_concave.spec.js` ball-on-
|
|
562
|
+
heightmap / mesh-cube settle tests and the `PhysicsSystem.spec.js`
|
|
563
|
+
torus-knot test. Per-triangle GJK+EPA remains only as the fallback for
|
|
564
|
+
*other* convex shapes vs triangles. `compute_penetration` now routes
|
|
565
|
+
through the shared narrowphase dispatch (`deepest_pair_penetration`), so it
|
|
566
|
+
uses the closed-form per-triangle solvers too — the old closed-mesh
|
|
567
|
+
over-report is gone; the half-space test is retained only as a
|
|
568
|
+
tunnel-recovery fallback.
|
|
569
|
+
- [x] **Edge-edge multi-point manifold** — *resolved by design (no code
|
|
570
|
+
change needed).* An empirical SAT-source sweep over a wide range of
|
|
571
|
+
box-box orientations shows the single-point edge-cross branch only ever
|
|
572
|
+
wins for **transverse** edge crossings (inter-edge angle ≈ 83-90°), where
|
|
573
|
+
a single closest-pair point is geometrically exact. A near-parallel edge
|
|
574
|
+
pair gives a near-degenerate `edgeA × edgeB` that never becomes the SAT
|
|
575
|
+
minimum, so near-parallel ("line") edge contacts resolve through the
|
|
576
|
+
multi-point **face-clipping** path instead — confirmed by regression
|
|
577
|
+
tests in `box_box_manifold.spec.js` (near-parallel tilted boxes → ≥ 2
|
|
578
|
+
points; transverse crossing → exactly 1 exact point). The originally
|
|
579
|
+
planned refinement targeted a case the geometry can't produce, so it is
|
|
580
|
+
closed rather than implemented.
|
|
581
|
+
- [x] **Per-contact source-collider tracking (materials)** — multi-material
|
|
582
|
+
compound bodies now get accurate per-contact friction / restitution. The
|
|
583
|
+
narrowphase combines the specific (colliderA, colliderB) pair's
|
|
584
|
+
coefficients at dispatch time (the only place that knows the source
|
|
585
|
+
collider on each side — `contact/combine_material.js`) and stamps them
|
|
586
|
+
into the manifold (CONTACT_STRIDE grown 14 → 16, offsets 14/15); the
|
|
587
|
+
solver reads them per contact instead of from the body's primary collider.
|
|
588
|
+
Regression test: an asymmetric-friction compound body yaws when shoved
|
|
589
|
+
(the grippy collider drags), and a symmetric control does not. Still
|
|
590
|
+
primary-collider-only: the contact-filter callback's collider args and the
|
|
591
|
+
body-level sensor / concave flags (smaller follow-up).
|
|
592
|
+
- [ ] **Joint-aware island sleep (ragdoll settle quality).** A draped,
|
|
593
|
+
self-colliding 10-joint ragdoll does not fully sleep in 10 s — surfaced by
|
|
594
|
+
a 1000-seed Monte-Carlo sweep (`PhysicsSystem.ragdoll.spec.js`, `.skip`):
|
|
595
|
+
for unlucky seeds a distal limb sustains a settled limit cycle (settled
|
|
596
|
+
finite-difference accel up to ~1094 m/s² / ~1479 rad/s² at a limb end vs a
|
|
597
|
+
~55 m/s² median — bounded, non-growing, penetration-free, so a quality gap
|
|
598
|
+
not a divergence). The sleep test today is per-body `|v|²+|ω|²`; an island
|
|
599
|
+
over-constrained by cone-twist limits + self-contacts keeps small residual
|
|
600
|
+
jiggle above the per-body threshold so it never crosses into sleep.
|
|
601
|
+
Candidate fixes: sleep a jointed/contacting island on its AGGREGATE motion
|
|
602
|
+
rather than the per-body minimum, and/or a settled-regime relaxation (zero
|
|
603
|
+
restitution + extra position iterations) once an island's energy is low.
|
|
604
|
+
The sweep flags the worst seeds for replay. (Test infra also adds
|
|
605
|
+
per-point kinematics tracking — joint anchors + limb ends, with
|
|
606
|
+
displacement→velocity→acceleration and the angular equivalents.)
|
|
607
|
+
- [x] **Box-on-heightmap settling — RESOLVED.** A dynamic box dropped onto a
|
|
608
|
+
static HeightMapShape — flat seam straddle AND the sloped dip — settles to
|
|
609
|
+
full rest; the `PhysicsSystem.heightmap.spec.js` dip-drop reproducer is
|
|
610
|
+
un-skipped and passing. Fixed by the combination of the same-feature-id
|
|
611
|
+
contact de-dup in `redetect_pair_geometry` (1:1 claimed matching, so the
|
|
612
|
+
several contacts one triangle emits — all sharing its feature_id — no longer
|
|
613
|
+
collapse onto a single candidate) and the HeightMapShape3D
|
|
614
|
+
collision-tessellation work; together they give the box a stable per-substep
|
|
615
|
+
contact set instead of a churning / collapsing one.
|
|
616
|
+
|
|
617
|
+
Root cause, for the record: the historical "never settles" rattle was a
|
|
618
|
+
depth divergence between the once-per-step `narrowphase_step` and the
|
|
619
|
+
per-substep `redetect_pair_geometry`. At the SAME pose, re-detection
|
|
620
|
+
over-reported a box-vs-triangle penetration as ~1 m (the "exit through the
|
|
621
|
+
far side" class) where narrowphase reported ~1 cm, launching the box into an
|
|
622
|
+
eternal vertical bounce (touch → ~1 m over-correction → separate → fall back
|
|
623
|
+
→ re-contact). Both paths call the identical `dispatch_pair`, so the
|
|
624
|
+
divergence was the same-fid collapse corrupting the matched geometry; with
|
|
625
|
+
the de-dup in place the two paths now agree (verified: both report
|
|
626
|
+
[0.0100 ×4] at a 0.01-deep flat contact, in-cell and 4-triangle straddle).
|
|
627
|
+
|
|
628
|
+
Guards: `narrowphase/redetect_pair_geometry.spec.js` now pins BOTH
|
|
629
|
+
invariants — contacts sharing a feature_id keep distinct witnesses, and
|
|
630
|
+
re-detect depth == narrowphase depth at a fixed pose (in-cell and
|
|
631
|
+
4-triangle seam-straddle cases); `ecs/PhysicsSystem.heightmap.spec.js` pins
|
|
632
|
+
the observable settle. The depth-equality regression test the earlier
|
|
633
|
+
diagnostic trail asked for is in place, closing this out.
|
|
634
|
+
|
|
635
|
+
### Performance / Scale
|
|
636
|
+
- [x] **Per-body linear CCD shape-cast** — opt-in continuous collision for
|
|
637
|
+
fast movers where the speculative margin isn't enough.
|
|
638
|
+
`RigidBodyFlags.CCD` (off by default) + `ccd/linear_sweep.js`.
|
|
639
|
+
- **Approach (Box2D `b2_continuousPhysics`-style conservative
|
|
640
|
+
advancement).** After the substep solver produces each body's final
|
|
641
|
+
pose (between `apply_restitution` and the sleep test), a flagged fast
|
|
642
|
+
mover's primary collider is swept along its NET step translation
|
|
643
|
+
(start-of-step → final pose) through the existing `shape_cast` TOI
|
|
644
|
+
engine. On the first blocker the body is clamped to the contact pose
|
|
645
|
+
and its inbound normal velocity removed (an inelastic stop); the next
|
|
646
|
+
discrete step resolves the now-touching contact with the real
|
|
647
|
+
material / restitution. Reuses `shape_cast` wholesale — no new
|
|
648
|
+
geometry. Start positions captured in Stage 1 over the post-wake awake
|
|
649
|
+
set; the pass iterates the awake list in storage order (deterministic).
|
|
650
|
+
- **Motion gate — absolute slop, NOT body extent.** A body is swept when
|
|
651
|
+
it moved more than `CCD_MIN_SWEEP_DISTANCE` (1 mm) this step. The gate
|
|
652
|
+
exists only to skip near-stationary bodies (degenerate sweep + cost).
|
|
653
|
+
It is deliberately *not* a fraction of the body's own size: tunnelling
|
|
654
|
+
risk is set by the **obstacle's** thickness, not the mover's — a 2 m
|
|
655
|
+
sphere drifting 0.5 m/step still passes clean through a 1 cm floor, so
|
|
656
|
+
an extent-based gate would (wrongly) wait until the body moved more
|
|
657
|
+
than its own radius and miss every thin-obstacle tunnel below that
|
|
658
|
+
speed.
|
|
659
|
+
- **No self-clamp on resting/sliding contacts.** The sweep ignores an
|
|
660
|
+
impact at `t ≈ 0` (`CCD_INITIAL_OVERLAP_EPS`): an initial overlap is a
|
|
661
|
+
contact the body already sits/slides on, owned by the discrete solver,
|
|
662
|
+
not a tunnel — clamping there would freeze the body to the surface.
|
|
663
|
+
- **Measured (falling-tower bench, 1000 random shapes onto a 1 cm floor,
|
|
664
|
+
600 ticks; clean A/B on identical code via `ccdEnabled`).** This bench
|
|
665
|
+
is the WRONG validation vehicle for CCD, and the numbers prove it: CCD
|
|
666
|
+
off → **10/1000 tunnel**, median **42.7 ms**/step; all 1000 flagged →
|
|
667
|
+
**50/1000 tunnel**, median **61.6 ms**. CCD makes it *worse*. The bench
|
|
668
|
+
is a dense-pile **squeeze-through** scenario — 1000 bodies stacked on a
|
|
669
|
+
1 cm floor, forced through by the column's weight over many steps — which
|
|
670
|
+
is a *solver* limitation, not a missed single-step fly-through. CCD's
|
|
671
|
+
post-solve clamp + velocity-kill fights the solver in a dense settling
|
|
672
|
+
pile (it teleports mutually-stacking dynamics and breaks warm-start), so
|
|
673
|
+
flagging a whole pile is an anti-pattern. CCD's real job — stopping a
|
|
674
|
+
sparse fast mover against thin geometry — is validated by
|
|
675
|
+
`ccd/linear_sweep.spec.js` (9 tests: a fast cube tunnels a thin floor
|
|
676
|
+
without the flag and is stopped with it; deterministic; resting bodies
|
|
677
|
+
undisturbed). **Correction:** an earlier version of this entry and the
|
|
678
|
+
commit message (`42163b0d4`) claimed a "0/1000 tunnel, 58.2 ms" on-leg
|
|
679
|
+
— that was never measured (a session tooling glitch); the real on-leg
|
|
680
|
+
is 50/1000 / 61.6 ms.
|
|
681
|
+
- **Scope (v1, documented):** linear sweep only (orientation fixed
|
|
682
|
+
through the sweep); primary same-entity collider (child-entity
|
|
683
|
+
colliders, synced outside the step, are not swept); EXACT against
|
|
684
|
+
static geometry, APPROXIMATE against other dynamics (their
|
|
685
|
+
start-of-step broadphase AABBs); the CCD stop is inelastic (the impact
|
|
686
|
+
itself doesn't bounce — restitution applies on the next discrete
|
|
687
|
+
contact); Dynamic bodies only. A body both resting on one surface and
|
|
688
|
+
tunnelling another in the same step resolves only the resting contact
|
|
689
|
+
(the `t ≈ 0` skip) — rare; the next discrete step catches the rest.
|
|
690
|
+
- **Follow-ups (the dense-pile finding points the way):** sweep against
|
|
691
|
+
STATIC geometry only by default — the dynamic-vs-dynamic clamp is the
|
|
692
|
+
source of the dense-pile interference above and is only ever
|
|
693
|
+
approximate anyway, so dropping it should make CCD purely additive
|
|
694
|
+
(stops you at static walls/floors, never fights the dynamic stack);
|
|
695
|
+
a proper TOI sub-solver for bullet-vs-dynamic; rotational / angular
|
|
696
|
+
CCD; multi-collider sweep for compound bodies.
|
|
697
|
+
- [x] **Broadphase BVH balance — SAH rotation.** The dynamic AABB tree
|
|
698
|
+
(`core/bvh2/bvh3/BVH.js`, a Box2D port) used SAH-cost insertion but a
|
|
699
|
+
*height-only* AVL rotation (`balance_height`): height-balanced yet not
|
|
700
|
+
SAH-balanced, so queries walked more nodes than needed. Replaced the
|
|
701
|
+
rotation in `bubble_up_update` with `balance_rotate` — the Box2D-v3 /
|
|
702
|
+
Kensler SAH-reducing rotation (for node A with children B, C, evaluate the
|
|
703
|
+
four child↔grandchild swaps and apply the one that most reduces the
|
|
704
|
+
surface-area cost). Deterministic; identical pair set.
|
|
705
|
+
- Measured (same-session A/B, heavy benches): raycast **−9%**
|
|
706
|
+
(28.2→25.6 µs/ray), falling-tower median **−10%**, settling-grid
|
|
707
|
+
median **−12%**, and the **990/1000-churn stress −27%**
|
|
708
|
+
(63.95→46.68 ms mean over 10k ticks) — biggest where the tree churns
|
|
709
|
+
hardest. Determinism (8-trial bit-identical) holds.
|
|
710
|
+
- **Insertion cost (measured):** `balance_rotate` does 4 surface-area
|
|
711
|
+
evaluations per bubble-up level vs `balance_height`'s single height
|
|
712
|
+
compare, so *pure bulk insertion* is **~1.4–1.5× slower** — the 100k
|
|
713
|
+
synthetic insert bench (`BVH.spec.js`, drift-controlled interleaved
|
|
714
|
+
A/B) drops from **~37k → ~25k inserts/sec** (~27→~40 µs/insert). This
|
|
715
|
+
is the balancer's worst case (insert-only, zero queries/refits to
|
|
716
|
+
amortise against). It does not show up end-to-end: static trees are
|
|
717
|
+
built once then queried forever, dynamic bodies insert once then
|
|
718
|
+
refit/query every frame, and even the 990/1000-swap stress test — the
|
|
719
|
+
maximal insert-churn workload — is net **−27%**. Accepted.
|
|
720
|
+
- **Tradeoff (documented):** the contact solver's Gauss-Seidel order
|
|
721
|
+
follows broadphase traversal order (see `generate_pairs`), so the
|
|
722
|
+
different tree shape shifts convergence on near-aligned stacks — the
|
|
723
|
+
synthetic 128-cube wall now sleeps at ~10 s (was ~6.9 s). It still
|
|
724
|
+
settles, doesn't creep / topple (all bug-guard assertions hold); only
|
|
725
|
+
the sleep *time* moved (that test's budget was bumped 9→11 s with a
|
|
726
|
+
note). Random-shape scenes (falling tower) were faster *and* settled
|
|
727
|
+
fine.
|
|
728
|
+
- **Follow-up:** decouple the solve order from tree shape — sort the
|
|
729
|
+
broadphase pair list by `(idA, idB)` before narrowphase so contact
|
|
730
|
+
order is body-id-deterministic regardless of tree shape. Then no tree
|
|
731
|
+
change can affect convergence (and the stack settles identically under
|
|
732
|
+
either balancer). Has a per-step sort cost + wide test re-baseline, so
|
|
733
|
+
it's its own task. `balance_height` is retained for comparison /
|
|
734
|
+
fallback.
|
|
735
|
+
- [ ] **Per-island parallel solve**: today's island data layout would
|
|
736
|
+
allow worker-based solving once `SharedArrayBuffer` is available.
|
|
737
|
+
Out-of-scope unless / until SAB is universally usable.
|
|
738
|
+
|
|
739
|
+
### Features
|
|
740
|
+
- [ ] **Convex collision proxies for dynamic concave bodies.** The long-term
|
|
741
|
+
replacement for the interim per-substep concave re-detection (see
|
|
742
|
+
Limitations) — and how every major engine handles dynamic non-convex
|
|
743
|
+
shapes: collide a *few* convex pieces, never the raw concave mesh.
|
|
744
|
+
1. **3D convex hull builder** (meep has only 2D hulls today —
|
|
745
|
+
`core/geom/2d/convex-hull/`). A single hull of a mesh is one
|
|
746
|
+
collider / one broadphase leaf and covers the overwhelming majority
|
|
747
|
+
of dynamic objects (thrown props, debris). Pairs with the existing
|
|
748
|
+
"Convex hull shape + eigen-inertia" item below.
|
|
749
|
+
2. **Few-hull (V-HACD-style) approximate convex decomposition** for
|
|
750
|
+
shapes whose concavity matters (a cup, a chair): ~8–64 fat convex
|
|
751
|
+
hulls = 8–64 colliders, two orders of magnitude below a tet mesh.
|
|
752
|
+
Each hull is convex → stable contact feature → the TGS analytic refresh
|
|
753
|
+
is exact → no per-substep re-detection, no rocking. Granularity is the
|
|
754
|
+
whole point: collider/BVH-leaf count must stay small for an *awake*
|
|
755
|
+
dynamic body (the volumetric tet-mesher under `core/geom/3d/tetrahedra/`
|
|
756
|
+
is the wrong tool here — thousands of pieces — and belongs to a future
|
|
757
|
+
FEM/soft-body subsystem, not rigid collision).
|
|
758
|
+
- [ ] **Convex hull shape** with eigen-based principal-axes inertia
|
|
759
|
+
derivation. Hooks `matrix_eigenvalues_in_place` from the existing
|
|
760
|
+
linalg layer.
|
|
761
|
+
- [~] **Cylinder / cone shapes.**
|
|
762
|
+
- [x] **`CylinderShape3D`** — Y-aligned solid cylinder (radius + full
|
|
763
|
+
height, flat caps; the capsule's flat-cap sibling). Exact `support`,
|
|
764
|
+
capped-cylinder SDF, bounds, `contains` / `nearest_point` /
|
|
765
|
+
volume-sampling, equals/hash, `'cylinder'` JSON tag, `isCylinderShape3D`
|
|
766
|
+
marker. Convex → routes through the narrowphase **GJK + EPA** fallback
|
|
767
|
+
(no marker dispatch needed); spec asserts overlap-detected +
|
|
768
|
+
MTV-separates vs sphere/box. Closed-form cylinder-vs-X contact pairs
|
|
769
|
+
are a future refinement (the curved side is the usual smooth-support
|
|
770
|
+
EPA case — same status as pre-closed-form sphere/capsule).
|
|
771
|
+
- [ ] Closed-form cylinder contact pairs (cylinder × box / sphere / capsule
|
|
772
|
+
/ plane) for multi-point cap manifolds + stable resting.
|
|
773
|
+
- [ ] **Cone shape** (+ closed-form / GJK fallback).
|
|
774
|
+
|
|
775
|
+
### Rendering integration
|
|
776
|
+
- [ ] **Fixed-step → render interpolation.** Not implemented yet. Physics writes
|
|
777
|
+
each body's pose straight into the ECS `Transform` once per fixed step
|
|
778
|
+
(`EntityManager.fixedUpdateStepSize`); with a render rate that doesn't match
|
|
779
|
+
the fixed rate, the rendered motion aliases (stutter / temporal aliasing,
|
|
780
|
+
worst at low fixed rates). The standard fix is to keep the previous and
|
|
781
|
+
current fixed-step pose per body and have the renderer lerp position /
|
|
782
|
+
slerp rotation by the accumulator's fractional remainder
|
|
783
|
+
(`alpha = leftover / fixedStep`). Open design questions: where the
|
|
784
|
+
double-buffered "previous pose" lives so it does NOT bloat the simulation
|
|
785
|
+
hot state or the netcode-replicated component set (a render-side component
|
|
786
|
+
vs. the physics body), and how teleports / kinematic snaps opt out of
|
|
787
|
+
interpolation for a frame. Sits at the physics↔render seam, not in the
|
|
788
|
+
solver.
|
|
789
|
+
|
|
790
|
+
### API polish
|
|
791
|
+
- [x] **`overlap(shape, position, rotation, output, output_offset,
|
|
792
|
+
filter?)`** — broadphase + narrowphase overlap query for kinematic
|
|
793
|
+
/ AOE / selection use cases. Body_ids written into a caller-sized
|
|
794
|
+
Uint32Array buffer. Convex query shape only; concave candidates
|
|
795
|
+
are routed through the per-triangle decomposition path.
|
|
796
|
+
- [x] **`shapeCast(ray, shape, rotation, result, filter?)`** for
|
|
797
|
+
character controllers and kinematic shape sweeps. Broadphase
|
|
798
|
+
swept-AABB against both BVHs; per-candidate AABB-slab interval
|
|
799
|
+
narrowing + coarse step + GJK bisection for time-of-impact. The
|
|
800
|
+
output `result.normal` is the true contact-surface normal at the
|
|
801
|
+
kiss point, computed by re-running GJK + EPA at `best_t` on the
|
|
802
|
+
winning candidate (falls back to `-ray.direction` only on EPA
|
|
803
|
+
degeneracies).
|
|
804
|
+
- [x] **`compute_penetration(out_direction, shape_a, pos_a, rot_a,
|
|
805
|
+
shape_b, pos_b, rot_b)`** — standalone geometry primitive (no
|
|
806
|
+
PhysicsSystem) for resolving overlap between two shapes at given
|
|
807
|
+
poses. Returns depth + outward direction. **Hardened** to route through
|
|
808
|
+
the shared narrowphase dispatch (`deepest_pair_penetration`): exact
|
|
809
|
+
closed-form for sphere/box/capsule pairs (box-box via SAT), GJK+EPA for
|
|
810
|
+
general convex, closed-form per-triangle for convex × concave; the
|
|
811
|
+
half-space test is retained only for tunnel recovery.
|
|
812
|
+
|
|
813
|
+
### Raycast narrowphase (done)
|
|
814
|
+
|
|
815
|
+
**Problem.** `raycast` (and the suspension ray inside `RaycastVehicle`) resolves
|
|
816
|
+
only to the nearest BVH leaf's *inflated* AABB: `result.t` is the distance to
|
|
817
|
+
that fattened box and `result.normal` is its face normal. Exact for an
|
|
818
|
+
axis-aligned box (modulo the broadphase margin), coarse for spheres / capsules /
|
|
819
|
+
rotated boxes / meshes / heightmaps. Refine each candidate against the true
|
|
820
|
+
shape to return the exact surface distance + normal. `shapeCast` already does
|
|
821
|
+
this for swept convex shapes via GJK+EPA; `raycast` should get the same
|
|
822
|
+
treatment with cheap analytic primitives on the hot path.
|
|
823
|
+
|
|
824
|
+
**Design.** Mirror `narrowphase_step`'s dispatch: closed-form ray tests for the
|
|
825
|
+
common primitives, a generic GJK fallback for the rest. The structural change is
|
|
826
|
+
in the BVH walk — the nearest *leaf AABB* is **not** the nearest *shape hit* (a
|
|
827
|
+
ray can clip a near fat-AABB but miss its shape while hitting a farther one), so
|
|
828
|
+
every crossing leaf must be refined, with subtrees pruned by inflated-AABB
|
|
829
|
+
`t_near` vs the best *refined* `t` (conservative-correct: a shape hit is always
|
|
830
|
+
≥ its tight AABB entry ≥ its inflated AABB entry). A leaf whose ray crosses the
|
|
831
|
+
fat AABB but misses the true shape now contributes **no hit** — the key
|
|
832
|
+
correctness gain.
|
|
833
|
+
|
|
834
|
+
Phasing (each phase: implement → spec → run from `H:/git/moh` → commit):
|
|
835
|
+
|
|
836
|
+
1. [x] **Ray-primitive helpers** — landed as `narrowphase/ray_shapes.js`
|
|
837
|
+
(local-frame `ray_sphere_local` / `ray_box_local` / `ray_capsule_local`,
|
|
838
|
+
not `core/geom`: the (`t`, normal, miss = `Infinity`, first-hit-from-outside)
|
|
839
|
+
convention is raycast-specific, and the dispatch shares one ray→local
|
|
840
|
+
transform across them). Built local-frame (unit direction ⇒ `t` preserved;
|
|
841
|
+
rotate the local normal back). Triangle MT is inlined in the concave path
|
|
842
|
+
(the existing `computeTriangleRayIntersection` writes a `SurfacePoint3` and
|
|
843
|
+
returns no `t` — unsuited to the buffer-flyweight loop). Colocated specs.
|
|
844
|
+
2. [x] **Ray-narrowphase dispatch** `narrowphase/refine_ray_hit.js`:
|
|
845
|
+
`(shape, position, rotation, ox,oy,oz, dx,dy,dz, tMax, outNormal) → t`.
|
|
846
|
+
Type-marker dispatch (`isUnitSphereShape3D` / `isBoxShape3D` /
|
|
847
|
+
`isCapsuleShape3D`) to the analytic primitives; a generic convex fallback
|
|
848
|
+
for `TransformedShape3D` / `UnionShape3D` / other (GJK ray-cast, or reuse
|
|
849
|
+
`shape_cast` with a zero-radius `PointShape3D`).
|
|
850
|
+
3. [x] **Concave path** in the dispatch: for `is_convex === false` (mesh /
|
|
851
|
+
heightmap), enumerate the triangles overlapping the ray's swept AABB
|
|
852
|
+
(`mesh_enumerate_triangles` / `heightmap_enumerate_triangles`), Möller–
|
|
853
|
+
Trumbore each, take the nearest; normal from the triangle winding.
|
|
854
|
+
4. [x] **Rewire `queries/raycast.js`**: at each leaf, call `refine_ray_hit` on
|
|
855
|
+
the true shape + pose instead of accepting the AABB `t_near`; track the best
|
|
856
|
+
refined `(t, body, normal)`; keep subtree pruning on inflated-AABB `t_near`.
|
|
857
|
+
Same signature / `PhysicsSurfacePoint` result; drop the AABB-face-normal
|
|
858
|
+
block. Multi-collider bodies still resolve the primary collider only
|
|
859
|
+
(inherited BVH-leaf limitation; note it).
|
|
860
|
+
5. [x] **Tests**: per-shape exactness (sphere / OBB / capsule / mesh /
|
|
861
|
+
heightmap) — exact `t` and true normal; the **fat-AABB-cross-but-shape-miss
|
|
862
|
+
⇒ no hit** case (the correctness win); nearest-of-several across a near miss;
|
|
863
|
+
`filter` and `tMax` honoured. Re-verify `RaycastVehicle` (ride height now
|
|
864
|
+
exact — tighten the test bands if they shift by the old broadphase margin).
|
|
865
|
+
6. [x] **Bench + docs**: a raycast micro-bench (analytic fast-path cost; confirm
|
|
866
|
+
the fat-AABB-miss rejection doesn't regress throughput); update the "Public
|
|
867
|
+
queries" entry, `raycast.js` header, and the `RaycastVehicle` "AABB-level"
|
|
868
|
+
caveat once exact.
|
|
869
|
+
|
|
870
|
+
Note: this sharpens `RaycastVehicle` suspension on non-box ground and every
|
|
871
|
+
shape query; it does not change the broadphase or any API surface.
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Future / out-of-scope
|
|
876
|
+
|
|
877
|
+
These are explicit architectural exclusions or post-v1 explorations.
|
|
878
|
+
|
|
879
|
+
### Architecture
|
|
880
|
+
- **Cross-runtime bit-exact determinism**: a soft-float library would
|
|
881
|
+
replace `Math.sin/cos/exp/log/pow` in the hot path. The codebase is
|
|
882
|
+
already structured to make this a swap-in at `quat_integrate.js` and
|
|
883
|
+
tangent-basis construction in `build_manifold.js`. Not pursued because
|
|
884
|
+
the same-runtime determinism we have covers the common cases (single-
|
|
885
|
+
device replay, networked lockstep where all clients run the same JS
|
|
886
|
+
engine).
|
|
887
|
+
- **WASM / SIMD**: the engine targets pure-JS portability. SIMD would
|
|
888
|
+
invalidate the determinism story (V8 doesn't expose deterministic
|
|
889
|
+
Float64x2 ops).
|
|
890
|
+
- **Multi-threaded solver**: workers don't share memory cheaply without
|
|
891
|
+
`SharedArrayBuffer` plus the COOP/COEP HTTP headers, which are not
|
|
892
|
+
always available. Single-threaded is good-enough for the awake-body
|
|
893
|
+
budget that matters.
|
|
894
|
+
- **Packed-SoA body dynamics state — DECIDED AGAINST; do not re-open.** A
|
|
895
|
+
reviewer will note that `BodyStorage` is SoA for *identity* (entity /
|
|
896
|
+
generation / kind / flags / awake-set) but the per-body *dynamics* state —
|
|
897
|
+
velocity, inertia, mass, force/torque accumulators — lives on the `RigidBody`
|
|
898
|
+
**component object** (each carrying several `Vector3` = `Float64Array` + an
|
|
899
|
+
`onChanged` Signal), in a sparse `__bodies[]` array reached by pointer-chase,
|
|
900
|
+
not packed by slot. The same is true of `Collider` and `Joint`. This is a
|
|
901
|
+
deliberate, settled choice, not an oversight: **meep is an ECS engine, and
|
|
902
|
+
`RigidBody` / `Collider` / `Joint` are public-API components.** Them being
|
|
903
|
+
first-class objects is a UX choice and uniformity with the rest of the engine —
|
|
904
|
+
it is what makes them authorable, serializable (`toJSON`/`fromJSON` + binary
|
|
905
|
+
adapters), value-diffable (`equals`/`hash` for netcode replication), and
|
|
906
|
+
observable (`Vector3.onChanged`), exactly like every other component. A
|
|
907
|
+
packed-SoA body pool would buy locality on the *awake* integration sweeps at
|
|
908
|
+
the cost of breaking that uniformity and the public component contract. The
|
|
909
|
+
design instead leans on "mostly-sleeping" (per-body iteration is over the
|
|
910
|
+
*awake* set only), keeps the genuinely O(contacts) inner loop in flat SoA
|
|
911
|
+
(manifolds + solver scratch), and has the hot paths index the component
|
|
912
|
+
vectors' `Float64Array` backing directly to skip the observer (see
|
|
913
|
+
Determinism). This is final — do not resurface it as a "fix".
|
|
914
|
+
- **Single in-flight solve (module-scoped solver scratch).**
|
|
915
|
+
`solver/solve_contacts.js` keeps its cross-stage state (the `g_*` counters and
|
|
916
|
+
the `scratch_*` arrays) at module scope — one copy shared by all worlds, not
|
|
917
|
+
per `PhysicsSystem`. Deliberate: every world reuses one set of scratch, and
|
|
918
|
+
it's safe because the engine is single-threaded and steps one `fixedUpdate` at
|
|
919
|
+
a time. The ceiling it sets: two `PhysicsSystem` instances cannot be stepped
|
|
920
|
+
concurrently or re-entrantly (the second clobbers the first's solver scratch).
|
|
921
|
+
Accepted for a single-world, single-threaded engine; lifting it would need
|
|
922
|
+
per-system scratch (or a solver-context object threaded through the stages).
|
|
923
|
+
|
|
924
|
+
### Simulation extensions
|
|
925
|
+
- **Soft body / cloth / fluids**: the SoA layout in `BodyStorage` and the
|
|
926
|
+
manifold cache are rigid-body shaped. A soft-body system would be a
|
|
927
|
+
parallel subsystem, not an extension.
|
|
928
|
+
- **Reduced-coordinate articulations** (MuJoCo / Featherstone-style):
|
|
929
|
+
game-physics audience runs in maximal coordinates by convention. Not
|
|
930
|
+
on the roadmap.
|
|
931
|
+
|
|
932
|
+
### Game-side
|
|
933
|
+
- **Vehicle physics** (suspensions, drivetrains): a domain layer that
|
|
934
|
+
sits on top of the rigid-body primitives, not in `meep/`.
|
|
935
|
+
- **Character controllers**: same — `engine/control/first-person/` is the
|
|
936
|
+
natural home.
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
## Notable design files
|
|
941
|
+
|
|
942
|
+
- Original design plan: `C:\Users\Alex\.claude\plans\let-s-plan-to-implement-transient-harp.md`
|
|
943
|
+
- This file (state of play): `engine/physics/PLAN.md`
|