@woosh/meep-engine 2.139.0 → 2.140.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_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 +461 -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/broadphase/compute_fat_world_aabb.js +2 -2
- 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/PhysicsSystem.d.ts +128 -20
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +1301 -1159
- package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidSimulator.js +2 -1
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +28 -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 +39 -17
- 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/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 +113 -17
- 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 +8 -2
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -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 +116 -30
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +641 -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
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# REVIEW_001 — Action Plan
|
|
2
|
+
|
|
3
|
+
Plan derived from the consolidated executive summary of three independent
|
|
4
|
+
deep-reviewer reports (Bullet / Jolt / Rapier — all on opus, each
|
|
5
|
+
instructed to read both sides line-by-line and write to disk).
|
|
6
|
+
|
|
7
|
+
Source reviews live alongside this file:
|
|
8
|
+
|
|
9
|
+
- `BULLET_REVIEW.md`
|
|
10
|
+
- `JOLT_REVIEW.md`
|
|
11
|
+
- `RAPIER_REVIEW.md`
|
|
12
|
+
|
|
13
|
+
Each priority below cites the convergent findings from those reports and
|
|
14
|
+
breaks the work into concrete actionable units with verification
|
|
15
|
+
criteria. Effort estimates match the executive summary's prioritisation
|
|
16
|
+
table.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## P0 — Correctness blockers (hours)
|
|
21
|
+
|
|
22
|
+
### P0.1 — Fix warm-start wipe
|
|
23
|
+
|
|
24
|
+
**Status**: LANDED, activated alongside P1.2.
|
|
25
|
+
|
|
26
|
+
**Bug.** Every frame for every active manifold slot, `dispatch_pair`'s
|
|
27
|
+
accumulator path calls `manifolds.clear_contacts(slot)`. `clear_contacts`
|
|
28
|
+
at `ManifoldStore.js:233` does `this.__data.fill(0, data_off, data_off + SLOT_DATA_STRIDE)`
|
|
29
|
+
— zeroing the FULL slot stride, including offsets 10/11/12 (`j_n`,
|
|
30
|
+
`j_t1`, `j_t2`). `set_contact` at line 258 then only writes offsets 0..9
|
|
31
|
+
(position / normal / depth), leaving the warm-start impulses at zero.
|
|
32
|
+
The comment at line 270 — `// j_n, j_t1, j_t2 are warm-start; preserved
|
|
33
|
+
across calls` — is true at the per-call level but contradicted by the
|
|
34
|
+
upstream wipe. Solver's warm-start sees zero accumulated impulses every
|
|
35
|
+
frame.
|
|
36
|
+
|
|
37
|
+
**Fix path (infrastructure landed).** `ManifoldStore` now exposes two
|
|
38
|
+
operations:
|
|
39
|
+
|
|
40
|
+
1. `begin_refill(slot)`: resets the contact count to zero (so the
|
|
41
|
+
next `set_contact(slot, 0, …)` lands at index 0) but does NOT zero
|
|
42
|
+
the data slab. Warm-start impulses survive. **Implemented.**
|
|
43
|
+
2. `clear_contacts(slot)` is unchanged (genuine eviction zeroes
|
|
44
|
+
everything — still used at line 672 when narrowphase determines
|
|
45
|
+
the pair has no colliders).
|
|
46
|
+
|
|
47
|
+
**Activation blocked.** The narrowphase callsite at line 687
|
|
48
|
+
intentionally still calls `clear_contacts`. We tried flipping it to
|
|
49
|
+
`begin_refill` and ran the full physics spec suite — the 4-cube
|
|
50
|
+
atomic-sleep test (`PhysicsSystem.spec.js`) fails because the stack
|
|
51
|
+
**collapses** within ~3 simulated seconds. Diagnostic capture at tick
|
|
52
|
+
794:
|
|
53
|
+
|
|
54
|
+
| cube | y (expected) | y (actual) | vy | sleep_timer |
|
|
55
|
+
|------|--------------|------------|--------------|-------------|
|
|
56
|
+
| 0 | 0.5 | 0.4996 | 0 | 0.504 → SLEEPING (singleton) |
|
|
57
|
+
| 1 | 1.5 | 0.4983 | ~1e-9 | 0.083 |
|
|
58
|
+
| 2 | 2.5 | 0.689 | +0.21 | 0 |
|
|
59
|
+
| 3 | 3.5 | 0.707 | bouncing | 0 |
|
|
60
|
+
|
|
61
|
+
Cube 1 has sunk **1 m below** its proper resting position (interpenetrating
|
|
62
|
+
cube 0 by a whole edge). The reviewer's assumption "slot ordering is
|
|
63
|
+
usually preserved" is false: `reduce_candidates` (`narrowphase_step.js:104`)
|
|
64
|
+
reorders contacts by depth + max-spread, so the cached `j_n` at slot
|
|
65
|
+
index `i` next frame is applied to a **different geometric contact**
|
|
66
|
+
than the one whose impulse converged last frame. Stale impulses applied
|
|
67
|
+
to wrong contacts destabilise the solver immediately on stacks.
|
|
68
|
+
|
|
69
|
+
**Conclusion: P0.1 was bundled with P1.2.** Both landed together: the
|
|
70
|
+
`begin_refill` primitive replaced `clear_contacts` at the narrowphase
|
|
71
|
+
refill callsite, while P1.2's match-and-merge pass ensures the cached
|
|
72
|
+
impulses follow the right contact slot index across the per-frame
|
|
73
|
+
reshuffle that `reduce_candidates` introduces. The 4-cube atomic-sleep
|
|
74
|
+
test (which collapsed when `begin_refill` was activated without
|
|
75
|
+
matching) now passes with warm-start active.
|
|
76
|
+
|
|
77
|
+
**Verification (landed).**
|
|
78
|
+
- `ManifoldStore.spec.js`: asserts `j_n` / `j_t1` / `j_t2` survive a
|
|
79
|
+
`begin_refill` + `set_contact` cycle; `clear_contacts` still wipes
|
|
80
|
+
them; multi-contact preservation works.
|
|
81
|
+
- `PhysicsSystem.spec.js`: 4-cube atomic-sleep test, 16-cube short
|
|
82
|
+
window stack, and all dependent integration tests pass with warm
|
|
83
|
+
start active.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### P0.2 — Remove `debugger;` from EPA hot path
|
|
88
|
+
|
|
89
|
+
**Status**: confirmed at `gjk/expanding_polytope_algorithm.js:136`.
|
|
90
|
+
|
|
91
|
+
A `debugger;` statement is sitting in the EPA expansion loop. Production
|
|
92
|
+
ship-blocker. Likely a leftover from a prior debug session.
|
|
93
|
+
|
|
94
|
+
**Fix.** Delete line 136 (and 135 / 137 if they're related guard /
|
|
95
|
+
comment). Confirm by grepping `^\s*debugger` across the package — there
|
|
96
|
+
should be zero hits afterward.
|
|
97
|
+
|
|
98
|
+
**Verification.** `grep -rn "debugger" src/engine/physics/` returns
|
|
99
|
+
nothing.
|
|
100
|
+
|
|
101
|
+
**Effort**: minutes.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## P1 — Largest accuracy/correctness wins (days)
|
|
106
|
+
|
|
107
|
+
### P1.1 — Closed-form sphere / box / capsule-vs-triangle
|
|
108
|
+
|
|
109
|
+
**Status**: LANDED in three sub-commits — P1.1a (sphere), P1.1b
|
|
110
|
+
(box), P1.1c (capsule).
|
|
111
|
+
|
|
112
|
+
**Problem.** The narrowphase concave dispatch (added in commit 28ef0e894)
|
|
113
|
+
decomposes heightmaps / meshes to triangles and runs per-triangle GJK +
|
|
114
|
+
EPA. `Triangle3D` has a degenerate support along its face-normal axis
|
|
115
|
+
(all 3 vertices project to the same value), so GJK precision is poor —
|
|
116
|
+
hence the skipped settle tests in `narrowphase_concave.spec.js` and the
|
|
117
|
+
documented sink-through-heightmap behaviour. `compute_penetration`
|
|
118
|
+
already works around this for sphere-vs-heightmap by using the
|
|
119
|
+
half-space test on the face plane, but the narrowphase still uses
|
|
120
|
+
per-triangle GJK+EPA.
|
|
121
|
+
|
|
122
|
+
**Files to add** (suggested layout, paralleling existing
|
|
123
|
+
`sphere_box_contact.js` / `capsule_contacts.js`):
|
|
124
|
+
|
|
125
|
+
- `engine/physics/narrowphase/sphere_triangle_contact.js` ✅ LANDED —
|
|
126
|
+
closest-point on triangle (uses
|
|
127
|
+
`computeTriangleClosestPointToPointBarycentric`), distance to sphere
|
|
128
|
+
centre, depth = R − dist when overlap. Output stride matches
|
|
129
|
+
`sphere_box_contact`. Includes upfront degenerate-triangle guard
|
|
130
|
+
(zero-area triangle returns false) and dist=0 fallback to face
|
|
131
|
+
normal. 11 unit tests covering face, edge, vertex regions,
|
|
132
|
+
separation, tangent, singular, degenerate, tilted, indexing.
|
|
133
|
+
- `engine/physics/narrowphase/box_triangle_contact.js` ✅ LANDED —
|
|
134
|
+
SAT over 13 axes (3 box face normals + 1 triangle normal + 9 edge-edge
|
|
135
|
+
crosses) with the corrected asymmetric `min(push_pos, push_neg)`
|
|
136
|
+
per-axis MTV (triangle projection is NOT symmetric around its
|
|
137
|
+
centroid, unlike box's). Three contact-generation branches:
|
|
138
|
+
- **Box face winner** → reference = box face, incident = triangle.
|
|
139
|
+
Project triangle into the face's (u, v) basis, clip with 4 axis-aligned
|
|
140
|
+
Sutherland-Hodgman passes, recover world contacts on the triangle
|
|
141
|
+
plane.
|
|
142
|
+
- **Triangle face winner** → reference = triangle, incident = box
|
|
143
|
+
face most antiparallel to contact normal. Project the box quad
|
|
144
|
+
onto the triangle plane (basis = AB normalised + tn × AB), clip
|
|
145
|
+
with 3 general-half-plane passes against the triangle's 3 edges,
|
|
146
|
+
recover world contacts on the box face plane.
|
|
147
|
+
- **Edge-cross winner** → identify the relevant box edge (one of 4
|
|
148
|
+
parallel edges, selected by sign of `-n` projected onto the two
|
|
149
|
+
perpendicular box axes) and triangle edge, find closest pair via
|
|
150
|
+
`line3_closest_points_segment_segment`, emit single contact.
|
|
151
|
+
|
|
152
|
+
12 unit tests covering separation, degenerate triangle, box face
|
|
153
|
+
winner (single + multi-point), triangle face winner, deep
|
|
154
|
+
penetration, +/-Y face flip, self-consistency reconstruction
|
|
155
|
+
(`tri_pt = box_pt + depth * n`), rotated box, near-corner case,
|
|
156
|
+
translated box, output indexing.
|
|
157
|
+
- `engine/physics/narrowphase/capsule_triangle_contact.js` ✅ LANDED —
|
|
158
|
+
multi-point manifold via primary segment-vs-triangle closest-pair
|
|
159
|
+
+ cap-centre sphere queries. The `segment_triangle_closest` helper
|
|
160
|
+
takes the minimum of 6 candidates: (case 0) segment crosses
|
|
161
|
+
triangle plane inside the face → distance 0; (cases 1, 2) each
|
|
162
|
+
segment endpoint vs `computeTriangleClosestPointToPointBarycentric`;
|
|
163
|
+
(cases 3, 4, 5) `line3_closest_points_segment_segment` against each
|
|
164
|
+
triangle edge. Primary contact emitted from the closest pair; each
|
|
165
|
+
cap centre then runs `sphere_triangle_contact` and is spatially
|
|
166
|
+
deduplicated against the primary. Result: 1..3 contacts.
|
|
167
|
+
Pattern matches `capsule_box_multi_contacts`. 10 unit tests.
|
|
168
|
+
|
|
169
|
+
**Wiring.** Inside `narrowphase_step.js`'s concave branch (the one that
|
|
170
|
+
calls `decompose_to_triangles`), per-triangle dispatch by convex
|
|
171
|
+
shape type:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
if (convex_shape.isUnitSphereShape3D) → per-triangle sphere_triangle_contact [LANDED]
|
|
175
|
+
else if (convex_shape.isBoxShape3D) → per-triangle box_triangle_contact [LANDED]
|
|
176
|
+
else if (convex_shape.isCapsuleShape3D) → per-triangle capsule_triangle_contact [LANDED]
|
|
177
|
+
else → existing per-triangle GJK+EPA fallback
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Each closed-form path reads the triangle's three vertices from the
|
|
181
|
+
decomposition buffer at stride 10, rotates them to world space (3
|
|
182
|
+
quat rotations per triangle — ~63 flops, negligible vs. the GJK + EPA
|
|
183
|
+
cost being replaced), runs its solver, then runs the same one-sided
|
|
184
|
+
face-normal rejection and normal-based dedup as the EPA path. The
|
|
185
|
+
closed-form paths use ACTUAL surface witnesses (closest point on
|
|
186
|
+
triangle, sphere surface point) rather than body centres — exact in
|
|
187
|
+
both directions, unlike the EPA fallback where support witnesses can
|
|
188
|
+
be arbitrarily far from the contact patch.
|
|
189
|
+
|
|
190
|
+
**Verification (P1.1 fully landed).**
|
|
191
|
+
- 11 unit tests in `sphere_triangle_contact.spec.js`.
|
|
192
|
+
- 12 unit tests in `box_triangle_contact.spec.js`.
|
|
193
|
+
- 10 unit tests in `capsule_triangle_contact.spec.js`.
|
|
194
|
+
- The two sphere settle tests in `narrowphase_concave.spec.js`
|
|
195
|
+
un-skipped and passing.
|
|
196
|
+
- The torus-knot test in `PhysicsSystem.spec.js` un-skipped and
|
|
197
|
+
passing.
|
|
198
|
+
- Full physics suite: 686 passing (was 650 pre-P1.1a — delta:
|
|
199
|
+
+11 sphere_triangle + 12 box_triangle + 10 capsule_triangle unit
|
|
200
|
+
tests + 2 un-skipped sphere settle tests + 1 un-skipped torus-knot
|
|
201
|
+
test).
|
|
202
|
+
|
|
203
|
+
**Optional follow-up** (not blocking).
|
|
204
|
+
- Tighten `compute_penetration.spec.js`'s mesh-cube test (currently
|
|
205
|
+
documented as approximate due to closed-mesh over-reporting on side
|
|
206
|
+
faces). Note: `compute_penetration` is a separate code path from
|
|
207
|
+
`narrowphase_step.js` — it uses its own GJK+EPA / MPR pipeline and
|
|
208
|
+
the half-space workaround; landing the narrowphase fast-path
|
|
209
|
+
doesn't change it. Refactoring `compute_penetration` to share the
|
|
210
|
+
sphere / box / capsule-triangle fast-paths is a separate cleanup.
|
|
211
|
+
|
|
212
|
+
**Effort**: LANDED.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### P1.2 — Feature-id contact tracking across frames
|
|
217
|
+
|
|
218
|
+
**Status**: LANDED.
|
|
219
|
+
|
|
220
|
+
**Problem.** Even after the P0 warm-start wipe is fixed, the manifold
|
|
221
|
+
slot reuses contact-index ordering across frames. Contact slice indices
|
|
222
|
+
shift due to:
|
|
223
|
+
|
|
224
|
+
- Depth-spread reduction in `reduce_candidates` (`narrowphase_step.js:104`)
|
|
225
|
+
— picks deepest, then max-spread; ordering depends on candidate
|
|
226
|
+
generation order which depends on shape-pair details.
|
|
227
|
+
- Box-box face clipping reorders contacts based on which clipped
|
|
228
|
+
vertices survive (`box_box_manifold.js`).
|
|
229
|
+
- The triangle decomposition emits cells in row-major order — a sphere
|
|
230
|
+
rolling along a heightmap sees different cell indices each frame.
|
|
231
|
+
|
|
232
|
+
Reviewers cite two reference designs:
|
|
233
|
+
|
|
234
|
+
1. **Parry's `TrackedContact { fid1, fid2 }`** — each contact carries
|
|
235
|
+
feature ids on both sides; the cache matches by `(fid1, fid2)` pair.
|
|
236
|
+
2. **Jolt's `mContactPointPreserveLambdaMaxDistSq`** — match by
|
|
237
|
+
local-space position proximity (default 0.04 m squared).
|
|
238
|
+
|
|
239
|
+
We're already laid out for (1): the decomposition pipeline emits stable
|
|
240
|
+
feature ids per triangle, and the convex primitive narrowphase paths
|
|
241
|
+
can derive feature ids from vertex / edge / face indices.
|
|
242
|
+
|
|
243
|
+
**What landed.**
|
|
244
|
+
|
|
245
|
+
1. `CONTACT_STRIDE` extended from 13 to 14 (added `feature_id` at
|
|
246
|
+
offset 13). `CANDIDATE_STRIDE` extended from 10 to 11 in
|
|
247
|
+
`narrowphase_step.js` (mirroring slot layout).
|
|
248
|
+
2. `set_contact` gained an optional `feature_id` parameter (default 0
|
|
249
|
+
— preserves backwards-compatible call signatures).
|
|
250
|
+
3. New `clear_impulses(slot, idx)` method on `ManifoldStore`: zeroes
|
|
251
|
+
only the j_n / j_t1 / j_t2 fields at one contact index. Used by
|
|
252
|
+
the match-and-merge pass when a new candidate has no matching
|
|
253
|
+
prev-frame contact.
|
|
254
|
+
4. New `feature_id_of(slot, idx)` accessor for testability.
|
|
255
|
+
5. Per-dispatch feature_id derivation in `narrowphase_step.js`:
|
|
256
|
+
- `sphere_sphere`: fid = 1 (single contact, real feature)
|
|
257
|
+
- `sphere_box`: fid = voronoi region 1..27 via new
|
|
258
|
+
`sphere_box_voronoi_fid` helper (inverse-rotates sphere centre
|
|
259
|
+
to box-local frame, buckets each component into {<-h, [-h,h], >h})
|
|
260
|
+
- `box_box`: fid = 0 (clipped-contact ordering isn't cross-frame
|
|
261
|
+
stable; position-fallback handles per-contact matching)
|
|
262
|
+
- `capsule_capsule`, `capsule_sphere`: fid = 1
|
|
263
|
+
- `capsule_box` multi-point: fid = k+1 (per-sub-contact index)
|
|
264
|
+
- Triangle (concave) path: fid = triangle's pre-existing
|
|
265
|
+
decomposition fid
|
|
266
|
+
- GJK+EPA fallback: fid = 0 (no good source)
|
|
267
|
+
6. Match-and-merge pass replaces `clear_contacts(slot)` at the
|
|
268
|
+
narrowphase refill callsite:
|
|
269
|
+
- Snapshots prev contacts' `(feature_id, world_a, j_n, j_t1, j_t2)`
|
|
270
|
+
upfront into scratch (decouples read from write to avoid hazards
|
|
271
|
+
when the matching mapping shuffles indices)
|
|
272
|
+
- For each new candidate, finds best prev match — feature_id first
|
|
273
|
+
(only if both sides have fid ≠ 0), position-fallback within
|
|
274
|
+
`MATCH_TOL_SQR = 0.02 * 0.02` second
|
|
275
|
+
- Each prev contact can be claimed by at most one candidate
|
|
276
|
+
(`prev_claimed[]` flags)
|
|
277
|
+
- Calls `begin_refill(slot)` (count → 0, data preserved)
|
|
278
|
+
- Writes each new candidate at slot index k; copies matched prev
|
|
279
|
+
impulses to slot index k OR calls `clear_impulses(slot, k)` for
|
|
280
|
+
unmatched candidates
|
|
281
|
+
7. Separation path (cand_count === 0) calls `clear_contacts(slot)` —
|
|
282
|
+
genuine eviction, drops stale impulses before contact is
|
|
283
|
+
re-established at a different feature.
|
|
284
|
+
|
|
285
|
+
**Verification.**
|
|
286
|
+
- `ManifoldStore.spec.js`: 5 new tests covering `set_contact`
|
|
287
|
+
feature_id round-trip, default fid = 0, impulse-preservation under
|
|
288
|
+
fid'd writes, `clear_impulses` scoping (one index, geometry +
|
|
289
|
+
feature_id intact), and per-index targeting.
|
|
290
|
+
- `PhysicsSystem.spec.js`: 4-cube atomic-sleep test passes (was the
|
|
291
|
+
regression that gated P0.1 alone — confirms warm-start activation
|
|
292
|
+
is now stable for stacks).
|
|
293
|
+
- Full physics test suite: 650 passing (was 645 pre-P1.2 — the
|
|
294
|
+
delta is the 5 new ManifoldStore tests; the 4-cube atomic-sleep
|
|
295
|
+
test which transiently regressed under P0.1-alone is now stable).
|
|
296
|
+
|
|
297
|
+
**Effort**: landed.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## P2 — Performance + precision (day-scale)
|
|
302
|
+
|
|
303
|
+
### P2.1 — GJK separating-axis cache per manifold slot
|
|
304
|
+
|
|
305
|
+
**Status**: LANDED (second attempt). The first attempt regressed basic
|
|
306
|
+
settle tests and was reverted; this second pass landed the same
|
|
307
|
+
optimisation via a different mechanism (no try/finally) in 6 inert /
|
|
308
|
+
single-activation steps, each independently verified.
|
|
309
|
+
|
|
310
|
+
**Mechanism — `gjk_core` + `gjk_with_axis`.** Instead of try/finally,
|
|
311
|
+
gjk's body was extracted into `gjk_core(simplex, A, B, dir)` taking
|
|
312
|
+
the search-direction buffer as a parameter. The existing
|
|
313
|
+
`gjk(simplex, A, B)` is now a thin wrapper that sets the module's
|
|
314
|
+
`scratch_dir` to `(1, 0, 0)` and delegates — semantically identical to
|
|
315
|
+
the old code. The new `gjk_with_axis(simplex, A, B, axis_buf, axis_off)`
|
|
316
|
+
creates a `subarray` view into `axis_buf[axis_off..+2]` and passes it
|
|
317
|
+
to `gjk_core`; in-place writes to the view propagate to the caller's
|
|
318
|
+
buffer automatically, so no try/finally writeback is needed. A guard
|
|
319
|
+
catches zero / NaN / Inf seeds and resets to `(1, 0, 0)`.
|
|
320
|
+
|
|
321
|
+
**Storage — `ManifoldStore.__slot_axis`.** A `Float64Array(capacity * 3)`
|
|
322
|
+
parallel to the data buffer. Zeroed on slot `acquire` (so a recycled
|
|
323
|
+
slot doesn't leak the previous pair's axis). Grows alongside the
|
|
324
|
+
other slot arrays. Public accessors `slot_axis_buffer` and
|
|
325
|
+
`slot_axis_offset(slot)`.
|
|
326
|
+
|
|
327
|
+
**Activation — narrowphase.** The outer narrowphase loop computes
|
|
328
|
+
`(gjk_axis_buf, gjk_axis_off)` from the slot and passes them through
|
|
329
|
+
`dispatch_pair`. Both GJK + EPA fallback paths (body-level for
|
|
330
|
+
non-closed-form convex pairs; per-triangle for non-(sphere/box/
|
|
331
|
+
capsule) convex shapes against concave shapes) call `gjk_with_axis`
|
|
332
|
+
with these arguments.
|
|
333
|
+
|
|
334
|
+
**Sequencing — six steps with independent verification.** Each step
|
|
335
|
+
either had no behaviour change (1, 3, 4) or was a small enough
|
|
336
|
+
activation that a regression would be precisely revertable (5, 6).
|
|
337
|
+
Verified at every step against the full physics suite:
|
|
338
|
+
|
|
339
|
+
| Step | Change | Tests passing |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| 1 | Extract `gjk_core` from `gjk`; existing wrapper preserves semantics | 687 / 687 |
|
|
342
|
+
| 2 | Add `gjk_with_axis` + 6 unit tests | 693 / 693 |
|
|
343
|
+
| 3 | Add `ManifoldStore.__slot_axis` (dead code) | 693 / 693 |
|
|
344
|
+
| 4 | Plumb axis args through `dispatch_pair` (dead code) | 693 / 693 |
|
|
345
|
+
| 5 | Activate at the body-level GJK + EPA fallback | 693 / 693 |
|
|
346
|
+
| 6 | Activate at the per-triangle EPA path in concave dispatch | 693 / 693 |
|
|
347
|
+
|
|
348
|
+
**Why this attempt worked where the first didn't.** The first attempt
|
|
349
|
+
wrapped the existing gjk body in try/finally to write back the cached
|
|
350
|
+
axis on every return path. That should have been semantically
|
|
351
|
+
identical for existing (cache-less) callers, yet basic settle tests
|
|
352
|
+
regressed. Working theory: V8's JIT shape-inference handled the
|
|
353
|
+
try/finally-wrapped function differently in ways that surfaced in the
|
|
354
|
+
existing physics tests' numerical paths. The second attempt avoids
|
|
355
|
+
try/finally entirely — `gjk_core` takes `dir` as a parameter, and
|
|
356
|
+
writeback is automatic via shared memory (the caller's buffer IS the
|
|
357
|
+
working buffer through a subarray view).
|
|
358
|
+
|
|
359
|
+
**Improvement.** Bullet's `m_cachedSeparatingAxis` and Jolt's `ioV`
|
|
360
|
+
in/out pattern store the last successful separation direction (or
|
|
361
|
+
overlap-resolution direction) per manifold slot. Reuse next frame as
|
|
362
|
+
GJK's initial direction. Reviewers cite 5–10× iteration reduction for
|
|
363
|
+
quiescent persistent contacts — the common case in any settled stack.
|
|
364
|
+
|
|
365
|
+
**Plan.**
|
|
366
|
+
|
|
367
|
+
1. Add 3 floats to `SLOT_META_STRIDE` (or a parallel `Float32Array(slot_count * 3)`)
|
|
368
|
+
in `ManifoldStore` for the cached axis.
|
|
369
|
+
2. Add an overload (or new entry point) to `gjk` accepting an initial
|
|
370
|
+
direction. Backward-compatible: existing callers (shape_cast,
|
|
371
|
+
compute_penetration, overlap) pass `null` and default to the
|
|
372
|
+
current `(1, 0, 0)`.
|
|
373
|
+
3. In `narrowphase_step.js`'s `dispatch_pair`, before the GJK+EPA
|
|
374
|
+
fallback (line 312 area), read the cached axis from the slot; pass
|
|
375
|
+
it to GJK. After GJK terminates (with or without overlap), write
|
|
376
|
+
the final search direction back to the slot.
|
|
377
|
+
|
|
378
|
+
**Verification.**
|
|
379
|
+
- Microbench: count GJK iterations on the existing
|
|
380
|
+
`PhysicsSystem.bench.spec.js` torus-knot scene (currently skipped
|
|
381
|
+
for accuracy but useful here for iteration count). Expect 5–10×
|
|
382
|
+
reduction.
|
|
383
|
+
- Determinism: same scene state → same iteration counts.
|
|
384
|
+
|
|
385
|
+
**Effort**: 1 day.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### P2.2 — Adaptive EPA tolerance + bail-out fix
|
|
390
|
+
|
|
391
|
+
**Status**: PARTIALLY LANDED. Adaptive tolerance landed; bail-out
|
|
392
|
+
magnitude consistency reverted (caused energy injection in closed-form
|
|
393
|
+
dispatch paths that share the EPA depth metric — sphere-vs-sphere
|
|
394
|
+
stack regressions launched bodies metres straight up).
|
|
395
|
+
|
|
396
|
+
**Two issues.**
|
|
397
|
+
|
|
398
|
+
1. `EPA_TOLERANCE = 0.0001` is absolute. For small-depth contacts (sub-mm)
|
|
399
|
+
this is comparable to the depth itself; EPA terminates with whatever
|
|
400
|
+
intermediate face it has, producing a noisy normal direction. Jolt
|
|
401
|
+
uses a relative tolerance scaled by current closest-face distance.
|
|
402
|
+
2. The bail-out path at line 343 uses `dot_p_search_dir` where the
|
|
403
|
+
converged path uses `min_dist`. Inconsistent under iteration-cap
|
|
404
|
+
exit — the documented "best-effort fallback" returns a different
|
|
405
|
+
magnitude than the converged path of the same geometry.
|
|
406
|
+
|
|
407
|
+
**Plan and outcome.**
|
|
408
|
+
|
|
409
|
+
1. ✅ LANDED. Tolerance is relative: `EPA_TOLERANCE_REL = 1e-4`
|
|
410
|
+
floored by `EPA_TOLERANCE_ABS = 1e-6`. Terminate when
|
|
411
|
+
`dot_p_search_dir − min_dist < EPS_REL * max(|min_dist|, EPS_ABS)`.
|
|
412
|
+
This fixes EPA's failure-to-converge for sub-mm contacts where the
|
|
413
|
+
old absolute tolerance was larger than the depth itself.
|
|
414
|
+
2. ❌ REVERTED. Swapping the convergence-path result magnitude from
|
|
415
|
+
`dot_p_search_dir` to `min_dist` (for consistency with the
|
|
416
|
+
iteration-cap bail-out path) was tried — and caused energy
|
|
417
|
+
injection in closed-form dispatch tests that share the EPA depth
|
|
418
|
+
metric (sphere-vs-sphere with restitution 0 launched a ball to 7
|
|
419
|
+
m). The convergence path retains its original magnitude. The
|
|
420
|
+
"inconsistency with bail-out" the reviewer noted is real but
|
|
421
|
+
benign in practice: bail-out fires on smooth-shape EPA failure
|
|
422
|
+
modes that the closed-form paths now bypass entirely (P1.1
|
|
423
|
+
sphere/box/capsule-vs-triangle); the consistency gap doesn't
|
|
424
|
+
surface anywhere observable.
|
|
425
|
+
|
|
426
|
+
**Verification.**
|
|
427
|
+
- EPA spec needs new cases at very small depth (sub-µm) confirming
|
|
428
|
+
termination is now stable and direction is well-defined.
|
|
429
|
+
- The shape_cast bail-out fallback comparison should produce
|
|
430
|
+
consistent normals between converged and non-converged exits.
|
|
431
|
+
|
|
432
|
+
**Effort**: ½ day. Less critical post-P0 because `shape_cast` and
|
|
433
|
+
`compute_penetration` now use MPR for shallow-overlap normal recovery
|
|
434
|
+
— this fix is for the GJK+EPA fallback in `narrowphase_step.js` only.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## P3 — Mitigation improvements (half-day each)
|
|
439
|
+
|
|
440
|
+
### P3.1 — Wire MPR as EPA fallback for mesh-involving pairs
|
|
441
|
+
|
|
442
|
+
**Status**: LANDED.
|
|
443
|
+
|
|
444
|
+
`gjk/mpr.js` was already implemented and tested; `shape_cast.js` and
|
|
445
|
+
`compute_penetration.js` already used it. The two narrowphase EPA call
|
|
446
|
+
sites — the per-triangle EPA path inside the concave dispatch (when
|
|
447
|
+
the convex shape is not sphere/box/capsule) and the body-level
|
|
448
|
+
GJK+EPA fallback — now both fall back to MPR when EPA returns
|
|
449
|
+
zero / negative / NaN / Inf depth. MPR's portal-refinement convergence
|
|
450
|
+
properties tend to succeed where EPA's face-expansion hits its
|
|
451
|
+
iteration cap.
|
|
452
|
+
|
|
453
|
+
**Plan.**
|
|
454
|
+
|
|
455
|
+
1. In `dispatch_pair`'s GJK+EPA fallback, after EPA returns:
|
|
456
|
+
- If depth > 0 and direction passes the body-centre sanity check
|
|
457
|
+
→ keep the EPA result.
|
|
458
|
+
- Else → re-run with MPR (same simplex inputs are not needed; MPR
|
|
459
|
+
starts from the PosedShape pair). If MPR returns true with a
|
|
460
|
+
positive depth, use that.
|
|
461
|
+
2. For pairs involving a non-convex shape (heightmap/mesh dispatch via
|
|
462
|
+
the concave path), once the closed-form triangle solvers from P1.1
|
|
463
|
+
land, MPR fallback is only needed for the GJK+EPA leg of the
|
|
464
|
+
concave loop — same pattern.
|
|
465
|
+
|
|
466
|
+
**Verification.**
|
|
467
|
+
- The torus-knot test in `PhysicsSystem.spec.js` (currently skipped)
|
|
468
|
+
should make progress with the MPR fallback even before P1.1 lands.
|
|
469
|
+
- `compute_penetration.spec.js`'s smooth-shape direction-precision
|
|
470
|
+
tests can tighten their tolerances.
|
|
471
|
+
|
|
472
|
+
**Effort**: ½ day.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### P3.2 — Box-box edge-edge closest-pair manifold
|
|
477
|
+
|
|
478
|
+
**Status**: LANDED.
|
|
479
|
+
|
|
480
|
+
In `box_box_manifold.js`'s edge-cross-axis-winner branch, the previous
|
|
481
|
+
body-centre-midpoint fallback (both contact points collapsed to the
|
|
482
|
+
midpoint of the two box centres — wrong lever arms, slow drift in
|
|
483
|
+
skewed-box-stack scenarios) is replaced with a proper closest-pair
|
|
484
|
+
computation:
|
|
485
|
+
|
|
486
|
+
1. Decode `(i, j)` from `best_source = 6 + i*3 + j` — box A's local
|
|
487
|
+
axis index and box B's.
|
|
488
|
+
2. Pick A's edge among 4 parallel edges along axis `i` by signing the
|
|
489
|
+
perpendicular axes against `-n` (the direction from A's centre
|
|
490
|
+
toward B). Same for B with `+n`.
|
|
491
|
+
3. `line3_closest_points_segment_segment(out_st, edge_a, edge_b)`
|
|
492
|
+
returns `(s, t)`; reconstruct `contact_on_A = edge_a_p1 + s * (edge_a_p2 - edge_a_p1)`
|
|
493
|
+
and similarly for B.
|
|
494
|
+
|
|
495
|
+
The "second contact for near-parallel edges" sub-improvement from the
|
|
496
|
+
original plan turned out to be unnecessary in practice: when two box
|
|
497
|
+
edges are near-parallel, the SAT axis `cross(edge_dir_a, edge_dir_b)`
|
|
498
|
+
is degenerate (magnitude below `PARALLEL_EPS_SQR`) and the test_axis
|
|
499
|
+
helper rejects it before it can win. Near-parallel-edge configurations
|
|
500
|
+
end up with a face-axis SAT winner, which the face-clipping path
|
|
501
|
+
already handles with multiple contacts.
|
|
502
|
+
|
|
503
|
+
**Improvement.** When SAT identifies an edge-edge separating axis
|
|
504
|
+
between two boxes (the cross-product axes in `box_box_manifold.js`),
|
|
505
|
+
the current fallback emits a single contact at the midpoint of the
|
|
506
|
+
edges' projections. The codebase already has
|
|
507
|
+
`core/geom/3d/line/line3_closest_points_segment_segment.js` from a
|
|
508
|
+
previous slice. Use it to emit the proper closest-pair, then evaluate
|
|
509
|
+
spread for a second contact when the edges are near-parallel (within
|
|
510
|
+
some `EDGE_PARALLEL_COS_TOL` like 0.999).
|
|
511
|
+
|
|
512
|
+
**Plan.**
|
|
513
|
+
|
|
514
|
+
1. In `box_box_manifold.js`'s edge-edge branch, replace the midpoint
|
|
515
|
+
contact with `line3_closest_points_segment_segment`'s output.
|
|
516
|
+
2. Detect near-parallel edges via the cos-angle test on the edge
|
|
517
|
+
direction vectors; in that case emit a second contact at the other
|
|
518
|
+
endpoint of the parallel overlap interval. Standard SAT-with-clipping
|
|
519
|
+
pattern.
|
|
520
|
+
|
|
521
|
+
**Verification.**
|
|
522
|
+
- A box stacked at an angle on another box should reach steady state
|
|
523
|
+
without the slow drift that the single-midpoint contact causes.
|
|
524
|
+
Add a regression test alongside the existing box-box stack test.
|
|
525
|
+
|
|
526
|
+
**Effort**: ½ day.
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## P4 — Strategic items (weeks; defer until pressed)
|
|
531
|
+
|
|
532
|
+
### P4.1 — Split-impulse architecture → TGS substepping
|
|
533
|
+
|
|
534
|
+
**Status**: PLAN.md documents this with the three interacting failure
|
|
535
|
+
modes; reviewers confirmed the path forward in detail.
|
|
536
|
+
|
|
537
|
+
**Recommendation.** Don't pursue until tall stacks become a frequent
|
|
538
|
+
gameplay pain point. The current 4-cube atomic-sleep test passes; the
|
|
539
|
+
16-cube short-window test catches manifold regressions. The pure-JS
|
|
540
|
+
single-threaded budget is the more likely scaling wall — TGS doesn't
|
|
541
|
+
help there.
|
|
542
|
+
|
|
543
|
+
If/when we do it: Box2D-Lite-style split impulse — separate
|
|
544
|
+
position-correction pseudo-velocity that doesn't contaminate real
|
|
545
|
+
velocity, restitution as one-shot impulse at first contact (not a bias
|
|
546
|
+
inside the loop), forces apply once at full dt before substeps.
|
|
547
|
+
|
|
548
|
+
**Effort**: weeks.
|
|
549
|
+
|
|
550
|
+
### P4.2 — Per-body linear CCD shape-cast
|
|
551
|
+
|
|
552
|
+
**Status**: PLAN.md backlog ("Per-body linear CCD shape-cast").
|
|
553
|
+
|
|
554
|
+
**Recommendation.** Already a focused future-work item with the 1km
|
|
555
|
+
falling-tower reproducer (~180/1000 bodies tunnel). Worth scheduling
|
|
556
|
+
once a gameplay use case demands it (e.g. fast projectiles, vehicle
|
|
557
|
+
physics).
|
|
558
|
+
|
|
559
|
+
**Effort**: week.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Not prioritised in the executive summary
|
|
564
|
+
|
|
565
|
+
### Shape-cast bisection vs GJK conservative advancement
|
|
566
|
+
|
|
567
|
+
Reviewers noted Parry's `gjk::directional_distance` (3–5× fewer GJK
|
|
568
|
+
calls per query) but did not prioritise it. Our current bisection +
|
|
569
|
+
slab-narrowing is functionally correct and acceptable; flag for revisit
|
|
570
|
+
if `shape_cast` shows up in profiles. Effort would be a day.
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## Out of scope
|
|
575
|
+
|
|
576
|
+
Confirmed from the consolidated review — these continue to be valid
|
|
577
|
+
non-goals:
|
|
578
|
+
|
|
579
|
+
- SIMD / WASM — V8 doesn't expose deterministic Float64x2 ops.
|
|
580
|
+
- `SharedArrayBuffer` / multi-threaded solver — COOP/COEP headers
|
|
581
|
+
aren't always available.
|
|
582
|
+
- Cross-runtime bit-exact determinism — would require a soft-float
|
|
583
|
+
library; same-runtime determinism covers single-device replay +
|
|
584
|
+
lockstep clients on matched JS engines.
|
|
585
|
+
- Reduced-coordinate articulations — game-physics audience runs in
|
|
586
|
+
maximal coordinates by convention.
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## What's already strong (no action; for record)
|
|
591
|
+
|
|
592
|
+
All three reviewers independently called these out as better than at
|
|
593
|
+
least one reference engine. Useful confirmation that the design bets
|
|
594
|
+
in PLAN.md paid off.
|
|
595
|
+
|
|
596
|
+
- End-to-end SoA layout with generation-tracked stable IDs and
|
|
597
|
+
min-heap free list (deterministic ID reuse).
|
|
598
|
+
- Atomic per-island sleep with chain wake via circular doubly-linked
|
|
599
|
+
`sleep_group_next` / `sleep_group_prev` — wake propagation
|
|
600
|
+
completes in one frame, vs Bullet/Jolt's bottom-up multi-frame
|
|
601
|
+
propagation.
|
|
602
|
+
- Deterministic island roots (union-find with union-by-min-index) —
|
|
603
|
+
Rapier and Jolt do not strictly guarantee this.
|
|
604
|
+
- Flat shape hierarchy, linear narrowphase dispatch,
|
|
605
|
+
monomorphic-when-hot `support()`.
|
|
606
|
+
- Coulomb-cone disk friction clamp (true cone) — Bullet's default is
|
|
607
|
+
the anisotropic box clamp.
|
|
608
|
+
- One-sided face-normal rejection on concave dispatch.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Recommended sequencing
|
|
613
|
+
|
|
614
|
+
1. **P0.2 + P0.1 infra prep (LANDED)** — `debugger;` removed from EPA;
|
|
615
|
+
`ManifoldStore.begin_refill` added. Surgical, no behaviour change.
|
|
616
|
+
The initial attempt at full P0.1 activation (flipping the callsite
|
|
617
|
+
to `begin_refill`) was reverted after the 4-cube atomic-sleep test
|
|
618
|
+
caught a stack collapse, confirming the reviewer's "slot ordering
|
|
619
|
+
is usually preserved" assumption is false for stacks.
|
|
620
|
+
2. **P1.2 + P0.1 activation (LANDED)** — feature-id contact tracking
|
|
621
|
+
and the match-and-merge pass. Warm-start now active and stable on
|
|
622
|
+
stacks; full physics suite green (650 passing).
|
|
623
|
+
3. **P1.1 in three sub-commits — LANDED**:
|
|
624
|
+
- **P1.1a (sphere-vs-triangle).** `sphere_triangle_contact` module
|
|
625
|
+
+ spec + narrowphase wiring; un-skipped two settle tests in
|
|
626
|
+
`narrowphase_concave.spec.js`.
|
|
627
|
+
- **P1.1b (box-vs-triangle).** `box_triangle_contact` module (SAT
|
|
628
|
+
+ clipping for both face winners + closest-pair for edge-cross)
|
|
629
|
+
+ spec + narrowphase wiring; un-skipped the torus-knot settle
|
|
630
|
+
test in `PhysicsSystem.spec.js`.
|
|
631
|
+
- **P1.1c (capsule-vs-triangle).** `capsule_triangle_contact`
|
|
632
|
+
module (segment-triangle closest-pair + cap-centre sphere
|
|
633
|
+
queries with spatial dedup) + spec + narrowphase wiring.
|
|
634
|
+
4. **P2.1 (GJK axis cache) + P2.2 (EPA tolerance) together** — both
|
|
635
|
+
are localised, neither depends on the others.
|
|
636
|
+
5. **P3.1 (MPR fallback) — LANDED.** Wired at both narrowphase EPA
|
|
637
|
+
call sites (per-triangle concave + body-level fallback). Behind
|
|
638
|
+
the existing depth-validity gate so it only fires when EPA fails.
|
|
639
|
+
6. **P3.2 (box-box edge-edge) — LANDED.** `line3_closest_points_segment_segment`-based
|
|
640
|
+
contact-pair reconstruction; near-parallel edges already handled
|
|
641
|
+
by SAT degeneracy fallthrough to face axes.
|
|
642
|
+
7. **P4 items**: defer until gameplay pressure.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
2
2
|
|
|
3
3
|
const scratch_local = new Float64Array(6);
|
|
4
4
|
|
|
@@ -41,7 +41,7 @@ export function compute_fat_world_aabb(
|
|
|
41
41
|
const p = transform.position;
|
|
42
42
|
const q = transform.rotation;
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
aabb3_transform_oriented(
|
|
45
45
|
result, result_offset,
|
|
46
46
|
scratch_local[0], scratch_local[1], scratch_local[2],
|
|
47
47
|
scratch_local[3], scratch_local[4], scratch_local[5],
|