@woosh/meep-engine 2.153.0 → 2.154.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.
Files changed (96) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  3. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  4. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  5. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  6. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  7. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  8. package/src/engine/physics/PLAN.md +4 -4
  9. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  10. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  11. package/src/engine/physics/body/BodyStorage.js +452 -450
  12. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  13. package/src/engine/physics/body/SolverBodyState.js +6 -5
  14. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  15. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  16. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  17. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  18. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  19. package/src/engine/physics/computeInterceptPoint.js +8 -3
  20. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  21. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  22. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  23. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  24. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  25. package/src/engine/physics/ecs/BodyKind.js +25 -24
  26. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  27. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  28. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  29. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  30. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  32. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  33. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  34. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  35. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  36. package/src/engine/physics/ecs/is_sensor.js +27 -0
  37. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  38. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  39. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  40. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  41. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  42. package/src/engine/physics/gjk/gjk.js +3 -52
  43. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
  44. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  45. package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
  46. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  47. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  48. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  49. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  50. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  51. package/src/engine/physics/gjk/mpr.js +362 -344
  52. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  53. package/src/engine/physics/island/IslandBuilder.js +431 -428
  54. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  55. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  56. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  57. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  58. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  59. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  60. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  61. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  62. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  63. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  64. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  65. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  66. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  67. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  68. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  69. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  70. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  71. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  72. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  73. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  74. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  75. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  76. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  77. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  78. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  79. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  80. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  81. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  82. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  83. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  84. package/src/engine/physics/queries/overlap_shape.js +16 -17
  85. package/src/engine/physics/queries/raycast.d.ts +5 -0
  86. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  87. package/src/engine/physics/queries/raycast.js +16 -8
  88. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  89. package/src/engine/physics/queries/shape_cast.js +13 -7
  90. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  91. package/src/engine/physics/solver/solve_contacts.js +8 -11
  92. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  93. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  94. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  95. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  96. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -1,344 +1,362 @@
