@woosh/meep-engine 2.145.0 → 2.146.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
  5. package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -352
  6. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
  7. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  8. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
  9. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  10. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
  11. package/src/engine/control/first-person/TODO.md +13 -11
  12. package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
  13. package/src/engine/control/first-person/abilities/WallJump.js +11 -3
  14. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  15. package/src/engine/control/first-person/abilities/WallRun.js +12 -0
  16. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  17. package/src/engine/control/first-person/collision/KinematicMover.js +634 -592
  18. package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
  19. package/src/engine/physics/PLAN.md +943 -809
  20. package/src/engine/physics/body/BodyStorage.d.ts +9 -0
  21. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  22. package/src/engine/physics/body/BodyStorage.js +23 -0
  23. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  24. package/src/engine/physics/broadphase/generate_pairs.js +7 -0
  25. package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
  26. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
  27. package/src/engine/physics/ccd/linear_sweep.js +238 -0
  28. package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
  29. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  30. package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
  31. package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
  32. package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
  33. package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
  34. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
  35. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  36. package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
  37. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
  38. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
  39. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
  40. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  41. package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
  42. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  43. package/src/engine/physics/queries/overlap_shape.js +185 -183
  44. package/src/engine/simulation/Ticker.d.ts +14 -0
  45. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  46. package/src/engine/simulation/Ticker.js +136 -1
