@woosh/meep-engine 3.6.0 → 3.7.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.6.0",
9
+ "version": "3.7.0",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -6,17 +6,22 @@
6
6
  * start(), stop(), render(), updateSize()}` — because that is what `Engine` needs in order to boot,
7
7
  * and because the facade is deliberately not allowed to grow past §4.2 (D14).
8
8
  *
9
- * **Two things have been let through since, each by name.** {@link add_extension} /
9
+ * **Three things have been let through since, each by name.** {@link add_extension} /
10
10
  * {@link remove_extension} (E9), because an extension has to survive the restart that replaces the
11
11
  * renderer holding it, and
12
12
  * {@link scene_context} (E13, D29), because the managers a system needs — animation above all —
13
13
  * hang off Shade's per-scene GPU state and there is no other door to them. Both take or return
14
14
  * something the caller already owns; neither hands out the renderer.
15
15
  *
16
- * What is deliberately absent, and stays absent: `renderer` / `getRenderer()` (44 callers on the
17
- * old engine, each of them a migration item rather than a reason to re-open the hatch), `layers`
18
- * (CPU visibility dies with GPU culling), `scene` (entities reach the scene through the mesh
19
- * system, not through the facade), and the three-shaped material manager.
16
+ * {@link renderer} is the third, and much the widest: it hands the renderer straight out, so
17
+ * everything the other two are shaped to avoid is reachable through it. It is an escape hatch for
18
+ * callers that have no other route, not a migration target — the old engine's 44
19
+ * `.renderer`/`getRenderer()` call sites are each still a migration item.
20
+ *
21
+ * What is deliberately absent, and stays absent: `getRenderer()` (the old engine's accessor, and
22
+ * the shape those call sites are being ported off), `layers` (CPU visibility dies with GPU
23
+ * culling), `scene` (entities reach the scene through the mesh system, not through the facade),
24
+ * and the three-shaped material manager.
20
25
  *
21
26
  * **Lifecycle differs from the old engine on purpose.** `GraphicsEngine` starts inside the `Engine`
22
27
  * constructor, which is why stopping an engine used to leave a restarted one with no renderer at
@@ -1 +1 @@
1
- {"version":3,"file":"GraphicsEngine3.d.ts","sourceRoot":"","sources":["../../../../src/engine/graphics3/GraphicsEngine3.js"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;IACI;;;;;OAKG;IACH,gCAEC;IAUD;;;;OAIG;IACH,yBAEC;IA0DG;;;;;;OAMG;IACH,QAFU,kBAAkB,CAEsB;IAElD;QACI;;;;;;;;WAQG;mBADO,MAAM;QAIhB;;;;;WAKG;oBADO,MAAM;QAIhB;;;;WAIG;qBADO,MAAM;QAIhB;;;;;;;WAOG;;QAGH;;;;;WAKG;;MAEN;IAED;;OAEG;IACH,YAFU,OAAO,CAEe;IAEhC;;;;;OAKG;IACH,4BAFU,OAAO,CAEyB;IAE1C;;;;;;OAMG;IACH,UAFU,SAAS,CAEY;IAG/B;;OAEG;IACH,YAFU,iBAAiB,GAAC,IAAI,CAEV;IAEtB;;;;;OAKG;IACH,UAFU,OAAO,CAEG;IAEpB;;;;;OAKG;IACH,UAFU,OAAO,CAEG;IAEpB;;OAEG;IACH,YAFU,MAAM,CAEG;IAoBvB;;;;;;;;;;;;OAYG;IACH,mDAEC;IAED;;;;;;;;OAQG;IACH,iBAFW,KAAK,QAoBf;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,iDAOC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,2DAWC;IAED;;;;;;;;;;OAUG;IACH,oCAFa,MAAM,CAclB;IAED;;;;;;OAMG;IACH,8CAFa,OAAO,CAgBnB;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,qBALW,KAAK,GACH,kBAAgB,IAAI,CAehC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,yBARW,KAAK,yBAEL,MAAM,UACN,MAAM,GACJ,QAAQ,SAAS,CAAC,CAwD9B;IAED;;;;OAIG;IACH,0BAEC;IAED;;;;;;;;;;OAUG;IACH,SAFa,QAAQ,IAAI,CAAC,CA4DzB;IAED;;;;;OAKG;IACH,aAiBC;IASD;;;;;;;;OAQG;IACH,8BAHW,OAAO,UAAQ,UACf,OAAO,UAAQ,QAQzB;IAED;;;;;;;;;;;;;;;OAeG;IACH,yBALW,MAAM,KACN,MAAM,6CAMhB;IAED;;;;;;;OAOG;IACH,mBA0BC;IAED;;;;;OAKG;IACH,4BAHW,MAAM,GACJ,OAAO,CAoDnB;;CACJ;yBAxwBwB,kCAAkC;mCAMxB,yBAAyB;mBAbzC,oCAAoC;oBACnC,4BAA4B;wBACxB,4BAA4B;sBAQ9B,kCAAkC;yCAJf,kDAAkD;sBAErE,qCAAqC;0BACjC,0CAA0C"}
1
+ {"version":3,"file":"GraphicsEngine3.d.ts","sourceRoot":"","sources":["../../../../src/engine/graphics3/GraphicsEngine3.js"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IACI;;;;;OAKG;IACH,gCAEC;IAUD;;;;OAIG;IACH,yBAEC;IA0DG;;;;;;OAMG;IACH,QAFU,kBAAkB,CAEsB;IAElD;QACI;;;;;;;;WAQG;mBADO,MAAM;QAIhB;;;;;WAKG;oBADO,MAAM;QAIhB;;;;WAIG;qBADO,MAAM;QAIhB;;;;;;;WAOG;;QAGH;;;;;WAKG;;MAEN;IAED;;OAEG;IACH,YAFU,OAAO,CAEe;IAEhC;;;;;OAKG;IACH,4BAFU,OAAO,CAEyB;IAE1C;;;;;;OAMG;IACH,UAFU,SAAS,CAEY;IAG/B;;OAEG;IACH,YAFU,iBAAiB,GAAC,IAAI,CAEV;IAEtB;;;;;OAKG;IACH,UAFU,OAAO,CAEG;IAEpB;;;;;OAKG;IACH,UAFU,OAAO,CAEG;IAEpB;;OAEG;IACH,YAFU,MAAM,CAEG;IAoBvB;;;;;;;;;;;;OAYG;IACH,mDAEC;IAED;;;;;;;;OAQG;IACH,iBAFW,KAAK,QAoBf;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,iDAOC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,2DAWC;IAED;;;;;;;;;;OAUG;IACH,oCAFa,MAAM,CAclB;IAED;;;;;;OAMG;IACH,8CAFa,OAAO,CAgBnB;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,qBALW,KAAK,GACH,kBAAgB,IAAI,CAehC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,yBARW,KAAK,yBAEL,MAAM,UACN,MAAM,GACJ,QAAQ,SAAS,CAAC,CAwD9B;IAED;;;;OAIG;IACH,0BAEC;IAED;;;;;;;;;;OAUG;IACH,SAFa,QAAQ,IAAI,CAAC,CA4DzB;IAED;;;;;OAKG;IACH,aAiBC;IASD;;;;;;;;OAQG;IACH,8BAHW,OAAO,UAAQ,UACf,OAAO,UAAQ,QAQzB;IAED;;;;;;;;;;;;;;;OAeG;IACH,yBALW,MAAM,KACN,MAAM,6CAMhB;IAED;;;;;;;OAOG;IACH,mBA0BC;IAED;;;;;OAKG;IACH,4BAHW,MAAM,GACJ,OAAO,CAoDnB;;CACJ;yBA7wBwB,kCAAkC;mCAMxB,yBAAyB;mBAbzC,oCAAoC;oBACnC,4BAA4B;wBACxB,4BAA4B;sBAQ9B,kCAAkC;yCAJf,kDAAkD;sBAErE,qCAAqC;0BACjC,0CAA0C"}
@@ -56,17 +56,22 @@ const TARGET_FRAME_RATE = 30;
56
56
  * start(), stop(), render(), updateSize()}` — because that is what `Engine` needs in order to boot,
57
57
  * and because the facade is deliberately not allowed to grow past §4.2 (D14).
58
58
  *
59
- * **Two things have been let through since, each by name.** {@link add_extension} /
59
+ * **Three things have been let through since, each by name.** {@link add_extension} /
60
60
  * {@link remove_extension} (E9), because an extension has to survive the restart that replaces the
61
61
  * renderer holding it, and
62
62
  * {@link scene_context} (E13, D29), because the managers a system needs — animation above all —
63
63
  * hang off Shade's per-scene GPU state and there is no other door to them. Both take or return
64
64
  * something the caller already owns; neither hands out the renderer.