1
- import { v3_length } from "../../../core/geom/vec3/v3_length.js";
2
- import { minkowski_support } from "./minkowski_support.js";
3
-
4
- /**
5
- * Maximum portal-refinement iterations. The portal must converge to
6
- * within {@link MPR_TOLERANCE} of the closest face before this many
7
- * iterations or we accept whatever it has and return it as an
8
- * approximation — same graceful-degradation strategy as EPA, but
9
- * generally MPR converges in 5–15 iterations on the shapes it does
10
- * well on (smooth curves, mixed convex bodies).
11
- * @type {number}
12
- */
13
- const MPR_MAX_ITERATIONS = 64;
14
-
15
- /**
16
- * Convergence threshold on the portal-normal direction. When the new
17
- * support point is within this distance of the current portal plane
18
- * (measured along the portal normal), we declare convergence and use
19
- * the portal as the closest-face approximation of the MTV.
20
- * @type {number}
21
- */
22
- const MPR_TOLERANCE = 1e-4;
23
-
24
- /**
25
- * Tolerance for collinearity / coincidence degeneracies during portal
26
- * discovery. Picked to be safely below practical world-distance noise
27
- * but large enough that real coincidences (shapes sharing a centre)
28
- * trip it.
29
- * @type {number}
30
- */
31
- const MPR_EPSILON = 1e-10;
32
-
33
- /**
34
- * Portal vertices in the Minkowski-difference space.
35
- * V0 is an interior point of the Mink-diff (the difference of the two
36
- * bodies' centres); V1, V2, V3 are the portal triangle's vertices; V4
37
- * is the candidate new vertex during refinement.
38
- */
39
- const V0 = new Float64Array(3);
40
- const V1 = new Float64Array(3);
41
- const V2 = new Float64Array(3);
42
- const V3 = new Float64Array(3);
43
- const V4 = new Float64Array(3);
44
-
45
- /**
46
- * Minkowski Portal Refinement (XenoCollide, Gary Snethen, GDC 2009).
47
- *
48
- * Determines whether two convex shapes overlap and, if so, returns the
49
- * Minimum Translation Vector (MTV) that would separate them. The
50
- * algorithm runs in two phases:
51
- *
52
- * 1. **Portal discovery.** Starting from an interior point V0 of the
53
- * Minkowski difference (the difference of the two body centres),
54
- * find three vertices V1, V2, V3 on the Mink-diff boundary that
55
- * form a triangle the ray V0→origin pierces. If at any point a
56
- * support hyperplane separates V0 from the origin, the shapes
57
- * can't overlap and we return false.
58
- *
59
- * 2. **Portal refinement.** Iteratively replace one of V1, V2, V3
60
- * with a new support V4 found in the direction of the portal
61
- * face's outward normal. When V4 doesn't bring the portal any
62
- * closer to the origin (within `MPR_TOLERANCE`), the portal face
63
- * *is* the closest face on the Mink-diff to the origin; its
64
- * outward normal is the MTV direction and the perpendicular
65
- * distance from the origin to the face is the penetration depth.
66
- *
67
- * Compared to GJK+EPA on the same overlap:
68
- * - One algorithm produces both the overlap test and the MTV (EPA is
69
- * a separate pass after GJK).
70
- * - Better behaviour on smooth or high-vertex-count shapes EPA's
71
- * polytope-expansion can stall on curved surfaces because each new
72
- * support produces a face with no flat region to converge on; MPR's
73
- * portal stays a single triangle and just slides toward the origin.
74
- * - Tends to converge in 5–15 iterations on typical inputs vs. EPA's
75
- * 32+ on smooth pairs.
76
- *
77
- * **Output convention matches EPA** ({@link expanding_polytope_algorithm}):
78
- * `result[result_offset .. result_offset+2]` is the MTV vector
79
- * direction is "from A's surface into B" (i.e. the direction you'd
80
- * translate B by to separate the shapes), magnitude is the depth.
81
- * The caller reads depth as `√(x² + + z²)` and normalises to get
82
- * the unit normal. This makes MPR a drop-in replacement for EPA at
83
- * any narrowphase call site.
84
- *
85
- * On failure modes:
86
- * - Returns `false` if the shapes don't overlap.
87
- * - On a degenerate Mink-diff (shapes touching at a point, perfectly
88
- * coincident centres with collinear support points), returns
89
- * `true` with a small fallback MTV same approach as EPA's
90
- * non-convergent fallback. The narrowphase consumer already
91
- * filters non-positive / non-finite depths.
92
- *
93
- * @param {Float64Array} result destination buffer for the MTV vector
94
- * @param {number} result_offset
95
- * @param {PosedShape} shape_a
96
- * @param {PosedShape} shape_b
97
- * @returns {boolean} true on overlap
98
- */
99
- export function mpr(result, result_offset, shape_a, shape_b) {
100
- // ── Step 1: interior point V0 = centerA − centerB ───────────────────
101
- //
102
- // PosedShape carries the body's world-space position; the centre of
103
- // its underlying shape (in body-local space) is the origin, so the
104
- // world centre is just (px, py, pz). The difference of two body
105
- // centres lies inside the Mink-diff for any convex pair whose shape
106
- // origins are interior to their geometry — true for spheres,
107
- // boxes, capsules, and any closed mesh whose bounding box contains
108
- // its centroid.
109
- V0[0] = shape_a.px - shape_b.px;
110
- V0[1] = shape_a.py - shape_b.py;
111
- V0[2] = shape_a.pz - shape_b.pz;
112
-
113
- // If the two centres coincide exactly, V0 is at the origin and we
114
- // can't direction-find from it. Perturb to break the symmetry —
115
- // any direction works; the algorithm will converge to the same
116
- // answer regardless of which we pick.
117
- if (V0[0] === 0 && V0[1] === 0 && V0[2] === 0) {
118
- V0[0] = MPR_EPSILON;
119
- }
120
-
121
- // ── Step 2: first support V1 in direction −V0 (toward the origin) ──
122
- minkowski_support(V1, 0, shape_a, shape_b, -V0[0], -V0[1], -V0[2]);
123
-
124
- // If V1 isn't past the origin along −V0, the origin lies outside
125
- // the Mink-diff and the shapes are separated.
126
- if (V1[0] * -V0[0] + V1[1] * -V0[1] + V1[2] * -V0[2] < 0) {
127
- return false;
128
- }
129
-
130
- // ── Step 3: portal discovery ────────────────────────────────────────
131
- //
132
- // Find V2 in a direction perpendicular to the V0–V1 axis. n = V1 × V0
133
- // is perpendicular to both; if it degenerates to zero, V0 and V1
134
- // are collinear with the origin, which means the origin lies on
135
- // the segment from V0 to V1 (since V0 is interior and V1 is past
136
- // the origin) definite overlap, emit a fallback MTV along V1.
137
- let nx = V1[1] * V0[2] - V1[2] * V0[1];
138
- let ny = V1[2] * V0[0] - V1[0] * V0[2];
139
- let nz = V1[0] * V0[1] - V1[1] * V0[0];
140
-
141
- if (nx * nx + ny * ny + nz * nz < MPR_EPSILON) {
142
- // Collinear degeneracy: project the answer along V1.
143
- const v1_len = v3_length(V1[0], V1[1], V1[2]);
144
- if (v1_len > MPR_EPSILON) {
145
- result[result_offset] = V1[0];
146
- result[result_offset + 1] = V1[1];
147
- result[result_offset + 2] = V1[2];
148
- } else {
149
- // V1 at origin shapes touching exactly. Emit a tiny MTV.
150
- result[result_offset] = MPR_EPSILON;
151
- result[result_offset + 1] = 0;
152
- result[result_offset + 2] = 0;
153
- }
154
- return true;
155
- }
156
-
157
- minkowski_support(V2, 0, shape_a, shape_b, nx, ny, nz);
158
-
159
- if (V2[0] * nx + V2[1] * ny + V2[2] * nz < 0) {
160
- return false;
161
- }
162
-
163
- // Compute the portal-triangle normal (V1 − V0) × (V2 − V0) and
164
- // orient it away from V0 (so it points roughly toward the origin).
165
- let v1mv0_x = V1[0] - V0[0], v1mv0_y = V1[1] - V0[1], v1mv0_z = V1[2] - V0[2];
166
- let v2mv0_x = V2[0] - V0[0], v2mv0_y = V2[1] - V0[1], v2mv0_z = V2[2] - V0[2];
167
- nx = v1mv0_y * v2mv0_z - v1mv0_z * v2mv0_y;
168
- ny = v1mv0_z * v2mv0_x - v1mv0_x * v2mv0_z;
169
- nz = v1mv0_x * v2mv0_y - v1mv0_y * v2mv0_x;
170
-
171
- // If the normal points along +V0 (back toward V0's side), swap V1/V2
172
- // and flip we want the portal facing away from V0 toward the
173
- // origin.
174
- if (nx * V0[0] + ny * V0[1] + nz * V0[2] > 0) {
175
- const tx = V1[0], ty = V1[1], tz = V1[2];
176
- V1[0] = V2[0]; V1[1] = V2[1]; V1[2] = V2[2];
177
- V2[0] = tx; V2[1] = ty; V2[2] = tz;
178
- nx = -nx; ny = -ny; nz = -nz;
179
- }
180
-
181
- // Portal-discovery iteration. We keep refining V3 until the
182
- // tetrahedron V0–V1–V2–V3 actually contains the origin.
183
- let discovered = false;
184
- for (let i = 0; i < MPR_MAX_ITERATIONS; i++) {
185
- minkowski_support(V3, 0, shape_a, shape_b, nx, ny, nz);
186
-
187
- if (V3[0] * nx + V3[1] * ny + V3[2] * nz < 0) {
188
- return false;
189
- }
190
-
191
- // Test if origin lies outside plane V0–V1–V3 (V2's side).
192
- // n_013 = (V1 − V0) × (V3 − V0) — points away from V2 if portal
193
- // is consistently oriented.
194
- const v3mv0_x = V3[0] - V0[0], v3mv0_y = V3[1] - V0[1], v3mv0_z = V3[2] - V0[2];
195
- const n013_x = v1mv0_y * v3mv0_z - v1mv0_z * v3mv0_y;
196
- const n013_y = v1mv0_z * v3mv0_x - v1mv0_x * v3mv0_z;
197
- const n013_z = v1mv0_x * v3mv0_y - v1mv0_y * v3mv0_x;
198
-
199
- // origin's signed distance from plane V0-V1-V3 along n013 is
200
- // dot(−V0, n013), since the plane passes through V0.
201
- if (-V0[0] * n013_x - V0[1] * n013_y - V0[2] * n013_z > 0) {
202
- // origin outside V0-V1-V3 plane on the V2 side — replace V2
203
- V2[0] = V3[0]; V2[1] = V3[1]; V2[2] = V3[2];
204
- v2mv0_x = v3mv0_x; v2mv0_y = v3mv0_y; v2mv0_z = v3mv0_z;
205
- nx = n013_x; ny = n013_y; nz = n013_z;
206
- continue;
207
- }
208
-
209
- // Test if origin lies outside plane V0-V2-V3 (V1's side).
210
- const n023_x = v2mv0_y * v3mv0_z - v2mv0_z * v3mv0_y;
211
- const n023_y = v2mv0_z * v3mv0_x - v2mv0_x * v3mv0_z;
212
- const n023_z = v2mv0_x * v3mv0_y - v2mv0_y * v3mv0_x;
213
-
214
- if (-V0[0] * n023_x - V0[1] * n023_y - V0[2] * n023_z < 0) {
215
- // origin outside V0-V2-V3 plane on V1's side — replace V1
216
- V1[0] = V3[0]; V1[1] = V3[1]; V1[2] = V3[2];
217
- v1mv0_x = v3mv0_x; v1mv0_y = v3mv0_y; v1mv0_z = v3mv0_z;
218
- // New portal normal: (V1' - V0) × (V2 - V0) where V1' is the new V1 (= old V3)
219
- nx = v3mv0_y * v2mv0_z - v3mv0_z * v2mv0_y;
220
- ny = v3mv0_z * v2mv0_x - v3mv0_x * v2mv0_z;
221
- nz = v3mv0_x * v2mv0_y - v3mv0_y * v2mv0_x;
222
- continue;
223
- }
224
-
225
- // Origin is inside the tetrahedron V0-V1-V2-V3 portal found.
226
- discovered = true;
227
- break;
228
- }
229
-
230
- if (!discovered) {
231
- // Ran out of portal-discovery iterations treat as a near-miss.
232
- return false;
233
- }
234
-
235
- // ── Step 4: portal refinement ───────────────────────────────────────
236
- //
237
- // Now we know V0–V1–V2–V3 is a valid tetrahedron containing the
238
- // origin. Iteratively replace one of V1/V2/V3 with a new support
239
- // V4 (found in the direction of the portal triangle's outward
240
- // normal). When V4 doesn't extend the portal further, the portal
241
- // face is the closest Mink-diff face to the origin and its normal
242
- // is the MTV.
243
- for (let i = 0; i < MPR_MAX_ITERATIONS; i++) {
244
- // Portal face normal = (V2 - V1) × (V3 - V1)
245
- const e1_x = V2[0] - V1[0], e1_y = V2[1] - V1[1], e1_z = V2[2] - V1[2];
246
- const e2_x = V3[0] - V1[0], e2_y = V3[1] - V1[1], e2_z = V3[2] - V1[2];
247
- let pn_x = e1_y * e2_z - e1_z * e2_y;
248
- let pn_y = e1_z * e2_x - e1_x * e2_z;
249
- let pn_z = e1_x * e2_y - e1_y * e2_x;
250
- const pn_len = v3_length(pn_x, pn_y, pn_z);
251
- if (pn_len < MPR_EPSILON) {
252
- // Degenerate portal (collinear vertices) return the
253
- // current state as a fallback approximation.
254
- result[result_offset] = pn_x;
255
- result[result_offset + 1] = pn_y;
256
- result[result_offset + 2] = pn_z;
257
- return true;
258
- }
259
- const inv_pn = 1 / pn_len;
260
- pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
261
-
262
- // Perpendicular distance from the origin to the portal plane,
263
- // = dot(V1, portal_normal). This is the candidate depth on
264
- // every iteration; we accept it once V4 fails to push the
265
- // portal further.
266
- const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
267
-
268
- // New support V4 in the portal normal direction.
269
- minkowski_support(V4, 0, shape_a, shape_b, pn_x, pn_y, pn_z);
270
-
271
- const v4_dist = V4[0] * pn_x + V4[1] * pn_y + V4[2] * pn_z;
272
-
273
- // Convergence: V4 doesn't extend the portal beyond the current
274
- // face (within tolerance). Emit MTV = normal × depth.
275
- if (v4_dist - portal_dist < MPR_TOLERANCE) {
276
- result[result_offset] = pn_x * portal_dist;
277
- result[result_offset + 1] = pn_y * portal_dist;
278
- result[result_offset + 2] = pn_z * portal_dist;
279
- return true;
280
- }
281
-
282
- // V4 didn't pass the origin along the portal normal the
283
- // shapes aren't actually overlapping (shouldn't happen after
284
- // successful portal discovery, but defensive).
285
- if (v4_dist < 0) {
286
- return false;
287
- }
288
-
289
- // Replace one of V1, V2, V3 with V4 such that the new portal
290
- // still contains the V0→origin ray. The split is determined by
291
- // three cross-product / dot tests against V4 × V0: signs of
292
- // (Vi · (V4 × V0)) place V0 in one of the three sub-portals.
293
- //
294
- // The branch structure here mirrors libccd / bullet's MPR
295
- // (`btMprExpandPortal`).
296
- const c_x = V4[1] * V0[2] - V4[2] * V0[1];
297
- const c_y = V4[2] * V0[0] - V4[0] * V0[2];
298
- const c_z = V4[0] * V0[1] - V4[1] * V0[0];
299
-
300
- const d1 = V1[0] * c_x + V1[1] * c_y + V1[2] * c_z;
301
-
302
- if (d1 >= 0) {
303
- const d2 = V2[0] * c_x + V2[1] * c_y + V2[2] * c_z;
304
- if (d2 >= 0) {
305
- // origin sits in sub-portal between V4 and V1 drop V1
306
- V1[0] = V4[0]; V1[1] = V4[1]; V1[2] = V4[2];
307
- } else {
308
- // sub-portal between V4 and V3 drop V3
309
- V3[0] = V4[0]; V3[1] = V4[1]; V3[2] = V4[2];
310
- }
311
- } else {
312
- const d3 = V3[0] * c_x + V3[1] * c_y + V3[2] * c_z;
313
- if (d3 >= 0) {
314
- // sub-portal between V4 and V2 drop V2
315
- V2[0] = V4[0]; V2[1] = V4[1]; V2[2] = V4[2];
316
- } else {
317
- // sub-portal between V4 and V1 (other side) — drop V1
318
- V1[0] = V4[0]; V1[1] = V4[1]; V1[2] = V4[2];
319
- }
320
- }
321
- }
322
-
323
- // Refinement ran out of iterations — emit the current portal face
324
- // as the best-known MTV, same graceful-degradation as EPA.
325
- const e1_x = V2[0] - V1[0], e1_y = V2[1] - V1[1], e1_z = V2[2] - V1[2];
326
- const e2_x = V3[0] - V1[0], e2_y = V3[1] - V1[1], e2_z = V3[2] - V1[2];
327
- let pn_x = e1_y * e2_z - e1_z * e2_y;
328
- let pn_y = e1_z * e2_x - e1_x * e2_z;
329
- let pn_z = e1_x * e2_y - e1_y * e2_x;
330
- const pn_len = v3_length(pn_x, pn_y, pn_z);
331
- if (pn_len < MPR_EPSILON) {
332
- result[result_offset] = 0;
333
- result[result_offset + 1] = 0;
334
- result[result_offset + 2] = 0;
335
- return true;
336
- }
337
- const inv_pn = 1 / pn_len;
338
- pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
339
- const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
340
- result[result_offset] = pn_x * portal_dist;
341
- result[result_offset + 1] = pn_y * portal_dist;
342
- result[result_offset + 2] = pn_z * portal_dist;
343
- return true;
344
- }
1
+ import { v3_length } from "../../../core/geom/vec3/v3_length.js";
2
+ import { minkowski_support } from "./minkowski_support.js";
3
+
4
+ /**
5
+ * Maximum portal-refinement iterations. The portal must converge to
6
+ * within {@link MPR_TOLERANCE} of the closest face before this many
7
+ * iterations or we accept whatever it has and return it as an
8
+ * approximation — same graceful-degradation strategy as EPA, but
9
+ * generally MPR converges in 5–15 iterations on the shapes it does
10
+ * well on (smooth curves, mixed convex bodies).
11
+ * @type {number}
12
+ */
13
+ const MPR_MAX_ITERATIONS = 64;
14
+
15
+ /**
16
+ * Convergence threshold on the portal-normal direction. When the new
17
+ * support point is within this distance of the current portal plane
18
+ * (measured along the portal normal), we declare convergence and use
19
+ * the portal as the closest-face approximation of the MTV.
20
+ * @type {number}
21
+ */
22
+ const MPR_TOLERANCE = 1e-4;
23
+
24
+ /**
25
+ * Tolerance for collinearity / coincidence degeneracies during portal
26
+ * discovery. Picked to be safely below practical world-distance noise
27
+ * but large enough that real coincidences (shapes sharing a centre)
28
+ * trip it.
29
+ * @type {number}
30
+ */
31
+ const MPR_EPSILON = 1e-10;
32
+
33
+ /**
34
+ * Scale-relative collinearity threshold (sin²θ). A cross product |a×b|² equals
35
+ * |a|²·|b|²·sin²θ, so comparing |a×b|² against this fraction of |a|²·|b|² tests
36
+ * the ANGLE, independent of shape scale unlike an absolute epsilon on the
37
+ * (length⁴ / length²) cross magnitude, which fired spuriously for sub-millimetre
38
+ * shapes and returned a bogus, inflated MTV. ~ (1e-6 rad)².
39
+ * @type {number}
40
+ */
41
+ const MPR_COLLINEAR_REL_SQR = 1e-12;
42
+
43
+ /**
44
+ * Portal vertices in the Minkowski-difference space.
45
+ * V0 is an interior point of the Mink-diff (the difference of the two
46
+ * bodies' centres); V1, V2, V3 are the portal triangle's vertices; V4
47
+ * is the candidate new vertex during refinement.
48
+ */
49
+ const V0 = new Float64Array(3);
50
+ const V1 = new Float64Array(3);
51
+ const V2 = new Float64Array(3);
52
+ const V3 = new Float64Array(3);
53
+ const V4 = new Float64Array(3);
54
+
55
+ /**
56
+ * Minkowski Portal Refinement (XenoCollide, Gary Snethen, GDC 2009).
57
+ *
58
+ * Determines whether two convex shapes overlap and, if so, returns the
59
+ * Minimum Translation Vector (MTV) that would separate them. The
60
+ * algorithm runs in two phases:
61
+ *
62
+ * 1. **Portal discovery.** Starting from an interior point V0 of the
63
+ * Minkowski difference (the difference of the two body centres),
64
+ * find three vertices V1, V2, V3 on the Mink-diff boundary that
65
+ * form a triangle the ray V0→origin pierces. If at any point a
66
+ * support hyperplane separates V0 from the origin, the shapes
67
+ * can't overlap and we return false.
68
+ *
69
+ * 2. **Portal refinement.** Iteratively replace one of V1, V2, V3
70
+ * with a new support V4 found in the direction of the portal
71
+ * face's outward normal. When V4 doesn't bring the portal any
72
+ * closer to the origin (within `MPR_TOLERANCE`), the portal face
73
+ * *is* the closest face on the Mink-diff to the origin; its
74
+ * outward normal is the MTV direction and the perpendicular
75
+ * distance from the origin to the face is the penetration depth.
76
+ *
77
+ * Compared to GJK+EPA on the same overlap:
78
+ * - One algorithm produces both the overlap test and the MTV (EPA is
79
+ * a separate pass after GJK).
80
+ * - Better behaviour on smooth or high-vertex-count shapes EPA's
81
+ * polytope-expansion can stall on curved surfaces because each new
82
+ * support produces a face with no flat region to converge on; MPR's
83
+ * portal stays a single triangle and just slides toward the origin.
84
+ * - Tends to converge in 5–15 iterations on typical inputs vs. EPA's
85
+ * 32+ on smooth pairs.
86
+ *
87
+ * **Output convention matches the EPA penetration query** ({@link gjk_epa_penetration}'s MTV form):
88
+ * `result[result_offset .. result_offset+2]` is the MTV vector —
89
+ * direction is "from A's surface into B" (i.e. the direction you'd
90
+ * translate B by to separate the shapes), magnitude is the depth.
91
+ * The caller reads depth as `√(x² + y² + z²)` and normalises to get
92
+ * the unit normal. This makes MPR a drop-in replacement for EPA at
93
+ * any narrowphase call site.
94
+ *
95
+ * On failure modes:
96
+ * - Returns `false` if the shapes don't overlap.
97
+ * - On a degenerate Mink-diff (shapes touching at a point, perfectly
98
+ * coincident centres with collinear support points), returns
99
+ * `true` with a small fallback MTV — same approach as EPA's
100
+ * non-convergent fallback. The narrowphase consumer already
101
+ * filters non-positive / non-finite depths.
102
+ *
103
+ * @param {Float64Array} result destination buffer for the MTV vector
104
+ * @param {number} result_offset
105
+ * @param {PosedShape} shape_a
106
+ * @param {PosedShape} shape_b
107
+ * @returns {boolean} true on overlap
108
+ */
109
+ export function mpr(result, result_offset, shape_a, shape_b) {
110
+ // ── Step 1: interior point V0 = centerA centerB ───────────────────
111
+ //
112
+ // PosedShape carries the body's world-space position; the centre of
113
+ // its underlying shape (in body-local space) is the origin, so the
114
+ // world centre is just (px, py, pz). The difference of two body
115
+ // centres lies inside the Mink-diff for any convex pair whose shape
116
+ // origins are interior to their geometry — true for spheres,
117
+ // boxes, capsules, and any closed mesh whose bounding box contains
118
+ // its centroid.
119
+ V0[0] = shape_a.px - shape_b.px;
120
+ V0[1] = shape_a.py - shape_b.py;
121
+ V0[2] = shape_a.pz - shape_b.pz;
122
+
123
+ // If the two centres coincide exactly, V0 is at the origin and we
124
+ // can't direction-find from it. Perturb to break the symmetry
125
+ // any direction works; the algorithm will converge to the same
126
+ // answer regardless of which we pick.
127
+ if (V0[0] === 0 && V0[1] === 0 && V0[2] === 0) {
128
+ V0[0] = MPR_EPSILON;
129
+ }
130
+
131
+ // ── Step 2: first support V1 in direction −V0 (toward the origin) ──
132
+ minkowski_support(V1, 0, shape_a, shape_b, -V0[0], -V0[1], -V0[2]);
133
+
134
+ // If V1 isn't past the origin along −V0, the origin lies outside
135
+ // the Mink-diff and the shapes are separated.
136
+ if (V1[0] * -V0[0] + V1[1] * -V0[1] + V1[2] * -V0[2] < 0) {
137
+ return false;
138
+ }
139
+
140
+ // ── Step 3: portal discovery ────────────────────────────────────────
141
+ //
142
+ // Find V2 in a direction perpendicular to the V0–V1 axis. n = V1 × V0
143
+ // is perpendicular to both; if it degenerates to zero, V0 and V1
144
+ // are collinear with the origin, which means the origin lies on
145
+ // the segment from V0 to V1 (since V0 is interior and V1 is past
146
+ // the origin) definite overlap, emit a fallback MTV along V1.
147
+ let nx = V1[1] * V0[2] - V1[2] * V0[1];
148
+ let ny = V1[2] * V0[0] - V1[0] * V0[2];
149
+ let nz = V1[0] * V0[1] - V1[1] * V0[0];
150
+
151
+ // Scale-relative collinearity test: |V1×V0|² vs sin²θ·|V1|²·|V0|².
152
+ const v0_len_sqr = V0[0] * V0[0] + V0[1] * V0[1] + V0[2] * V0[2];
153
+ const v1_len_sqr = V1[0] * V1[0] + V1[1] * V1[1] + V1[2] * V1[2];
154
+ if (nx * nx + ny * ny + nz * nz < MPR_COLLINEAR_REL_SQR * v0_len_sqr * v1_len_sqr) {
155
+ // Collinear degeneracy: project the answer along V1.
156
+ const v1_len = v3_length(V1[0], V1[1], V1[2]);
157
+ if (v1_len > MPR_EPSILON) {
158
+ result[result_offset] = V1[0];
159
+ result[result_offset + 1] = V1[1];
160
+ result[result_offset + 2] = V1[2];
161
+ } else {
162
+ // V1 at origin — shapes touching exactly. Emit a tiny MTV.
163
+ result[result_offset] = MPR_EPSILON;
164
+ result[result_offset + 1] = 0;
165
+ result[result_offset + 2] = 0;
166
+ }
167
+ return true;
168
+ }
169
+
170
+ minkowski_support(V2, 0, shape_a, shape_b, nx, ny, nz);
171
+
172
+ if (V2[0] * nx + V2[1] * ny + V2[2] * nz < 0) {
173
+ return false;
174
+ }
175
+
176
+ // Compute the portal-triangle normal (V1 V0) × (V2 V0) and
177
+ // orient it away from V0 (so it points roughly toward the origin).
178
+ let v1mv0_x = V1[0] - V0[0], v1mv0_y = V1[1] - V0[1], v1mv0_z = V1[2] - V0[2];
179
+ let v2mv0_x = V2[0] - V0[0], v2mv0_y = V2[1] - V0[1], v2mv0_z = V2[2] - V0[2];
180
+ nx = v1mv0_y * v2mv0_z - v1mv0_z * v2mv0_y;
181
+ ny = v1mv0_z * v2mv0_x - v1mv0_x * v2mv0_z;
182
+ nz = v1mv0_x * v2mv0_y - v1mv0_y * v2mv0_x;
183
+
184
+ // If the normal points along +V0 (back toward V0's side), swap V1/V2
185
+ // and flip we want the portal facing away from V0 toward the
186
+ // origin.
187
+ if (nx * V0[0] + ny * V0[1] + nz * V0[2] > 0) {
188
+ const tx = V1[0], ty = V1[1], tz = V1[2];
189
+ V1[0] = V2[0]; V1[1] = V2[1]; V1[2] = V2[2];
190
+ V2[0] = tx; V2[1] = ty; V2[2] = tz;
191
+ nx = -nx; ny = -ny; nz = -nz;
192
+ }
193
+
194
+ // Portal-discovery iteration. We keep refining V3 until the
195
+ // tetrahedron V0–V1–V2–V3 actually contains the origin.
196
+ let discovered = false;
197
+ for (let i = 0; i < MPR_MAX_ITERATIONS; i++) {
198
+ minkowski_support(V3, 0, shape_a, shape_b, nx, ny, nz);
199
+
200
+ if (V3[0] * nx + V3[1] * ny + V3[2] * nz < 0) {
201
+ return false;
202
+ }
203
+
204
+ // Test if origin lies outside plane V0–V1–V3 (V2's side).
205
+ // n_013 = (V1 V0) × (V3 V0) — points away from V2 if portal
206
+ // is consistently oriented.
207
+ const v3mv0_x = V3[0] - V0[0], v3mv0_y = V3[1] - V0[1], v3mv0_z = V3[2] - V0[2];
208
+ const n013_x = v1mv0_y * v3mv0_z - v1mv0_z * v3mv0_y;
209
+ const n013_y = v1mv0_z * v3mv0_x - v1mv0_x * v3mv0_z;
210
+ const n013_z = v1mv0_x * v3mv0_y - v1mv0_y * v3mv0_x;
211
+
212
+ // origin's signed distance from plane V0-V1-V3 along n013 is
213
+ // dot(−V0, n013), since the plane passes through V0.
214
+ if (-V0[0] * n013_x - V0[1] * n013_y - V0[2] * n013_z > 0) {
215
+ // origin outside V0-V1-V3 plane on the V2 side — replace V2
216
+ V2[0] = V3[0]; V2[1] = V3[1]; V2[2] = V3[2];
217
+ v2mv0_x = v3mv0_x; v2mv0_y = v3mv0_y; v2mv0_z = v3mv0_z;
218
+ nx = n013_x; ny = n013_y; nz = n013_z;
219
+ continue;
220
+ }
221
+
222
+ // Test if origin lies outside plane V0-V2-V3 (V1's side).
223
+ const n023_x = v2mv0_y * v3mv0_z - v2mv0_z * v3mv0_y;
224
+ const n023_y = v2mv0_z * v3mv0_x - v2mv0_x * v3mv0_z;
225
+ const n023_z = v2mv0_x * v3mv0_y - v2mv0_y * v3mv0_x;
226
+
227
+ if (-V0[0] * n023_x - V0[1] * n023_y - V0[2] * n023_z < 0) {
228
+ // origin outside V0-V2-V3 plane on V1's side — replace V1
229
+ V1[0] = V3[0]; V1[1] = V3[1]; V1[2] = V3[2];
230
+ v1mv0_x = v3mv0_x; v1mv0_y = v3mv0_y; v1mv0_z = v3mv0_z;
231
+ // New portal normal: (V1' - V0) × (V2 - V0) where V1' is the new V1 (= old V3)
232
+ nx = v3mv0_y * v2mv0_z - v3mv0_z * v2mv0_y;
233
+ ny = v3mv0_z * v2mv0_x - v3mv0_x * v2mv0_z;
234
+ nz = v3mv0_x * v2mv0_y - v3mv0_y * v2mv0_x;
235
+ continue;
236
+ }
237
+
238
+ // Origin is inside the tetrahedron V0-V1-V2-V3 portal found.
239
+ discovered = true;
240
+ break;
241
+ }
242
+
243
+ if (!discovered) {
244
+ // Ran out of portal-discovery iterations treat as a near-miss.
245
+ return false;
246
+ }
247
+
248
+ // ── Step 4: portal refinement ───────────────────────────────────────
249
+ //
250
+ // Now we know V0–V1–V2–V3 is a valid tetrahedron containing the
251
+ // origin. Iteratively replace one of V1/V2/V3 with a new support
252
+ // V4 (found in the direction of the portal triangle's outward
253
+ // normal). When V4 doesn't extend the portal further, the portal
254
+ // face is the closest Mink-diff face to the origin and its normal
255
+ // is the MTV.
256
+ for (let i = 0; i < MPR_MAX_ITERATIONS; i++) {
257
+ // Portal face normal = (V2 - V1) × (V3 - V1)
258
+ const e1_x = V2[0] - V1[0], e1_y = V2[1] - V1[1], e1_z = V2[2] - V1[2];
259
+ const e2_x = V3[0] - V1[0], e2_y = V3[1] - V1[1], e2_z = V3[2] - V1[2];
260
+ let pn_x = e1_y * e2_z - e1_z * e2_y;
261
+ let pn_y = e1_z * e2_x - e1_x * e2_z;
262
+ let pn_z = e1_x * e2_y - e1_y * e2_x;
263
+ // Scale-relative degeneracy: |e1×e2|² vs sin²θ·|e1|²·|e2|².
264
+ const pn_len_sqr = pn_x * pn_x + pn_y * pn_y + pn_z * pn_z;
265
+ const e1_len_sqr = e1_x * e1_x + e1_y * e1_y + e1_z * e1_z;
266
+ const e2_len_sqr = e2_x * e2_x + e2_y * e2_y + e2_z * e2_z;
267
+ if (pn_len_sqr < MPR_COLLINEAR_REL_SQR * e1_len_sqr * e2_len_sqr) {
268
+ // Degenerate portal (collinear vertices) return the
269
+ // current state as a fallback approximation.
270
+ result[result_offset] = pn_x;
271
+ result[result_offset + 1] = pn_y;
272
+ result[result_offset + 2] = pn_z;
273
+ return true;
274
+ }
275
+ const inv_pn = 1 / Math.sqrt(pn_len_sqr);
276
+ pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
277
+
278
+ // Perpendicular distance from the origin to the portal plane,
279
+ // = dot(V1, portal_normal). This is the candidate depth on
280
+ // every iteration; we accept it once V4 fails to push the
281
+ // portal further.
282
+ const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
283
+
284
+ // New support V4 in the portal normal direction.
285
+ minkowski_support(V4, 0, shape_a, shape_b, pn_x, pn_y, pn_z);
286
+
287
+ const v4_dist = V4[0] * pn_x + V4[1] * pn_y + V4[2] * pn_z;
288
+
289
+ // Convergence: V4 doesn't extend the portal beyond the current
290
+ // face (within tolerance). Emit MTV = normal × depth.
291
+ if (v4_dist - portal_dist < MPR_TOLERANCE) {
292
+ result[result_offset] = pn_x * portal_dist;
293
+ result[result_offset + 1] = pn_y * portal_dist;
294
+ result[result_offset + 2] = pn_z * portal_dist;
295
+ return true;
296
+ }
297
+
298
+ // V4 didn't pass the origin along the portal normal — the
299
+ // shapes aren't actually overlapping (shouldn't happen after
300
+ // successful portal discovery, but defensive).
301
+ if (v4_dist < 0) {
302
+ return false;
303
+ }
304
+
305
+ // Replace one of V1, V2, V3 with V4 such that the new portal
306
+ // still contains the V0→origin ray. The split is determined by
307
+ // three cross-product / dot tests against V4 × V0: signs of
308
+ // (Vi · (V4 × V0)) place V0 in one of the three sub-portals.
309
+ //
310
+ // The branch structure here mirrors libccd / bullet's MPR
311
+ // (`btMprExpandPortal`).
312
+ const c_x = V4[1] * V0[2] - V4[2] * V0[1];
313
+ const c_y = V4[2] * V0[0] - V4[0] * V0[2];
314
+ const c_z = V4[0] * V0[1] - V4[1] * V0[0];
315
+
316
+ const d1 = V1[0] * c_x + V1[1] * c_y + V1[2] * c_z;
317
+
318
+ if (d1 >= 0) {
319
+ const d2 = V2[0] * c_x + V2[1] * c_y + V2[2] * c_z;
320
+ if (d2 >= 0) {
321
+ // origin sits in sub-portal between V4 and V1 — drop V1
322
+ V1[0] = V4[0]; V1[1] = V4[1]; V1[2] = V4[2];
323
+ } else {
324
+ // sub-portal between V4 and V3 drop V3
325
+ V3[0] = V4[0]; V3[1] = V4[1]; V3[2] = V4[2];
326
+ }
327
+ } else {
328
+ const d3 = V3[0] * c_x + V3[1] * c_y + V3[2] * c_z;
329
+ if (d3 >= 0) {
330
+ // sub-portal between V4 and V2 — drop V2
331
+ V2[0] = V4[0]; V2[1] = V4[1]; V2[2] = V4[2];
332
+ } else {
333
+ // sub-portal between V4 and V1 (other side) — drop V1
334
+ V1[0] = V4[0]; V1[1] = V4[1]; V1[2] = V4[2];
335
+ }
336
+ }
337
+ }
338
+
339
+ // Refinement ran out of iterations emit the current portal face
340
+ // as the best-known MTV, same graceful-degradation as EPA.
341
+ const e1_x = V2[0] - V1[0], e1_y = V2[1] - V1[1], e1_z = V2[2] - V1[2];
342
+ const e2_x = V3[0] - V1[0], e2_y = V3[1] - V1[1], e2_z = V3[2] - V1[2];
343
+ let pn_x = e1_y * e2_z - e1_z * e2_y;
344
+ let pn_y = e1_z * e2_x - e1_x * e2_z;
345
+ let pn_z = e1_x * e2_y - e1_y * e2_x;
346
+ const pn_len_sqr = pn_x * pn_x + pn_y * pn_y + pn_z * pn_z;
347
+ const e1_len_sqr = e1_x * e1_x + e1_y * e1_y + e1_z * e1_z;
348
+ const e2_len_sqr = e2_x * e2_x + e2_y * e2_y + e2_z * e2_z;
349
+ if (pn_len_sqr < MPR_COLLINEAR_REL_SQR * e1_len_sqr * e2_len_sqr) {
350
+ result[result_offset] = 0;
351
+ result[result_offset + 1] = 0;
352
+ result[result_offset + 2] = 0;
353
+ return true;
354
+ }
355
+ const inv_pn = 1 / Math.sqrt(pn_len_sqr);
356
+ pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
357
+ const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
358
+ result[result_offset] = pn_x * portal_dist;
359
+ result[result_offset + 1] = pn_y * portal_dist;
360
+ result[result_offset + 2] = pn_z * portal_dist;
361
+ return true;
362
+ }