@woosh/meep-engine 2.139.0 → 2.141.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/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
- package/src/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
- package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
- package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
- package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
- package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
- package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
- package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
- package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/Triangle3D.js +318 -0
- package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
- package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
- package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
- package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
- package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
- package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_negate_array.js +2 -2
- package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
- package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
- package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
- package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
- package/src/engine/control/first-person/TODO.md +173 -127
- package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Slide.js +9 -1
- package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
- package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
- package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
- package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
- package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
- package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
- package/src/engine/physics/BULLET_REVIEW.md +945 -0
- package/src/engine/physics/CANNON_REVIEW.md +1300 -0
- package/src/engine/physics/JOLT_REVIEW.md +913 -0
- package/src/engine/physics/PLAN.md +578 -236
- package/src/engine/physics/RAPIER_REVIEW.md +934 -0
- package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
- package/src/engine/physics/REVIEW_002.md +151 -0
- package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
- package/src/engine/physics/constraint/DofMode.d.ts +28 -0
- package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
- package/src/engine/physics/constraint/DofMode.js +35 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
- package/src/engine/physics/constraint/solve_constraints.js +436 -0
- package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +608 -499
- package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
- package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.d.ts +179 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
- package/src/engine/physics/ecs/Joint.js +234 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +180 -20
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +1423 -1159
- package/src/engine/physics/fluid/FluidField.d.ts +14 -10
- package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidField.js +14 -10
- package/src/engine/physics/fluid/FluidSimulator.js +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +30 -6
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +44 -18
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
- package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
- package/src/engine/physics/gjk/gjk.d.ts +28 -2
- package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
- package/src/engine/physics/gjk/gjk.js +421 -378
- package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
- package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
- package/src/engine/physics/gjk/minkowski_support.js +75 -0
- package/src/engine/physics/gjk/mpr.d.ts +56 -0
- package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
- package/src/engine/physics/gjk/mpr.js +344 -0
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
- package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
- package/src/engine/physics/integration/integrate_position.d.ts +25 -7
- package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
- package/src/engine/physics/integration/integrate_position.js +43 -12
- package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
- package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
- package/src/engine/physics/integration/integrate_velocity.js +82 -1
- package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
- package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
- package/src/engine/physics/island/IslandBuilder.js +33 -16
- package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
- package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/PosedShape.js +28 -30
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +140 -18
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
- package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
- package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
- package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts +41 -2
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +1497 -382
- package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
- package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
- package/src/engine/physics/queries/overlap_shape.js +183 -0
- package/src/engine/physics/queries/shape_cast.d.ts +56 -0
- package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
- package/src/engine/physics/queries/shape_cast.js +387 -0
- package/src/engine/physics/solver/solve_contacts.d.ts +146 -32
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +809 -223
- package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
|
@@ -1,236 +1,578 @@
|
|
|
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 + midpoint fallback | 1 point |
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- `
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
the
|
|
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
|
-
|
|
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 + midpoint fallback | 1 point |
|
|
63
|
+
| convex × concave (heightmap, mesh) | per-triangle GJK + EPA via decomposition dispatcher | 1 point per triangle (deepest wins) |
|
|
64
|
+
| anything else | GJK + EPA | 1 point (may fail on smooth shapes) |
|
|
65
|
+
|
|
66
|
+
### Non-convex shapes
|
|
67
|
+
- **`is_convex` flag** on `AbstractShape3D.prototype` (default `true`).
|
|
68
|
+
Overridden to `false` on `HeightMapShape3D`, `MeshShape3D`, `UnionShape3D`.
|
|
69
|
+
`TransformedShape3D` inherits via getter that reads the wrapped subject.
|
|
70
|
+
- **`HeightMapShape3D`** — orientation-vector + `Sampler2D`-backed terrain
|
|
71
|
+
shape. Heights sampled via `sampleChannelCatmullRomUV` (matching the
|
|
72
|
+
terrain system's geometry construction). Compute_bounding_box,
|
|
73
|
+
contains_point, signed_distance, nearest_point_on_surface all
|
|
74
|
+
implemented; `support` throws (non-convex by construction).
|
|
75
|
+
- **`Triangle3D`** — buffer-flyweight convex shape. `bind(buffer, offset)`
|
|
76
|
+
repoints at 9 consecutive floats in an external Float64Array. Zero
|
|
77
|
+
allocation per emission; used by the decomposition path.
|
|
78
|
+
- **Triangle decomposition machinery** under
|
|
79
|
+
`engine/physics/narrowphase/decomposition/`:
|
|
80
|
+
- `TRIANGLE_FLOAT_STRIDE = 10` per triangle (`vA.xyz`, `vB.xyz`,
|
|
81
|
+
`vC.xyz`, `feature_id`).
|
|
82
|
+
- `heightmap_enumerate_triangles(out, offset, shape, ...aabb)` —
|
|
83
|
+
Arvo-projects the convex's AABB into heightmap-local, intersects
|
|
84
|
+
with the footprint to derive a cell range, emits 2 triangles per
|
|
85
|
+
cell with stable feature_ids.
|
|
86
|
+
- `mesh_enumerate_triangles(out, offset, shape, ...aabb)` — linear
|
|
87
|
+
O(N) scan over `MeshShape3D.indices` with tight per-triangle AABB
|
|
88
|
+
filtering. feature_id = triangle index.
|
|
89
|
+
- `aabb_world_to_local(out, world_aabb, pos, rot)` — 8-corner
|
|
90
|
+
projection of a world AABB into a body's local frame.
|
|
91
|
+
- `decompose_to_triangles(...)` — dispatcher switching on shape
|
|
92
|
+
type marker.
|
|
93
|
+
- **Narrowphase concave dispatch** in `narrowphase_step.js`: detects
|
|
94
|
+
`is_convex === false`, computes convex's world AABB, projects to
|
|
95
|
+
concave's local frame, decomposes, per-triangle GJK + EPA with
|
|
96
|
+
one-sided face-normal rejection and contact-normal dedup. Concave-vs-
|
|
97
|
+
concave dynamic pairs are explicitly refused.
|
|
98
|
+
|
|
99
|
+
### Solver
|
|
100
|
+
- Sequential impulse with warm-starting (10 velocity iterations by default).
|
|
101
|
+
- Coulomb friction with disk-clamped tangent impulses.
|
|
102
|
+
- Baumgarte position correction folded into the velocity solve.
|
|
103
|
+
- Full angular Jacobian (`I_w⁻¹ = R · diag · R^T`) and angular impulse
|
|
104
|
+
application.
|
|
105
|
+
- Public force/impulse-at-point API (`applyForceAt`, `applyImpulseAt`,
|
|
106
|
+
`applyTorque`).
|
|
107
|
+
|
|
108
|
+
### Sleep + events
|
|
109
|
+
- Per-island **atomic sleep**: an island sleeps when `max(|v|² + |ω|²)`
|
|
110
|
+
across all members stays below the threshold long enough; the whole
|
|
111
|
+
island sleeps in the same frame. Replaces the per-body chatter on
|
|
112
|
+
weakly-connected piles.
|
|
113
|
+
- **Atomic wake**: members of a sleeping island are threaded into a
|
|
114
|
+
circular doubly-linked list (`sleep_group_next` / `sleep_group_prev`);
|
|
115
|
+
waking any one member walks the chain and wakes the rest in the same
|
|
116
|
+
call. A 100-block stack hit at the base wakes top-down in one frame
|
|
117
|
+
rather than over 100 frames of broadphase propagation.
|
|
118
|
+
- `DisableSleep` on any island member exempts the whole island.
|
|
119
|
+
- ContactBegin / Stay / End buffer + dispatch through both
|
|
120
|
+
`PhysicsSystem.onContactBegin/Stay/End` Signals and the per-entity
|
|
121
|
+
`entity.sendEvent(PhysicsEvents.ContactBegin, ...)` channel (when a
|
|
122
|
+
dataset is attached).
|
|
123
|
+
|
|
124
|
+
### Islands
|
|
125
|
+
- **Union-find** with path halving + union by min-index over the awake-body
|
|
126
|
+
+ touched-contact graph (`engine/physics/island/union_find.js`).
|
|
127
|
+
- **`IslandBuilder`** produces deterministic CSR-style output: bodies and
|
|
128
|
+
manifold slots grouped by island, sorted ascending within and across
|
|
129
|
+
islands. Static / kinematic bodies are constraint anchors only — they
|
|
130
|
+
don't merge islands, so disjoint piles on the same floor are separate
|
|
131
|
+
islands.
|
|
132
|
+
- **Solver iterates per island**: impulse convergence happens inside an
|
|
133
|
+
island without waiting on unrelated bodies' Gauss-Seidel updates, and
|
|
134
|
+
disconnected awake bodies don't pay each other's solver cost.
|
|
135
|
+
|
|
136
|
+
### Compound bodies
|
|
137
|
+
- A body has 0..N attached colliders. Each collider has its own world
|
|
138
|
+
transform and its own BVH leaf.
|
|
139
|
+
- Same-entity colliders, child-entity colliders (via `ParentEntity`), or
|
|
140
|
+
hybrids all supported.
|
|
141
|
+
- `ColliderObserverSystem` auto-attaches colliders via the dataset when
|
|
142
|
+
paired with `PhysicsSystem` in an EntityManager.
|
|
143
|
+
- Narrowphase runs the cross-product over both bodies' collider lists per
|
|
144
|
+
body-pair, accumulates candidates, reduces to ≤4 contacts by
|
|
145
|
+
depth + spread.
|
|
146
|
+
|
|
147
|
+
### Public queries
|
|
148
|
+
- `raycast(origin, dir, max_dist, filter?)` — nearest broadphase AABB hit
|
|
149
|
+
across both trees.
|
|
150
|
+
- `shapeCast(ray, shape, rotation, result, filter?)` — broadphase swept
|
|
151
|
+
AABB against both BVHs; per-candidate AABB-slab interval narrowing,
|
|
152
|
+
coarse step over the narrowed window, GJK bisection to time-of-impact.
|
|
153
|
+
Output normal is the true contact-surface normal at the kiss point,
|
|
154
|
+
recovered by re-running GJK + EPA at `best_t` on the winning candidate.
|
|
155
|
+
Falls back to `-ray.direction` only on EPA degeneracies (NaN / zero
|
|
156
|
+
depth). Tests cover axis-aligned, off-axis, and oblique cube-vs-cube;
|
|
157
|
+
sphere-vs-smooth-shape near-tangent has documented angular tolerance
|
|
158
|
+
bands inherited from EPA on smooth supports.
|
|
159
|
+
- `overlap(shape, position, rotation, output, output_offset, filter?)`
|
|
160
|
+
— broadphase + per-candidate GJK overlap detection. Writes body_ids
|
|
161
|
+
into a caller-sized buffer; returns count. Convex query shapes only
|
|
162
|
+
(concave throws). Concave candidates routed through the per-triangle
|
|
163
|
+
decomposition path. Designed for speculative kinematic queries on
|
|
164
|
+
kinematic bodies (character controllers, AOE selection).
|
|
165
|
+
|
|
166
|
+
### Standalone narrowphase utilities
|
|
167
|
+
- `compute_penetration(out_direction, shape_a, pos_a, rot_a, shape_b,
|
|
168
|
+
pos_b, rot_b)` — non-system geometry primitive: positive penetration
|
|
169
|
+
depth + outward direction (B → A convention) on overlap, 0 otherwise.
|
|
170
|
+
Convex × convex uses GJK + EPA. Convex × concave uses per-triangle
|
|
171
|
+
half-space test (`convex.support(-face_normal)` projected onto each
|
|
172
|
+
triangle's plane), aggregated deepest-wins. The half-space approach
|
|
173
|
+
sidesteps `Triangle3D`'s degenerate support along face-normal axes
|
|
174
|
+
(the same issue that makes per-triangle GJK return false positives
|
|
175
|
+
on clearly non-overlapping sphere-above-flat configurations).
|
|
176
|
+
Concave × concave throws (M×N triangle pairs is out of scope).
|
|
177
|
+
Naturally handles "body inside the concave solid" — reports the depth
|
|
178
|
+
needed to push back through the nearest face. Documented limitation:
|
|
179
|
+
closed meshes can over-report on side faces whose 2D extent the
|
|
180
|
+
convex shape's flank crosses; a future closed-form triangle-vs-X
|
|
181
|
+
solver fixes this.
|
|
182
|
+
|
|
183
|
+
### Determinism
|
|
184
|
+
- Direct typed-array writes on hot paths (bypassing `Vector3#set`'s observer
|
|
185
|
+
dispatch) — Transform writes still go through `set()` because external
|
|
186
|
+
systems subscribe (TransformAttachment, EntityNode, FogOfWarRevealer,
|
|
187
|
+
ViewportPosition).
|
|
188
|
+
- Active body iteration sorted by body index.
|
|
189
|
+
- Pair canonicalisation `(min, max)`.
|
|
190
|
+
- Min-heap free list for slot reuse.
|
|
191
|
+
- No `Math.random` anywhere in the simulation step.
|
|
192
|
+
- Same-runtime bit-exact determinism by design; cross-runtime is a known
|
|
193
|
+
future seam.
|
|
194
|
+
|
|
195
|
+
### Migration
|
|
196
|
+
- `Motion` / `MotionSystem` / `MotionSerializationAdapter` relocated from
|
|
197
|
+
the meep core (`engine/ecs/`) to the game-domain layer
|
|
198
|
+
(`mir-engine/model/game/ecs/`). meep no longer ships the legacy shim.
|
|
199
|
+
|
|
200
|
+
### Alternative narrowphase: MPR
|
|
201
|
+
- `engine/physics/gjk/mpr.js` — Minkowski Portal Refinement (XenoCollide,
|
|
202
|
+
Snethen GDC 2009). Single-pass overlap test + MTV computation,
|
|
203
|
+
output convention matches EPA so it's drop-in compatible at any
|
|
204
|
+
narrowphase call site. Tends to converge in 5–15 iterations on
|
|
205
|
+
smooth shapes where EPA stalls (the polytope-on-curved-surface
|
|
206
|
+
failure mode the torus-knot reproducer exercised). Not yet wired
|
|
207
|
+
into `narrowphase_step` — available as a swap candidate / per-pair
|
|
208
|
+
preference once we want to fall back to it on EPA non-convergence,
|
|
209
|
+
or as the default for any shape pair that involves a mesh.
|
|
210
|
+
|
|
211
|
+
### Bonus utilities
|
|
212
|
+
- `core/geom/3d/line/line3_closest_points_segment_segment.js` — generally
|
|
213
|
+
useful 3D segment-segment closest-pair via Ericson §5.1.9.
|
|
214
|
+
- `core/collection/PairUint32Map.js` — non-allocating
|
|
215
|
+
`Map<(u32, u32) → u32>` with Robin Hood + Fibonacci hash.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Limitations / Known caveats
|
|
220
|
+
|
|
221
|
+
- **Multi-collider material precision**: solver reads friction/restitution
|
|
222
|
+
from the first-attached collider of each body. Mixed-material compound
|
|
223
|
+
bodies lose accuracy here. The contact-filter callback's `colliderA/B`
|
|
224
|
+
arguments are similarly the body's primary collider, not the specific
|
|
225
|
+
collider in contact.
|
|
226
|
+
- **EPA on smooth shapes**: degenerates (no flat face to converge on).
|
|
227
|
+
Mitigated by closed-form paths for sphere/cube/capsule pairs; exotic
|
|
228
|
+
convex shapes vs spheres can still fail.
|
|
229
|
+
- **EPA on `Triangle3D`** (concave-shape narrowphase): the triangle's
|
|
230
|
+
support is degenerate along its face-normal axis (all 3 vertices
|
|
231
|
+
project to the same value), so per-triangle GJK + EPA in the
|
|
232
|
+
narrowphase concave dispatch produces imprecise depths near the
|
|
233
|
+
iteration cap. A sphere dropping onto a flat heightmap decelerates
|
|
234
|
+
~70% on first contact but eventually sinks through over ~50 steps —
|
|
235
|
+
the `narrowphase_concave.spec.js` "drop and settle" cases are
|
|
236
|
+
`test.skip` for this reason. Workaround in `compute_penetration` is
|
|
237
|
+
the half-space pre-test that avoids running GJK on degenerate
|
|
238
|
+
triangle supports altogether; long-term fix is closed-form
|
|
239
|
+
triangle-vs-primitive solvers.
|
|
240
|
+
- **Box-box edge-edge contact**: single midpoint contact rather than
|
|
241
|
+
multi-point. Skewed-orientation cube collisions are stable-enough but
|
|
242
|
+
not as precise as face-face.
|
|
243
|
+
- **CCD floor only**: speculative margin via the fattened AABB prevents
|
|
244
|
+
most tunnelling. No per-body swept shape-cast for very fast objects.
|
|
245
|
+
- **Cross-runtime determinism is not guaranteed**: `Math.sin/cos/exp/log`
|
|
246
|
+
are ULP-correct but not bit-exact across V8 / SpiderMonkey / JSC.
|
|
247
|
+
- **Dynamic concave bodies settle poorly under TGS**: the substep loop
|
|
248
|
+
re-derives contact geometry analytically from the per-triangle contact
|
|
249
|
+
feature (witness anchors + normal) captured once by narrowphase and held
|
|
250
|
+
fixed for the whole outer step. For a convex body the contact feature is
|
|
251
|
+
stable under the small per-step motion, so this is exact; for a *dynamic
|
|
252
|
+
concave mesh body* (e.g. a torus knot rocking on its own lobes) the
|
|
253
|
+
supporting triangle itself changes as the body rocks, so freezing the
|
|
254
|
+
feature pumps a little energy in and the body rocks / slowly sinks instead
|
|
255
|
+
of settling (the `PhysicsSystem.spec.js` torus-knot dynamic-settle test is
|
|
256
|
+
`test.skip` for this reason). Note this is NOT a contact-precision issue —
|
|
257
|
+
the knot already uses the exact closed-form box-triangle solver (P1.1b);
|
|
258
|
+
the problem is purely that TGS freezes *which* feature is in contact across
|
|
259
|
+
substeps. The common concave case — a convex dynamic body on static concave
|
|
260
|
+
terrain — is unaffected (the convex side's feature is stable), and that is
|
|
261
|
+
the only concave case the engine targets.
|
|
262
|
+
|
|
263
|
+
**Interim fix (implemented): per-substep concave re-detection.** For
|
|
264
|
+
contact pairs involving a concave body, the substep loop re-runs the
|
|
265
|
+
concave narrowphase geometry at the current substep pose (instead of the
|
|
266
|
+
analytic refresh that freezes the feature) and re-prepares those contacts
|
|
267
|
+
from the fresh witness/normal/depth — so the contact normal tracks the
|
|
268
|
+
rocking body and no energy is pumped in. Convex pairs keep the cheap
|
|
269
|
+
analytic refresh. This is ~Nx narrowphase cost on concave-involved pairs
|
|
270
|
+
(acceptable — they're rare), gated by collider convexity. Un-skips the
|
|
271
|
+
torus-knot dynamic-settle test.
|
|
272
|
+
|
|
273
|
+
**Better long-term fix: convex collision proxies (not raw concave).** Every
|
|
274
|
+
major engine (Box2D, Jolt, PhysX, Rapier) requires dynamic bodies to be
|
|
275
|
+
convex or convex-decomposed; raw concave meshes are static-only. The right
|
|
276
|
+
granularity is a *few* convex pieces — NOT the thousands of tets a
|
|
277
|
+
volumetric mesher produces (tet count ≈ collider/BVH-leaf count, which
|
|
278
|
+
explodes the broadphase for an awake body; tet meshing is for a future
|
|
279
|
+
FEM/soft-body subsystem, not rigid collision). See the "Convex collision
|
|
280
|
+
proxies for dynamic concave bodies" backlog item — a 3D convex hull builder
|
|
281
|
+
(single-hull proxy covers most dynamic objects) plus an optional
|
|
282
|
+
few-hull (V-HACD-style) decomposition. Those supersede the interim
|
|
283
|
+
per-substep re-detection once built.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Backlog (planned, in scope)
|
|
288
|
+
|
|
289
|
+
### Solver quality (next major work)
|
|
290
|
+
|
|
291
|
+
These items move the engine from "competent" to "great". TGS is the next
|
|
292
|
+
significant solver-architecture change; joints come after, once the TGS
|
|
293
|
+
scaffolding is in place.
|
|
294
|
+
|
|
295
|
+
- **TGS (Temporal Gauss-Seidel) substepping with split-impulse** — Phases
|
|
296
|
+
1–3 **LANDED**. The solver is now a staged TGS pipeline
|
|
297
|
+
(`solver/solve_contacts.js`: `prepare_contacts` → per substep
|
|
298
|
+
[`refresh_contacts` → `warm_start_contacts` → `solve_velocity` →
|
|
299
|
+
`solve_position`] → `apply_restitution`), driven by the substep loop in
|
|
300
|
+
`PhysicsSystem.fixedUpdate`. Defaults: `substeps = 4`,
|
|
301
|
+
`velocityIterations = 4`, `positionIterations = 1` (all fields on
|
|
302
|
+
`PhysicsSystem`).
|
|
303
|
+
- **Phase 1 — split impulse.** Position correction runs on a per-body
|
|
304
|
+
pseudo-velocity (`__pseudo_velocity`) folded into the pose by
|
|
305
|
+
`integrate_position` and discarded; depth correction never
|
|
306
|
+
contaminates persistent velocity.
|
|
307
|
+
- **Phase 2 — one-shot restitution.** Velocity pass is pure
|
|
308
|
+
non-penetration; restitution is a single post-loop pass driving
|
|
309
|
+
`vn → -e·vn_approach`, gated on a running max normal impulse
|
|
310
|
+
(`maxNormalImpulse`) so transient collisions still bounce under
|
|
311
|
+
per-substep warm-start.
|
|
312
|
+
- **Phase 3 — substep loop.** `substeps` sub-iterations at `h = dt/N`.
|
|
313
|
+
Forces consumed once at full `dt` before the loop; gravity applied
|
|
314
|
+
per substep; **warm-start replayed per substep** (the crux — a
|
|
315
|
+
per-substep impulse balances one substep of gravity, so resting
|
|
316
|
+
stacks hold at zero velocity). Contact geometry is re-derived
|
|
317
|
+
**analytically** each substep from frozen local witness anchors +
|
|
318
|
+
the trusted prepare-time depth (a sign-robust delta), so narrowphase
|
|
319
|
+
runs **once** per outer step — cheaper than the originally-planned
|
|
320
|
+
per-substep match-and-merge refresh, and exact for convex
|
|
321
|
+
primitives whose contact feature is stable under small motion.
|
|
322
|
+
|
|
323
|
+
Results vs the single-step solver: a 100:1 mass ratio now stacks
|
|
324
|
+
instead of crushing through (regression test added); 8-cube stacks
|
|
325
|
+
settle to zero velocity and sleep (were impossible long-term under SI);
|
|
326
|
+
falling-tower bench cost unchanged (~48 ms/1000 active bodies);
|
|
327
|
+
`substeps = 1` reproduces the single-step result bit-for-bit-ish
|
|
328
|
+
(one-frame restitution delay aside).
|
|
329
|
+
|
|
330
|
+
**Hard-won lessons (for REVIEW_002):**
|
|
331
|
+
- Warm-start MUST be per-substep, not once. Replaying a full-frame
|
|
332
|
+
impulse once while gravity arrives per substep over-pushes resting
|
|
333
|
+
contacts and *explodes* deep stacks. Per-substep warm-start +
|
|
334
|
+
per-substep gravity cancel exactly at rest.
|
|
335
|
+
- Restitution must gate on the *running max* normal impulse, not the
|
|
336
|
+
end-of-loop value — per-substep warm-start relaxes a transient
|
|
337
|
+
contact's `j_n` back to ~0 by the end, which would suppress the
|
|
338
|
+
bounce.
|
|
339
|
+
- Analytic separation re-derivation beats per-substep narrowphase
|
|
340
|
+
for convex shapes (cheaper, no manifold-lifecycle churn) but is
|
|
341
|
+
only as good as the frozen normal — see the concave caveat below.
|
|
342
|
+
|
|
343
|
+
Follow-ups since the core landed:
|
|
344
|
+
- [x] **Box-box SAT reference tie-break deadband** — aligned cube
|
|
345
|
+
stacks (4–10 high) now settle to zero velocity and sleep; the
|
|
346
|
+
reference-face flip-flop that creeped/toppled them is gone.
|
|
347
|
+
- [x] **Per-substep contact re-detection for concave pairs** — lifts
|
|
348
|
+
the dynamic-concave-body limitation; the torus-knot dynamic-settle
|
|
349
|
+
test is un-skipped. Concave pairs re-run narrowphase geometry each
|
|
350
|
+
substep (`redetect_concave_contacts`); convex pairs keep the cheap
|
|
351
|
+
analytic refresh.
|
|
352
|
+
|
|
353
|
+
Remaining (Phases 4–6) — now complete:
|
|
354
|
+
- [x] Regression coverage: heavy-on-light pyramid (10× capstone on two
|
|
355
|
+
light cubes settles + sleeps) and a ragdoll-stub (shoulder
|
|
356
|
+
ball-socket + elbow hinge arm hangs, stays articulated, settles).
|
|
357
|
+
- [x] REVIEW_002 retrospective — `engine/physics/REVIEW_002.md`.
|
|
358
|
+
|
|
359
|
+
References: Catto 2018 ("Soft Constraints" GDC talk + the TGS
|
|
360
|
+
follow-up); Box2D v3 source (`b2ApplyRestitution`, the substep solver
|
|
361
|
+
stages); Rapier as the closest architectural sibling.
|
|
362
|
+
|
|
363
|
+
- [ ] **Constraints / joints — the next major work.** Now unblocked: TGS is
|
|
364
|
+
in (joint-chain convergence is a TGS sweet spot), warm-start +
|
|
365
|
+
per-substep + island machinery is reusable, and the SPOOK compliance
|
|
366
|
+
dial already in the contact solver gives soft/spring constraints
|
|
367
|
+
essentially for free. Target use cases: chains/ropes, ragdolls,
|
|
368
|
+
vehicles (incl. suspension), plus the common mechanical set (doors,
|
|
369
|
+
pistons, welds, grab/drag, winches, drivetrains).
|
|
370
|
+
|
|
371
|
+
**Foundational work (do first): generalise the solver to constraint
|
|
372
|
+
rows.** Today `solver/solve_contacts.js` is hard-coded to the
|
|
373
|
+
contact-shape constraint (normal + 2 friction tangents, ≥0 clamp,
|
|
374
|
+
restitution, penetration bias). Joints are equality / inequality
|
|
375
|
+
constraints on relative velocity at anchors, generally bilateral
|
|
376
|
+
(impulse may be ±) with optional limits and motors. The clean shape —
|
|
377
|
+
and what Jolt / Box2D-v3 do — is a **generic constraint row**: a
|
|
378
|
+
Jacobian (linear + angular parts per body), an effective mass, a bias
|
|
379
|
+
(position error × SPOOK gain, or motor target), and impulse bounds
|
|
380
|
+
`[lo, hi]` (`[0,∞)` for a contact/limit, `(−∞,∞)` for an equality,
|
|
381
|
+
`[−maxForce·h, +maxForce·h]` for a motor). Each joint type just fills
|
|
382
|
+
in its rows; the existing per-body impulse-apply primitive
|
|
383
|
+
(`apply_impulse_to_body` + `world_inverse_inertia_apply`), the
|
|
384
|
+
per-substep warm-start, the islands, and the split-impulse / SPOOK
|
|
385
|
+
position handling are all reused. Contacts become *one* constraint
|
|
386
|
+
type among several rather than the hard-coded path.
|
|
387
|
+
|
|
388
|
+
The specific constraint set, its use-case mapping, and per-type
|
|
389
|
+
architecture-fit assessment are under review (see the constraints
|
|
390
|
+
sketch). High level: ball-socket / distance / spring / weld and the
|
|
391
|
+
grab constraint are near drop-ins on the row machinery; hinge /
|
|
392
|
+
prismatic / cone-twist / motors / limits add angular-row + bounded-row
|
|
393
|
+
mechanics (still within the impulse framework); raycast vehicles,
|
|
394
|
+
conveyor surface-velocity, and gear/pulley coupling are higher-level
|
|
395
|
+
systems or contact modifiers that sit *on top of* the primitives
|
|
396
|
+
rather than being generic rows.
|
|
397
|
+
|
|
398
|
+
**Decision: build ONE configurable 6-DOF constraint** (PhysX D6 / Jolt
|
|
399
|
+
SixDOF), implemented mode-by-mode. The `Joint` ECS component carries
|
|
400
|
+
`dofMode[6]` (3 linear, 3 angular) each `{locked|free|limited|spring|
|
|
401
|
+
motor}` + per-DOF limit/spring/motor config + warm-start accumulators.
|
|
402
|
+
Concrete joints are configs, not new code (ball-socket = lock 3 linear;
|
|
403
|
+
hinge = lock 3 linear + 2 angular; weld = lock 6; cone-twist = lock 3
|
|
404
|
+
linear + limit 3 angular; suspension = spring 1 linear + lock rest).
|
|
405
|
+
|
|
406
|
+
Phasing:
|
|
407
|
+
1. [x] Constraint-row solver as a **parallel row set** in the TGS
|
|
408
|
+
substep loop (contacts left untouched, not ported — lower risk).
|
|
409
|
+
`constraint/solve_constraints.js` reuses `world_inverse_inertia`,
|
|
410
|
+
per-substep warm-start, and the SPOOK position bias; `Joint`
|
|
411
|
+
component + `link_joint`/`unlink_joint` in PhysicsSystem;
|
|
412
|
+
`jointIterations` knob. Bodies need no collider.
|
|
413
|
+
2. [x] **LOCKED linear DOFs → ball-socket.** Pendulum (anchor pinned
|
|
414
|
+
to a world pivot, body swings) and a 2-link chain (body↔body,
|
|
415
|
+
joints stay connected, chain hangs) pass. → **chains, ropes,
|
|
416
|
+
pendulums working.**
|
|
417
|
+
3. [x] LOCKED angular + linear DOFs in the frame basis — **weld,
|
|
418
|
+
hinge, prismatic done**. Joint frame bases
|
|
419
|
+
(`localBasisA`/`localBasisB`); BOTH linear and angular rows now
|
|
420
|
+
resolve in frame A's axes (cleared the world-axis linear debt — the
|
|
421
|
+
solver is fully frame-relative). Angular: relative rotation
|
|
422
|
+
`qD = conj(qA)·qB` → small-angle error, ωB−ωA rows + SPOOK bias.
|
|
423
|
+
Linear: `C·axis` error, vA−vB rows. `asWeld()` / `asHinge(axis)` /
|
|
424
|
+
`asPrismatic(axis)` presets. Verified: weld holds pose + orientation
|
|
425
|
+
against an off-centre torque; hinge swings about its free axis only
|
|
426
|
+
(locked axes < 0.02); prismatic slides along its one free axis,
|
|
427
|
+
locked on the others; all LOCKED-mode tests still green after the
|
|
428
|
+
frame-basis rewrite.
|
|
429
|
+
4. [ ] LIMITED + MOTOR (bounded rows) → doors, pistons, wheel
|
|
430
|
+
spin/drive, joint ROM.
|
|
431
|
+
5. [ ] SPRING (SPOOK soft) → suspension, bungees, soft ragdolls.
|
|
432
|
+
6. [ ] Cone-twist / swing-twist angular limits → ragdolls.
|
|
433
|
+
7. [ ] Vehicle layer — recommend a **raycast-vehicle controller**
|
|
434
|
+
(raycast + suspension force + tire friction; what most games ship)
|
|
435
|
+
on top of the queries, with simulated wheels via the 6-DOF as an
|
|
436
|
+
option. → vehicles.
|
|
437
|
+
8. [ ] Extras: pulley, gear, conveyor (contact surface-velocity),
|
|
438
|
+
breakable-joint flag.
|
|
439
|
+
|
|
440
|
+
Foundation gaps — both now closed:
|
|
441
|
+
- [x] **Island integration.** Jointed dynamic-dynamic bodies are
|
|
442
|
+
unioned into one island (`IslandBuilder` Pass 1b), so a chain /
|
|
443
|
+
ragdoll sleeps and wakes as a unit; `__wake_joints` propagates wake
|
|
444
|
+
across a joint when one side is awake and the other asleep
|
|
445
|
+
(e.g. a kinematic/motor driver pulling a sleeping chain). Verified:
|
|
446
|
+
a damped chain settles and both links sleep in one sleep group.
|
|
447
|
+
- [x] **Generation-checked body references.** `solve_joints`,
|
|
448
|
+
`IslandBuilder` Pass 1b and `__wake_joints` all gate on
|
|
449
|
+
`storage.is_valid(packedId)`, so a joint to an unlinked / slot-reused
|
|
450
|
+
body goes inert instead of attaching to the wrong body or crashing.
|
|
451
|
+
Verified: unlinking a jointed body leaves the joint inert and the
|
|
452
|
+
survivor free.
|
|
453
|
+
|
|
454
|
+
References: Catto / Box2D-v3 joint solvers; Jolt's `Constraint` base
|
|
455
|
+
(`SetupVelocityConstraint` / `WarmStartVelocityConstraint` /
|
|
456
|
+
`SolveVelocityConstraint` / `SolvePositionConstraint`); PhysX D6 /
|
|
457
|
+
ODE joint taxonomy.
|
|
458
|
+
|
|
459
|
+
### Stability
|
|
460
|
+
- [ ] **Closed-form triangle-vs-primitive solvers**
|
|
461
|
+
(`triangle_sphere_contact`, `triangle_box_contact`,
|
|
462
|
+
`triangle_capsule_contact`). The decomposition machinery is in
|
|
463
|
+
place (`Triangle3D` flyweight, `heightmap_enumerate_triangles` /
|
|
464
|
+
`mesh_enumerate_triangles`, `decompose_to_triangles` dispatcher,
|
|
465
|
+
`aabb_world_to_local`, `narrowphase_step.js` concave branch), but
|
|
466
|
+
the per-triangle narrowphase uses GJK + EPA which hits the
|
|
467
|
+
smooth-shape iteration cap and `Triangle3D`'s degenerate-support
|
|
468
|
+
issue. Closed-form solvers per primitive bypass both. This is now
|
|
469
|
+
the single biggest accuracy gap in the engine — it would:
|
|
470
|
+
- Unblock the `narrowphase_concave.spec.js` skipped tests (ball
|
|
471
|
+
drops on heightmap / mesh-cube settle correctly).
|
|
472
|
+
- Unblock the `PhysicsSystem.spec.js` torus-knot test.
|
|
473
|
+
- Improve `compute_penetration`'s closed-mesh accuracy (currently
|
|
474
|
+
documented over-reports on side faces).
|
|
475
|
+
Existing primitive pair solvers (`sphere_box_contact`,
|
|
476
|
+
`capsule_box_multi_contacts`, `box_box_manifold`) are the
|
|
477
|
+
blueprint. Triangle is roughly a box with two half-extents = 0.
|
|
478
|
+
- [ ] **Edge-edge multi-point manifold** for skewed box contacts.
|
|
479
|
+
- [ ] **Per-contact source-collider tracking** so multi-material compound
|
|
480
|
+
bodies get accurate per-contact friction/restitution. Requires
|
|
481
|
+
stashing the collider identity in the manifold contact stride.
|
|
482
|
+
|
|
483
|
+
### Performance / Scale
|
|
484
|
+
- [ ] **Per-body linear CCD shape-cast**: optional opt-in for fast-moving
|
|
485
|
+
bodies where speculative margin isn't enough. The bench's falling
|
|
486
|
+
tower (1km drop onto a 1cm floor) is the concrete reproducer —
|
|
487
|
+
180 / 1000 bodies tunnel.
|
|
488
|
+
- [ ] **Per-island parallel solve**: today's island data layout would
|
|
489
|
+
allow worker-based solving once `SharedArrayBuffer` is available.
|
|
490
|
+
Out-of-scope unless / until SAB is universally usable.
|
|
491
|
+
|
|
492
|
+
### Features
|
|
493
|
+
- [ ] **Convex collision proxies for dynamic concave bodies.** The long-term
|
|
494
|
+
replacement for the interim per-substep concave re-detection (see
|
|
495
|
+
Limitations) — and how every major engine handles dynamic non-convex
|
|
496
|
+
shapes: collide a *few* convex pieces, never the raw concave mesh.
|
|
497
|
+
1. **3D convex hull builder** (meep has only 2D hulls today —
|
|
498
|
+
`core/geom/2d/convex-hull/`). A single hull of a mesh is one
|
|
499
|
+
collider / one broadphase leaf and covers the overwhelming majority
|
|
500
|
+
of dynamic objects (thrown props, debris). Pairs with the existing
|
|
501
|
+
"Convex hull shape + eigen-inertia" item below.
|
|
502
|
+
2. **Few-hull (V-HACD-style) approximate convex decomposition** for
|
|
503
|
+
shapes whose concavity matters (a cup, a chair): ~8–64 fat convex
|
|
504
|
+
hulls = 8–64 colliders, two orders of magnitude below a tet mesh.
|
|
505
|
+
Each hull is convex → stable contact feature → the TGS analytic refresh
|
|
506
|
+
is exact → no per-substep re-detection, no rocking. Granularity is the
|
|
507
|
+
whole point: collider/BVH-leaf count must stay small for an *awake*
|
|
508
|
+
dynamic body (the volumetric tet-mesher under `core/geom/3d/tetrahedra/`
|
|
509
|
+
is the wrong tool here — thousands of pieces — and belongs to a future
|
|
510
|
+
FEM/soft-body subsystem, not rigid collision).
|
|
511
|
+
- [ ] **Convex hull shape** with eigen-based principal-axes inertia
|
|
512
|
+
derivation. Hooks `matrix_eigenvalues_in_place` from the existing
|
|
513
|
+
linalg layer.
|
|
514
|
+
- [ ] **Cylinder / cone shapes** (closed-form pairs against the existing
|
|
515
|
+
family + GJK+EPA fallback for general convex).
|
|
516
|
+
|
|
517
|
+
### API polish
|
|
518
|
+
- [x] **`overlap(shape, position, rotation, output, output_offset,
|
|
519
|
+
filter?)`** — broadphase + narrowphase overlap query for kinematic
|
|
520
|
+
/ AOE / selection use cases. Body_ids written into a caller-sized
|
|
521
|
+
Uint32Array buffer. Convex query shape only; concave candidates
|
|
522
|
+
are routed through the per-triangle decomposition path.
|
|
523
|
+
- [x] **`shapeCast(ray, shape, rotation, result, filter?)`** for
|
|
524
|
+
character controllers and kinematic shape sweeps. Broadphase
|
|
525
|
+
swept-AABB against both BVHs; per-candidate AABB-slab interval
|
|
526
|
+
narrowing + coarse step + GJK bisection for time-of-impact. The
|
|
527
|
+
output `result.normal` is the true contact-surface normal at the
|
|
528
|
+
kiss point, computed by re-running GJK + EPA at `best_t` on the
|
|
529
|
+
winning candidate (falls back to `-ray.direction` only on EPA
|
|
530
|
+
degeneracies).
|
|
531
|
+
- [x] **`compute_penetration(out_direction, shape_a, pos_a, rot_a,
|
|
532
|
+
shape_b, pos_b, rot_b)`** — standalone geometry primitive (no
|
|
533
|
+
PhysicsSystem) for resolving overlap between two shapes at given
|
|
534
|
+
poses. Returns depth + outward direction. Convex × convex via
|
|
535
|
+
GJK + EPA; convex × concave via per-triangle half-space test.
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Future / out-of-scope
|
|
540
|
+
|
|
541
|
+
These are explicit architectural exclusions or post-v1 explorations.
|
|
542
|
+
|
|
543
|
+
### Architecture
|
|
544
|
+
- **Cross-runtime bit-exact determinism**: a soft-float library would
|
|
545
|
+
replace `Math.sin/cos/exp/log/pow` in the hot path. The codebase is
|
|
546
|
+
already structured to make this a swap-in at `quat_integrate.js` and
|
|
547
|
+
tangent-basis construction in `build_manifold.js`. Not pursued because
|
|
548
|
+
the same-runtime determinism we have covers the common cases (single-
|
|
549
|
+
device replay, networked lockstep where all clients run the same JS
|
|
550
|
+
engine).
|
|
551
|
+
- **WASM / SIMD**: the engine targets pure-JS portability. SIMD would
|
|
552
|
+
invalidate the determinism story (V8 doesn't expose deterministic
|
|
553
|
+
Float64x2 ops).
|
|
554
|
+
- **Multi-threaded solver**: workers don't share memory cheaply without
|
|
555
|
+
`SharedArrayBuffer` plus the COOP/COEP HTTP headers, which are not
|
|
556
|
+
always available. Single-threaded is good-enough for the awake-body
|
|
557
|
+
budget that matters.
|
|
558
|
+
|
|
559
|
+
### Simulation extensions
|
|
560
|
+
- **Soft body / cloth / fluids**: the SoA layout in `BodyStorage` and the
|
|
561
|
+
manifold cache are rigid-body shaped. A soft-body system would be a
|
|
562
|
+
parallel subsystem, not an extension.
|
|
563
|
+
- **Reduced-coordinate articulations** (MuJoCo / Featherstone-style):
|
|
564
|
+
game-physics audience runs in maximal coordinates by convention. Not
|
|
565
|
+
on the roadmap.
|
|
566
|
+
|
|
567
|
+
### Game-side
|
|
568
|
+
- **Vehicle physics** (suspensions, drivetrains): a domain layer that
|
|
569
|
+
sits on top of the rigid-body primitives, not in `meep/`.
|
|
570
|
+
- **Character controllers**: same — `engine/control/first-person/` is the
|
|
571
|
+
natural home.
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Notable design files
|
|
576
|
+
|
|
577
|
+
- Original design plan: `C:\Users\Alex\.claude\plans\let-s-plan-to-implement-transient-harp.md`
|
|
578
|
+
- This file (state of play): `engine/physics/PLAN.md`
|