@woosh/meep-engine 3.5.0 → 3.6.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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "3.5.0",
9
+ "version": "3.6.0",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -3,15 +3,42 @@
3
3
  * Writes the unit separation normal (A→B: the direction A must move along
4
4
  * NEGATED to separate — equivalently, negate `out` for the B→A push axis;
5
5
  * the narrowphase sign-check sites canonicalise it against the body centres)
6
- * into `out` and returns the depth (positive on overlap, 0 if separated
7
- * `out` untouched on 0).
6
+ * into `out` and returns the depth. `out` is untouched unless the return is
7
+ * positive.
8
8
  *
9
9
  * @param {Float64Array|number[]} out destination for the unit normal, length ≥ 3
10
10
  * @param {SupportProvider} A world-space support provider (e.g. PosedShape3D)
11
11
  * @param {SupportProvider} B world-space support provider (e.g. PosedShape3D)
12
- * @returns {number} penetration depth, or 0 if separated
12
+ * @returns {number} the penetration depth (> 0) on overlap;
13
+ * {@link GJK_SEPARATED} when GJK PROVED the shapes apart; `0` for no usable
14
+ * axis — EPA degenerated on an established overlap, or GJK reached no
15
+ * verdict at all. `0` is not a separation and callers with a fallback
16
+ * should still run it
13
17
  */
14
18
  export function gjk_epa_penetration(out: Float64Array | number[], A: SupportProvider, B: SupportProvider): number;
19
+ /**
20
+ * Sentinel returned by {@link gjk_epa_penetration} when GJK PROVED the shapes
21
+ * apart — {@link GJK_APART}, a support plane with the whole Minkowski
22
+ * difference on the far side of the origin — as distinct from `0`, "no usable
23
+ * axis": a degenerate EPA, or a GJK that reached no verdict at all.
24
+ *
25
+ * The two need separate values because callers act on them differently. A
26
+ * proof of separation is the final answer and the pair is done; the rest is a
27
+ * gap in this query that a fallback (MPR) may still be able to fill. Note the
28
+ * asymmetry — this is claimed ONLY from the proof, never from GJK's other two
29
+ * non-overlap exits, which are inconclusive rather than negative.
30
+ * Collapsing both to `0`, as this function used to, sent every non-touching
31
+ * pair the broadphase produced into that fallback — where MPR, whose portal
32
+ * argument assumes an overlap it was never told to doubt, answered a question
33
+ * GJK had already settled and reported the gap as a penetration of its own
34
+ * size (see the separating-plane certificate in {@link mpr}).
35
+ *
36
+ * Negative so that the existing `depth > 0` guard at every call site keeps
37
+ * rejecting it unchanged.
38
+ *
39
+ * @type {number}
40
+ */
41
+ export const GJK_SEPARATED: number;
15
42
  /**
16
43
  * A convex shape exposing a world-space support mapping: `support(out, off, dx,
17
44
  * dy, dz)` writes the farthest point of the shape in direction `(dx, dy, dz)`
@@ -1 +1 @@
1
- {"version":3,"file":"gjk_epa_penetration.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/gjk_epa_penetration.js"],"names":[],"mappings":"AAofA;;;;;;;;;;;;GAYG;AACH,yCALW,YAAY,GAAC,MAAM,EAAE,KACrB,eAAe,KACf,eAAe,GACb,MAAM,CAQlB;;;;;;8BAvbY;IAAE,OAAO,SAAW,YAAY,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,KAAG,IAAI,CAAA;CAAE"}
1
+ {"version":3,"file":"gjk_epa_penetration.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/gjk_epa_penetration.js"],"names":[],"mappings":"AA2iBA;;;;;;;;;;;;;;;;GAgBG;AACH,yCATW,YAAY,GAAC,MAAM,EAAE,KACrB,eAAe,KACf,eAAe,GACb,MAAM,CA0BlB;AA7DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,4BAFU,MAAM,CAEgB;;;;;;8BAzdnB;IAAE,OAAO,SAAW,YAAY,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,KAAG,IAAI,CAAA;CAAE"}
@@ -81,14 +81,45 @@ const CEN = new Float64Array(3); // polytope centroid (for outward orientation)
81
81
  * @typedef {{ support: function(Float64Array, number, number, number, number): void }} SupportProvider
82
82
  */
83
83
 
84
+ /**
85
+ * {@link gjk} enclosed the origin: the shapes overlap and {@link SIMPLEX} holds
86
+ * a rank-4 simplex EPA can expand from.
87
+ * @type {number}
88
+ */
89
+ const GJK_ENCLOSED = 4;
90
+
91
+ /**
92
+ * {@link gjk} found a support plane with the whole Minkowski difference on the
93
+ * far side of the origin. That is a proof of separation, and the only one of
94
+ * this function's non-overlap exits that is.
95
+ * @type {number}
96
+ */
97
+ const GJK_APART = -1;
98
+
99
+ /**
100
+ * {@link gjk} ran out of road: the search direction collapsed, or the iteration
101
+ * cap hit. Neither says the shapes are apart — the direction collapses when the
102
+ * origin lands exactly ON a simplex feature, which is a TOUCH (two spheres at
103
+ * exactly 2r do this), and the cap is just non-convergence on a smooth support.
104
+ * Callers must treat it as "no answer" and keep whatever fallback they have.
105
+ * @type {number}
106
+ */
107
+ const GJK_INCONCLUSIVE = 0;
108
+
84
109
  /**
85
110
  * GJK intersection. Fills {@link SIMPLEX} with a rank-4 simplex enclosing the
86
- * origin and returns 4 on overlap; returns 0 if the shapes are separated.
111
+ * origin and returns {@link GJK_ENCLOSED} on overlap.
87
112
  * `SIMPLEX` slot 0 is the most-recent point throughout (Muratori convention).
88
113
  *
114
+ * The two non-overlap verdicts are deliberately distinct: only
115
+ * {@link GJK_APART} is a proof, and collapsing it together with
116
+ * {@link GJK_INCONCLUSIVE} silently converts "I could not tell" into "they are
117
+ * not touching" — which drops touching contacts.
118
+ *
89
119
  * @param {SupportProvider} A first support provider
90
120
  * @param {SupportProvider} B second support provider
91
- * @returns {number} 4 if the origin is enclosed (shapes overlap), 0 if separated
121
+ * @returns {number} {@link GJK_ENCLOSED}, {@link GJK_APART}, or
122
+ * {@link GJK_INCONCLUSIVE}
92
123
  */