@@ -61,6 +61,7 @@ export class BodyStorage {
61
61
  __awake_count: number;
62
62
  __free_heap: Uint32Array;
63
63
  __free_count: number;
64
+ __entity_to_index: Map<any, any>;
64
65
  /**
65
66
  * Currently allocated body count (live, regardless of awake/sleeping).
66
67
  * @returns {number}
@@ -109,6 +110,14 @@ export class BodyStorage {
109
110
  * @returns {number} entity for the body, or -1 if the slot is free.
110
111
  */
111
112
  entity_at(index: number): number;
113
+ /**
114
+ * Body index for `entity`, or {@link BODY_INDEX_ABSENT} if no live body owns
115
+ * it. O(1) reverse of {@link entity_at} — the lookup callers use on the
116
+ * link / attach / joint paths instead of scanning the slot table.
117
+ * @param {number} entity
118
+ * @returns {number}
119
+ */
120
+ index_of_entity(entity: number): number;
112
121
  /**
113
122
  * @param {number} index
114
123
  * @returns {number}
@@ -1 +1 @@
1
- {"version":3,"file":"BodyStorage.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/body/BodyStorage.js"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,oCAJW,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAlCD;;;;GAIG;AACH,gCAFU,MAAM,CAEoB;AA+BpC;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAEI;;;OAGG;IACH,+BAFW,MAAM,EA+BhB;IAxBG,mBAAqB;IAGrB,gBAAgB;IAEhB,uBAAqC;IACrC,0BAAwC;IACxC,oBAAkC;IAClC,qBAAmC;IAGnC,oBAAkC;IAGlC,0BAAwC;IACxC,wBAAsC;IACtC,sBAAsB;IAGtB,yBAAuC;IACvC,qBAAqB;IAMzB;;;OAGG;IACH,mBAEC;IAED;;;OAGG;IACH,uBAEC;IAED;;;OAGG;IACH,0BAEC;IAED;;;;OAIG;IACH,8BAEC;IAED;;;;;;;OAOG;IACH,iBAHW,MAAM,GACJ,MAAM,CAyBlB;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAoBhB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,OAAO,CAWnB;IAED;;;OAGG;IACH,iBAHW,MAAM,GACJ,MAAM,CAOlB;IAED;;;OAGG;IACH,qBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,eAHW,MAAM,GACJ,QAAQ,GAAC,MAAM,CAI3B;IAED;;;OAGG;IACH,gBAHW,MAAM,QACN,QAAQ,GAAC,MAAM,QAIzB;IAED;;;OAGG;IACH,gBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,iBAHW,MAAM,SACN,MAAM,QAIhB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;OAIG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,kBAFW,MAAM,QAShB;IAED;;;OAGG;IACH,qBAFW,MAAM,QAShB;IAED;;;;OAIG;IACH,0BAOC;IAED;;OAEG;IACH,eAiCC;IAID;;;OAGG;IACH,oBAgBC;IAED;;;OAGG;IACH,mBAqBC;CACJ;yBAzawB,oBAAoB"}
1
+ {"version":3,"file":"BodyStorage.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/body/BodyStorage.js"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,oCAJW,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAlCD;;;;GAIG;AACH,gCAFU,MAAM,CAEoB;AA+BpC;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAEI;;;OAGG;IACH,+BAFW,MAAM,EAqChB;IA9BG,mBAAqB;IAGrB,gBAAgB;IAEhB,uBAAqC;IACrC,0BAAwC;IACxC,oBAAkC;IAClC,qBAAmC;IAGnC,oBAAkC;IAGlC,0BAAwC;IACxC,wBAAsC;IACtC,sBAAsB;IAGtB,yBAAuC;IACvC,qBAAqB;IAMrB,iCAAkC;IAMtC;;;OAGG;IACH,mBAEC;IAED;;;OAGG;IACH,uBAEC;IAED;;;OAGG;IACH,0BAEC;IAED;;;;OAIG;IACH,8BAEC;IAED;;;;;;;OAOG;IACH,iBAHW,MAAM,GACJ,MAAM,CA0BlB;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAwBhB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,OAAO,CAWnB;IAED;;;OAGG;IACH,iBAHW,MAAM,GACJ,MAAM,CAOlB;IAED;;;;;;OAMG;IACH,wBAHW,MAAM,GACJ,MAAM,CAKlB;IAED;;;OAGG;IACH,qBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,eAHW,MAAM,GACJ,QAAQ,GAAC,MAAM,CAI3B;IAED;;;OAGG;IACH,gBAHW,MAAM,QACN,QAAQ,GAAC,MAAM,QAIzB;IAED;;;OAGG;IACH,gBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,iBAHW,MAAM,SACN,MAAM,QAIhB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;OAIG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,kBAFW,MAAM,QAShB;IAED;;;OAGG;IACH,qBAFW,MAAM,QAShB;IAED;;;;OAIG;IACH,0BAOC;IAED;;OAEG;IACH,eAiCC;IAID;;;OAGG;IACH,oBAgBC;IAED;;;OAGG;IACH,mBAqBC;CACJ;yBAhcwB,oBAAoB"}
@@ -106,6 +106,12 @@ export class BodyStorage {
106
106
  this.__free_heap = new Uint32Array(cap);
107
107
  this.__free_count = 0;
108
108
 
109
+ // Entity → body-index map (one body per entity). Keeps {@link
110
+ // index_of_entity} O(1) instead of an O(N) scan over the slot table on
111
+ // every collider attach / detach and joint link. Maintained on
112
+ // allocate / free; never grows with the typed arrays (a plain Map).
113
+ this.__entity_to_index = new Map();
114
+
109
115
  // Initialise reverse map to BODY_INDEX_ABSENT.
110
116
  this.__awake_pos.fill(BODY_INDEX_ABSENT);
111
117
  }
@@ -167,6 +173,7 @@ export class BodyStorage {
167
173
  this.__kinds[index] = BodyKind.Dynamic;
168
174
  this.__flags[index] = 0;
169
175
  this.__alive[index] = 1;
176
+ this.__entity_to_index.set(entity, index);
170
177
 
171
178
  // Insert into awake set.
172
179
  const awake_pos = this.__awake_count++;
@@ -196,6 +203,10 @@ export class BodyStorage {
196
203
 
197
204
  this.__alive[index] = 0;
198
205
 
206
+ // Drop the entity → index mapping (the slot still holds the old entity
207
+ // value until reallocation, so delete by it now while it's valid).
208
+ this.__entity_to_index.delete(this.__entities[index]);
209
+
199
210
  // Bump generation; wraps mod 256.
200
211
  this.__generations[index] = (this.__generations[index] + 1) & GENERATION_MASK;
201
212
 
@@ -229,6 +240,18 @@ export class BodyStorage {
229
240
  return this.__entities[index];
230
241
  }
231
242
 
243
+ /**
244
+ * Body index for `entity`, or {@link BODY_INDEX_ABSENT} if no live body owns
245
+ * it. O(1) reverse of {@link entity_at} — the lookup callers use on the
246
+ * link / attach / joint paths instead of scanning the slot table.
247
+ * @param {number} entity
248
+ * @returns {number}
249
+ */
250
+ index_of_entity(entity) {
251
+ const idx = this.__entity_to_index.get(entity);
252
+ return idx === undefined ? BODY_INDEX_ABSENT : idx;
253
+ }
254
+
232
255
  /**
233
256
  * @param {number} index
234
257
  * @returns {number}
@@ -1 +1 @@
1
- {"version":3,"file":"generate_pairs.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/broadphase/generate_pairs.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,uIARW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAY;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC,+CAElF,MAAM,OAAO,MAAM,KAAK,OAAO,QAsEhD"}
1
+ {"version":3,"file":"generate_pairs.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/broadphase/generate_pairs.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,uIARW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAY;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC,+CAElF,MAAM,OAAO,MAAM,KAAK,OAAO,QA4EhD"}
@@ -1,3 +1,4 @@
1
+ import { assert } from "../../../core/assert.js";
1
2
  import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
2
3
 
3
4
  const scratch_aabb = new Float64Array(6);
@@ -65,6 +66,11 @@ export function generate_pairs(
65
66
  dynamic_bvh,
66
67
  scratch_aabb
67
68
  );
69
+ // The BVH query writes leaves unconditionally — at capacity it both
70
+ // drops leaves (typed-array OOB writes no-op) AND returns a count
71
+ // past the buffer end, so the loop below would read `undefined`
72
+ // candidates and build garbage pairs. Guard the buffer size.
73
+ assert.lessThan(n, candidates.length, 'generate_pairs: dynamic broadphase overflowed the candidate buffer');
68
74
  for (let c = 0; c < n; c++) {
69
75
  const other = candidates[c];
70
76
  if (other === my_packed) continue;
@@ -85,6 +91,7 @@ export function generate_pairs(
85
91
  static_bvh,
86
92
  scratch_aabb
87
93
  );
94
+ assert.lessThan(n, candidates.length, 'generate_pairs: static broadphase overflowed the candidate buffer');
88
95
  for (let c = 0; c < n; c++) {
89
96
  const other = candidates[c];
90
97
  const idA = my_packed < other ? my_packed : other;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Sweep `shape` (held at the fixed `rotation`) linearly from `(fx,fy,fz)` to
3
+ * `(tx,ty,tz)` through both broadphase trees, ignoring colliders owned by
4
+ * `exclude_entity`.
5
+ *
6
+ * Returns the traversable fraction of the segment in `[0, 1]`: `1` means the
7
+ * path is clear; a value `< 1` means a blocker was hit at that fraction and
8
+ * `result` has been filled — `result.position` is the swept-shape centre at
9
+ * first contact, `result.normal` is the blocker's outward surface normal
10
+ * (B → A), `result.entity` / `result.body_id` the blocker.
11
+ *
12
+ * @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
13
+ * @param {import("../../../core/geom/3d/shape/AbstractShape3D.js").AbstractShape3D} shape
14
+ * @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
15
+ * @param {number} fx
16
+ * @param {number} fy
17
+ * @param {number} fz
18
+ * @param {number} tx
19
+ * @param {number} ty
20
+ * @param {number} tz
21
+ * @param {number} exclude_entity entity to ignore (the swept body itself)
22
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on a clear path
23
+ * @returns {number} traversable fraction in [0, 1]
24
+ */
25
+ export function ccd_sweep_segment(system: import("../ecs/PhysicsSystem.js").PhysicsSystem, shape: import("../../../core/geom/3d/shape/AbstractShape3D.js").AbstractShape3D, rotation: {
26
+ x: number;
27
+ y: number;
28
+ z: number;
29
+ w: number;
30
+ }, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, exclude_entity: number, result: PhysicsSurfacePoint): number;
31
+ /**
32
+ * Post-solve continuous-collision pass over the awake set. For each awake
33
+ * Dynamic body flagged {@link RigidBodyFlags.CCD} that moved more than
34
+ * {@link CCD_MIN_SWEEP_DISTANCE} this step, sweep its primary collider along the
35
+ * step's net translation and stop it at the first blocker (clamp pose + remove
36
+ * inbound normal velocity).
37
+ *
38
+ * Start-of-step positions are captured into `system.__ccd_start_pos` (3 doubles
39
+ * per body index) before the substep loop; this pass reads the final pose from
40
+ * the live Transform. Iterates the awake list in storage order, so it is
41
+ * deterministic.
42
+ *
43
+ * @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
44
+ */
45
+ export function ccd_resolve(system: import("../ecs/PhysicsSystem.js").PhysicsSystem): void;
46
+ /**
47
+ * Continuous collision detection — linear shape-cast (the "speculative margin
48
+ * floor, upgraded to an opt-in per-body sweep" item from PLAN.md).
49
+ *
50
+ * The discrete pipeline detects contacts by overlap at step boundaries: a body
51
+ * that moves more than its own thickness in one step can start a step on one
52
+ * side of a thin wall and end on the other, never overlapping it on any frame
53
+ * the broadphase samples — it tunnels. The speculative fat-AABB margin
54
+ * (`compute_fat_world_aabb`) covers moderate speeds; genuinely fast movers
55
+ * (bullets, dropped debris, a body flung by an explosion) need a swept test.
56
+ *
57
+ * Approach (Box2D `b2_continuousPhysics`-style conservative advancement): after
58
+ * the substep solver has produced each body's final pose, sweep a CCD-flagged
59
+ * fast mover's primary collider along its NET step translation
60
+ * (start-of-step → final pose) using the existing {@link shape_cast} TOI engine.
61
+ * On the first blocker, clamp the body to the contact pose and remove the
62
+ * inbound normal component of its velocity (an inelastic stop) — the next
63
+ * discrete step then resolves the now-touching contact with the real
64
+ * material / restitution.
65
+ *
66
+ * Scope (v1, deliberate):
67
+ * - LINEAR sweep only: the orientation is held fixed through the sweep (the
68
+ * per-step angular motion is small at the fixed-step rate).
69
+ * - PRIMARY same-entity collider: a compound body sweeps its first collider;
70
+ * child-entity colliders (whose transform is synced outside the step) are
71
+ * not swept.
72
+ * - EXACT against static geometry (the static BVH holds tight, never-moved
73
+ * leaves); APPROXIMATE against other dynamic bodies (they have moved this
74
+ * step too, but the sweep sees their start-of-step broadphase AABBs) — the
75
+ * speculative-margin floor for the dynamic-vs-dynamic case.
76
+ * - The CCD stop itself is inelastic; the impact does not bounce. Restitution
77
+ * applies on the next discrete contact.
78
+ */
79
+ /**
80
+ * Minimum per-step displacement (metres) before a CCD sweep is worth running.
81
+ * Its ONLY job is to skip a body that didn't meaningfully move — a resting /
82
+ * sleeping-soon body whose displacement is sub-millimetre jitter — which avoids
83
+ * a degenerate zero-length sweep and saves the query cost.
84
+ *
85
+ * It must NOT be tied to the body's own size. Tunnelling risk is governed by the
86
+ * *obstacle's* thickness, not the mover's: a 2 m sphere drifting at 0.5 m/step
87
+ * still passes clean through a 1 cm floor. A small absolute slop catches that;
88
+ * gating on a fraction of the body's extent would (wrongly) wait until the body
89
+ * moved more than its own radius and miss every thin-obstacle tunnel below that
90
+ * speed. The discrete narrowphase still owns any obstacle thicker than a body's
91
+ * per-step move, so CCD reliably prevents tunnelling of obstacles thicker than
92
+ * this slop.
93
+ * @type {number}
94
+ */
95
+ export const CCD_MIN_SWEEP_DISTANCE: number;
96
+ import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
97
+ //# sourceMappingURL=linear_sweep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear_sweep.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ccd/linear_sweep.js"],"names":[],"mappings":"AA8FA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,0CAbW,OAAO,yBAAyB,EAAE,aAAa,SAC/C,OAAO,gDAAgD,EAAE,eAAe,YACxE;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,MACrC,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,kBACN,MAAM,UACN,mBAAmB,GACjB,MAAM,CAmClB;AAED;;;;;;;;;;;;;GAaG;AACH,oCAFW,OAAO,yBAAyB,EAAE,aAAa,QAwEzD;AAvOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,qCAFU,MAAM,CAE2B;oCArDP,mCAAmC"}
@@ -0,0 +1,238 @@
1
+ import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
2
+ import { BodyKind } from "../ecs/BodyKind.js";
3
+ import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
4
+ import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
5
+ import { shape_cast } from "../queries/shape_cast.js";
6
+
7
+ /**
8
+ * Continuous collision detection — linear shape-cast (the "speculative margin
9
+ * floor, upgraded to an opt-in per-body sweep" item from PLAN.md).
10
+ *
11
+ * The discrete pipeline detects contacts by overlap at step boundaries: a body
12
+ * that moves more than its own thickness in one step can start a step on one
13
+ * side of a thin wall and end on the other, never overlapping it on any frame
14
+ * the broadphase samples — it tunnels. The speculative fat-AABB margin
15
+ * (`compute_fat_world_aabb`) covers moderate speeds; genuinely fast movers
16
+ * (bullets, dropped debris, a body flung by an explosion) need a swept test.
17
+ *
18
+ * Approach (Box2D `b2_continuousPhysics`-style conservative advancement): after
19
+ * the substep solver has produced each body's final pose, sweep a CCD-flagged
20
+ * fast mover's primary collider along its NET step translation
21
+ * (start-of-step → final pose) using the existing {@link shape_cast} TOI engine.
22
+ * On the first blocker, clamp the body to the contact pose and remove the
23
+ * inbound normal component of its velocity (an inelastic stop) — the next
24
+ * discrete step then resolves the now-touching contact with the real
25
+ * material / restitution.
26
+ *
27
+ * Scope (v1, deliberate):
28
+ * - LINEAR sweep only: the orientation is held fixed through the sweep (the
29
+ * per-step angular motion is small at the fixed-step rate).
30
+ * - PRIMARY same-entity collider: a compound body sweeps its first collider;
31
+ * child-entity colliders (whose transform is synced outside the step) are
32
+ * not swept.
33
+ * - EXACT against static geometry (the static BVH holds tight, never-moved
34
+ * leaves); APPROXIMATE against other dynamic bodies (they have moved this
35
+ * step too, but the sweep sees their start-of-step broadphase AABBs) — the
36
+ * speculative-margin floor for the dynamic-vs-dynamic case.
37
+ * - The CCD stop itself is inelastic; the impact does not bounce. Restitution
38
+ * applies on the next discrete contact.
39
+ */
40
+
41
+ /**
42
+ * Minimum per-step displacement (metres) before a CCD sweep is worth running.
43
+ * Its ONLY job is to skip a body that didn't meaningfully move — a resting /
44
+ * sleeping-soon body whose displacement is sub-millimetre jitter — which avoids
45
+ * a degenerate zero-length sweep and saves the query cost.
46
+ *
47
+ * It must NOT be tied to the body's own size. Tunnelling risk is governed by the
48
+ * *obstacle's* thickness, not the mover's: a 2 m sphere drifting at 0.5 m/step
49
+ * still passes clean through a 1 cm floor. A small absolute slop catches that;
50
+ * gating on a fraction of the body's extent would (wrongly) wait until the body
51
+ * moved more than its own radius and miss every thin-obstacle tunnel below that
52
+ * speed. The discrete narrowphase still owns any obstacle thicker than a body's
53
+ * per-step move, so CCD reliably prevents tunnelling of obstacles thicker than
54
+ * this slop.
55
+ * @type {number}
56
+ */
57
+ export const CCD_MIN_SWEEP_DISTANCE = 1e-3;
58
+
59
+ /**
60
+ * Impact distances at or below this (metres) mean the body was already
61
+ * overlapping the target at the start of the step — i.e. a resting / sliding
62
+ * contact the discrete solver owns, not a tunnel. The sweep ignores these so
63
+ * CCD never clamps a body to a surface it is merely sitting or sliding on.
64
+ * @type {number}
65
+ */
66
+ const CCD_INITIAL_OVERLAP_EPS = 1e-6;
67
+
68
+ // ── Module scratch ──────────────────────────────────────────────────────────
69
+ // CCD runs inside the step loop; reuse buffers so the pass allocates nothing.
70
+
71
+ /** Swept ray, re-seeded per sweep (origin, unit direction, tMax). */
72
+ const _ray = new Ray3();
73
+
74
+ /** Sweep result reused across bodies inside {@link ccd_resolve}. */
75
+ const _hit = new PhysicsSurfacePoint();
76
+
77
+ /**
78
+ * Entity whose colliders the current sweep must ignore (the moving body's own).
79
+ * Module-scoped so {@link _exclude_self} stays a stable, non-allocating
80
+ * reference handed to {@link shape_cast}.
81
+ * @type {number}
82
+ */
83
+ let _exclude_entity = -1;
84
+
85
+ /**
86
+ * Self-exclusion filter for the sweep: drop candidates owned by the moving
87
+ * body's own entity (otherwise the body would "hit itself" at `t = 0`).
88
+ * @param {number} entity
89
+ * @returns {boolean}
90
+ */
91
+ function _exclude_self(entity) {
92
+ return entity !== _exclude_entity;
93
+ }
94
+
95
+ /**
96
+ * Sweep `shape` (held at the fixed `rotation`) linearly from `(fx,fy,fz)` to
97
+ * `(tx,ty,tz)` through both broadphase trees, ignoring colliders owned by
98
+ * `exclude_entity`.
99
+ *
100
+ * Returns the traversable fraction of the segment in `[0, 1]`: `1` means the
101
+ * path is clear; a value `< 1` means a blocker was hit at that fraction and
102
+ * `result` has been filled — `result.position` is the swept-shape centre at
103
+ * first contact, `result.normal` is the blocker's outward surface normal
104
+ * (B → A), `result.entity` / `result.body_id` the blocker.
105
+ *
106
+ * @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
107
+ * @param {import("../../../core/geom/3d/shape/AbstractShape3D.js").AbstractShape3D} shape
108
+ * @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
109
+ * @param {number} fx
110
+ * @param {number} fy
111
+ * @param {number} fz
112
+ * @param {number} tx
113
+ * @param {number} ty
114
+ * @param {number} tz
115
+ * @param {number} exclude_entity entity to ignore (the swept body itself)
116
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on a clear path
117
+ * @returns {number} traversable fraction in [0, 1]
118
+ */
119
+ export function ccd_sweep_segment(
120
+ system, shape, rotation,
121
+ fx, fy, fz, tx, ty, tz,
122
+ exclude_entity, result,
123
+ ) {
124
+ const dx = tx - fx;
125
+ const dy = ty - fy;
126
+ const dz = tz - fz;
127
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
128
+ if (len === 0) return 1;
129
+
130
+ const inv = 1 / len;
131
+ _ray[0] = fx; _ray[1] = fy; _ray[2] = fz;
132
+ _ray[3] = dx * inv; _ray[4] = dy * inv; _ray[5] = dz * inv;
133
+ _ray[6] = len; // tMax in metres — direction is unit length
134
+
135
+ _exclude_entity = exclude_entity;
136
+ if (!shape_cast(system, _ray, shape, rotation, result, _exclude_self)) {
137
+ return 1;
138
+ }
139
+
140
+ // An impact at t ≈ 0 means the swept shape was already overlapping the
141
+ // target at the segment start — a resting / sliding contact, not a tunnel.
142
+ // The discrete solver owns it; clamping here would freeze a body onto a
143
+ // surface it is sitting or sliding on. Treat as a clear path.
144
+ if (result.t <= CCD_INITIAL_OVERLAP_EPS) return 1;
145
+
146
+ // result.t is the impact distance along the unit direction; normalise to a
147
+ // segment fraction. shape_cast reports the just-SEPARATING side of its
148
+ // bisection, so the swept shape at `result.t` is provably not overlapping —
149
+ // safe to place the body there.
150
+ const frac = result.t * inv;
151
+ return frac < 1 ? frac : 1;
152
+ }
153
+
154
+ /**
155
+ * Post-solve continuous-collision pass over the awake set. For each awake
156
+ * Dynamic body flagged {@link RigidBodyFlags.CCD} that moved more than
157
+ * {@link CCD_MIN_SWEEP_DISTANCE} this step, sweep its primary collider along the
158
+ * step's net translation and stop it at the first blocker (clamp pose + remove
159
+ * inbound normal velocity).
160
+ *
161
+ * Start-of-step positions are captured into `system.__ccd_start_pos` (3 doubles
162
+ * per body index) before the substep loop; this pass reads the final pose from
163
+ * the live Transform. Iterates the awake list in storage order, so it is
164
+ * deterministic.
165
+ *
166
+ * @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
167
+ */
168
+ export function ccd_resolve(system) {
169
+ const storage = system.storage;
170
+ const count = storage.awake_count;
171
+ const lists = system.__body_collider_lists;
172
+ const start = system.__ccd_start_pos;
173
+ const CCD = RigidBodyFlags.CCD;
174
+
175
+ for (let i = 0; i < count; i++) {
176
+ const idx = storage.awake_at(i);
177
+
178
+ const rb = system.__bodies[idx];
179
+ if (rb === undefined) continue;
180
+ if ((rb.flags & CCD) === 0) continue;
181
+ if (rb.kind !== BodyKind.Dynamic) continue;
182
+
183
+ const list = lists[idx];
184
+ if (list === undefined || list.length === 0) continue;
185
+ const primary = list[0];
186
+ const tr = primary.transform;
187
+ const p = tr.position;
188
+
189
+ const base = idx * 3;
190
+ const sx = start[base];
191
+ const sy = start[base + 1];
192
+ const sz = start[base + 2];
193
+ const ex = p[0];
194
+ const ey = p[1];
195
+ const ez = p[2];
196
+
197
+ const dx = ex - sx;
198
+ const dy = ey - sy;
199
+ const dz = ez - sz;
200
+ const disp2 = dx * dx + dy * dy + dz * dz;
201
+ if (disp2 === 0) continue;
202
+
203
+ // Motion gate: skip a body that barely moved (resting / negligible
204
+ // jitter) — avoids a degenerate sweep and the query cost. NOT tied to
205
+ // body size: a body tunnels a thin obstacle at speeds well below its
206
+ // own extent, so the threshold is a small absolute slop. A resting /
207
+ // sliding body that does clear it is still safe — the sweep ignores its
208
+ // initial-overlap contact (see CCD_INITIAL_OVERLAP_EPS).
209
+ if (disp2 <= CCD_MIN_SWEEP_DISTANCE * CCD_MIN_SWEEP_DISTANCE) continue;
210
+
211
+ const shape = primary.collider.shape;
212
+ const frac = ccd_sweep_segment(
213
+ system, shape, tr.rotation,
214
+ sx, sy, sz, ex, ey, ez,
215
+ primary.entity, _hit,
216
+ );
217
+ if (frac >= 1) continue; // clear path — no tunnelling this step
218
+
219
+ // Clamp the body to the first-contact pose (swept-shape centre at the
220
+ // TOI), so it cannot end the step on the far side of the blocker.
221
+ const hp = _hit.position;
222
+ p.set(hp[0], hp[1], hp[2]);
223
+
224
+ // Remove the inbound normal component of velocity (inelastic stop).
225
+ // `_hit.normal` is the blocker's outward surface normal; the body is
226
+ // moving into it (v·n < 0). Zeroing that component leaves the tangential
227
+ // slide intact and never adds energy. The next discrete step applies the
228
+ // real restitution / friction on the established contact.
229
+ const n = _hit.normal;
230
+ const lv = rb.linearVelocity;
231
+ const vn = lv[0] * n[0] + lv[1] * n[1] + lv[2] * n[2];
232
+ if (vn < 0) {
233
+ lv[0] -= vn * n[0];
234
+ lv[1] -= vn * n[1];
235
+ lv[2] -= vn * n[2];
236
+ }
237
+ }
238
+ }
@@ -196,6 +196,21 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
196
196
  * @type {Float64Array}
197
197
  */
198
198
  __pseudo_velocity: Float64Array;
199
+ /**
200
+ * Master switch for the continuous-collision pass. When false the
201
+ * {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
202
+ * @type {boolean}
203
+ */
204
+ ccdEnabled: boolean;
205
+ /**
206
+ * Start-of-step world positions for CCD-flagged bodies — 3 doubles per
207
+ * body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
208
+ * loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
209
+ * here to the final pose. Grows to `storage.high_water_mark * 3`; only
210
+ * CCD-flagged slots are written each step.
211
+ * @type {Float64Array}
212
+ */
213
+ __ccd_start_pos: Float64Array;
199
214
  /**
200
215
  * Bound reference to {@link __pair_filter} so we hand the same
201
216
  * callable to {@link generate_pairs} each step without per-step
@@ -322,9 +337,9 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
322
337
  */
323
338
  detach_collider(body_entity: number, collider: Collider): void;
324
339
  /**
325
- * Linear scan over body slots looking for the one whose entity matches.
326
- * O(N) where N is the live body countonly called on the link/unlink
327
- * paths, not during simulation, so the scan cost is bounded.
340
+ * Resolve an entity to its body index, or -1 if no live body owns it.
341
+ * O(1) via {@link BodyStorage#index_of_entity}'s entity index mapused
342
+ * on the collider attach / detach and joint link paths.
328
343
  *
329
344
  * @private
330
345
  * @param {number} entity
@@ -1 +1 @@
1
- {"version":3,"file":"PhysicsSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/PhysicsSystem.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PhysicsSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/PhysicsSystem.js"],"names":[],"mappings":"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"}
@@ -21,6 +21,7 @@ import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
21
21
  import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
22
22
  import { raycast as raycast_query } from "../queries/raycast.js";
23
23
  import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
24
+ import { ccd_resolve } from "../ccd/linear_sweep.js";
24
25
  import { returnTrue } from "../../../core/function/returnTrue.js";
25
26
  import {
26
27
  prepare_contacts,
@@ -294,6 +295,23 @@ export class PhysicsSystem extends System {
294
295
  */
295
296
  this.__pseudo_velocity = new Float64Array(0);
296
297
 
298
+ /**
299
+ * Master switch for the continuous-collision pass. When false the
300
+ * {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
301
+ * @type {boolean}
302
+ */
303
+ this.ccdEnabled = true;
304
+
305
+ /**
306
+ * Start-of-step world positions for CCD-flagged bodies — 3 doubles per
307
+ * body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
308
+ * loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
309
+ * here to the final pose. Grows to `storage.high_water_mark * 3`; only
310
+ * CCD-flagged slots are written each step.
311
+ * @type {Float64Array}
312
+ */
313
+ this.__ccd_start_pos = new Float64Array(0);
314
+
297
315
  /**
298
316
  * Bound reference to {@link __pair_filter} so we hand the same
299
317
  * callable to {@link generate_pairs} each step without per-step
@@ -586,20 +604,16 @@ export class PhysicsSystem extends System {
586
604
  }
587
605
 
588
606
  /**
589
- * Linear scan over body slots looking for the one whose entity matches.
590
- * O(N) where N is the live body countonly called on the link/unlink
591
- * paths, not during simulation, so the scan cost is bounded.
607
+ * Resolve an entity to its body index, or -1 if no live body owns it.
608
+ * O(1) via {@link BodyStorage#index_of_entity}'s entity index mapused
609
+ * on the collider attach / detach and joint link paths.
592
610
  *
593
611
  * @private
594
612
  * @param {number} entity
595
613
  * @returns {number} body index or -1
596
614
  */
597
615
  __find_body_index_by_entity(entity) {
598
- const hwm = this.storage.high_water_mark;
599
- for (let i = 0; i < hwm; i++) {
600
- if (this.storage.entity_at(i) === entity) return i;
601
- }
602
- return -1;
616
+ return this.storage.index_of_entity(entity);
603
617
  }
604
618
 
605
619
  /**
@@ -1347,6 +1361,34 @@ export class PhysicsSystem extends System {
1347
1361
  const h = dt / N;
1348
1362
  const count_after_wake = this.storage.awake_count;
1349
1363
 
1364
+ // CCD: capture start-of-step positions for flagged bodies over the
1365
+ // post-wake awake set (poses are unchanged until the substep loop below
1366
+ // integrates them). The CCD pass after the solver sweeps from here to
1367
+ // each body's final pose. Reads the primary collider's transform so the
1368
+ // start matches the end the resolve pass reads. Zero-cost when no body
1369
+ // is flagged.
1370
+ const ccd_on = this.ccdEnabled;
1371
+ if (ccd_on) {
1372
+ const ccd_need = this.storage.high_water_mark * 3;
1373
+ if (this.__ccd_start_pos.length < ccd_need) {
1374
+ this.__ccd_start_pos = new Float64Array(ccd_need);
1375
+ }
1376
+ const ccd_start = this.__ccd_start_pos;
1377
+ for (let i = 0; i < count_after_wake; i++) {
1378
+ const idx = this.storage.awake_at(i);
1379
+ const rb = this.__bodies[idx];
1380
+ if (rb.kind !== BodyKind.Dynamic) continue;
1381
+ if ((rb.flags & RigidBodyFlags.CCD) === 0) continue;
1382
+ const list = this.__body_collider_lists[idx];
1383
+ if (list === undefined || list.length === 0) continue;
1384
+ const cp = list[0].transform.position;
1385
+ const cb = idx * 3;
1386
+ ccd_start[cb] = cp[0];
1387
+ ccd_start[cb + 1] = cp[1];
1388
+ ccd_start[cb + 2] = cp[2];
1389
+ }
1390
+ }
1391
+
1350
1392
  // Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
1351
1393
  // then capture the reference. Inside the loop we only zero its live
1352
1394
  // region per substep — re-capturing is unnecessary since it won't
@@ -1403,6 +1445,15 @@ export class PhysicsSystem extends System {
1403
1445
  // the approach velocity captured at prepare time.
1404
1446
  apply_restitution(this.manifolds, this);
1405
1447
 
1448
+ // Stage 8.5: continuous collision — sweep CCD-flagged fast movers along
1449
+ // their net step translation and stop them at the first blocker, so they
1450
+ // can't tunnel through thin geometry between discrete steps. Runs on the
1451
+ // final post-solve poses, before the sleep test sees the clamped
1452
+ // velocities. No-op when no awake body is flagged.
1453
+ if (ccd_on) {
1454
+ ccd_resolve(this);
1455
+ }
1456
+
1406
1457
  // Stage 9: sleep test.
1407
1458
  this.__sleep_test(dt);
1408
1459
 
@@ -8,6 +8,11 @@
8
8
  * component during solve.
9
9
  * - {@link DisableSleep }: this body never enters the sleeping set. Bodies near it
10
10
  * may still sleep when stable.
11
+ * - {@link CCD }: opt-in continuous collision detection. After the solver, the
12
+ * body's net step translation is shape-cast against the broadphase and the
13
+ * body is stopped at the first blocker, so a fast mover can't tunnel through
14
+ * thin geometry between discrete steps. Off by default — it costs one swept
15
+ * query per fast-moving flagged body per step. See `ccd/linear_sweep.js`.
11
16
  */
12
17
  export type RigidBodyFlags = number;
13
18
  export namespace RigidBodyFlags {
@@ -17,5 +22,6 @@ export namespace RigidBodyFlags {
17
22
  let LockRotY: number;
18
23
  let LockRotZ: number;
19
24
  let DisableSleep: number;
25
+ let CCD: number;
20
26
  }
21
27
  //# sourceMappingURL=RigidBodyFlags.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RigidBodyFlags.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/RigidBodyFlags.js"],"names":[],"mappings":";;;;;;;;;;;6BAYU,MAAM"}
1
+ {"version":3,"file":"RigidBodyFlags.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/RigidBodyFlags.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;6BAiBU,MAAM"}