65
65
  *
66
- * What is deliberately absent, and stays absent: `renderer` / `getRenderer()` (44 callers on the
67
- * old engine, each of them a migration item rather than a reason to re-open the hatch), `layers`
68
- * (CPU visibility dies with GPU culling), `scene` (entities reach the scene through the mesh
69
- * system, not through the facade), and the three-shaped material manager.
66
+ * {@link renderer} is the third, and much the widest: it hands the renderer straight out, so
67
+ * everything the other two are shaped to avoid is reachable through it. It is an escape hatch for
68
+ * callers that have no other route, not a migration target — the old engine's 44
69
+ * `.renderer`/`getRenderer()` call sites are each still a migration item.
70
+ *
71
+ * What is deliberately absent, and stays absent: `getRenderer()` (the old engine's accessor, and
72
+ * the shape those call sites are being ported off), `layers` (CPU visibility dies with GPU
73
+ * culling), `scene` (entities reach the scene through the mesh system, not through the facade),
74
+ * and the three-shaped material manager.
70
75
  *
71
76
  * **Lifecycle differs from the old engine on purpose.** `GraphicsEngine` starts inside the `Engine`
72
77
  * constructor, which is why stopping an engine used to leave a restarted one with no renderer at
@@ -30,10 +30,12 @@
30
30
  * refinement which the broadphase-only architecture doesn't have
31
31
  * wired up yet.
32
32
  * - `result.normal` — target surface's outward normal at the kiss
33
- * point (unit length), computed by running GJK + EPA against the
34
- * just-overlapping configuration at the TOI. Falls back to
35
- * `-ray.direction` on the rare case EPA degenerates (NaN / zero
36
- * depth) see comment near the EPA call.
33
+ * point (unit length), recovered by running MPR against the
34
+ * just-overlapping configuration at the TOI. A pair that is exactly
35
+ * tangent there carries no MTV to read, so a second probe nudged
36
+ * toward the target's centre supplies one; `-ray.direction` remains
37
+ * the last-resort fallback if both degenerate. See the
38
+ * contact-normal recovery block near the end of the function.
37
39
  * - `result.entity` / `result.body_id` — the hit body.
38
40
  *
39
41
  * Output on miss: untouched; same convention as `raycast`.
@@ -52,12 +54,15 @@
52
54
  * distinguishes bodies when colliders live on child entities)
53
55
  * @param {boolean} [skip_initial_overlaps=false] how a candidate ALREADY
54
56
  * overlapping the swept shape at `t = 0` is treated. `false` (kinematic
55
- * "can I move?" semantics): the cast is blocked where it stands — report a
56
- * hit at `t = 0`. `true` (CCD semantics): such a candidate is a resting /
57
- * sliding contact the discrete solver already owns, NOT a tunnelling
58
- * threat skip it and keep scanning the remaining candidates for real
59
- * blockers along the sweep. Without the skip, one resting contact at the
60
- * start of the sweep would blind the cast to every other obstacle.
57
+ * "can I move?" semantics): targets are solid, so the cast is blocked where
58
+ * it stands — report a hit at `t = 0`. The one exception is a convex target
59
+ * the shape merely TOUCHES (no depth beyond {@link CONTACT_EPSILON}): a
60
+ * tangency blocks only a sweep driving into it, and otherwise steps aside so
61
+ * the scan can reach real blockers. `true` (CCD semantics): such a candidate
62
+ * is a resting / sliding contact the discrete solver already owns, NOT a
63
+ * tunnelling threat — skip it and keep scanning the remaining candidates for
64
+ * real blockers along the sweep. Either way, one contact at the start of the
65
+ * sweep must never blind the cast to every other obstacle.
61
66
  * @returns {boolean}
62
67
  */