93
124
  function gjk(A, B) {
94
125
  minkowski_support(SIMPLEX, 0, A, B, 1, 0, 0);
@@ -102,11 +133,11 @@ function gjk(A, B) {
102
133
 
103
134
  for (let iter = 0; iter < GJK_MAX_ITER; iter++) {
104
135
  if (v3_length_sqr(DIR[0], DIR[1], DIR[2]) < DIR_EPS) {
105
- return 0; // search direction collapsed → touching/separated
136
+ return GJK_INCONCLUSIVE; // direction collapsed → touching, or apart; unknown
106
137
  }
107
138
  minkowski_support(_w, 0, A, B, DIR[0], DIR[1], DIR[2]);
108
139
  if (v3_dot(_w[0], _w[1], _w[2], DIR[0], DIR[1], DIR[2]) < 0) {
109
- return 0; // farthest point short of the origin → separated
140
+ return GJK_APART; // farthest point short of the origin → proven separated
110
141
  }
111
142
  // push the new point to slot 0, shifting the existing points down one slot
112
143
  // (copyWithin is memmove-style: the overlapping forward copy is well-defined)
@@ -118,10 +149,10 @@ function gjk(A, B) {
118
149
 
119
150
  count = do_simplex(count);
120
151
  if (count === 0) {
121
- return 4; // origin enclosed by the tetrahedron
152
+ return GJK_ENCLOSED; // origin enclosed by the tetrahedron
122
153
  }
123
154
  }
124
- return 0;
155
+ return GJK_INCONCLUSIVE; // iteration cap → non-convergent, not a separation
125
156
  }
126
157
 
127
158
  /**
@@ -498,23 +529,65 @@ function epa(A, B, out) {
498
529
  return FACE_DIST[best];
499
530
  }
500
531
 
532
+ /**
533
+ * Sentinel returned by {@link gjk_epa_penetration} when GJK PROVED the shapes
534
+ * apart — {@link GJK_APART}, a support plane with the whole Minkowski
535
+ * difference on the far side of the origin — as distinct from `0`, "no usable
536
+ * axis": a degenerate EPA, or a GJK that reached no verdict at all.
537
+ *
538
+ * The two need separate values because callers act on them differently. A
539
+ * proof of separation is the final answer and the pair is done; the rest is a
540
+ * gap in this query that a fallback (MPR) may still be able to fill. Note the
541
+ * asymmetry — this is claimed ONLY from the proof, never from GJK's other two
542
+ * non-overlap exits, which are inconclusive rather than negative.
543
+ * Collapsing both to `0`, as this function used to, sent every non-touching
544
+ * pair the broadphase produced into that fallback — where MPR, whose portal
545
+ * argument assumes an overlap it was never told to doubt, answered a question
546
+ * GJK had already settled and reported the gap as a penetration of its own
547
+ * size (see the separating-plane certificate in {@link mpr}).
548
+ *
549
+ * Negative so that the existing `depth > 0` guard at every call site keeps
550
+ * rejecting it unchanged.
551
+ *
552
+ * @type {number}
553
+ */
554
+ export const GJK_SEPARATED = -1;
555
+
501
556
  /**
502
557
  * Penetration depth + normal between two convex support providers at world pose.
503
558
  * Writes the unit separation normal (A→B: the direction A must move along
504
559
  * NEGATED to separate — equivalently, negate `out` for the B→A push axis;
505
560
  * the narrowphase sign-check sites canonicalise it against the body centres)
506
- * into `out` and returns the depth (positive on overlap, 0 if separated
507
- * `out` untouched on 0).
561
+ * into `out` and returns the depth. `out` is untouched unless the return is
562
+ * positive.
508
563
  *
509
564
  * @param {Float64Array|number[]} out destination for the unit normal, length ≥ 3
510
565
  * @param {SupportProvider} A world-space support provider (e.g. PosedShape3D)
511
566
  * @param {SupportProvider} B world-space support provider (e.g. PosedShape3D)
512
- * @returns {number} penetration depth, or 0 if separated
567
+ * @returns {number} the penetration depth (> 0) on overlap;
568
+ * {@link GJK_SEPARATED} when GJK PROVED the shapes apart; `0` for no usable
569
+ * axis — EPA degenerated on an established overlap, or GJK reached no
570
+ * verdict at all. `0` is not a separation and callers with a fallback
571
+ * should still run it
513
572
  */
514
573
  export function gjk_epa_penetration(out, A, B) {
515
- if (gjk(A, B) < 4) {
574
+ const verdict = gjk(A, B);
575
+
576
+ if (verdict === GJK_APART) {
577
+ return GJK_SEPARATED;
578
+ }
579
+
580
+ if (verdict !== GJK_ENCLOSED) {
581
+ // GJK_INCONCLUSIVE — no simplex to expand and no proof of separation
582
+ // either, so this reports the same "no usable axis" as a degenerate
583
+ // EPA and leaves the caller's fallback in play. Reporting it as
584
+ // GJK_SEPARATED would be an overclaim, and an expensive one: the
585
+ // collapse exit fires on EXACTLY touching pairs (two spheres at 2r),
586
+ // whose resting contact would then be dropped without anything ever
587
+ // looking at the geometry.
516
588
  return 0;
517
589
  }
590
+
518
591
  const depth = epa(A, B, out);
519
592
  return depth > 0 && Number.isFinite(depth) ? depth : 0;
520
593
  }
@@ -39,7 +39,13 @@
39
39
  * any narrowphase call site.
40
40
  *
41
41
  * On failure modes:
42
- * - Returns `false` if the shapes don't overlap.
42
+ * - Returns `false` if the shapes don't overlap, decided by a
43
+ * separating-plane certificate — the support of the Mink-diff along
44
+ * the portal normal fails to reach the origin — which is one exact
45
+ * evaluation carrying none of the refinement's error. A miss leaves
46
+ * `result` untouched: the separation distance is deliberately NOT
47
+ * written, because every caller reads the depth as |MTV| and cannot
48
+ * tell a gap from a penetration once the sign is gone.
43
49
  * - On a degenerate Mink-diff (shapes touching at a point, perfectly
44
50
  * coincident centres with collinear support points), returns
45
51
  * `true` with a small fallback MTV — same approach as EPA's
@@ -1 +1 @@
1
- {"version":3,"file":"mpr.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/mpr.js"],"names":[],"mappings":"AAsDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,4BANW,YAAY,iBACZ,MAAM,iDAGJ,OAAO,CAqQnB"}
1
+ {"version":3,"file":"mpr.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/gjk/mpr.js"],"names":[],"mappings":"AAsDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,4BANW,YAAY,iBACZ,MAAM,iDAGJ,OAAO,CA8TnB"}
@@ -93,7 +93,13 @@ const V4 = new Float64Array(3);
93
93
  * any narrowphase call site.
94
94
  *
95
95
  * On failure modes:
96
- * - Returns `false` if the shapes don't overlap.
96
+ * - Returns `false` if the shapes don't overlap, decided by a
97
+ * separating-plane certificate — the support of the Mink-diff along
98
+ * the portal normal fails to reach the origin — which is one exact
99
+ * evaluation carrying none of the refinement's error. A miss leaves
100
+ * `result` untouched: the separation distance is deliberately NOT
101
+ * written, because every caller reads the depth as |MTV| and cannot
102
+ * tell a gap from a penetration once the sign is gone.
97
103
  * - On a degenerate Mink-diff (shapes touching at a point, perfectly
98
104
  * coincident centres with collinear support points), returns
99
105
  * `true` with a small fallback MTV — same approach as EPA's
@@ -116,6 +122,17 @@ export function mpr(result, result_offset, shape_a, shape_b) {
116
122
  // origins are interior to their geometry — true for spheres,
117
123
  // boxes, capsules, and any closed mesh whose bounding box contains
118
124
  // its centroid.
125
+ //
126
+ // This is a PRECONDITION, not a preference, and nothing here can
127
+ // check it: every argument below assumes the ray V0→origin starts
128
+ // inside. An authored ConvexHullShape3D whose vertices sit far from
129
+ // its own local origin (a level brush keeping map coordinates under
130
+ // an identity collider transform, say) violates it, and MPR then
131
+ // returns a confidently wrong MTV — measured at 17.8 m, the slab's
132
+ // half-width, for a 0.05 m overlap. Callers that can answer overlap
133
+ // some other way should do so and reach for MPR only when that
134
+ // answer is genuinely unavailable; {@link narrowphase_step} keeps it
135
+ // behind a GJK verdict for exactly this reason.
119
136
  V0[0] = shape_a.px - shape_b.px;
120
137
  V0[1] = shape_a.py - shape_b.py;
121
138
  V0[2] = shape_a.pz - shape_b.pz;
@@ -281,10 +298,22 @@ export function mpr(result, result_offset, shape_a, shape_b) {
281
298
  const inv_pn = 1 / Math.sqrt(pn_len_sqr);
282
299
  pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
283
300
 
284
- // Perpendicular distance from the origin to the portal plane,
285
- // = dot(V1, portal_normal). This is the candidate depth on
286
- // every iteration; we accept it once V4 fails to push the
287
- // portal further.
301
+ // Signed offset of the portal plane from the origin along the
302
+ // outward normal, = dot(V1, portal_normal), and the depth this
303
+ // returns once the portal settles. In exact arithmetic its sign
304
+ // would also BE the overlap test — V0 is interior and the ray
305
+ // V0→origin pierces the portal, so the origin is inside the
306
+ // Mink-diff exactly when it lies on V0's side of that plane —
307
+ // and a negative value would mean the ray left the Mink-diff
308
+ // before reaching the origin, making |portal_dist| a SEPARATION
309
+ // rather than a depth.
310
+ //
311
+ // It is not the test used, because this is an iterate: near zero
312
+ // its sign is refinement noise, not geometry. The certificate
313
+ // below decides instead, and getting a magnitude out of a
314
+ // separated pair — which every caller then reads as a depth,
315
+ // |MTV| having thrown the sign away — is the failure that made
316
+ // this loop worth re-deriving.
288
317
  const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
289
318
 
290
319
  // New support V4 in the portal normal direction.
@@ -292,8 +321,41 @@ export function mpr(result, result_offset, shape_a, shape_b) {
292
321
 
293
322
  const v4_dist = V4[0] * pn_x + V4[1] * pn_y + V4[2] * pn_z;
294
323
 
324
+ // Separating-plane certificate — and it has to be tested BEFORE
325
+ // the convergence branch below. If the farthest point of the
326
+ // Mink-diff along pn does not reach the origin, the plane
327
+ // through the origin with normal pn separates the two shapes,
328
+ // which is a proof of a miss. It can never reject a true
329
+ // overlap: the origin is then inside the Mink-diff, whose
330
+ // support is ≥ 0 in EVERY direction.
331
+ //
332
+ // The order is the whole point. A small shape near a large flat
333
+ // face seeds a portal already within MPR_TOLERANCE of the
334
+ // support plane, so the convergence test succeeds on the first
335
+ // iteration and this proof was never reached — the old order
336
+ // returned `true` with a negative portal_dist, and callers
337
+ // (which read the depth as |MTV|) saw the gap as a penetration
338
+ // of exactly its own size.
339
+ if (v4_dist < 0) {
340
+ return false;
341
+ }
342
+
295
343
  // Convergence: V4 doesn't extend the portal beyond the current
296
- // face (within tolerance). Emit MTV = normal × depth.
344
+ // face (within tolerance), so the portal face is the closest
345
+ // face of the Mink-diff and its offset is the depth.
346
+ //
347
+ // Note what does NOT appear here: a `portal_dist <= 0` gate. It
348
+ // is tempting — a negative offset is exactly what a miss looks
349
+ // like — but portal_dist is the one quantity in this loop that
350
+ // carries the refinement's error, and near zero that error is
351
+ // the whole signal. A capsule resting exactly on a floor
352
+ // converges at portal_dist ≈ −3e-6 with v4_dist ≈ +2.5e-6: a
353
+ // real touching contact whose sign is pure noise, which such a
354
+ // gate drops, taking the resting contact with it. The certificate
355
+ // above is error-free (one exact support evaluation) and is the
356
+ // right arbiter; by the time it passes, convergence bounds
357
+ // portal_dist below at −MPR_TOLERANCE, which is the same
358
+ // accuracy the depth itself is quoted to.
297
359
  if (v4_dist - portal_dist < MPR_TOLERANCE) {
298
360
  result[result_offset] = pn_x * portal_dist;
299
361
  result[result_offset + 1] = pn_y * portal_dist;
@@ -301,13 +363,6 @@ export function mpr(result, result_offset, shape_a, shape_b) {
301
363
  return true;
302
364
  }
303
365
 
304
- // V4 didn't pass the origin along the portal normal — the
305
- // shapes aren't actually overlapping (shouldn't happen after
306
- // successful portal discovery, but defensive).
307
- if (v4_dist < 0) {
308
- return false;
309
- }
310
-
311
366
  // Replace one of V1, V2, V3 with V4 such that the new portal
312
367
  // still contains the V0→origin ray. The split is determined by
313
368
  // three cross-product / dot tests against V4 × V0: signs of
@@ -360,6 +415,14 @@ export function mpr(result, result_offset, shape_a, shape_b) {
360
415
  }
361
416
  const inv_pn = 1 / Math.sqrt(pn_len_sqr);
362
417
  pn_x *= inv_pn; pn_y *= inv_pn; pn_z *= inv_pn;
418
+ // The portal never settled, so this face is a guess — but the
419
+ // separating-plane certificate costs one support call and holds
420
+ // regardless of how the refinement terminated. Apply the same rule
421
+ // both exits use rather than emitting an unexamined MTV.
422
+ minkowski_support(V4, 0, shape_a, shape_b, pn_x, pn_y, pn_z);
423
+ if (V4[0] * pn_x + V4[1] * pn_y + V4[2] * pn_z < 0) {
424
+ return false;
425
+ }
363
426
  const portal_dist = V1[0] * pn_x + V1[1] * pn_y + V1[2] * pn_z;
364
427
  result[result_offset] = pn_x * portal_dist;
365
428
  result[result_offset + 1] = pn_y * portal_dist;
@@ -1 +1 @@
1
- {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AAyyCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qDAVW,YAAY,GAAC,MAAM,EAAE,iCAGrB;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iCAErC;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,GACnC,MAAM,CAuClB;AAED;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QA4LlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QA2H3D"}
1
+ {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA2zCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qDAVW,YAAY,GAAC,MAAM,EAAE,iCAGrB;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iCAErC;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,GACnC,MAAM,CAuClB;AAED;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QA4LlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QA2H3D"}
@@ -3,7 +3,7 @@ import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
3
3
  import { body_id_index } from "../body/BodyStorage.js";
4
4
  import { combine_friction, combine_restitution } from "../contact/combine_material.js";
5
5
  import { CONTACT_STRIDE, MAX_CONTACTS_PER_MANIFOLD } from "../contact/ManifoldStore.js";
6
- import { gjk_epa_penetration } from "../../../core/geom/3d/gjk/gjk_epa_penetration.js";
6
+ import { GJK_SEPARATED, gjk_epa_penetration } from "../../../core/geom/3d/gjk/gjk_epa_penetration.js";
7
7
  import { reduce_manifold_contacts } from "./reduce_manifold_contacts.js";
8
8
  import { mpr } from "../../../core/geom/3d/gjk/mpr.js";
9
9
  import { box_box_manifold, BOX_BOX_OUT_LENGTH } from "./box_box_manifold.js";
@@ -1124,12 +1124,15 @@ function dispatch_pair(count, colA, trA, colB, trB) {
1124
1124
  // expanding_polytope_algorithm pair was replaced because it returned
1125
1125
  // a non-minimal axis on degenerate simplices (see memory:
1126
1126
  // feedback_epa_unreliable_polytopes); MPR is kept as a secondary
1127
- // safety net for the rare case the robust query reports no overlap.
1127
+ // safety net for the case EPA degenerates on a real overlap, or
1128
+ // GJK reaches no verdict — never for a triangle GJK proved clear.
1128
1129
  let ex, ey, ez, depth;
1129
1130
  const pen_depth = gjk_epa_penetration(epa_result, posed_a, posed_b);
1130
1131
  if (pen_depth > 0 && Number.isFinite(pen_depth)) {
1131
1132
  ex = epa_result[0] * pen_depth; ey = epa_result[1] * pen_depth; ez = epa_result[2] * pen_depth;
1132
1133
  depth = pen_depth;
1134
+ } else if (pen_depth === GJK_SEPARATED) {
1135
+ continue;
1133
1136
  } else {
1134
1137
  if (!mpr(epa_result, 0, posed_a, posed_b)) continue;
1135
1138
  ex = epa_result[0]; ey = epa_result[1]; ez = epa_result[2];
@@ -1247,7 +1250,22 @@ function dispatch_pair(count, colA, trA, colB, trB) {
1247
1250
  if (pen_depth > 0 && Number.isFinite(pen_depth)) {
1248
1251
  ex = epa_result[0] * pen_depth; ey = epa_result[1] * pen_depth; ez = epa_result[2] * pen_depth;
1249
1252
  depth = pen_depth;
1253
+ } else if (pen_depth === GJK_SEPARATED) {
1254
+ // GJK proved the pair apart — the answer, not a gap in the query, so
1255
+ // the safety net below has nothing to add and must not be consulted.
1256
+ // This branch is the common case on this path, not an edge: the
1257
+ // broadphase hands over every pair whose fat AABBs touch, and most of
1258
+ // them are near misses. Asking MPR about them is what turned a clear
1259
+ // gap into a ContactBegin carrying a positive depth equal to the gap
1260
+ // — for a sphere near a large ConvexHullShape3D face, which is every
1261
+ // projectile in a brush-built level (mpr.js's separating-plane
1262
+ // certificate now refuses that answer at the source too).
1263
+ return count;
1250
1264
  } else {
1265
+ // No usable axis: EPA degenerated on a real overlap, or GJK itself
1266
+ // reached no verdict (its search direction collapses on an EXACTLY
1267
+ // touching pair). Neither is a separation, so the net still gets to
1268
+ // look. THIS is what it is for.
1251
1269
  if (!mpr(epa_result, 0, posed_a, posed_b)) return count;
1252
1270
  ex = epa_result[0]; ey = epa_result[1]; ez = epa_result[2];
1253
1271
  depth = Math.sqrt(ex * ex + ey * ey + ez * ez);
@@ -390,10 +390,10 @@ export class StaticSceneBVH {
390
390
  let nearest_distance = ray.tMax;
391
391
 
392
392
  const hit_count = bvh_query_user_data_ray(
393
- bvh,
394
- bvh.root,
395
393
  scratch_instance_hits,
396
394
  0,
395
+ bvh,
396
+ bvh.root,
397
397
  ray.origin_x, ray.origin_y, ray.origin_z,
398
398
  ray.direction_x, ray.direction_y, ray.direction_z
399
399
  );
@@ -501,10 +501,10 @@ export class StaticSceneBVH {
501
501
  const bvh = this.instance_bvh;
502
502
 
503
503
  const hit_count = bvh_query_user_data_ray(
504
- bvh,
505
- bvh.root,
506
504
  scratch_instance_hits,
507
505
  0,
506
+ bvh,
507
+ bvh.root,
508
508
  ray.origin_x, ray.origin_y, ray.origin_z,
509
509
  ray.direction_x, ray.direction_y, ray.direction_z
510
510
  );
@@ -1,51 +0,0 @@
1
- /**
2
- * Prepend a preamble to a three.js-generated shader and splice a chunk in after
3
- * a named anchor.
4
- *
5
- * WHY THIS EXISTS
6
- *
7
- * The LPV/GI and LPG material paths have eight shader builders between them
8
- * (`space_fragment` and `space_vertex`, vertex and fragment, times two
9
- * subsystems) and every one of them was the same three statements: concatenate
10
- * the preambles ahead of the source, `insert_after` an anchor, return. What
11
- * actually differs is data — which preamble, which anchor, which chunk.
12
- *
13
- * More importantly the two anchors were spelled out in eight separate string
14
- * literals. They are three.js's own chunk include lines, and a mismatch fails
15
- * *quietly* (a console warning and an unmodified shader), so having one
16
- * definition each is worth more than the lines it saves.
17
- *
18
- * Shader assembly happens once per material compile, not per frame.
19
- *
20
- * @param {string} source shader source three.js handed to `onBeforeCompile`
21
- * @param {string} preamble prepended verbatim, ahead of the source
22
- * @param {string} anchor see {@link GLSL_ANCHOR_LIGHTS_FRAGMENT_BEGIN} / {@link GLSL_ANCHOR_FOG_VERTEX}
23
- * @param {string} chunk GLSL spliced in directly after `anchor`
24
- * @returns {string}
25
- */
26
- export function build_injected_shader(source: string, preamble: string, anchor: string, chunk: string): string;
27
- /**
28
- * three.js chunk marker after which a probe-field contribution is added to
29
- * `irradiance` in the fragment stage.
30
- *
31
- * `<lights_fragment_begin>` is where three declares `irradiance` and has not yet
32
- * consumed it, so anything added immediately after it participates in the
33
- * lighting equations normally. The literal must match three's include line
34
- * exactly — `insert_after` warns and silently returns the source unchanged when
35
- * it does not find the term, which shows up as GI simply not appearing.
36
- *
37
- * @type {string}
38
- */
39
- export const GLSL_ANCHOR_LIGHTS_FRAGMENT_BEGIN: string;
40
- /**
41
- * three.js chunk marker after which per-vertex probe work is injected.
42
- *
43
- * `<fog_vertex>` is the last chunk in the standard vertex `main()`, so by that
44
- * point `worldPosition` (when declared), `position` and `normal` are all
45
- * available. Same exact-match requirement as
46
- * {@link GLSL_ANCHOR_LIGHTS_FRAGMENT_BEGIN}.
47
- *
48
- * @type {string}
49
- */
50
- export const GLSL_ANCHOR_FOG_VERTEX: string;
51
- //# sourceMappingURL=build_injected_shader.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build_injected_shader.d.ts","sourceRoot":"","sources":["../../../../../src/engine/graphics/sh3/build_injected_shader.js"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8CANW,MAAM,YACN,MAAM,UACN,MAAM,SACN,MAAM,GACJ,MAAM,CAIlB;AArDD;;;;;;;;;;;GAWG;AACH,gDAFU,MAAM,CAEoE;AAEpF;;;;;;;;;GASG;AACH,qCAFU,MAAM,CAE8C"}
@@ -1,56 +0,0 @@
1
- import { insert_after } from "../../../core/primitives/strings/insert_after.js";
2
-
3
- /**
4
- * three.js chunk marker after which a probe-field contribution is added to
5
- * `irradiance` in the fragment stage.
6
- *
7
- * `<lights_fragment_begin>` is where three declares `irradiance` and has not yet
8
- * consumed it, so anything added immediately after it participates in the
9
- * lighting equations normally. The literal must match three's include line
10
- * exactly — `insert_after` warns and silently returns the source unchanged when
11
- * it does not find the term, which shows up as GI simply not appearing.
12
- *
13
- * @type {string}
14
- */
15
- export const GLSL_ANCHOR_LIGHTS_FRAGMENT_BEGIN = '#include <lights_fragment_begin>';
16
-
17
- /**
18
- * three.js chunk marker after which per-vertex probe work is injected.
19
- *
20
- * `<fog_vertex>` is the last chunk in the standard vertex `main()`, so by that
21
- * point `worldPosition` (when declared), `position` and `normal` are all
22
- * available. Same exact-match requirement as
23
- * {@link GLSL_ANCHOR_LIGHTS_FRAGMENT_BEGIN}.
24
- *
25
- * @type {string}
26
- */
27
- export const GLSL_ANCHOR_FOG_VERTEX = '#include <fog_vertex>';
28
-
29
- /**
30
- * Prepend a preamble to a three.js-generated shader and splice a chunk in after
31
- * a named anchor.
32
- *
33
- * WHY THIS EXISTS
34
- *
35
- * The LPV/GI and LPG material paths have eight shader builders between them
36
- * (`space_fragment` and `space_vertex`, vertex and fragment, times two
37
- * subsystems) and every one of them was the same three statements: concatenate
38
- * the preambles ahead of the source, `insert_after` an anchor, return. What
39
- * actually differs is data — which preamble, which anchor, which chunk.
40
- *
41
- * More importantly the two anchors were spelled out in eight separate string
42
- * literals. They are three.js's own chunk include lines, and a mismatch fails
43
- * *quietly* (a console warning and an unmodified shader), so having one
44
- * definition each is worth more than the lines it saves.
45
- *
46
- * Shader assembly happens once per material compile, not per frame.
47
- *
48
- * @param {string} source shader source three.js handed to `onBeforeCompile`
49
- * @param {string} preamble prepended verbatim, ahead of the source
50
- * @param {string} anchor see {@link GLSL_ANCHOR_LIGHTS_FRAGMENT_BEGIN} / {@link GLSL_ANCHOR_FOG_VERTEX}
51
- * @param {string} chunk GLSL spliced in directly after `anchor`
52
- * @returns {string}
53
- */
54
- export function build_injected_shader(source, preamble, anchor, chunk) {
55
- return insert_after(preamble + source, anchor, chunk);
56
- }
@@ -1,9 +0,0 @@
1
- /**
2
- *
3
- * @param {Uint8Array} data
4
- * @param {THREE.WebGLRenderer} renderer
5
- * @param {THREE.WebGLCubeRenderTarget} cubeRenderTarget
6
- * @return {Float64Array}
7
- */
8
- export function fromCubeRenderTarget(data: Uint8Array, renderer: THREE.WebGLRenderer, cubeRenderTarget: THREE.WebGLCubeRenderTarget): Float64Array;
9
- //# sourceMappingURL=fromCubeRenderTarget.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fromCubeRenderTarget.d.ts","sourceRoot":"","sources":["../../../../../src/engine/graphics/sh3/fromCubeRenderTarget.js"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,2CALW,UAAU,iFAGT,YAAY,CAgIvB"}
@@ -1,142 +0,0 @@
1
- import { sh3_basis_at } from "../../../core/geom/3d/sphere/harmonics/sh3_basis_at.js";
2
- import { sh3_dering_optimize_positive } from "../../../core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js";
3
- import { sh3_vector_accumulate } from "../../../core/geom/3d/sphere/harmonics/sh3_vector_accumulate.js";
4
- import { vector_scale_offset } from "../../../core/geom/vec/vector_scale_offset.js";
5
- import { v3_length_sqr } from "../../../core/geom/vec3/v3_length_sqr.js";
6
-
7
- const scratch_color = new Float32Array(3);
8
-
9
- /**
10
- *
11
- * @param {Uint8Array} data
12
- * @param {THREE.WebGLRenderer} renderer
13
- * @param {THREE.WebGLCubeRenderTarget} cubeRenderTarget
14
- * @return {Float64Array}
15
- */
16
- export function fromCubeRenderTarget(
17
- data,
18
- renderer,
19
- cubeRenderTarget
20
- ) {
21
-
22
- // The renderTarget must be set to RGBA in order to make readRenderTargetPixels works
23
- let total_weight = 0;
24
-
25
- const __shared_buffer = new ArrayBuffer((27 + 9) * 8);
26
-
27
- const sh_basis = new Float64Array(__shared_buffer, 0, 9);
28
- const coefficients = new Float64Array(__shared_buffer, 9 * 8, 27);
29
-
30
- const image_size = cubeRenderTarget.width;
31
-
32
- const pixel_size = 2 / image_size;
33
-
34
- let normal_x = 0, normal_y = 0, normal_z = 0;
35
-
36
- for (let faceIndex = 0; faceIndex < 6; faceIndex++) {
37
-
38
- renderer.readRenderTargetPixels(cubeRenderTarget, 0, 0, image_size, image_size, data, faceIndex);
39
-
40
- // RGBA assumed
41
-
42
- for (let i = 0, il = data.length; i < il; i += 4) {
43
-
44
-
45
- // pixel coordinate on unit cube
46
-
47
- const pixelIndex = i / 4;
48
-
49
- const col = -1 + (pixelIndex % image_size + 0.5) * pixel_size;
50
-
51
- const row = 1 - (Math.floor(pixelIndex / image_size) + 0.5) * pixel_size;
52
-
53
-
54
- switch (faceIndex) {
55
-
56
- case 0:
57
- normal_x = 1;
58
- normal_y = row;
59
- normal_z = -col;
60
- break;
61
-
62
- case 1:
63
- normal_x = -1;
64
- normal_y = row;
65
- normal_z = col;
66
- break;
67
-
68
- case 2:
69
- normal_x = col;
70
- normal_y = 1;
71
- normal_z = -row;
72
- break;
73
-
74
- case 3:
75
- normal_x = col;
76
- normal_y = -1;
77
- normal_z = row;
78
- break;
79
-
80
- case 4:
81
- normal_x = col;
82
- normal_y = row;
83
- normal_z = 1;
84
- break;
85
-
86
- case 5:
87
- normal_x = -col;
88
- normal_y = row;
89
- normal_z = -1;
90
- break;
91
-
92
- }
93
-
94
- // weight assigned to this pixel
95
-
96
- const length_squared = v3_length_sqr(normal_x, normal_y, normal_z);
97
-
98
- const length = Math.sqrt(length_squared);
99
-
100
- const weight = 4 / (length * length_squared);
101
-
102
- total_weight += weight;
103
-
104
- // direction vector to this pixel
105
- normal_x /= length;
106
- normal_y /= length;
107
- normal_z /= length;
108
-
109
- // evaluate SH basis functions in direction dir
110
- sh3_basis_at(normal_x, normal_y, normal_z, sh_basis);
111
-
112
- // pixel color, already in linear space so no sRGB->Linear conversion necessary
113
-
114
- // 0.003921 constant value is 1/255, a conversion value from UINT8 to normalized float
115
- const weight_conv = weight * 0.00392156862745098;
116
-
117
- scratch_color[0] = data[i] * weight_conv;
118
- scratch_color[1] = data[i + 1] * weight_conv;
119
- scratch_color[2] = data[i + 2] * weight_conv;
120
-
121
- // accumulate
122
- sh3_vector_accumulate(
123
- coefficients, 0,
124
- sh_basis, 0,
125
- scratch_color, 0,
126
- 3
127
- );
128
-
129
- }
130
-
131
- }
132
-
133
- // normalize
134
- const norm = (4 * Math.PI) / total_weight;
135
-
136
- vector_scale_offset(coefficients, 0, coefficients, 0, 27, norm);
137
-
138
- sh3_dering_optimize_positive(coefficients, 0, coefficients, 0, 3);
139
-
140
- return coefficients;
141
-
142
- }
@@ -1,3 +0,0 @@
1
- declare const _default: "\n#define RECIPROCAL_PI 0.318309886\n\nvec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {\n // matrix is assumed to be orthogonal\n return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );\n}\n\n// source: https://graphics.stanford.edu/papers/envmap/envmap.pdf\n//\n// SIGN CONVENTION: coefficients 1, 3, 5 and 7 are SUBTRACTED here, matching\n// three.js's SphericalHarmonics3 and the JS reference\n// core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js. The LPV\n// and LPG shader chunks (sh3/{gi,lpg}/material/chunk_*_get_irradiance_at.js)\n// ADD them instead — see the note in chunk_lpv_get_irradiance_at.js. The\n// constants are the same in all four; only these signs differ.\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n // normal is assumed to have unit length\n float x = normal.x, y = normal.y, z = normal.z;\n\n // band 0\n vec3 result = shCoefficients[ 0 ] * 0.8862269254527579;\n\n // band 1\n result -= shCoefficients[ 1 ] * 1.0233267079464885 * y;\n result += shCoefficients[ 2 ] * 1.0233267079464885 * z;\n result -= shCoefficients[ 3 ] * 1.0233267079464885 * x;\n\n // band 2\n result += shCoefficients[ 4 ] * 0.8580855308097834 * x * y;\n result -= shCoefficients[ 5 ] * 0.8580855308097834 * y * z;\n result += shCoefficients[ 6 ] * ( 0.7431238683011272 * z * z - 0.24770795610037571 );\n result -= shCoefficients[ 7 ] * 0.8580855308097834 * x * z;\n result += shCoefficients[ 8 ] * 0.4290427654048917 * ( x * x - y * y );\n\n return result;\n}\n\nuniform vec3 sh[ 9 ]; // sh coefficients\nuniform float intensity; // light probe intensity\nvarying vec3 vNormal;\n\nvoid main() {\n vec3 normal = normalize( vNormal );\n vec3 worldNormal = normalize(inverseTransformDirection( normal, viewMatrix ));\n vec3 irradiance = shGetIrradianceAt( worldNormal, sh );\n vec3 outgoingLight = 1.0 * irradiance * intensity;\n gl_FragColor = linearToOutputTexel( vec4( outgoingLight, 1.0 ) );\n}\n";
2
- export default _default;
3
- //# sourceMappingURL=visualize.frag.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"visualize.frag.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/shader/visualize.frag.js"],"names":[],"mappings":""}
@@ -1,51 +0,0 @@
1
- // language=GLSL
2
- export default `
3
- #define RECIPROCAL_PI 0.318309886
4
-
5
- vec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {
6
- // matrix is assumed to be orthogonal
7
- return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );
8
- }
9
-
10
- // source: https://graphics.stanford.edu/papers/envmap/envmap.pdf
11
- //
12
- // SIGN CONVENTION: coefficients 1, 3, 5 and 7 are SUBTRACTED here, matching
13
- // three.js's SphericalHarmonics3 and the JS reference
14
- // core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js. The LPV
15
- // and LPG shader chunks (sh3/{gi,lpg}/material/chunk_*_get_irradiance_at.js)
16
- // ADD them instead — see the note in chunk_lpv_get_irradiance_at.js. The
17
- // constants are the same in all four; only these signs differ.
18
- vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {
19
- // normal is assumed to have unit length
20
- float x = normal.x, y = normal.y, z = normal.z;
21
-
22
- // band 0
23
- vec3 result = shCoefficients[ 0 ] * 0.8862269254527579;
24
-
25
- // band 1
26
- result -= shCoefficients[ 1 ] * 1.0233267079464885 * y;
27
- result += shCoefficients[ 2 ] * 1.0233267079464885 * z;
28
- result -= shCoefficients[ 3 ] * 1.0233267079464885 * x;
29
-
30
- // band 2
31
- result += shCoefficients[ 4 ] * 0.8580855308097834 * x * y;
32
- result -= shCoefficients[ 5 ] * 0.8580855308097834 * y * z;
33
- result += shCoefficients[ 6 ] * ( 0.7431238683011272 * z * z - 0.24770795610037571 );
34
- result -= shCoefficients[ 7 ] * 0.8580855308097834 * x * z;
35
- result += shCoefficients[ 8 ] * 0.4290427654048917 * ( x * x - y * y );
36
-
37
- return result;
38
- }
39
-
40
- uniform vec3 sh[ 9 ]; // sh coefficients
41
- uniform float intensity; // light probe intensity
42
- varying vec3 vNormal;
43
-
44
- void main() {
45
- vec3 normal = normalize( vNormal );
46
- vec3 worldNormal = normalize(inverseTransformDirection( normal, viewMatrix ));
47
- vec3 irradiance = shGetIrradianceAt( worldNormal, sh );
48
- vec3 outgoingLight = 1.0 * irradiance * intensity;
49
- gl_FragColor = linearToOutputTexel( vec4( outgoingLight, 1.0 ) );
50
- }
51
- `
@@ -1,3 +0,0 @@
1
- declare const _default: "\nvarying vec3 vNormal;\n\nvoid main() {\n\n vNormal = normalize( normalMatrix * normal );\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n}\n";
2
- export default _default;
3
- //# sourceMappingURL=visualize.vert.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"visualize.vert.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/shader/visualize.vert.js"],"names":[],"mappings":""}
@@ -1,12 +0,0 @@
1
- // language=GLSL
2
- export default `
3
- varying vec3 vNormal;
4
-
5
- void main() {
6
-
7
- vNormal = normalize( normalMatrix * normal );
8
-
9
- gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
10
-
11
- }
12
- `