@woosh/meep-engine 2.140.0 → 2.142.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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  3. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  4. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  5. package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
  6. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  7. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +67 -42
  8. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
  9. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
  10. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +340 -186
  11. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
  12. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
  13. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
  14. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
  15. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
  16. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
  17. package/src/engine/physics/PLAN.md +705 -461
  18. package/src/engine/physics/REVIEW_002.md +151 -0
  19. package/src/engine/physics/REVIEW_003.md +166 -0
  20. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  21. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  22. package/src/engine/physics/constraint/DofMode.js +35 -0
  23. package/src/engine/physics/constraint/solve_constraints.d.ts +38 -0
  24. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  25. package/src/engine/physics/constraint/solve_constraints.js +673 -0
  26. package/src/engine/physics/ecs/Joint.d.ts +294 -0
  27. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  28. package/src/engine/physics/ecs/Joint.js +402 -0
  29. package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
  30. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
  32. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  33. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  34. package/src/engine/physics/fluid/FluidField.js +14 -10
  35. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  36. package/src/engine/physics/fluid/FluidSimulator.js +0 -1
  37. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  38. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  39. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  40. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  41. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  42. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  43. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  44. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  45. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  46. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
  47. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  48. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
  49. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  50. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  51. package/src/engine/physics/island/IslandBuilder.js +33 -16
  52. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  53. package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
  54. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
  55. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  56. package/src/engine/physics/narrowphase/narrowphase_step.js +75 -0
  57. package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
  58. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
  59. package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
  60. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
  61. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
  62. package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
  63. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
  64. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
  65. package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
  66. package/src/engine/physics/queries/raycast.d.ts +11 -9
  67. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  68. package/src/engine/physics/queries/raycast.js +108 -159
  69. package/src/engine/physics/solver/solve_contacts.d.ts +28 -0
  70. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  71. package/src/engine/physics/solver/solve_contacts.js +169 -1
  72. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
  73. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
  74. package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
@@ -108,9 +108,10 @@ const MIC_SIGMA = 0.25;
108
108
  * the hot path that prevents MIC-PCG from parallelizing onto a GPU without
109
109
  * substantial reformulation. On single-thread JS that's not a concern.
110
110
  *
111
- * Solid cells (mask = 0) are excluded from the system: their pressure stays at
112
- * whatever it was (typically 0), their entries in r, z, s, As stay 0, and they
113
- * contribute nothing to dot products.
111
+ * Cells with no fluid neighbours solid cells (mask bit 7 set) and isolated
112
+ * fluid (mask = 0), both identified by `(mask & 63) === 0` are excluded from
113
+ * the system: their pressure stays at whatever it was (typically 0), their
114
+ * entries in r, z, s, As stay 0, and they contribute nothing to dot products.
114
115
  *
115
116
  * @param {Float32Array|Float16Array} pressure Mutated in place. Pre-fill with 0
116
117
  * or with the previous step's solution to warm-start.