63
68
  export function shape_cast(system: PhysicsSystem, ray: any, shape: AbstractShape3D, rotation: {
@@ -1 +1 @@
1
- {"version":3,"file":"shape_cast.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/shape_cast.js"],"names":[],"mappings":"AAyEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,8FAhBW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iDAE7B,MAAM,+BAA6B,MAAM,KAAG,OAAO,0BAI3D,OAAO,GAQL,OAAO,CA2RnB"}
1
+ {"version":3,"file":"shape_cast.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/shape_cast.js"],"names":[],"mappings":"AAoPA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,8FAnBW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iDAE7B,MAAM,+BAA6B,MAAM,KAAG,OAAO,0BAI3D,OAAO,GAWL,OAAO,CAsTnB"}
@@ -1,417 +1,620 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { bvh_query_user_data_intersects_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js";
3
- import { returnTrue } from "../../../core/function/returnTrue.js";
4
- import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
5
- import { body_id_index } from "../body/BodyStorage.js";
6
- import { gjk } from "../../../core/geom/3d/gjk/gjk.js";
7
- import { mpr } from "../../../core/geom/3d/gjk/mpr.js";
8
- import { PosedShape3D } from "../../../core/geom/3d/shape/PosedShape3D.js";
9
-
10
- /**
11
- * Bisection halts when the search interval is below this fraction of
12
- * the sweep length. With `MAX_BISECTION = 32` and a unit-length sweep
13
- * the achieved precision is ≈ 1 / 2³² (well below any practical
14
- * world-scale tolerance), but we cap on relative tolerance so a
15
- * 10 m sweep doesn't insist on sub-micron precision.
16
- * @type {number}
17
- */
18
- const TOI_TOLERANCE = 1e-4;
19
-
20
- /**
21
- * Maximum bisection iterations. `log2(1 / TOI_TOLERANCE)` is enough
22
- * for any practical sweep; this is the hard upper bound to prevent
23
- * pathological inputs (NaN ray, etc.) from hanging the query.
24
- * @type {number}
25
- */
26
- const MAX_BISECTION = 32;
27
-
28
- /**
29
- * Coarse linear-search resolution for finding *any* overlap moment
30
- * along the per-candidate AABB-overlap interval (NOT the full sweep —
31
- * see `aabb_overlap_interval` for the analytical narrowing that runs
32
- * upstream). 32 substeps over the AABB-overlap window is dense enough
33
- * for any sweep where the shape's smallest extent is on the order of
34
- * its AABB's extent (true for spheres / boxes / capsules); the slab
35
- * intersection keeps that interval small even for kilometre-scale
36
- * sweeps, so this isn't the limit it would otherwise be.
37
- * @type {number}
38
- */
39
- const COARSE_STEPS = 32;
40
-
41
- const swept_aabb = new Float64Array(6);
42
- const local_aabb = new Float64Array(6);
43
- const initial_aabb = new Float64Array(6);
44
- const target_local_aabb = new Float64Array(6);
45
- const target_world_aabb = new Float64Array(6);
46
-
47
- // GJK simplex buffer (used to detect overlap during the bisection).
48
- const simplex_buf = new Float64Array(12);
49
-
50
- // Scratch for MPR's MTV output — same vec3 convention as EPA: direction
51
- // is "A's interior into B" with the magnitude being the depth. We use
52
- // MPR for the contact-normal recovery at the kiss point because it
53
- // converges reliably on smooth-vs-smooth supports where EPA hits its
54
- // iteration cap before tightening (sphere-vs-sphere shallow overlap).
55
- const mpr_result = new Float64Array(3);
56
-
57
- /**
58
- * Long-lived PosedShape3D adapters for the swept shape and the
59
- * candidate's collider. Both are re-bound per query — the swept one
60
- * also gets its position field re-written every bisection step
61
- * without re-running `setup()`, since shape and rotation are
62
- * constant through the sweep.
63
- */
64
- const swept_posed = new PosedShape3D();
65
- const candidate_posed = new PosedShape3D();
66
-
67
- /**
68
- * Buffer for the union of static-BVH and dynamic-BVH candidates that
69
- * overlap the swept AABB. Grows by doubling on the rare overflow.
70
- * @type {Uint32Array}
71
- */
72
- let scratch_candidates = new Uint32Array(64);
73
-
74
- /**
75
- * Sweep a convex shape along a ray and find the first body it would
76
- * hit. Used by character controllers (sweep a capsule along intended
77
- * movement to find blockers), high-speed projectile movement (avoid
78
- * tunnelling without paying for full per-body CCD), and any kinematic
79
- * "where would this body end up if I tried to move it?" query.
80
- *
81
- * Pipeline:
82
- * 1. Build the swept AABB: the shape's local-frame AABB rotated
83
- * into world by `rotation`, translated to `ray.origin`, then
84
- * stretched along `ray.direction * ray.tMax`.
85
- * 2. Query both broadphase BVHs (static + dynamic) for leaves whose
86
- * AABB overlaps the swept volume.
87
- * 3. For each candidate, bisect the [0, best_t] interval for the
88
- * smallest `t` at which the swept shape overlaps the candidate's
89
- * collider. GJK on each midpoint.
90
- *
91
- * Picking `best_t` (rather than always running the full [0, tMax]
92
- * bisection) is the early-termination optimisation: once we have a
93
- * hit at some t, no later candidate can produce a smaller TOI by
94
- * being checked at its full [0, tMax] interval.
95
- *
96
- * Output on hit:
97
- * - `result.t` sweep distance to impact (in the same units as
98
- * `ray.direction`'s magnitude; for a unit-direction ray, this is
99
- * metres).
100
- * - `result.position` — `ray.origin + t * ray.direction`, i.e. the
101
- * centre of the swept shape at the moment of first contact. NOT
102
- * a point on the target's surface; that would need narrowphase
103
- * refinement which the broadphase-only architecture doesn't have
104
- * wired up yet.
105
- * - `result.normal` target surface's outward normal at the kiss
106
- * point (unit length), computed by running GJK + EPA against the
107
- * just-overlapping configuration at the TOI. Falls back to
108
- * `-ray.direction` on the rare case EPA degenerates (NaN / zero
109
- * depth) — see comment near the EPA call.
110
- * - `result.entity` / `result.body_id` — the hit body.
111
- *
112
- * Output on miss: untouched; same convention as `raycast`.
113
- *
114
- * @param {PhysicsSystem} system
115
- * @param {Ray3|{origin_x:number,origin_y:number,origin_z:number,direction_x:number,direction_y:number,direction_z:number,tMax:number}} ray
116
- * origin + unit direction + `tMax`. Only the named accessors are read, so a
117
- * plain double-precision ray-like works `Ray3` is Float32Array-backed and
118
- * quantises large-coordinate sweeps (the CCD pass passes a f64 ray-like)
119
- * @param {AbstractShape3D} shape shape being swept, in its local frame
120
- * @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
121
- * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
122
- * @param {(entity:number, collider:Collider, body_id:number)=>boolean} [filter]
123
- * mandatory in contract; defaults to {@link returnTrue}. Receives the
124
- * candidate body's entity, primary collider, and packed body id (the id
125
- * distinguishes bodies when colliders live on child entities)
126
- * @param {boolean} [skip_initial_overlaps=false] how a candidate ALREADY
127
- * overlapping the swept shape at `t = 0` is treated. `false` (kinematic
128
- * "can I move?" semantics): the cast is blocked where it stands — report a
129
- * hit at `t = 0`. `true` (CCD semantics): such a candidate is a resting /
130
- * sliding contact the discrete solver already owns, NOT a tunnelling
131
- * threat skip it and keep scanning the remaining candidates for real
132
- * blockers along the sweep. Without the skip, one resting contact at the
133
- * start of the sweep would blind the cast to every other obstacle.
134
- * @returns {boolean}
135
- */
136
- export function shape_cast(system, ray, shape, rotation, result, filter = returnTrue, skip_initial_overlaps = false) {
137
- const ox = ray.origin_x;
138
- const oy = ray.origin_y;
139
- const oz = ray.origin_z;
140
- const dx = ray.direction_x;
141
- const dy = ray.direction_y;
142
- const dz = ray.direction_z;
143
- const tMax = ray.tMax;
144
- // A fresh Ray3 defaults tMax to Infinity, which a sweep cannot use (the
145
- // swept AABB and the bisection interval would be unbounded). Loud in dev
146
- // — silently returning false for the DEFAULT ray construction reads as
147
- // "nothing there" and is a debugging trap; prod degrades to a miss.
148
- assert.ok(Number.isFinite(tMax) && tMax > 0,
149
- `shape_cast: ray.tMax must be a positive finite sweep length, got ${tMax}`);
150
- if (!(tMax > 0) || !Number.isFinite(tMax)) return false;
151
-
152
- const rx = rotation.x, ry = rotation.y, rz = rotation.z, rw = rotation.w;
153
-
154
- // ── 1. Swept AABB ──────────────────────────────────────────────
155
- shape.compute_bounding_box(local_aabb);
156
- // Rotation around ray.origin gives the shape's initial AABB in
157
- // world space.
158
- aabb3_transform_oriented(
159
- initial_aabb, 0,
160
- local_aabb[0], local_aabb[1], local_aabb[2],
161
- local_aabb[3], local_aabb[4], local_aabb[5],
162
- ox, oy, oz,
163
- rx, ry, rz, rw
164
- );
165
- // Stretch by the sweep displacement. Positive direction components
166
- // push the +face out; negative components push the −face out.
167
- swept_aabb[0] = initial_aabb[0];
168
- swept_aabb[1] = initial_aabb[1];
169
- swept_aabb[2] = initial_aabb[2];
170
- swept_aabb[3] = initial_aabb[3];
171
- swept_aabb[4] = initial_aabb[4];
172
- swept_aabb[5] = initial_aabb[5];
173
- if (dx > 0) swept_aabb[3] += dx * tMax; else swept_aabb[0] += dx * tMax;
174
- if (dy > 0) swept_aabb[4] += dy * tMax; else swept_aabb[1] += dy * tMax;
175
- if (dz > 0) swept_aabb[5] += dz * tMax; else swept_aabb[2] += dz * tMax;
176
-
177
- // ── 2. Broadphase: gather candidates from both trees ───────────
178
- // The BVH query returns the TRUE overlap count and silently drops writes past
179
- // the buffer end. An undersized buffer would make the narrowphase loop read
180
- // `undefined` candidates and silently MISS the true nearest hit (tunnelling on
181
- // a long sweep through a dense scene). Grow by doubling and re-query until
182
- // both fit (deterministic queries → converges in one resize).
183
- let n_static, n_dynamic, n_total;
184
- for (;;) {
185
- n_static = bvh_query_user_data_intersects_aabb(scratch_candidates, 0, system.staticBvh, swept_aabb);
186
- n_dynamic = bvh_query_user_data_intersects_aabb(scratch_candidates, n_static, system.dynamicBvh, swept_aabb);
187
- n_total = n_static + n_dynamic;
188
- if (n_total <= scratch_candidates.length) break;
189
- scratch_candidates = new Uint32Array(Math.max(n_total, scratch_candidates.length * 2));
190
- }
191
- if (n_total === 0) return false;
192
-
193
- // ── 3. Narrowphase: per-candidate bisection ────────────────────
194
- //
195
- // Re-bind the swept shape's PosedShape3D once for shape + rotation;
196
- // we update `px/py/pz` per bisection midpoint without rerunning
197
- // setup (px/py/pz are public fields by contract).
198
- swept_posed.shape = shape;
199
- swept_posed.qx = rx; swept_posed.qy = ry; swept_posed.qz = rz; swept_posed.qw = rw;
200
-
201
- let best_t = tMax;
202
- let best_body_id = -1;
203
- let best_entity = -1;
204
-
205
- for (let i = 0; i < n_total; i++) {
206
- const body_id = scratch_candidates[i];
207
- const body_idx = body_id_index(body_id);
208
- const entity = system.entityOf(body_id);
209
- if (entity < 0) continue;
210
-
211
- const collider = system.__primary_collider(body_idx);
212
- if (collider === null) continue;
213
- if (!filter(entity, collider, body_id)) continue;
214
-
215
- const target_tr = system.__transforms[body_idx];
216
- candidate_posed.setup(collider.shape, target_tr.position, target_tr.rotation);
217
-
218
- // Already overlapping at t = 0? The bisection's "no overlap at lo,
219
- // overlap at hi" invariant doesn't hold for this candidate. Either
220
- // it IS the answer (kinematic semantics: blocked where it stands,
221
- // t = 0 — nothing can beat that, stop scanning) or it is a resting
222
- // contact to step over (CCD semantics: skip THIS candidate only;
223
- // the remaining candidates must still be scanned for real blockers).
224
- swept_posed.px = ox; swept_posed.py = oy; swept_posed.pz = oz;
225
- if (gjk(simplex_buf, swept_posed, candidate_posed)) {
226
- if (skip_initial_overlaps) continue;
227
- best_t = 0;
228
- best_body_id = body_id;
229
- best_entity = entity;
230
- break;
231
- }
232
-
233
- // ── Slab intersection: narrow the search interval analytically ─
234
- //
235
- // Compute the body's tight world AABB and intersect it with the
236
- // swept shape's AABB sliding along the sweep direction. The
237
- // result is the t-interval `[t_aabb_enter, t_aabb_exit]` during
238
- // which the two AABBs overlap. Outside this interval the actual
239
- // shapes can't overlap either (AABB is a superset of the
240
- // shape), so we get to skip all of [0, t_aabb_enter) and
241
- // (t_aabb_exit, best_t] without testing.
242
- //
243
- // This is the key to making `shapeCast` scale to long sweeps:
244
- // a 1 km cast of a 1 m sphere against another 1 m sphere has an
245
- // AABB-overlap window of ≈ 2 m (the sum of diameters along the
246
- // sweep axis), reducing the coarse-step grid from a 30 m
247
- // resolution (1000/32) to 6 cm (2/32).
248
- collider.shape.compute_bounding_box(target_local_aabb);
249
- aabb3_transform_oriented(
250
- target_world_aabb, 0,
251
- target_local_aabb[0], target_local_aabb[1], target_local_aabb[2],
252
- target_local_aabb[3], target_local_aabb[4], target_local_aabb[5],
253
- target_tr.position.x, target_tr.position.y, target_tr.position.z,
254
- target_tr.rotation.x, target_tr.rotation.y, target_tr.rotation.z, target_tr.rotation.w
255
- );
256
-
257
- let t_aabb_enter = -Infinity;
258
- let t_aabb_exit = Infinity;
259
- let skip_candidate = false;
260
- for (let ax = 0; ax < 3; ax++) {
261
- const init_min = initial_aabb[ax];
262
- const init_max = initial_aabb[ax + 3];
263
- const body_min = target_world_aabb[ax];
264
- const body_max = target_world_aabb[ax + 3];
265
- const d_ax = ax === 0 ? dx : (ax === 1 ? dy : dz);
266
- if (d_ax === 0) {
267
- // Static along this axis: AABBs must overlap in their
268
- // initial position or this candidate is unreachable.
269
- if (init_max < body_min || init_min > body_max) {
270
- skip_candidate = true;
271
- break;
272
- }
273
- continue;
274
- }
275
- // swept_min(t) = init_min + d·t, swept_max(t) = init_max + d·t.
276
- // Slab-enter: swept_max(t) = body_min → t = (body_min init_max) / d
277
- // Slab-exit: swept_min(t) = body_max → t = (body_max init_min) / d
278
- const t1 = (body_min - init_max) / d_ax;
279
- const t2 = (body_max - init_min) / d_ax;
280
- const lo = t1 < t2 ? t1 : t2;
281
- const hi = t1 < t2 ? t2 : t1;
282
- if (lo > t_aabb_enter) t_aabb_enter = lo;
283
- if (hi < t_aabb_exit) t_aabb_exit = hi;
284
- }
285
- if (skip_candidate) continue;
286
- if (t_aabb_enter > t_aabb_exit) continue;
287
-
288
- // Clamp to the relevant t-window: [0, best_t]. Anything beyond
289
- // best_t can't tighten the answer.
290
- const t_start = t_aabb_enter > 0 ? t_aabb_enter : 0;
291
- const t_end = t_aabb_exit < best_t ? t_aabb_exit : best_t;
292
- if (t_start >= t_end) continue;
293
-
294
- // Coarse-search within the narrowed interval to find any t for
295
- // which the shapes overlap (their AABBs do by construction; the
296
- // shapes themselves may not, e.g. two spheres passing alongside).
297
- const step_dt = (t_end - t_start) / COARSE_STEPS;
298
- let t_prev = t_start;
299
- let t_hi = -1;
300
- for (let s = 1; s <= COARSE_STEPS; s++) {
301
- const t = t_start + s * step_dt;
302
- swept_posed.px = ox + dx * t;
303
- swept_posed.py = oy + dy * t;
304
- swept_posed.pz = oz + dz * t;
305
- if (gjk(simplex_buf, swept_posed, candidate_posed)) {
306
- t_hi = t;
307
- break;
308
- }
309
- t_prev = t;
310
- }
311
- if (t_hi < 0) continue;
312
-
313
- // Bisect [t_prev, t_hi]: invariant is `gjk(t_lo) === false` and
314
- // `gjk(t_hi) === true`.
315
- let t_lo = t_prev;
316
- const stop_width = TOI_TOLERANCE * tMax;
317
- for (let bi = 0; bi < MAX_BISECTION && (t_hi - t_lo) > stop_width; bi++) {
318
- const t_mid = (t_lo + t_hi) * 0.5;
319
- swept_posed.px = ox + dx * t_mid;
320
- swept_posed.py = oy + dy * t_mid;
321
- swept_posed.pz = oz + dz * t_mid;
322
- if (gjk(simplex_buf, swept_posed, candidate_posed)) {
323
- t_hi = t_mid;
324
- } else {
325
- t_lo = t_mid;
326
- }
327
- }
328
-
329
- // Use t_lo (the just-SEPARATING side of the bisection) rather
330
- // than t_hi (just-overlapping). With the fixed GJK that no
331
- // longer flags 5 mm-clear gaps as overlap, the bisection is
332
- // tight; reporting t_lo gives kinematic callers a conservative
333
- // TOI — the swept shape at the reported moment is provably NOT
334
- // overlapping the target. The normal-probe below steps a few
335
- // mm past best_t to get a valid simplex for MPR.
336
- if (t_lo < best_t) {
337
- best_t = t_lo;
338
- best_body_id = body_id;
339
- best_entity = entity;
340
- }
341
- }
342
-
343
- if (best_body_id === -1) return false;
344
-
345
- const rp = result.position;
346
- const rn = result.normal;
347
-
348
- const swept_x = ox + dx * best_t;
349
- const swept_y = oy + dy * best_t;
350
- const swept_z = oz + dz * best_t;
351
-
352
- rp[0] = swept_x;
353
- rp[1] = swept_y;
354
- rp[2] = swept_z;
355
-
356
- // ── Contact-normal recovery via MPR ────────────────────────────
357
- //
358
- // Bisection converges with `swept_posed` somewhere within
359
- // `stop_width = TOI_TOLERANCE * tMax` of the true contact. At
360
- // exactly `best_t` the overlap depth is therefore sub-millimetre
361
- // on practical sweeps. EPA on smooth-vs-smooth supports at this
362
- // overlap can't tighten before its iteration cap and returns a
363
- // direction off by ~30° from truth. MPR is the better tool here:
364
- // it converges in 5-15 iterations on smooth surfaces, same MTV
365
- // output convention as EPA (direction = A's interior into B,
366
- // magnitude = depth).
367
- //
368
- // We still advance the swept centre by NORMAL_PROBE_OFFSET past
369
- // best_t (a few mm into the target) before sampling — the extra
370
- // overlap gives both MPR and the body-centre sign check a more
371
- // robust geometric basis. The reported `result.t` stays at
372
- // `best_t`; only the normal probe moves.
373
- //
374
- // Sign-validation: MPR's polytope can also settle on either side
375
- // of the origin for highly symmetric configurations, so we dot
376
- // against the body-centre axis and flip if needed (same pattern
377
- // as `narrowphase_step`'s EPA path). The stored normal is the
378
- // target's outward surface normal — `B → A` direction — i.e. the
379
- // negated (validated) MTV.
380
- const NORMAL_PROBE_OFFSET = 0.01;
381
- const probe_t = best_t + NORMAL_PROBE_OFFSET;
382
- const probe_x = ox + dx * probe_t;
383
- const probe_y = oy + dy * probe_t;
384
- const probe_z = oz + dz * probe_t;
385
- const best_body_idx = body_id_index(best_body_id);
386
- const best_collider = system.__primary_collider(best_body_idx);
387
- let normal_x = -dx, normal_y = -dy, normal_z = -dz;
388
- if (best_collider !== null) {
389
- const best_tr = system.__transforms[best_body_idx];
390
- candidate_posed.setup(best_collider.shape, best_tr.position, best_tr.rotation);
391
- swept_posed.px = probe_x;
392
- swept_posed.py = probe_y;
393
- swept_posed.pz = probe_z;
394
- if (mpr(mpr_result, 0, swept_posed, candidate_posed)) {
395
- let ex = mpr_result[0], ey = mpr_result[1], ez = mpr_result[2];
396
- const depth = Math.sqrt(ex * ex + ey * ey + ez * ez);
397
- if (depth > 0 && Number.isFinite(depth)) {
398
- const ab_x = best_tr.position.x - probe_x;
399
- const ab_y = best_tr.position.y - probe_y;
400
- const ab_z = best_tr.position.z - probe_z;
401
- if (ex * ab_x + ey * ab_y + ez * ab_z < 0) {
402
- ex = -ex; ey = -ey; ez = -ez;
403
- }
404
- const inv = 1 / depth;
405
- normal_x = -ex * inv;
406
- normal_y = -ey * inv;
407
- normal_z = -ez * inv;
408
- }
409
- }
410
- }
411
-
412
- rn[0] = normal_x; rn[1] = normal_y; rn[2] = normal_z;
413
- result.t = best_t;
414
- result.entity = best_entity;
415
- result.body_id = best_body_id;
416
- return true;
417
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import { bvh_query_user_data_intersects_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js";
3
+ import { returnTrue } from "../../../core/function/returnTrue.js";
4
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
5
+ import { body_id_index } from "../body/BodyStorage.js";
6
+ import { gjk } from "../../../core/geom/3d/gjk/gjk.js";
7
+ import { mpr } from "../../../core/geom/3d/gjk/mpr.js";
8
+ import { PosedShape3D } from "../../../core/geom/3d/shape/PosedShape3D.js";
9
+
10
+ /**
11
+ * Bisection halts when the search interval is below this fraction of
12
+ * the sweep length. With `MAX_BISECTION = 32` and a unit-length sweep
13
+ * the achieved precision is ≈ 1 / 2³² (well below any practical
14
+ * world-scale tolerance), but we cap on relative tolerance so a
15
+ * 10 m sweep doesn't insist on sub-micron precision.
16
+ * @type {number}
17
+ */
18
+ const TOI_TOLERANCE = 1e-4;
19
+
20
+ /**
21
+ * Maximum bisection iterations. `log2(1 / TOI_TOLERANCE)` is enough
22
+ * for any practical sweep; this is the hard upper bound to prevent
23
+ * pathological inputs (NaN ray, etc.) from hanging the query.
24
+ * @type {number}
25
+ */
26
+ const MAX_BISECTION = 32;
27
+
28
+ /**
29
+ * Coarse linear-search resolution for finding *any* overlap moment
30
+ * along the per-candidate AABB-overlap interval (NOT the full sweep —
31
+ * see `aabb_overlap_interval` for the analytical narrowing that runs
32
+ * upstream). 32 substeps over the AABB-overlap window is dense enough
33
+ * for any sweep where the shape's smallest extent is on the order of
34
+ * its AABB's extent (true for spheres / boxes / capsules); the slab
35
+ * intersection keeps that interval small even for kilometre-scale
36
+ * sweeps, so this isn't the limit it would otherwise be.
37
+ * @type {number}
38
+ */
39
+ const COARSE_STEPS = 32;
40
+
41
+ const swept_aabb = new Float64Array(6);
42
+ const local_aabb = new Float64Array(6);
43
+ const initial_aabb = new Float64Array(6);
44
+ const target_local_aabb = new Float64Array(6);
45
+ const target_world_aabb = new Float64Array(6);
46
+
47
+ // GJK simplex buffer (used to detect overlap during the bisection).
48
+ const simplex_buf = new Float64Array(12);
49
+
50
+ // Scratch for MPR's MTV output — same vec3 convention as EPA: direction
51
+ // is "A's interior into B" with the magnitude being the depth. We use
52
+ // MPR for the contact-normal recovery at the kiss point because it
53
+ // converges reliably on smooth-vs-smooth supports where EPA hits its
54
+ // iteration cap before tightening (sphere-vs-sphere shallow overlap).
55
+ const mpr_result = new Float64Array(3);
56
+
57
+ /**
58
+ * Long-lived PosedShape3D adapters for the swept shape and the
59
+ * candidate's collider. Both are re-bound per query — the swept one
60
+ * also gets its position field re-written every bisection step
61
+ * without re-running `setup()`, since shape and rotation are
62
+ * constant through the sweep.
63
+ */
64
+ const swept_posed = new PosedShape3D();
65
+ const candidate_posed = new PosedShape3D();
66
+
67
+ /**
68
+ * Buffer for the union of static-BVH and dynamic-BVH candidates that
69
+ * overlap the swept AABB. Grows by doubling on the rare overflow.
70
+ * @type {Uint32Array}
71
+ */
72
+ let scratch_candidates = new Uint32Array(64);
73
+
74
+ /**
75
+ * Depths at or below this are not a contact. A kissing pair is not always
76
+ * numerically exact MPR returns a few microns of "overlap" for a sphere
77
+ * resting on a plane where a box-on-box kiss returns a clean zero — so the
78
+ * tangency test needs the same tolerance the narrowphase already applies
79
+ * (`compute_penetration`'s CONTACT_EPSILON, same value and same reasoning).
80
+ * Well below any real overlap a caller would be asked to depenetrate from,
81
+ * and far below the skin widths character controllers work at.
82
+ * @type {number}
83
+ */
84
+ const CONTACT_EPSILON = 1e-4;
85
+
86
+ /**
87
+ * How far from parallel a sweep must be before it counts as driving INTO a
88
+ * tangent contact rather than sliding along it a sine, both inputs being
89
+ * unit vectors, so this is ~0.006°. Comfortably above the angular noise in an
90
+ * MPR-recovered normal and far below any inclination that would move a body
91
+ * meaningfully into a surface over one sweep.
92
+ * @type {number}
93
+ */
94
+ const GRAZING_SINE_EPSILON = 1e-4;
95
+
96
+ /**
97
+ * Distance past the reported TOI at which the contact normal is sampled.
98
+ * Bisection stops on the just-SEPARATING side, so MPR needs the pair
99
+ * nudged into genuine overlap before it has an MTV to report.
100
+ * @type {number}
101
+ */
102
+ const NORMAL_PROBE_OFFSET = 0.01;
103
+
104
+ /**
105
+ * Unit contact normal recovered by {@link probe_contact_normal}.
106
+ * @type {Float64Array}
107
+ */
108
+ const probe_normal = new Float64Array(3);
109
+
110
+ /**
111
+ * Recover the target's outward surface normal by running MPR on
112
+ * `swept_posed` displaced to `(px, py, pz)` against `candidate_posed`
113
+ * (both must already be bound by the caller).
114
+ *
115
+ * Returns `false` — leaving `out` untouched — when MPR reports no depth worth
116
+ * the name (at or below {@link CONTACT_EPSILON}). That is not an MPR failure
117
+ * so much as the honest answer for a pair that is TANGENT at the probe pose:
118
+ * a kiss carries no direction, so there is no normal to read off it. Callers
119
+ * use that verdict both ways as "no normal here" and as "this is a touch,
120
+ * not an overlap".
121
+ *
122
+ * Sign-validation: MPR's polytope can settle on either side of the origin
123
+ * for highly symmetric configurations, so we dot against the body-centre
124
+ * axis and flip if needed (same pattern as `narrowphase_step`'s EPA path).
125
+ * The stored normal is the target's outward surface normal — `B → A`
126
+ * direction i.e. the negated (validated) MTV.
127
+ *
128
+ * @param {Float64Array} out receives the unit normal on success
129
+ * @param {number} px swept shape's probe centre
130
+ * @param {number} py
131
+ * @param {number} pz
132
+ * @param {Transform} target_tr the target body's transform (centre for the
133
+ * sign check)
134
+ * @returns {boolean}
135
+ */
136
+ function probe_contact_normal(out, px, py, pz, target_tr) {
137
+ swept_posed.px = px;
138
+ swept_posed.py = py;
139
+ swept_posed.pz = pz;
140
+
141
+ if (!mpr(mpr_result, 0, swept_posed, candidate_posed)) return false;
142
+
143
+ let ex = mpr_result[0], ey = mpr_result[1], ez = mpr_result[2];
144
+ const depth = Math.sqrt(ex * ex + ey * ey + ez * ez);
145
+ if (!(depth > CONTACT_EPSILON) || !Number.isFinite(depth)) return false;
146
+
147
+ const ab_x = target_tr.position.x - px;
148
+ const ab_y = target_tr.position.y - py;
149
+ const ab_z = target_tr.position.z - pz;
150
+ if (ex * ab_x + ey * ab_y + ez * ab_z < 0) {
151
+ ex = -ex; ey = -ey; ez = -ez;
152
+ }
153
+
154
+ const inv = 1 / depth;
155
+ out[0] = -ex * inv;
156
+ out[1] = -ey * inv;
157
+ out[2] = -ez * inv;
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Recover the target's outward surface normal for a contact whose pose is
163
+ * `(cx, cy, cz)` — the swept shape's centre AT the contact — sweeping along
164
+ * unit `(dx, dy, dz)`. `swept_posed` (shape + rotation) and `candidate_posed`
165
+ * must already be bound by the caller; on success the unit normal is written
166
+ * to `out`.
167
+ *
168
+ * MPR needs genuine overlap before it has an MTV to report, and bisection
169
+ * stops on the just-SEPARATING side, so neither the contact pose itself nor
170
+ * the reported `t` is a usable sample point. Two probes displace it:
171
+ *
172
+ * Probe 1 steps `NORMAL_PROBE_OFFSET` ALONG the sweep. That is the whole
173
+ * answer wherever the sweep runs INTO the surface.
174
+ *
175
+ * Probe 2 covers what probe 1 structurally cannot. A GRAZING contact a body
176
+ * merely touching geometry it is sliding past, level brushes flush against a
177
+ * character, a seam between coplanar faces has the sweep running PARALLEL to
178
+ * the contact plane, so advancing along it leaves the pair exactly tangent
179
+ * however far it goes. A sweep pointing AWAY from the surface is worse still:
180
+ * probe 1 separates them further. Either way MPR reports zero depth, and zero
181
+ * depth carries no direction.
182
+ *
183
+ * So probe 2 displaces toward the target's CENTRE, which is interior to it
184
+ * (MPR's own precondition) and so closes on it from any contact orientation.
185
+ * It starts from the CONTACT pose rather than probe 1's — anchoring it at
186
+ * probe 1's pose lets a sweep directly away from the surface cancel the two
187
+ * displacements against each other and land back on the tangency it was trying
188
+ * to escape.
189
+ *
190
+ * The distance DOUBLES until the overlap is measurable, because one fixed
191
+ * offset cannot serve both ends of the range. Only the component along the
192
+ * (unknown) contact normal deepens the overlap, and that component shrinks
193
+ * with the target's size: 1 cm toward the centre of a 1 m brush sinks ~1 cm,
194
+ * but toward the centre of a 1 km floor 400 m away it sinks 30 µm — under
195
+ * {@link CONTACT_EPSILON}, and the normal would be lost on exactly the large
196
+ * flat geometry a character spends its life on. Growing from the smallest
197
+ * offset keeps the displacement as shallow as it can be, which is what stops a
198
+ * target thinner than the probe from reporting its FAR face.
199
+ *
200
+ * Getting this right matters more than precision: the alternative is the
201
+ * `-ray.direction` fallback, which opposes the sweep BY CONSTRUCTION. A caller
202
+ * testing `direction · normal < 0` to ask "is this surface in my way?" is then
203
+ * told yes for every direction it could possibly sweep, including straight
204
+ * down a wall it is only brushing — a kinematic character in that state has no
205
+ * escape (see `KinematicMover._slide`).
206
+ *
207
+ * @param {Float64Array} out receives the unit normal on success
208
+ * @param {number} cx swept shape's centre at the contact
209
+ * @param {number} cy
210
+ * @param {number} cz
211
+ * @param {number} dx unit sweep direction
212
+ * @param {number} dy
213
+ * @param {number} dz
214
+ * @param {Transform} target_tr the target body's transform
215
+ * @returns {boolean} false when every probe degenerates — `out` is untouched
216
+ */
217
+ function recover_contact_normal(out, cx, cy, cz, dx, dy, dz, target_tr) {
218
+ if (probe_contact_normal(
219
+ out,
220
+ cx + dx * NORMAL_PROBE_OFFSET,
221
+ cy + dy * NORMAL_PROBE_OFFSET,
222
+ cz + dz * NORMAL_PROBE_OFFSET,
223
+ target_tr,
224
+ )) return true;
225
+
226
+ const ab_x = target_tr.position.x - cx;
227
+ const ab_y = target_tr.position.y - cy;
228
+ const ab_z = target_tr.position.z - cz;
229
+ const ab_len = Math.sqrt(ab_x * ab_x + ab_y * ab_y + ab_z * ab_z);
230
+ // Coincident centres leave no direction to close along, and the shapes are
231
+ // then as overlapped as they get — probe 1 already had its answer. The
232
+ // finite check also bounds the loop below against a degenerate pose.
233
+ if (!(ab_len > 0) || !Number.isFinite(ab_len)) return false;
234
+
235
+ const ux = ab_x / ab_len, uy = ab_y / ab_len, uz = ab_z / ab_len;
236
+ // Bounded by construction doubling from a fixed offset reaches `ab_len`
237
+ // in log2(ab_len / NORMAL_PROBE_OFFSET) steps. Stopping there keeps the
238
+ // probe on the near side of the target's centre.
239
+ for (let d = NORMAL_PROBE_OFFSET; d < ab_len; d *= 2) {
240
+ if (probe_contact_normal(out, cx + ux * d, cy + uy * d, cz + uz * d, target_tr)) return true;
241
+ }
242
+ return false;
243
+ }
244
+
245
+ /**
246
+ * Sweep a convex shape along a ray and find the first body it would
247
+ * hit. Used by character controllers (sweep a capsule along intended
248
+ * movement to find blockers), high-speed projectile movement (avoid
249
+ * tunnelling without paying for full per-body CCD), and any kinematic
250
+ * "where would this body end up if I tried to move it?" query.
251
+ *
252
+ * Pipeline:
253
+ * 1. Build the swept AABB: the shape's local-frame AABB rotated
254
+ * into world by `rotation`, translated to `ray.origin`, then
255
+ * stretched along `ray.direction * ray.tMax`.
256
+ * 2. Query both broadphase BVHs (static + dynamic) for leaves whose
257
+ * AABB overlaps the swept volume.
258
+ * 3. For each candidate, bisect the [0, best_t] interval for the
259
+ * smallest `t` at which the swept shape overlaps the candidate's
260
+ * collider. GJK on each midpoint.
261
+ *
262
+ * Picking `best_t` (rather than always running the full [0, tMax]
263
+ * bisection) is the early-termination optimisation: once we have a
264
+ * hit at some t, no later candidate can produce a smaller TOI by
265
+ * being checked at its full [0, tMax] interval.
266
+ *
267
+ * Output on hit:
268
+ * - `result.t` sweep distance to impact (in the same units as
269
+ * `ray.direction`'s magnitude; for a unit-direction ray, this is
270
+ * metres).
271
+ * - `result.position` — `ray.origin + t * ray.direction`, i.e. the
272
+ * centre of the swept shape at the moment of first contact. NOT
273
+ * a point on the target's surface; that would need narrowphase
274
+ * refinement which the broadphase-only architecture doesn't have
275
+ * wired up yet.
276
+ * - `result.normal` target surface's outward normal at the kiss
277
+ * point (unit length), recovered by running MPR against the
278
+ * just-overlapping configuration at the TOI. A pair that is exactly
279
+ * tangent there carries no MTV to read, so a second probe nudged
280
+ * toward the target's centre supplies one; `-ray.direction` remains
281
+ * the last-resort fallback if both degenerate. See the
282
+ * contact-normal recovery block near the end of the function.
283
+ * - `result.entity` / `result.body_id` — the hit body.
284
+ *
285
+ * Output on miss: untouched; same convention as `raycast`.
286
+ *
287
+ * @param {PhysicsSystem} system
288
+ * @param {Ray3|{origin_x:number,origin_y:number,origin_z:number,direction_x:number,direction_y:number,direction_z:number,tMax:number}} ray
289
+ * origin + unit direction + `tMax`. Only the named accessors are read, so a
290
+ * plain double-precision ray-like works `Ray3` is Float32Array-backed and
291
+ * quantises large-coordinate sweeps (the CCD pass passes a f64 ray-like)
292
+ * @param {AbstractShape3D} shape shape being swept, in its local frame
293
+ * @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
294
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
295
+ * @param {(entity:number, collider:Collider, body_id:number)=>boolean} [filter]
296
+ * mandatory in contract; defaults to {@link returnTrue}. Receives the
297
+ * candidate body's entity, primary collider, and packed body id (the id
298
+ * distinguishes bodies when colliders live on child entities)
299
+ * @param {boolean} [skip_initial_overlaps=false] how a candidate ALREADY
300
+ * overlapping the swept shape at `t = 0` is treated. `false` (kinematic
301
+ * "can I move?" semantics): targets are solid, so the cast is blocked where
302
+ * it stands — report a hit at `t = 0`. The one exception is a convex target
303
+ * the shape merely TOUCHES (no depth beyond {@link CONTACT_EPSILON}): a
304
+ * tangency blocks only a sweep driving into it, and otherwise steps aside so
305
+ * the scan can reach real blockers. `true` (CCD semantics): such a candidate
306
+ * is a resting / sliding contact the discrete solver already owns, NOT a
307
+ * tunnelling threat — skip it and keep scanning the remaining candidates for
308
+ * real blockers along the sweep. Either way, one contact at the start of the
309
+ * sweep must never blind the cast to every other obstacle.
310
+ * @returns {boolean}
311
+ */
312
+ export function shape_cast(system, ray, shape, rotation, result, filter = returnTrue, skip_initial_overlaps = false) {
313
+ const ox = ray.origin_x;
314
+ const oy = ray.origin_y;
315
+ const oz = ray.origin_z;
316
+ const dx = ray.direction_x;
317
+ const dy = ray.direction_y;
318
+ const dz = ray.direction_z;
319
+ const tMax = ray.tMax;
320
+ // A fresh Ray3 defaults tMax to Infinity, which a sweep cannot use (the
321
+ // swept AABB and the bisection interval would be unbounded). Loud in dev
322
+ // silently returning false for the DEFAULT ray construction reads as
323
+ // "nothing there" and is a debugging trap; prod degrades to a miss.
324
+ assert.ok(Number.isFinite(tMax) && tMax > 0,
325
+ `shape_cast: ray.tMax must be a positive finite sweep length, got ${tMax}`);
326
+ if (!(tMax > 0) || !Number.isFinite(tMax)) return false;
327
+
328
+ const rx = rotation.x, ry = rotation.y, rz = rotation.z, rw = rotation.w;
329
+
330
+ // ── 1. Swept AABB ──────────────────────────────────────────────
331
+ shape.compute_bounding_box(local_aabb);
332
+ // Rotation around ray.origin gives the shape's initial AABB in
333
+ // world space.
334
+ aabb3_transform_oriented(
335
+ initial_aabb, 0,
336
+ local_aabb[0], local_aabb[1], local_aabb[2],
337
+ local_aabb[3], local_aabb[4], local_aabb[5],
338
+ ox, oy, oz,
339
+ rx, ry, rz, rw
340
+ );
341
+ // Stretch by the sweep displacement. Positive direction components
342
+ // push the +face out; negative components push the −face out.
343
+ swept_aabb[0] = initial_aabb[0];
344
+ swept_aabb[1] = initial_aabb[1];
345
+ swept_aabb[2] = initial_aabb[2];
346
+ swept_aabb[3] = initial_aabb[3];
347
+ swept_aabb[4] = initial_aabb[4];
348
+ swept_aabb[5] = initial_aabb[5];
349
+ if (dx > 0) swept_aabb[3] += dx * tMax; else swept_aabb[0] += dx * tMax;
350
+ if (dy > 0) swept_aabb[4] += dy * tMax; else swept_aabb[1] += dy * tMax;
351
+ if (dz > 0) swept_aabb[5] += dz * tMax; else swept_aabb[2] += dz * tMax;
352
+
353
+ // ── 2. Broadphase: gather candidates from both trees ───────────
354
+ // The BVH query returns the TRUE overlap count and silently drops writes past
355
+ // the buffer end. An undersized buffer would make the narrowphase loop read
356
+ // `undefined` candidates and silently MISS the true nearest hit (tunnelling on
357
+ // a long sweep through a dense scene). Grow by doubling and re-query until
358
+ // both fit (deterministic queries → converges in one resize).
359
+ let n_static, n_dynamic, n_total;
360
+ for (;;) {
361
+ n_static = bvh_query_user_data_intersects_aabb(scratch_candidates, 0, system.staticBvh, swept_aabb);
362
+ n_dynamic = bvh_query_user_data_intersects_aabb(scratch_candidates, n_static, system.dynamicBvh, swept_aabb);
363
+ n_total = n_static + n_dynamic;
364
+ if (n_total <= scratch_candidates.length) break;
365
+ scratch_candidates = new Uint32Array(Math.max(n_total, scratch_candidates.length * 2));
366
+ }
367
+ if (n_total === 0) return false;
368
+
369
+ // ── 3. Narrowphase: per-candidate bisection ────────────────────
370
+ //
371
+ // Re-bind the swept shape's PosedShape3D once for shape + rotation;
372
+ // we update `px/py/pz` per bisection midpoint without rerunning
373
+ // setup (px/py/pz are public fields by contract).
374
+ swept_posed.shape = shape;
375
+ swept_posed.qx = rx; swept_posed.qy = ry; swept_posed.qz = rz; swept_posed.qw = rw;
376
+
377
+ let best_t = tMax;
378
+ let best_body_id = -1;
379
+ let best_entity = -1;
380
+
381
+ for (let i = 0; i < n_total; i++) {
382
+ const body_id = scratch_candidates[i];
383
+ const body_idx = body_id_index(body_id);
384
+ const entity = system.entityOf(body_id);
385
+ if (entity < 0) continue;
386
+
387
+ const collider = system.__primary_collider(body_idx);
388
+ if (collider === null) continue;
389
+ if (!filter(entity, collider, body_id)) continue;
390
+
391
+ const target_tr = system.__transforms[body_idx];
392
+ candidate_posed.setup(collider.shape, target_tr.position, target_tr.rotation);
393
+
394
+ // Already overlapping at t = 0? The bisection's "no overlap at lo,
395
+ // overlap at hi" invariant doesn't hold for this candidate. Under CCD
396
+ // semantics it is a resting contact to step over skip THIS candidate
397
+ // only, and keep scanning for real blockers.
398
+ //
399
+ // Under kinematic semantics targets are SOLID: a shape that starts
400
+ // genuinely inside one is blocked where it stands, `t = 0`, whatever
401
+ // direction it is trying to sweep. Nothing can beat that, so stop
402
+ // scanning. Getting out is depenetration's job, not the sweep's.
403
+ //
404
+ // Exact TANGENCY is the exception, and it needs one. A zero-depth
405
+ // touch is not "inside" anything — the shape is resting against a
406
+ // face, or flush with a brush seam, and is free to slide along it or
407
+ // leave. It also survives depenetration, which has no depth to push
408
+ // out of, so it persists frame after frame.
409
+ //
410
+ // Reporting it as a `t = 0` hit ends the scan, and that is the whole
411
+ // problem: the caller is handed a contact it correctly judges harmless
412
+ // for this direction and concludes the sweep is clear, while the body
413
+ // it was actually about to hit was never tested. A character flush
414
+ // against a wall falls through the floor that way. So a tangency
415
+ // blocks only when the sweep drives INTO it, and otherwise steps aside
416
+ // and lets the scan continue.
417
+ //
418
+ // MPR's depth at the START pose is what separates the two: positive
419
+ // means genuine overlap, none means tangency. Tangency is also exactly
420
+ // where the normal is hardest to recover, hence the second probe; if
421
+ // even that degenerates there is no normal to judge with and the
422
+ // contact blocks, stopping short being the safe failure.
423
+ swept_posed.px = ox; swept_posed.py = oy; swept_posed.pz = oz;
424
+ if (gjk(simplex_buf, swept_posed, candidate_posed)) {
425
+ if (skip_initial_overlaps) continue;
426
+
427
+ // The tangency exception reads MPR's depth and normal, both of
428
+ // which assume a convex target. This query has never supported
429
+ // concave ones (`overlap_shape` decomposes them per-triangle;
430
+ // nothing here does), so rather than extend a convex-only
431
+ // inference to geometry the module cannot see properly, leave
432
+ // those on the unconditional "solid, blocked at t = 0" path.
433
+ const convex_target = collider.shape.is_convex !== false;
434
+ if (convex_target && !probe_contact_normal(probe_normal, ox, oy, oz, target_tr)) {
435
+ const known = recover_contact_normal(
436
+ probe_normal, ox, oy, oz, dx, dy, dz, target_tr,
437
+ );
438
+ if (known) {
439
+ // Both vectors are unit, so this is the sine of the angle
440
+ // between the sweep and the contact plane. Zero means
441
+ // exactly parallel — sliding along the face — and positive
442
+ // means leaving it; neither blocks. The tolerance matters
443
+ // because the parallel case sits right on the sign change,
444
+ // and an MPR-recovered normal carries ~1e-6 of angular
445
+ // noise: without it a sweep ALONG a face flips to
446
+ // "head-on" on solver noise alone.
447
+ const into = dx * probe_normal[0] + dy * probe_normal[1] + dz * probe_normal[2];
448
+ if (into > -GRAZING_SINE_EPSILON) continue;
449
+ }
450
+ }
451
+
452
+ best_t = 0;
453
+ best_body_id = body_id;
454
+ best_entity = entity;
455
+ break;
456
+ }
457
+
458
+ // ── Slab intersection: narrow the search interval analytically ─
459
+ //
460
+ // Compute the body's tight world AABB and intersect it with the
461
+ // swept shape's AABB sliding along the sweep direction. The
462
+ // result is the t-interval `[t_aabb_enter, t_aabb_exit]` during
463
+ // which the two AABBs overlap. Outside this interval the actual
464
+ // shapes can't overlap either (AABB is a superset of the
465
+ // shape), so we get to skip all of [0, t_aabb_enter) and
466
+ // (t_aabb_exit, best_t] without testing.
467
+ //
468
+ // This is the key to making `shapeCast` scale to long sweeps:
469
+ // a 1 km cast of a 1 m sphere against another 1 m sphere has an
470
+ // AABB-overlap window of ≈ 2 m (the sum of diameters along the
471
+ // sweep axis), reducing the coarse-step grid from a 30 m
472
+ // resolution (1000/32) to 6 cm (2/32).
473
+ collider.shape.compute_bounding_box(target_local_aabb);
474
+ aabb3_transform_oriented(
475
+ target_world_aabb, 0,
476
+ target_local_aabb[0], target_local_aabb[1], target_local_aabb[2],
477
+ target_local_aabb[3], target_local_aabb[4], target_local_aabb[5],
478
+ target_tr.position.x, target_tr.position.y, target_tr.position.z,
479
+ target_tr.rotation.x, target_tr.rotation.y, target_tr.rotation.z, target_tr.rotation.w
480
+ );
481
+
482
+ let t_aabb_enter = -Infinity;
483
+ let t_aabb_exit = Infinity;
484
+ let skip_candidate = false;
485
+ for (let ax = 0; ax < 3; ax++) {
486
+ const init_min = initial_aabb[ax];
487
+ const init_max = initial_aabb[ax + 3];
488
+ const body_min = target_world_aabb[ax];
489
+ const body_max = target_world_aabb[ax + 3];
490
+ const d_ax = ax === 0 ? dx : (ax === 1 ? dy : dz);
491
+ if (d_ax === 0) {
492
+ // Static along this axis: AABBs must overlap in their
493
+ // initial position or this candidate is unreachable.
494
+ if (init_max < body_min || init_min > body_max) {
495
+ skip_candidate = true;
496
+ break;
497
+ }
498
+ continue;
499
+ }
500
+ // swept_min(t) = init_min + d·t, swept_max(t) = init_max + d·t.
501
+ // Slab-enter: swept_max(t) = body_min → t = (body_min − init_max) / d
502
+ // Slab-exit: swept_min(t) = body_max → t = (body_max − init_min) / d
503
+ const t1 = (body_min - init_max) / d_ax;
504
+ const t2 = (body_max - init_min) / d_ax;
505
+ const lo = t1 < t2 ? t1 : t2;
506
+ const hi = t1 < t2 ? t2 : t1;
507
+ if (lo > t_aabb_enter) t_aabb_enter = lo;
508
+ if (hi < t_aabb_exit) t_aabb_exit = hi;
509
+ }
510
+ if (skip_candidate) continue;
511
+ if (t_aabb_enter > t_aabb_exit) continue;
512
+
513
+ // Clamp to the relevant t-window: [0, best_t]. Anything beyond
514
+ // best_t can't tighten the answer.
515
+ const t_start = t_aabb_enter > 0 ? t_aabb_enter : 0;
516
+ const t_end = t_aabb_exit < best_t ? t_aabb_exit : best_t;
517
+ if (t_start >= t_end) continue;
518
+
519
+ // Coarse-search within the narrowed interval to find any t for
520
+ // which the shapes overlap (their AABBs do by construction; the
521
+ // shapes themselves may not, e.g. two spheres passing alongside).
522
+ const step_dt = (t_end - t_start) / COARSE_STEPS;
523
+ let t_prev = t_start;
524
+ let t_hi = -1;
525
+ for (let s = 1; s <= COARSE_STEPS; s++) {
526
+ const t = t_start + s * step_dt;
527
+ swept_posed.px = ox + dx * t;
528
+ swept_posed.py = oy + dy * t;
529
+ swept_posed.pz = oz + dz * t;
530
+ if (gjk(simplex_buf, swept_posed, candidate_posed)) {
531
+ t_hi = t;
532
+ break;
533
+ }
534
+ t_prev = t;
535
+ }
536
+ if (t_hi < 0) continue;
537
+
538
+ // Bisect [t_prev, t_hi]: invariant is `gjk(t_lo) === false` and
539
+ // `gjk(t_hi) === true`.
540
+ let t_lo = t_prev;
541
+ const stop_width = TOI_TOLERANCE * tMax;
542
+ for (let bi = 0; bi < MAX_BISECTION && (t_hi - t_lo) > stop_width; bi++) {
543
+ const t_mid = (t_lo + t_hi) * 0.5;
544
+ swept_posed.px = ox + dx * t_mid;
545
+ swept_posed.py = oy + dy * t_mid;
546
+ swept_posed.pz = oz + dz * t_mid;
547
+ if (gjk(simplex_buf, swept_posed, candidate_posed)) {
548
+ t_hi = t_mid;
549
+ } else {
550
+ t_lo = t_mid;
551
+ }
552
+ }
553
+
554
+ // Use t_lo (the just-SEPARATING side of the bisection) rather
555
+ // than t_hi (just-overlapping). With the fixed GJK that no
556
+ // longer flags 5 mm-clear gaps as overlap, the bisection is
557
+ // tight; reporting t_lo gives kinematic callers a conservative
558
+ // TOI — the swept shape at the reported moment is provably NOT
559
+ // overlapping the target. The normal-probe below steps a few
560
+ // mm past best_t to get a valid simplex for MPR.
561
+ if (t_lo < best_t) {
562
+ best_t = t_lo;
563
+ best_body_id = body_id;
564
+ best_entity = entity;
565
+ }
566
+ }
567
+
568
+ if (best_body_id === -1) return false;
569
+
570
+ const rp = result.position;
571
+ const rn = result.normal;
572
+
573
+ const swept_x = ox + dx * best_t;
574
+ const swept_y = oy + dy * best_t;
575
+ const swept_z = oz + dz * best_t;
576
+
577
+ rp[0] = swept_x;
578
+ rp[1] = swept_y;
579
+ rp[2] = swept_z;
580
+
581
+ // ── Contact-normal recovery via MPR ────────────────────────────
582
+ //
583
+ // Bisection converges with `swept_posed` somewhere within
584
+ // `stop_width = TOI_TOLERANCE * tMax` of the true contact. At
585
+ // exactly `best_t` the overlap depth is therefore sub-millimetre
586
+ // on practical sweeps. EPA on smooth-vs-smooth supports at this
587
+ // overlap can't tighten before its iteration cap and returns a
588
+ // direction off by ~30° from truth. MPR is the better tool here:
589
+ // it converges in 5-15 iterations on smooth surfaces, same MTV
590
+ // output convention as EPA (direction = A's interior into B,
591
+ // magnitude = depth).
592
+ //
593
+ // The probe pose is `NORMAL_PROBE_OFFSET` past `best_t` along the sweep
594
+ // (a few mm into the target) — bisection stops on the just-SEPARATING
595
+ // side, so there is no MTV to read exactly at `best_t`. The reported
596
+ // `result.t` stays at `best_t`; only the normal probe moves.
597
+ // `recover_contact_normal` handles the grazing case, where advancing
598
+ // along the sweep never produces overlap at all.
599
+ const contact_x = ox + dx * best_t;
600
+ const contact_y = oy + dy * best_t;
601
+ const contact_z = oz + dz * best_t;
602
+ const best_body_idx = body_id_index(best_body_id);
603
+ const best_collider = system.__primary_collider(best_body_idx);
604
+ let normal_x = -dx, normal_y = -dy, normal_z = -dz;
605
+ if (best_collider !== null) {
606
+ const best_tr = system.__transforms[best_body_idx];
607
+ candidate_posed.setup(best_collider.shape, best_tr.position, best_tr.rotation);
608
+ if (recover_contact_normal(probe_normal, contact_x, contact_y, contact_z, dx, dy, dz, best_tr)) {
609
+ normal_x = probe_normal[0];
610
+ normal_y = probe_normal[1];
611
+ normal_z = probe_normal[2];
612
+ }
613
+ }
614
+
615
+ rn[0] = normal_x; rn[1] = normal_y; rn[2] = normal_z;
616
+ result.t = best_t;
617
+ result.entity = best_entity;
618
+ result.body_id = best_body_id;
619
+ return true;
620
+ }
@@ -142,7 +142,7 @@ export async function brick4_bake_basic({
142
142
 
143
143
  const cycle_seed = randomIntegerBetween(random, 0, 0xFFFFFFFF) >>> 0;
144
144
 
145
- graph_compute_pass({
145
+ const bake_pass_outputs = graph_compute_pass({
146
146
  graph,
147
147
  shader: shader_brick4_bake_probes,
148
148
  group_count: [dispatch_x],
@@ -186,7 +186,7 @@ export async function brick4_bake_basic({
186
186
  sample_count: SAMPLES_PER_PROBE,
187
187
  output_offset: i,
188
188
  },
189
- input: gr_cycle_trace_data,
189
+ input: bake_pass_outputs.output,
190
190
  output: gr_encoded_probe_results,
191
191
  }
192
192
  })
@@ -83,7 +83,7 @@ const DEFAULT_SHADOWMAP_LOCAL_RESOLUTION = 128;
83
83
  * its turn without permanently starving other lights.
84
84
  * @type {number}
85
85
  */
86
- const SHADOWMAP_UPDATE_BUDGET = 32;
86
+ const SHADOWMAP_UPDATE_BUDGET = 8;
87
87
 
88
88
  /**
89
89
  * Shadows for a single scene