@@ -173,9 +174,12 @@ export function v3_grid_solve_pressure_pcg(
173
174
  // (Standard fluids practice — see Bridson §5.5. The alternative is to pin
174
175
  // one cell to a Dirichlet value and exclude it from the system, but mean
175
176
  // subtraction has the same effect with simpler bookkeeping.)
177
+ // A cell is a degree of freedom iff it has ≥1 fluid neighbour, i.e. any of
178
+ // the low 6 bits set. Solid cells (bit 7) and isolated fluid (all-zero) are
179
+ // both excluded by masking off bit 7 with & 63.
176
180
  let fluid_count = 0;
177
181
  for (let c = 0; c < cells; c++) {
178
- if (neighbour_mask[c] !== 0) fluid_count++;
182
+ if ((neighbour_mask[c] & 63) !== 0) fluid_count++;
179
183
  }
180
184
  if (fluid_count === 0) {
181
185
  return;
@@ -193,7 +197,7 @@ export function v3_grid_solve_pressure_pcg(
193
197
  precon.fill(0);
194
198
  for (let c = 0; c < cells; c++) {
195
199
  const mask = neighbour_mask[c];
196
- if (mask === 0) {
200
+ if ((mask & 63) === 0) {
197
201
  continue;
198
202
  }
199
203
  const a_diag = POPCOUNT_6[mask];
@@ -246,7 +250,7 @@ export function v3_grid_solve_pressure_pcg(
246
250
 
247
251
  for (let c = 0; c < cells; c++) {
248
252
  const mask = neighbour_mask[c];
249
- if (mask === 0) {
253
+ if ((mask & 63) === 0) {
250
254
  r[c] = 0;
251
255
  continue;
252
256
  }
@@ -285,7 +289,7 @@ export function v3_grid_solve_pressure_pcg(
285
289
  let s_dot_As = 0;
286
290
  for (let c = 0; c < cells; c++) {
287
291
  const mask = neighbour_mask[c];
288
- if (mask === 0) {
292
+ if ((mask & 63) === 0) {
289
293
  As[c] = 0;
290
294
  continue;
291
295
  }
@@ -379,11 +383,11 @@ export function v3_grid_solve_pressure_pcg(
379
383
  function subtract_mean_fluid(v, mask_array, cells, fluid_count) {
380
384
  let sum = 0;
381
385
  for (let c = 0; c < cells; c++) {
382
- if (mask_array[c] !== 0) sum += v[c];
386
+ if ((mask_array[c] & 63) !== 0) sum += v[c];
383
387
  }
384
388
  const mean = sum / fluid_count;
385
389
  for (let c = 0; c < cells; c++) {
386
- if (mask_array[c] !== 0) v[c] -= mean;
390
+ if ((mask_array[c] & 63) !== 0) v[c] -= mean;
387
391
  }
388
392
  }
389
393
 
@@ -391,7 +395,7 @@ function apply_preconditioner(r, z, precon, mask_array, cells, rx, slice) {
391
395
  // Forward solve L · y = r. Store y in z (we won't read z[c'] for c' > c).
392
396
  for (let c = 0; c < cells; c++) {
393
397
  const mask = mask_array[c];
394
- if (mask === 0) {
398
+ if ((mask & 63) === 0) {
395
399
  z[c] = 0;
396
400
  continue;
397
401
  }
@@ -410,7 +414,7 @@ function apply_preconditioner(r, z, precon, mask_array, cells, rx, slice) {
410
414
  // indices, processed first in reverse).
411
415
  for (let c = cells - 1; c >= 0; c--) {
412
416
  const mask = mask_array[c];
413
- if (mask === 0) {
417
+ if ((mask & 63) === 0) {
414
418
  continue;
415
419
  }
416
420
  let t = z[c]; // = y[c] from forward pass
@@ -13,25 +13,28 @@
13
13
  * pressure (consistent with the Neumann condition used in the solve), which keeps the
14
14
  * flow tangent to the boundary.
15
15
  *
16
- * Which neighbours count as fluid is read straight from `neighbour_mask`, the same
17
- * pre-baked per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
18
- * {@link FluidField.recomputeSolidNeighbourMask}). A set bit means "that neighbour is
19
- * fluid (in-bounds AND non-solid)", so a clear bit reflects the cell's own pressure —
20
- * folding the boundary check and the solid-neighbour check into one register-resident
21
- * bit-test per face. Encoding:
22
- *
23
- * bit 0 (= 1) : -x neighbour is fluid
24
- * bit 1 (= 2) : +x neighbour is fluid
25
- * bit 2 (= 4) : -y neighbour is fluid
26
- * bit 3 (= 8) : +y neighbour is fluid
27
- * bit 4 (= 16) : -z neighbour is fluid
28
- * bit 5 (= 32) : +z neighbour is fluid
29
- *
30
- * Unlike the solve, the mask can't drive the self-cell branch: both a solid cell and
31
- * an isolated fluid cell (no fluid neighbours) encode as `mask = 0`, yet they need
32
- * opposite handling the solid must be zeroed for no-slip, while the isolated fluid's
33
- * gradient already nets to zero (every face reflects its own pressure) and must be
34
- * left alone. `solid` is what tells them apart, so it stays.
16
+ * Everything this needs is read straight from `neighbour_mask`, the same pre-baked
17
+ * per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
18
+ * {@link FluidField.recomputeSolidNeighbourMask}). Encoding:
19
+ *
20
+ * bit 0 (= 1) : -x neighbour is fluid
21
+ * bit 1 (= 2) : +x neighbour is fluid
22
+ * bit 2 (= 4) : -y neighbour is fluid
23
+ * bit 3 (= 8) : +y neighbour is fluid
24
+ * bit 4 (= 16) : -z neighbour is fluid
25
+ * bit 5 (= 32) : +z neighbour is fluid
26
+ * bit 7 (= 128) : this cell is itself solid
27
+ *
28
+ * A set neighbour bit means "that neighbour is fluid (in-bounds AND non-solid)", so a
29
+ * clear bit reflects the cell's own pressure — folding the boundary check and the
30
+ * solid-neighbour check into one register-resident bit-test per face.
31
+ *
32
+ * Bit 7 carries the self-solid flag that used to require the separate `solid` array.
33
+ * The solve can't use the low 6 bits for this (a solid cell and an isolated fluid cell
34
+ * both have zero fluid-neighbour bits) but needs opposite handling here — the solid
35
+ * must be zeroed for no-slip, while the isolated fluid's gradient already nets to zero
36
+ * (every face reflects its own pressure) and must be left alone. Bit 7 distinguishes
37
+ * them, so a single mask read drives both the zeroing and the gradient.
35
38
  *
36
39
  * @param {Float32Array} vel_x Mutated in place.
37
40
  * @param {Float32Array} vel_y Mutated in place.
@@ -40,9 +43,8 @@
40
43
  * @param {number} res_x
41
44
  * @param {number} res_y
42
45
  * @param {number} res_z
43
- * @param {Uint8Array} solid Required (zero-filled for a wall-free domain).
44
46
  * @param {Uint8Array} neighbour_mask Length ≥ res_x*res_y*res_z. Same buffer the
45
- * pressure solve uses; MUST be recomputed whenever `solid` changes.
47
+ * pressure solve uses; MUST be recomputed whenever the solid mask changes.
46
48
  */
47
- export function v3_grid_subtract_pressure_gradient(vel_x: Float32Array, vel_y: Float32Array, vel_z: Float32Array, pressure: Float32Array, res_x: number, res_y: number, res_z: number, solid: Uint8Array, neighbour_mask: Uint8Array): void;
49
+ export function v3_grid_subtract_pressure_gradient(vel_x: Float32Array, vel_y: Float32Array, vel_z: Float32Array, pressure: Float32Array, res_x: number, res_y: number, res_z: number, neighbour_mask: Uint8Array): void;
48
50
  //# sourceMappingURL=v3_grid_subtract_pressure_gradient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v3_grid_subtract_pressure_gradient.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,0DAXW,YAAY,SACZ,YAAY,SACZ,YAAY,YACZ,YAAY,SACZ,MAAM,SACN,MAAM,SACN,MAAM,SACN,UAAU,kBACV,UAAU,QA8CpB"}
1
+ {"version":3,"file":"v3_grid_subtract_pressure_gradient.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,0DAVW,YAAY,SACZ,YAAY,SACZ,YAAY,YACZ,YAAY,SACZ,MAAM,SACN,MAAM,SACN,MAAM,kBACN,UAAU,QAgDpB"}
@@ -15,25 +15,28 @@ import { assert } from "../../../../core/assert.js";
15
15
  * pressure (consistent with the Neumann condition used in the solve), which keeps the
16
16
  * flow tangent to the boundary.
17
17
  *
18
- * Which neighbours count as fluid is read straight from `neighbour_mask`, the same
19
- * pre-baked per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
20
- * {@link FluidField.recomputeSolidNeighbourMask}). A set bit means "that neighbour is
21
- * fluid (in-bounds AND non-solid)", so a clear bit reflects the cell's own pressure —
22
- * folding the boundary check and the solid-neighbour check into one register-resident
23
- * bit-test per face. Encoding:
18
+ * Everything this needs is read straight from `neighbour_mask`, the same pre-baked
19
+ * per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
20
+ * {@link FluidField.recomputeSolidNeighbourMask}). Encoding:
24
21
  *
25
- * bit 0 (= 1) : -x neighbour is fluid
26
- * bit 1 (= 2) : +x neighbour is fluid
27
- * bit 2 (= 4) : -y neighbour is fluid
28
- * bit 3 (= 8) : +y neighbour is fluid
29
- * bit 4 (= 16) : -z neighbour is fluid
30
- * bit 5 (= 32) : +z neighbour is fluid
22
+ * bit 0 (= 1) : -x neighbour is fluid
23
+ * bit 1 (= 2) : +x neighbour is fluid
24
+ * bit 2 (= 4) : -y neighbour is fluid
25
+ * bit 3 (= 8) : +y neighbour is fluid
26
+ * bit 4 (= 16) : -z neighbour is fluid
27
+ * bit 5 (= 32) : +z neighbour is fluid
28
+ * bit 7 (= 128) : this cell is itself solid
31
29
  *
32
- * Unlike the solve, the mask can't drive the self-cell branch: both a solid cell and
33
- * an isolated fluid cell (no fluid neighbours) encode as `mask = 0`, yet they need
34
- * opposite handling — the solid must be zeroed for no-slip, while the isolated fluid's
35
- * gradient already nets to zero (every face reflects its own pressure) and must be
36
- * left alone. `solid` is what tells them apart, so it stays.
30
+ * A set neighbour bit means "that neighbour is fluid (in-bounds AND non-solid)", so a
31
+ * clear bit reflects the cell's own pressure folding the boundary check and the
32
+ * solid-neighbour check into one register-resident bit-test per face.
33
+ *
34
+ * Bit 7 carries the self-solid flag that used to require the separate `solid` array.
35
+ * The solve can't use the low 6 bits for this (a solid cell and an isolated fluid cell
36
+ * both have zero fluid-neighbour bits) but needs opposite handling here — the solid
37
+ * must be zeroed for no-slip, while the isolated fluid's gradient already nets to zero
38
+ * (every face reflects its own pressure) and must be left alone. Bit 7 distinguishes
39
+ * them, so a single mask read drives both the zeroing and the gradient.
37
40
  *
38
41
  * @param {Float32Array} vel_x Mutated in place.
39
42
  * @param {Float32Array} vel_y Mutated in place.
@@ -42,11 +45,10 @@ import { assert } from "../../../../core/assert.js";
42
45
  * @param {number} res_x
43
46
  * @param {number} res_y
44
47
  * @param {number} res_z
45
- * @param {Uint8Array} solid Required (zero-filled for a wall-free domain).
46
48
  * @param {Uint8Array} neighbour_mask Length ≥ res_x*res_y*res_z. Same buffer the
47
- * pressure solve uses; MUST be recomputed whenever `solid` changes.
49
+ * pressure solve uses; MUST be recomputed whenever the solid mask changes.
48
50
  */
49
- export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure, res_x, res_y, res_z, solid, neighbour_mask) {
51
+ export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure, res_x, res_y, res_z, neighbour_mask) {
50
52
  const cell_count = res_x * res_y * res_z;
51
53
  assert.greaterThanOrEqual(vel_x.length, cell_count, "vel_x covers grid");
52
54
  assert.greaterThanOrEqual(vel_y.length, cell_count, "vel_y covers grid");
@@ -65,7 +67,10 @@ export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure
65
67
  for (let x = 0; x < res_x; x++) {
66
68
  const c = z_off + y_off + x;
67
69
 
68
- if (solid[c] !== 0) {
70
+ const mask = neighbour_mask[c];
71
+
72
+ if (mask & 128) {
73
+ // Solid cell (bit 7): enforce no-slip.
69
74
  vel_x[c] = 0;
70
75
  vel_y[c] = 0;
71
76
  vel_z[c] = 0;
@@ -75,7 +80,6 @@ export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure
75
80
  // A clear bit means the neighbour is out-of-bounds or solid; reflect
76
81
  // the cell's own pressure so that face contributes 0 to the gradient,
77
82
  // consistent with ∂p/∂n = 0 at the wall.
78
- const mask = neighbour_mask[c];
79
83
  const p_xm = (mask & 1) ? pressure[c - 1] : pressure[c];
80
84
  const p_xp = (mask & 2) ? pressure[c + 1] : pressure[c];
81
85
  const p_ym = (mask & 4) ? pressure[c - res_x] : pressure[c];
@@ -117,8 +117,11 @@ export class IslandBuilder {
117
117
  * @param {ManifoldStore} manifolds
118
118
  * @param {RigidBody[]} bodies sparse, indexed by body index
119
119
  * @param {Array[]} body_collider_lists sparse, indexed by body index
120
+ * @param {Joint[]} joints live joints (sparse). Jointed dynamic-dynamic
121
+ * bodies are unioned into the same island so a chain / ragdoll sleeps
122
+ * and wakes as a unit. Callers with no joints pass an empty array.
120
123
  */
121
- build(storage: BodyStorage, manifolds: ManifoldStore, bodies: RigidBody[], body_collider_lists: any[][]): void;
124
+ build(storage: BodyStorage, manifolds: ManifoldStore, bodies: RigidBody[], body_collider_lists: any[][], joints: Joint[]): void;
122
125
  /**
123
126
  * Fill `body_offsets` + `body_data` with awake dynamic bodies grouped
124
127
  * by island, sorted ascending within each island.
@@ -1 +1 @@
1
- {"version":3,"file":"IslandBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/island/IslandBuilder.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IAyFI;;;;;OAKG;IACH,gCAUC;IAtGG;;;;;OAKG;IACH,QAFU,WAAW,CAEY;IAEjC;;;;OAIG;IACH,gBAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,cAFU,MAAM,CAEK;IAErB;;;;OAIG;IACH,cAFU,WAAW,CAEiB;IAEtC;;;;OAIG;IACH,WAFU,WAAW,CAEe;IAEpC;;;OAGG;IACH,YAFU,MAAM,CAEG;IAEnB;;;;;OAKG;IACH,iBAFU,WAAW,CAEoB;IAEzC;;;;OAIG;IACH,cAFU,WAAW,CAEkB;IAEvC;;;OAGG;IACH,eAFU,MAAM,CAEM;IAEtB;;;;;OAKG;IACH,yBAA0C;IAE1C;;;;;OAKG;IACH,wBAA0C;IAE1C;;;;OAIG;IACH,kBAAmC;IAqBvC;;;;;;;;OAQG;IACH,8DAHW,WAAW,uBACX,OAAO,QAuEjB;IAED;;;;OAIG;IACH,yBA+CC;IAED;;;;;;OAMG;IACH,4BAwCC;IAED;;;;;;;;OAQG;IACH,yBAcC;IAID;;;OAGG;IACH,+BAQC;IAED;;;OAGG;IACH,uCAOC;IAED;;;OAGG;IACH,oCAIC;IAED;;;OAGG;IACH,uCAIC;CACJ"}
1
+ {"version":3,"file":"IslandBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/island/IslandBuilder.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IAyFI;;;;;OAKG;IACH,gCAUC;IAtGG;;;;;OAKG;IACH,QAFU,WAAW,CAEY;IAEjC;;;;OAIG;IACH,gBAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,cAFU,MAAM,CAEK;IAErB;;;;OAIG;IACH,cAFU,WAAW,CAEiB;IAEtC;;;;OAIG;IACH,WAFU,WAAW,CAEe;IAEpC;;;OAGG;IACH,YAFU,MAAM,CAEG;IAEnB;;;;;OAKG;IACH,iBAFU,WAAW,CAEoB;IAEzC;;;;OAIG;IACH,cAFU,WAAW,CAEkB;IAEvC;;;OAGG;IACH,eAFU,MAAM,CAEM;IAEtB;;;;;OAKG;IACH,yBAA0C;IAE1C;;;;;OAKG;IACH,wBAA0C;IAE1C;;;;OAIG;IACH,kBAAmC;IAqBvC;;;;;;;;;;;OAWG;IACH,8DANW,WAAW,uBACX,OAAO,UACP,OAAO,QAgGjB;IAED;;;;OAIG;IACH,yBA+CC;IAED;;;;;;OAMG;IACH,4BAwCC;IAED;;;;;;;;OAQG;IACH,yBAcC;IAID;;;OAGG;IACH,+BAQC;IAED;;;OAGG;IACH,uCAOC;IAED;;;OAGG;IACH,oCAIC;IAED;;;OAGG;IACH,uCAIC;CACJ"}
@@ -1,6 +1,8 @@
1
+ import { ceilPowerOfTwo } from "../../../core/binary/operations/ceilPowerOfTwo.js";
1
2
  import { body_id_index } from "../body/BodyStorage.js";
2
3
  import { BodyKind } from "../ecs/BodyKind.js";
3
4
  import { ColliderFlags } from "../ecs/ColliderFlags.js";
5
+ import { JOINT_WORLD } from "../ecs/Joint.js";
4
6
  import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
5
7
  import { uf_find, uf_init, uf_union } from "./union_find.js";
6
8
 
@@ -149,8 +151,11 @@ export class IslandBuilder {
149
151
  * @param {ManifoldStore} manifolds
150
152
  * @param {RigidBody[]} bodies sparse, indexed by body index
151
153
  * @param {Array[]} body_collider_lists sparse, indexed by body index
154
+ * @param {Joint[]} joints live joints (sparse). Jointed dynamic-dynamic
155
+ * bodies are unioned into the same island so a chain / ragdoll sleeps
156
+ * and wakes as a unit. Callers with no joints pass an empty array.
152
157
  */
153
- build(storage, manifolds, bodies, body_collider_lists) {
158
+ build(storage, manifolds, bodies, body_collider_lists, joints) {
154
159
  const hwm = storage.high_water_mark;
155
160
  this.__ensure_body_capacity(hwm);
156
161
 
@@ -174,6 +179,29 @@ export class IslandBuilder {
174
179
  }
175
180
  }
176
181
 
182
+ // --- Pass 1b: union dynamic-dynamic bodies connected by a joint, so a
183
+ // chain / ragdoll forms one island (and so sleeps/wakes as a unit).
184
+ // Joint-to-world and joint-to-static/kinematic anchor the island
185
+ // without enlarging it — same rule as a static contact. Stale joint
186
+ // references (body unlinked / slot reused) are filtered by the
187
+ // generation-checked `is_valid`. (Empty when the caller has no
188
+ // joints — the loop is then a no-op.)
189
+ const jn = joints.length;
190
+ for (let i = 0; i < jn; i++) {
191
+ const joint = joints[i];
192
+ if (joint === undefined || joint === null) continue;
193
+ if (joint._bodyIdB === JOINT_WORLD) continue;
194
+ if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
195
+ const idxA = body_id_index(joint._bodyIdA);
196
+ const idxB = body_id_index(joint._bodyIdB);
197
+ const rbA = bodies[idxA];
198
+ const rbB = bodies[idxB];
199
+ if (rbA === undefined || rbB === undefined) continue;
200
+ if (rbA.kind === BodyKind.Dynamic && rbB.kind === BodyKind.Dynamic) {
201
+ uf_union(parent, idxA, idxB);
202
+ }
203
+ }
204
+
177
205
  // --- Pass 2: collect distinct roots over awake dynamic bodies.
178
206
  const island_of_body = this.island_of_body;
179
207
  // Cheap reset: only the indices we may write are below hwm.
@@ -357,7 +385,7 @@ export class IslandBuilder {
357
385
  */
358
386
  __ensure_body_capacity(n) {
359
387
  if (this.parent.length < n) {
360
- const cap = next_pow2(n);
388
+ const cap = ceilPowerOfTwo(n);
361
389
  this.parent = new Uint32Array(cap);
362
390
  this.island_of_body = new Int32Array(cap);
363
391
  this.__root_to_island = new Int32Array(cap);
@@ -371,7 +399,7 @@ export class IslandBuilder {
371
399
  */
372
400
  __ensure_island_count_capacity(n) {
373
401
  if (this.body_offsets.length < n + 1) {
374
- const cap = next_pow2(n + 1);
402
+ const cap = ceilPowerOfTwo(n + 1);
375
403
  this.body_offsets = new Uint32Array(cap);
376
404
  this.contact_offsets = new Uint32Array(cap);
377
405
  this.__cursors = new Uint32Array(cap);
@@ -384,7 +412,7 @@ export class IslandBuilder {
384
412
  */
385
413
  __ensure_body_data_capacity(n) {
386
414
  if (this.body_data.length < n) {
387
- this.body_data = new Uint32Array(next_pow2(n));
415
+ this.body_data = new Uint32Array(ceilPowerOfTwo(n));
388
416
  }
389
417
  }
390
418
 
@@ -394,18 +422,7 @@ export class IslandBuilder {
394
422
  */
395
423
  __ensure_contact_data_capacity(n) {
396
424
  if (this.contact_data.length < n) {
397
- this.contact_data = new Uint32Array(next_pow2(n));
425
+ this.contact_data = new Uint32Array(ceilPowerOfTwo(n));
398
426
  }
399
427
  }
400
428
  }
401
-
402
- /**
403
- * @param {number} n
404
- * @returns {number}
405
- */
406
- function next_pow2(n) {
407
- if (n <= 1) return 1;
408
- let p = 1;
409
- while (p < n) p <<= 1;
410
- return p;
411
- }
@@ -1 +1 @@
1
- {"version":3,"file":"box_box_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/box_box_manifold.js"],"names":[],"mappings":"AA+HA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,sCAvBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CA0anB;AAvhBD;;;GAGG;AACH,iCAFU,MAAM,CAEoD"}
1
+ {"version":3,"file":"box_box_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/box_box_manifold.js"],"names":[],"mappings":"AAqJA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,sCAvBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CA8anB;AA3hBD;;;GAGG;AACH,iCAFU,MAAM,CAEoD"}
@@ -39,6 +39,28 @@ const MAX_CONTACTS = 4;
39
39
  const CONTACT_STRIDE = 7;
40
40
  const PARALLEL_EPS_SQR = 1e-8;
41
41
 
42
+ /**
43
+ * Reference-axis tie-break deadband. SAT picks the minimum-overlap axis, but
44
+ * for aligned boxes two axes (A's face normal and B's face normal along the
45
+ * contact direction) have *equal* overlap, so floating-point noise from a
46
+ * sub-degree wobble flips which one wins frame to frame. That flips the
47
+ * reference/incident assignment, which reorders the contact points, which
48
+ * flips the solver's Gauss-Seidel sweep order — injecting an alternating
49
+ * bias that makes symmetric stacks creep and never sleep.
50
+ *
51
+ * The deadband makes a later axis replace the current best only if it
52
+ * reduces the overlap by more than `best · TIE_REL + TIE_ABS`. Axes are
53
+ * tested A-faces, then B-faces, then edge-crosses, so ties resolve to the
54
+ * earlier axis (A's face, then a face over an edge) — a stable, consistent
55
+ * choice. Genuine minima (overlap smaller by more than the band) still win,
56
+ * so SAT correctness for non-aligned boxes is unchanged; only the
57
+ * noise-driven flip on near-perfect alignment is suppressed. Box2D's
58
+ * `b2CollidePolygons` uses the same relative+absolute hysteresis.
59
+ * @type {number}
60
+ */
61
+ const TIE_REL = 0.02;
62
+ const TIE_ABS = 1e-5;
63
+
42
64
  /**
43
65
  * Length of `out` required by {@link box_box_manifold}.
44
66
  * @type {number}
@@ -182,7 +204,11 @@ export function box_box_manifold(
182
204
  const dist = proj < 0 ? -proj : proj;
183
205
  const overlap = (rA + rB) - dist;
184
206
  if (overlap < 0) return true;
185
- if (overlap < best_overlap) {
207
+ // Deadband: the first axis always wins; a later axis replaces it only
208
+ // if it reduces the overlap past the hysteresis band. This biases ties
209
+ // toward earlier-tested axes (A faces > B faces > edges) so aligned
210
+ // boxes pick a stable reference instead of flip-flopping on noise.
211
+ if (best_source === -1 || overlap < best_overlap - (best_overlap * TIE_REL + TIE_ABS)) {
186
212
  best_overlap = overlap;
187
213
  const sign = proj > 0 ? -1 : 1; // normal points B→A
188
214
  best_nx = ux * sign;
@@ -14,4 +14,37 @@ export function narrowphase_step(pair_list: PairList, manifolds: ManifoldStore,
14
14
  collider: Collider;
15
15
  transform: Transform;
16
16
  }>>): void;
17
+ /**
18
+ * Re-detect contact GEOMETRY for one existing manifold slot at the bodies'
19
+ * current poses, updating the witness points / normal / depth of the slot's
20
+ * existing contacts in place. Does NOT change the contact count, the
21
+ * feature ids, or the accumulated impulses — it only refreshes geometry.
22
+ *
23
+ * This is the per-substep concave path (TGS): for a contact pair involving a
24
+ * concave body, the contact *feature* (which triangle is deepest, and its
25
+ * normal) genuinely changes as the body rocks, so the solver's cheap analytic
26
+ * refresh — which freezes the feature for the whole outer step — pumps energy
27
+ * in. Re-running the narrowphase geometry each substep gives a fresh,
28
+ * correct normal so the body settles instead of rocking. Convex pairs keep
29
+ * the analytic refresh and never call this (their feature is stable).
30
+ *
31
+ * Matching is by feature id (stable per-triangle for the decomposition path),
32
+ * so a contact's geometry tracks the same triangle across substeps. A contact
33
+ * whose triangle isn't found this substep keeps its previous geometry (a rare
34
+ * transient; the once-per-frame {@link narrowphase_step} re-establishes the
35
+ * contact set next outer step). Count never changes here, so the solver's
36
+ * per-contact scratch (sized once at prepare) stays aligned.
37
+ *
38
+ * @param {ManifoldStore} manifolds
39
+ * @param {number} slot
40
+ * @param {Array<{collider: Collider, transform: Transform}>} list_a
41
+ * @param {Array<{collider: Collider, transform: Transform}>} list_b
42
+ */
43
+ export function redetect_pair_geometry(manifolds: ManifoldStore, slot: number, list_a: Array<{
44
+ collider: Collider;
45
+ transform: Transform;
46
+ }>, list_b: Array<{
47
+ collider: Collider;
48
+ transform: Transform;
49
+ }>): void;
17
50
  //# sourceMappingURL=narrowphase_step.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA6uCA;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAyJlE"}
1
+ {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA6uCA;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAyJlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QAiD3D"}
@@ -1420,3 +1420,78 @@ export function narrowphase_step(pair_list, manifolds, lists) {
1420
1420
  }
1421
1421
  }
1422
1422
  }
1423
+
1424
+ /**
1425
+ * Re-detect contact GEOMETRY for one existing manifold slot at the bodies'
1426
+ * current poses, updating the witness points / normal / depth of the slot's
1427
+ * existing contacts in place. Does NOT change the contact count, the
1428
+ * feature ids, or the accumulated impulses — it only refreshes geometry.
1429
+ *
1430
+ * This is the per-substep concave path (TGS): for a contact pair involving a
1431
+ * concave body, the contact *feature* (which triangle is deepest, and its
1432
+ * normal) genuinely changes as the body rocks, so the solver's cheap analytic
1433
+ * refresh — which freezes the feature for the whole outer step — pumps energy
1434
+ * in. Re-running the narrowphase geometry each substep gives a fresh,
1435
+ * correct normal so the body settles instead of rocking. Convex pairs keep
1436
+ * the analytic refresh and never call this (their feature is stable).
1437
+ *
1438
+ * Matching is by feature id (stable per-triangle for the decomposition path),
1439
+ * so a contact's geometry tracks the same triangle across substeps. A contact
1440
+ * whose triangle isn't found this substep keeps its previous geometry (a rare
1441
+ * transient; the once-per-frame {@link narrowphase_step} re-establishes the
1442
+ * contact set next outer step). Count never changes here, so the solver's
1443
+ * per-contact scratch (sized once at prepare) stays aligned.
1444
+ *
1445
+ * @param {ManifoldStore} manifolds
1446
+ * @param {number} slot
1447
+ * @param {Array<{collider: Collider, transform: Transform}>} list_a
1448
+ * @param {Array<{collider: Collider, transform: Transform}>} list_b
1449
+ */
1450
+ export function redetect_pair_geometry(manifolds, slot, list_a, list_b) {
1451
+ if (list_a === undefined || list_b === undefined) return;
1452
+ const la_len = list_a.length;
1453
+ const lb_len = list_b.length;
1454
+ if (la_len === 0 || lb_len === 0) return;
1455
+
1456
+ const count = manifolds.contact_count(slot);
1457
+ if (count === 0) return;
1458
+
1459
+ const gjk_axis_buf = manifolds.slot_axis_buffer;
1460
+ const gjk_axis_off = manifolds.slot_axis_offset(slot);
1461
+
1462
+ let cc = 0;
1463
+ for (let a = 0; a < la_len; a++) {
1464
+ const ea = list_a[a];
1465
+ for (let b = 0; b < lb_len; b++) {
1466
+ const eb = list_b[b];
1467
+ cc = dispatch_pair(cc, ea.collider, ea.transform, eb.collider, eb.transform,
1468
+ gjk_axis_buf, gjk_axis_off);
1469
+ }
1470
+ }
1471
+ if (cc === 0) return; // nothing re-detected this substep — keep frozen geometry
1472
+
1473
+ const data = manifolds.data_buffer;
1474
+ const slot_off = manifolds.slot_data_offset(slot);
1475
+ for (let j = 0; j < count; j++) {
1476
+ const off = slot_off + j * CONTACT_STRIDE;
1477
+ const fid = data[off + 13];
1478
+ if (fid === 0) continue; // no feature info to match on
1479
+ for (let k = 0; k < cc; k++) {
1480
+ const co = k * CANDIDATE_STRIDE;
1481
+ if (candidates[co + 10] === fid) {
1482
+ // Overwrite geometry only: witnesses, normal, depth.
1483
+ data[off] = candidates[co];
1484
+ data[off + 1] = candidates[co + 1];
1485
+ data[off + 2] = candidates[co + 2];
1486
+ data[off + 3] = candidates[co + 3];
1487
+ data[off + 4] = candidates[co + 4];
1488
+ data[off + 5] = candidates[co + 5];
1489
+ data[off + 6] = candidates[co + 6];
1490
+ data[off + 7] = candidates[co + 7];
1491
+ data[off + 8] = candidates[co + 8];
1492
+ data[off + 9] = candidates[co + 9];
1493
+ break;
1494
+ }
1495
+ }
1496
+ }
1497
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * # Local-frame ray ↔ primitive intersections (raycast narrowphase)
3
+ *
4
+ * Each function intersects a ray, **expressed in the shape's local frame**,
5
+ * against a canonical primitive at the origin (sphere at the origin; box
6
+ * axis-aligned spanning `[-h, +h]`; capsule along the local Y axis). They
7
+ * return the hit distance `t` along the ray (`Infinity` on a miss) and write
8
+ * the **local** outward surface normal (unit) into `outNormal[0..2]`.
9
+ *
10
+ * The ray-narrowphase dispatch transforms the world ray into the body's local
11
+ * frame once (rotate by the inverse body rotation — a unit direction stays
12
+ * unit, so `t` is preserved), calls the matching primitive, then rotates the
13
+ * returned local normal back to world. Keeping the primitives canonical lets
14
+ * the box and capsule tests be axis-aligned (cheap slab / cylinder math) and
15
+ * shares a single transform across them.
16
+ *
17
+ * Conventions:
18
+ * - the ray direction is unit length (the caller guarantees it);
19
+ * - the first surface crossing at or after the origin within `tMax` is
20
+ * returned; a ray starting inside the shape returns its exit crossing;
21
+ * - the normal is the geometric outward surface normal at the hit.
22
+ *
23
+ * @author Alex Goldring
24
+ * @copyright Company Named Limited (c) 2026
25
+ */
26
+ /**
27
+ * Ray vs a sphere of radius `r` centred at the local origin.
28
+ *
29
+ * @param {Float64Array} outNormal length-3, written on hit
30
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (local)
31
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (local, unit)
32
+ * @param {number} tMax
33
+ * @param {number} r sphere radius
34
+ * @returns {number} hit distance, or `Infinity` on miss
35
+ */
36
+ export function ray_sphere_local(outNormal: Float64Array, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tMax: number, r: number): number;
37
+ /**
38
+ * Ray vs an axis-aligned box spanning `[-hx,hx] × [-hy,hy] × [-hz,hz]` at the
39
+ * local origin (the canonical pose of {@link BoxShape3D}). Slab method, with
40
+ * the entry (or, origin-inside, exit) face's outward normal.
41
+ *
42
+ * @param {Float64Array} outNormal length-3, written on hit
43
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (local)
44
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (local, unit)
45
+ * @param {number} tMax
46
+ * @param {number} hx @param {number} hy @param {number} hz half-extents
47
+ * @returns {number} hit distance, or `Infinity` on miss
48
+ */
49
+ export function ray_box_local(outNormal: Float64Array, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tMax: number, hx: number, hy: number, hz: number): number;
50
+ /**
51
+ * Ray vs a capsule along the local Y axis: a cylinder of radius `r` over
52
+ * `y ∈ [−hh, hh]` capped by hemispheres of radius `r` at `(0, ±hh, 0)` — the
53
+ * canonical pose of {@link CapsuleShape3D} (`hh = height/2`). Tests the
54
+ * infinite cylinder (clamped to the segment) and the two cap spheres, taking
55
+ * the nearest valid crossing.
56
+ *
57
+ * @param {Float64Array} outNormal length-3, written on hit
58
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (local)
59
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (local, unit)
60
+ * @param {number} tMax
61
+ * @param {number} r capsule radius
62
+ * @param {number} hh half-height of the cylindrical section (`height/2`)
63
+ * @returns {number} hit distance, or `Infinity` on miss
64
+ */
65
+ export function ray_capsule_local(outNormal: Float64Array, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tMax: number, r: number, hh: number): number;
66
+ //# sourceMappingURL=ray_shapes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ray_shapes.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/ray_shapes.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;;;GASG;AACH,4CAPW,YAAY,MACZ,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,MAAa,MAAM,MAAa,MAAM,QAC5C,MAAM,KACN,MAAM,GACJ,MAAM,CAiBlB;AAED;;;;;;;;;;;GAWG;AACH,yCAPW,YAAY,MACZ,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,MAAa,MAAM,MAAa,MAAM,QAC5C,MAAM,MACN,MAAM,MAAa,MAAM,MAAa,MAAM,GAC1C,MAAM,CA+ClB;AAED;;;;;;;;;;;;;;GAcG;AACH,6CARW,YAAY,MACZ,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,MAAa,MAAM,MAAa,MAAM,QAC5C,MAAM,KACN,MAAM,MACN,MAAM,GACJ,MAAM,CA6DlB"}