@woosh/meep-engine 2.157.0 → 2.158.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/PosedShape3D.d.ts +17 -0
  3. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/PosedShape3D.js +50 -0
  5. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  6. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  7. package/src/engine/graphics/ecs/trail2d/Trail2DFlags.d.ts +1 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2DFlags.js +9 -1
  9. package/src/engine/physics/fluid/FluidField.d.ts +53 -9
  10. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  11. package/src/engine/physics/fluid/FluidField.js +684 -600
  12. package/src/engine/physics/fluid/FluidSimulator.d.ts +53 -38
  13. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  14. package/src/engine/physics/fluid/FluidSimulator.js +252 -178
  15. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +155 -26
  16. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts +72 -0
  17. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts.map +1 -0
  18. package/src/engine/physics/fluid/ecs/FluidObstacle.js +97 -0
  19. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +117 -0
  20. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts.map +1 -0
  21. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.js +348 -0
  22. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  23. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts +62 -12
  24. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts.map +1 -1
  25. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +135 -38
  26. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts.map +1 -1
  27. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +85 -38
  28. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts.map +1 -1
  29. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +104 -50
  30. package/src/engine/physics/fluid/prototype.js +25 -1
  31. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.d.ts +30 -0
  32. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.d.ts.map +1 -0
  33. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.js +92 -0
  34. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.d.ts +42 -0
  35. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.d.ts.map +1 -0
  36. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.js +319 -0
  37. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts +53 -0
  38. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts.map +1 -0
  39. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.js +236 -0
  40. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.d.ts +46 -0
  41. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.d.ts.map +1 -0
  42. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.js +217 -0
  43. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.d.ts +40 -0
  44. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.d.ts.map +1 -0
  45. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.js +165 -0
  46. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.d.ts +44 -0
  47. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.d.ts.map +1 -0
  48. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.js +95 -0
  49. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.d.ts +38 -0
  50. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.d.ts.map +1 -0
  51. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.js +77 -0
  52. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.d.ts +52 -0
  53. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.d.ts.map +1 -0
  54. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.js +131 -0
  55. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.d.ts +38 -0
  56. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.d.ts.map +1 -0
  57. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.js +104 -0
  58. package/src/engine/physics/fluid/effector/AmbientWindFluidEffector.d.ts +0 -41
  59. package/src/engine/physics/fluid/effector/AmbientWindFluidEffector.d.ts.map +0 -1
  60. package/src/engine/physics/fluid/effector/AmbientWindFluidEffector.js +0 -124
@@ -3,67 +3,156 @@ import { computeHashFloat } from "../../../../core/primitives/numbers/computeHas
3
3
  import { AbstractFluidEffector } from "./AbstractFluidEffector.js";
4
4
 
5
5
  /**
6
- * Adds the same acceleration to every fluid cell each step (gravity, ambient wind).
6
+ * The global atmosphere: every fluid face in every field integrates the same
7
+ * linear flow model each step,
7
8
  *
8
- * `force` is in WORLD units per second squared. The `world_to_grid` matrix is applied
9
- * (as a direction transform — translation ignored) inside {@link apply} to convert
10
- * the force into the target field's grid-cell units before depositing. The same
11
- * GlobalFluidEffector applied to fields with different `cell_size` therefore produces
12
- * the same world-space acceleration on each — gravity stays gravity.
9
+ * dv/dt = force + drag · (wind v)
13
10
  *
14
- * Solid cells are left untouched — the projection step would zero them anyway, but
15
- * skipping the write is slightly cheaper and keeps the solid mask authoritative for
16
- * effectors as well as the solver.
11
+ * One effector covers the whole family of "push everything" behaviours
12
+ * pick the regime by which fields you set:
13
+ *
14
+ * - **`force` alone** — constant body force (gravity, buoyancy). NOTE:
15
+ * incompressible projection cannot oppose a uniform force (a uniform
16
+ * field is divergence-free), so without `drag`, a sealed container, or
17
+ * {@link FluidSimulator#velocity_damping}, the velocity grows without
18
+ * bound. This is the legacy GlobalFluidEffector behaviour.
19
+ *
20
+ * - **`wind` + `drag`** — prevailing wind: the air relaxes toward the wind
21
+ * velocity at `drag` per second; gusts, wakes and splats decay back to
22
+ * the ambient flow. The fixed point is exactly `wind`.
23
+ *
24
+ * - **`force` + `drag` (+ `wind`)** — bounded forcing with a terminal
25
+ * velocity of `wind + force / drag`: falling smoke, drifting ash, rain
26
+ * sheets in a breeze.
27
+ *
28
+ * - **`drag` alone** — pure exponential calming toward still air. (The
29
+ * scene-content twin of {@link FluidSimulator#velocity_damping}, which
30
+ * is the solver-level stability knob that exists even when no effectors
31
+ * are wired.)
32
+ *
33
+ * `wind` only enters through the drag term — with `drag = 0` it is inert by
34
+ * construction (the model degenerates to dv/dt = force).
35
+ *
36
+ * The step uses the EXACT solution of the linear ODE, not an Euler step:
37
+ *
38
+ * v ← v∞ + (v − v∞) · e^(−drag·dt), v∞ = wind + force / drag
39
+ *
40
+ * (and plain `v += force·dt`, also exact, when `drag = 0`). Exactness makes
41
+ * the effector frame-rate independent — two half-steps land exactly where
42
+ * one full step does, including with both terms active.
43
+ *
44
+ * All quantities are WORLD-space: `force` in units/s², `wind` in units/s,
45
+ * `drag` in 1/s. The `world_to_grid` matrix's linear part converts the two
46
+ * vectors at apply time, so one instance produces the same world-space
47
+ * atmosphere on fields of any `cell_size` — gravity stays gravity.
48
+ *
49
+ * Pinned faces (either adjacent cell solid) are skipped — the projection
50
+ * owns those as boundary conditions, and skipping keeps the solid mask
51
+ * authoritative for effectors as well as the solver.
17
52
  */
18
53
  export class GlobalFluidEffector extends AbstractFluidEffector {
19
54
 
20
55
  /**
21
- * Acceleration vector in WORLD units per second squared (e.g. `[0, -9.8, 0]` for
22
- * Earth gravity in metres-and-seconds). Per-step deposit into the velocity field
23
- * is `world_to_grid · force × dt`.
56
+ * Body-force acceleration in WORLD units per second squared (e.g.
57
+ * `[0, -9.8, 0]` for Earth gravity in metres-and-seconds).
24
58
  *
25
59
  * @type {[number, number, number]}
26
60
  */
27
61
  force = [0, 0, 0];
28
62
 
63
+ /**
64
+ * Ambient wind velocity in WORLD units per second — the velocity the air
65
+ * relaxes toward. Only effective when {@link drag} is non-zero.
66
+ *
67
+ * @type {[number, number, number]}
68
+ */
69
+ wind = [0, 0, 0];
70
+
71
+ /**
72
+ * Relaxation rate toward the ambient flow, per second. Higher = stiffer
73
+ * atmosphere that re-asserts itself quickly after disturbances; lower =
74
+ * gusts and wakes linger. A disturbance decays to ~37% in `1/drag`
75
+ * seconds. `0` disables the relaxation term entirely.
76
+ *
77
+ * @type {number}
78
+ */
79
+ drag = 0;
80
+
29
81
  /**
30
82
  * @param {FluidField} field
31
83
  * @param {number} dt
32
84
  * @param {Float32Array|number[]} world_to_grid Column-major 4×4 affine; only the
33
85
  * linear (upper-left 3×3) part is
34
- * used since force is a direction.
86
+ * used since force and wind are
87
+ * directions.
35
88
  */
36
89
  apply(field, dt, world_to_grid) {
90
+ const m = world_to_grid;
91
+
92
+ // Direction-transform both vectors into grid units (translation in
93
+ // m[12..14] is for points, not directions). For the axis-aligned
94
+ // uniform-scale matrices built by FluidSystem this collapses to
95
+ // division by cell_size.
37
96
  const wfx = this.force[0];
38
97
  const wfy = this.force[1];
39
98
  const wfz = this.force[2];
99
+ const ax = m[0] * wfx + m[4] * wfy + m[8] * wfz;
100
+ const ay = m[1] * wfx + m[5] * wfy + m[9] * wfz;
101
+ const az = m[2] * wfx + m[6] * wfy + m[10] * wfz;
40
102
 
41
- if (wfx === 0 && wfy === 0 && wfz === 0) {
42
- return;
43
- }
103
+ const vu = field.velocity_x;
104
+ const vv = field.velocity_y;
105
+ const vw = field.velocity_z;
106
+ const pin_u = field.face_solid_x;
107
+ const pin_v = field.face_solid_y;
108
+ const pin_w = field.face_solid_z;
109
+ const n_u = vu.length;
110
+ const n_v = vv.length;
111
+ const n_w = vw.length;
44
112
 
45
- // Direction-transform: apply the linear part of world_to_grid (translation in
46
- // m[12..14] is for points, not directions). For the axis-aligned uniform-scale
47
- // matrices built by FluidSystem this collapses to division by cell_size, so
48
- // world m/s² becomes grid cells/s².
49
- const m = world_to_grid;
50
- const fx = (m[0] * wfx + m[4] * wfy + m[8] * wfz) * dt;
51
- const fy = (m[1] * wfx + m[5] * wfy + m[9] * wfz) * dt;
52
- const fz = (m[2] * wfx + m[6] * wfy + m[10] * wfz) * dt;
113
+ if (this.drag > 0) {
114
+ const wwx = this.wind[0];
115
+ const wwy = this.wind[1];
116
+ const wwz = this.wind[2];
117
+ const gx = m[0] * wwx + m[4] * wwy + m[8] * wwz;
118
+ const gy = m[1] * wwx + m[5] * wwy + m[9] * wwz;
119
+ const gz = m[2] * wwx + m[6] * wwy + m[10] * wwz;
53
120
 
54
- const vx = field.velocity_x;
55
- const vy = field.velocity_y;
56
- const vz = field.velocity_z;
57
- const solid = field.solid;
58
- const n = vx.length;
121
+ // Exact step of dv/dt = a + k(w − v): relax toward the terminal
122
+ // velocity v∞ = w + a/k by 1 − e^(−k·dt).
123
+ const inv_drag = 1 / this.drag;
124
+ const tx = gx + ax * inv_drag;
125
+ const ty = gy + ay * inv_drag;
126
+ const tz = gz + az * inv_drag;
127
+ const blend = 1 - Math.exp(-this.drag * dt);
59
128
 
60
- for (let i = 0; i < n; i++) {
61
- if (solid[i] !== 0) {
62
- continue;
129
+ for (let i = 0; i < n_u; i++) {
130
+ if (pin_u[i] === 0) vu[i] += blend * (tx - vu[i]);
131
+ }
132
+ for (let i = 0; i < n_v; i++) {
133
+ if (pin_v[i] === 0) vv[i] += blend * (ty - vv[i]);
63
134
  }
64
- vx[i] += fx;
65
- vy[i] += fy;
66
- vz[i] += fz;
135
+ for (let i = 0; i < n_w; i++) {
136
+ if (pin_w[i] === 0) vw[i] += blend * (tz - vw[i]);
137
+ }
138
+ return;
139
+ }
140
+
141
+ // No drag: pure body force, v += a·dt (exact for constant acceleration).
142
+ const dx = ax * dt;
143
+ const dy = ay * dt;
144
+ const dz = az * dt;
145
+ if (dx === 0 && dy === 0 && dz === 0) {
146
+ return;
147
+ }
148
+ for (let i = 0; i < n_u; i++) {
149
+ if (pin_u[i] === 0) vu[i] += dx;
150
+ }
151
+ for (let i = 0; i < n_v; i++) {
152
+ if (pin_v[i] === 0) vv[i] += dy;
153
+ }
154
+ for (let i = 0; i < n_w; i++) {
155
+ if (pin_w[i] === 0) vw[i] += dz;
67
156
  }
68
157
  }
69
158
 
@@ -78,9 +167,13 @@ export class GlobalFluidEffector extends AbstractFluidEffector {
78
167
  if (!(other instanceof GlobalFluidEffector)) {
79
168
  return false;
80
169
  }
81
- return this.force[0] === other.force[0]
170
+ return this.drag === other.drag
171
+ && this.force[0] === other.force[0]
82
172
  && this.force[1] === other.force[1]
83
- && this.force[2] === other.force[2];
173
+ && this.force[2] === other.force[2]
174
+ && this.wind[0] === other.wind[0]
175
+ && this.wind[1] === other.wind[1]
176
+ && this.wind[2] === other.wind[2];
84
177
  }
85
178
 
86
179
  /**
@@ -90,7 +183,11 @@ export class GlobalFluidEffector extends AbstractFluidEffector {
90
183
  return combine_hash(
91
184
  computeHashFloat(this.force[0]),
92
185
  computeHashFloat(this.force[1]),
93
- computeHashFloat(this.force[2])
186
+ computeHashFloat(this.force[2]),
187
+ computeHashFloat(this.wind[0]),
188
+ computeHashFloat(this.wind[1]),
189
+ computeHashFloat(this.wind[2]),
190
+ computeHashFloat(this.drag)
94
191
  );
95
192
  }
96
193
 
@@ -1 +1 @@
1
- {"version":3,"file":"ImpulseFluidEffector.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/effector/ImpulseFluidEffector.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;GAiBG;AACH;IAEI;;;OAGG;IACH,UAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEb;IAErB;;;;;;OAMG;IACH,OAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEhB;IAElB;;;;;;OAMG;IACH,QAFU,MAAM,CAEL;CAgKd;sCA9MqC,4BAA4B"}
1
+ {"version":3,"file":"ImpulseFluidEffector.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/effector/ImpulseFluidEffector.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;GAiBG;AACH;IAEI;;;OAGG;IACH,UAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEb;IAErB;;;;;;OAMG;IACH,OAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEhB;IAElB;;;;;;OAMG;IACH,QAFU,MAAM,CAEL;CA+Md;sCA7PqC,4BAA4B"}
@@ -93,52 +93,99 @@ export class ImpulseFluidEffector extends AbstractFluidEffector {
93
93
  const res_x = res[0];
94
94
  const res_y = res[1];
95
95
  const res_z = res[2];
96
- const slice_size = res_x * res_y;
97
-
98
- const x_min = Math.max(0, Math.floor(px - r));
99
- const x_max = Math.min(res_x - 1, Math.ceil(px + r));
100
- const y_min = Math.max(0, Math.floor(py - r));
101
- const y_max = Math.min(res_y - 1, Math.ceil(py + r));
102
- const z_min = Math.max(0, Math.floor(pz - r));
103
- const z_max = Math.min(res_z - 1, Math.ceil(pz + r));
104
-
105
- if (x_min > x_max || y_min > y_max || z_min > z_max) {
106
- return;
107
- }
108
96
 
109
97
  const r_inv = 1 / r;
110
98
 
111
- const vx = field.velocity_x;
112
- const vy = field.velocity_y;
113
- const vz = field.velocity_z;
114
- const solid = field.solid;
115
-
116
- for (let z = z_min; z <= z_max; z++) {
117
- const dz = z - pz;
118
- const z_off = z * slice_size;
119
-
120
- for (let y = y_min; y <= y_max; y++) {
121
- const dy = y - py;
122
- const y_off = y * res_x;
123
-
124
- for (let x = x_min; x <= x_max; x++) {
125
- const dx = x - px;
99
+ // Each velocity component deposits onto its own face lattice, with the
100
+ // falloff evaluated at the face position (the lattice is offset by
101
+ // −0.5 on its own axis relative to cell centers). Pinned faces
102
+ // (adjacent solid) are skipped.
126
103
 
127
- const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
128
- const t = dist * r_inv;
129
- if (t >= 1) {
130
- continue;
104
+ // u (x-faces) — face (x, y, z) sits at (x 0.5, y, z).
105
+ {
106
+ const vu = field.velocity_x;
107
+ const pin = field.face_solid_x;
108
+ const sx = res_x + 1;
109
+ const x_min = Math.max(0, Math.floor(px + 0.5 - r));
110
+ const x_max = Math.min(res_x, Math.ceil(px + 0.5 + r));
111
+ const y_min = Math.max(0, Math.floor(py - r));
112
+ const y_max = Math.min(res_y - 1, Math.ceil(py + r));
113
+ const z_min = Math.max(0, Math.floor(pz - r));
114
+ const z_max = Math.min(res_z - 1, Math.ceil(pz + r));
115
+ for (let z = z_min; z <= z_max; z++) {
116
+ const dz = z - pz;
117
+ const z_off = z * sx * res_y;
118
+ for (let y = y_min; y <= y_max; y++) {
119
+ const dy = y - py;
120
+ const y_off = z_off + y * sx;
121
+ for (let x = x_min; x <= x_max; x++) {
122
+ const dx = x - 0.5 - px;
123
+ const t = Math.sqrt(dx * dx + dy * dy + dz * dz) * r_inv;
124
+ if (t >= 1) continue;
125
+ const f = y_off + x;
126
+ if (pin[f] !== 0) continue;
127
+ const w = (1 - t) * (1 - t);
128
+ vu[f] += fx * w;
131
129
  }
132
- const w = (1 - t) * (1 - t);
130
+ }
131
+ }
132
+ }
133
133
 
134
- const c = z_off + y_off + x;
135
- if (solid[c] !== 0) {
136
- continue;
134
+ // v (y-faces) face (x, y, z) sits at (x, y − 0.5, z).
135
+ {
136
+ const vv = field.velocity_y;
137
+ const pin = field.face_solid_y;
138
+ const x_min = Math.max(0, Math.floor(px - r));
139
+ const x_max = Math.min(res_x - 1, Math.ceil(px + r));
140
+ const y_min = Math.max(0, Math.floor(py + 0.5 - r));
141
+ const y_max = Math.min(res_y, Math.ceil(py + 0.5 + r));
142
+ const z_min = Math.max(0, Math.floor(pz - r));
143
+ const z_max = Math.min(res_z - 1, Math.ceil(pz + r));
144
+ for (let z = z_min; z <= z_max; z++) {
145
+ const dz = z - pz;
146
+ const z_off = z * res_x * (res_y + 1);
147
+ for (let y = y_min; y <= y_max; y++) {
148
+ const dy = y - 0.5 - py;
149
+ const y_off = z_off + y * res_x;
150
+ for (let x = x_min; x <= x_max; x++) {
151
+ const dx = x - px;
152
+ const t = Math.sqrt(dx * dx + dy * dy + dz * dz) * r_inv;
153
+ if (t >= 1) continue;
154
+ const f = y_off + x;
155
+ if (pin[f] !== 0) continue;
156
+ const w = (1 - t) * (1 - t);
157
+ vv[f] += fy * w;
137
158
  }
159
+ }
160
+ }
161
+ }
138
162
 
139
- vx[c] += fx * w;
140
- vy[c] += fy * w;
141
- vz[c] += fz * w;
163
+ // w (z-faces) face (x, y, z) sits at (x, y, z − 0.5).
164
+ {
165
+ const vw = field.velocity_z;
166
+ const pin = field.face_solid_z;
167
+ const slice = res_x * res_y;
168
+ const x_min = Math.max(0, Math.floor(px - r));
169
+ const x_max = Math.min(res_x - 1, Math.ceil(px + r));
170
+ const y_min = Math.max(0, Math.floor(py - r));
171
+ const y_max = Math.min(res_y - 1, Math.ceil(py + r));
172
+ const z_min = Math.max(0, Math.floor(pz + 0.5 - r));
173
+ const z_max = Math.min(res_z, Math.ceil(pz + 0.5 + r));
174
+ for (let z = z_min; z <= z_max; z++) {
175
+ const dz = z - 0.5 - pz;
176
+ const z_off = z * slice;
177
+ for (let y = y_min; y <= y_max; y++) {
178
+ const dy = y - py;
179
+ const y_off = z_off + y * res_x;
180
+ for (let x = x_min; x <= x_max; x++) {
181
+ const dx = x - px;
182
+ const t = Math.sqrt(dx * dx + dy * dy + dz * dz) * r_inv;
183
+ if (t >= 1) continue;
184
+ const f = y_off + x;
185
+ if (pin[f] !== 0) continue;
186
+ const w = (1 - t) * (1 - t);
187
+ vw[f] += fz * w;
188
+ }
142
189
  }
143
190
  }
144
191
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WakeFluidEffector.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/effector/WakeFluidEffector.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH;IAEI;;;;;OAKG;IACH,UAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEb;IAErB;;;;;;OAMG;IACH,mBAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEJ;IAE9B;;;;;OAKG;IACH,QAFU,MAAM,CAEL;IAEX;;;;;OAKG;IACH,UAFU,MAAM,CAEH;IAoBb;;;;;OAKG;IACH,oBAKC;;CAwOJ;sCA3VqC,4BAA4B"}
1
+ {"version":3,"file":"WakeFluidEffector.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/effector/WakeFluidEffector.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH;IAEI;;;;;OAKG;IACH,UAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEb;IAErB;;;;;;OAMG;IACH,mBAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEJ;IAE9B;;;;;OAKG;IACH,QAFU,MAAM,CAEL;IAEX;;;;;OAKG;IACH,UAFU,MAAM,CAEH;IAoBb;;;;;OAKG;IACH,oBAKC;;CA8RJ;sCAjZqC,4BAA4B"}
@@ -204,67 +204,121 @@ export class WakeFluidEffector extends AbstractFluidEffector {
204
204
  const res_x = res[0];
205
205
  const res_y = res[1];
206
206
  const res_z = res[2];
207
- const slice_size = res_x * res_y;
208
207
 
209
- // AABB of the swept tube, clipped to grid.
210
- const x_min = Math.max(0, Math.floor(Math.min(ax, bx) - r));
211
- const x_max = Math.min(res_x - 1, Math.ceil(Math.max(ax, bx) + r));
212
- const y_min = Math.max(0, Math.floor(Math.min(ay, by) - r));
213
- const y_max = Math.min(res_y - 1, Math.ceil(Math.max(ay, by) + r));
214
- const z_min = Math.max(0, Math.floor(Math.min(az, bz) - r));
215
- const z_max = Math.min(res_z - 1, Math.ceil(Math.max(az, bz) + r));
208
+ const r_inv = 1 / r;
209
+ const inv_seg_len_sq = 1 / seg_len_sq;
216
210
 
217
- if (x_min > x_max || y_min > y_max || z_min > z_max) {
211
+ // Conservative AABB of the swept tube in cell coordinates; each face
212
+ // lattice clips it to its own index range below.
213
+ const box_x_min = Math.min(ax, bx) - r;
214
+ const box_x_max = Math.max(ax, bx) + r;
215
+ const box_y_min = Math.min(ay, by) - r;
216
+ const box_y_max = Math.max(ay, by) + r;
217
+ const box_z_min = Math.min(az, bz) - r;
218
+ const box_z_max = Math.max(az, bz) + r;
219
+
220
+ if (Math.max(0, Math.floor(box_x_min)) > Math.min(res_x, Math.ceil(box_x_max))
221
+ || Math.max(0, Math.floor(box_y_min)) > Math.min(res_y, Math.ceil(box_y_max))
222
+ || Math.max(0, Math.floor(box_z_min)) > Math.min(res_z, Math.ceil(box_z_max))) {
218
223
  // Swept tube doesn't intersect the field at all.
219
224
  this.#advance_trail_if_manual();
220
225
  return;
221
226
  }
222
227
 
223
- const vx = field.velocity_x;
224
- const vy = field.velocity_y;
225
- const vz = field.velocity_z;
226
- const solid = field.solid;
227
-
228
- const r_inv = 1 / r;
229
- const inv_seg_len_sq = 1 / seg_len_sq;
230
-
231
- for (let z = z_min; z <= z_max; z++) {
232
- const z_off = z * slice_size;
233
- const cell_dz = z - az;
234
-
235
- for (let y = y_min; y <= y_max; y++) {
236
- const y_off = y * res_x;
237
- const cell_dy = y - ay;
238
-
239
- for (let x = x_min; x <= x_max; x++) {
240
- const cell_dx = x - ax;
241
-
242
- // Project (cell - a) onto segment direction; clamp to [0, 1].
243
- let t = (cell_dx * seg_dx + cell_dy * seg_dy + cell_dz * seg_dz) * inv_seg_len_sq;
244
- if (t < 0) t = 0;
245
- else if (t > 1) t = 1;
246
-
247
- // Perpendicular offset from the closest point on the segment.
248
- const ndx = cell_dx - t * seg_dx;
249
- const ndy = cell_dy - t * seg_dy;
250
- const ndz = cell_dz - t * seg_dz;
251
-
252
- const dist = Math.sqrt(ndx * ndx + ndy * ndy + ndz * ndz);
253
- const tt = dist * r_inv;
254
- if (tt >= 1) {
255
- continue;
228
+ // Weight of the (1 − d/R)² falloff at a query position, or 0 outside
229
+ // the tube.
230
+ const tube_weight = (qx, qy, qz) => {
231
+ const dx = qx - ax;
232
+ const dy = qy - ay;
233
+ const dz = qz - az;
234
+ let t = (dx * seg_dx + dy * seg_dy + dz * seg_dz) * inv_seg_len_sq;
235
+ if (t < 0) t = 0;
236
+ else if (t > 1) t = 1;
237
+ const ndx = dx - t * seg_dx;
238
+ const ndy = dy - t * seg_dy;
239
+ const ndz = dz - t * seg_dz;
240
+ const tt = Math.sqrt(ndx * ndx + ndy * ndy + ndz * ndz) * r_inv;
241
+ if (tt >= 1) return 0;
242
+ const fall = 1 - tt;
243
+ return fall * fall;
244
+ };
245
+
246
+ // Each component deposits onto its own face lattice at face positions
247
+ // (offset −0.5 on its own axis). Pinned faces are skipped.
248
+
249
+ // u (x-faces).
250
+ {
251
+ const vu = field.velocity_x;
252
+ const pin = field.face_solid_x;
253
+ const sx = res_x + 1;
254
+ const x_min = Math.max(0, Math.floor(box_x_min + 0.5));
255
+ const x_max = Math.min(res_x, Math.ceil(box_x_max + 0.5));
256
+ const y_min = Math.max(0, Math.floor(box_y_min));
257
+ const y_max = Math.min(res_y - 1, Math.ceil(box_y_max));
258
+ const z_min = Math.max(0, Math.floor(box_z_min));
259
+ const z_max = Math.min(res_z - 1, Math.ceil(box_z_max));
260
+ for (let z = z_min; z <= z_max; z++) {
261
+ const z_off = z * sx * res_y;
262
+ for (let y = y_min; y <= y_max; y++) {
263
+ const y_off = z_off + y * sx;
264
+ for (let x = x_min; x <= x_max; x++) {
265
+ const w = tube_weight(x - 0.5, y, z);
266
+ if (w === 0) continue;
267
+ const f = y_off + x;
268
+ if (pin[f] !== 0) continue;
269
+ vu[f] += impulse_x * w;
256
270
  }
271
+ }
272
+ }
273
+ }
257
274
 
258
- const w = (1 - tt) * (1 - tt);
259
-
260
- const c = z_off + y_off + x;
261
- if (solid[c] !== 0) {
262
- continue;
275
+ // v (y-faces).
276
+ {
277
+ const vv = field.velocity_y;
278
+ const pin = field.face_solid_y;
279
+ const x_min = Math.max(0, Math.floor(box_x_min));
280
+ const x_max = Math.min(res_x - 1, Math.ceil(box_x_max));
281
+ const y_min = Math.max(0, Math.floor(box_y_min + 0.5));
282
+ const y_max = Math.min(res_y, Math.ceil(box_y_max + 0.5));
283
+ const z_min = Math.max(0, Math.floor(box_z_min));
284
+ const z_max = Math.min(res_z - 1, Math.ceil(box_z_max));
285
+ for (let z = z_min; z <= z_max; z++) {
286
+ const z_off = z * res_x * (res_y + 1);
287
+ for (let y = y_min; y <= y_max; y++) {
288
+ const y_off = z_off + y * res_x;
289
+ for (let x = x_min; x <= x_max; x++) {
290
+ const w = tube_weight(x, y - 0.5, z);
291
+ if (w === 0) continue;
292
+ const f = y_off + x;
293
+ if (pin[f] !== 0) continue;
294
+ vv[f] += impulse_y * w;
263
295
  }
296
+ }
297
+ }
298
+ }
264
299
 
265
- vx[c] += impulse_x * w;
266
- vy[c] += impulse_y * w;
267
- vz[c] += impulse_z * w;
300
+ // w (z-faces).
301
+ {
302
+ const vw = field.velocity_z;
303
+ const pin = field.face_solid_z;
304
+ const slice = res_x * res_y;
305
+ const x_min = Math.max(0, Math.floor(box_x_min));
306
+ const x_max = Math.min(res_x - 1, Math.ceil(box_x_max));
307
+ const y_min = Math.max(0, Math.floor(box_y_min));
308
+ const y_max = Math.min(res_y - 1, Math.ceil(box_y_max));
309
+ const z_min = Math.max(0, Math.floor(box_z_min + 0.5));
310
+ const z_max = Math.min(res_z, Math.ceil(box_z_max + 0.5));
311
+ for (let z = z_min; z <= z_max; z++) {
312
+ const z_off = z * slice;
313
+ for (let y = y_min; y <= y_max; y++) {
314
+ const y_off = z_off + y * res_x;
315
+ for (let x = x_min; x <= x_max; x++) {
316
+ const w = tube_weight(x, y, z - 0.5);
317
+ if (w === 0) continue;
318
+ const f = y_off + x;
319
+ if (pin[f] !== 0) continue;
320
+ vw[f] += impulse_z * w;
321
+ }
268
322
  }
269
323
  }
270
324
  }
@@ -67,12 +67,35 @@ function main(engine) {
67
67
  new Entity().add(effectors).build(ecd);
68
68
 
69
69
  // Debug visualisation — a per-frame behavior, separate from the fluid stepping.
70
+ // The MAC velocity lives on three face lattices of different sizes; the
71
+ // visualiser wants cell-resolution channels, so sample to cell centers
72
+ // into cached buffers each frame.
73
+ const cell_count = field.cellCount();
74
+ const view_vx = new Float32Array(cell_count);
75
+ const view_vy = new Float32Array(cell_count);
76
+ const view_vz = new Float32Array(cell_count);
77
+ const sample_scratch = new Float32Array(3);
78
+
79
+ function refresh_velocity_views() {
80
+ for (let z = 0; z < RES_Z; z++) {
81
+ for (let y = 0; y < RES_Y; y++) {
82
+ for (let x = 0; x < RES_X; x++) {
83
+ field.sampleVelocity(sample_scratch, x, y, z);
84
+ const c = field.cellIndex(x, y, z);
85
+ view_vx[c] = sample_scratch[0];
86
+ view_vy[c] = sample_scratch[1];
87
+ view_vz[c] = sample_scratch[2];
88
+ }
89
+ }
90
+ }
91
+ }
92
+
70
93
  const slice_view = new SliceVisualiser();
71
94
  slice_view.size.set(RES_X * 6, RES_Y * 2);
72
95
  slice_view.scale.setScalar(2);
73
96
  slice_view.transformOrigin.set(0, 0);
74
97
  slice_view.setChannels(
75
- [field.velocity_x, field.velocity_y, field.velocity_z, field.getScalarData("dye")],
98
+ [view_vx, view_vy, view_vz, field.getScalarData("dye")],
76
99
  field.getResolution()
77
100
  );
78
101
 
@@ -81,6 +104,7 @@ function main(engine) {
81
104
  // FluidSystem.fixedUpdate is what advances the sim; we just observe and
82
105
  // time the draw cost here.
83
106
  const t0 = performance.now();
107
+ refresh_velocity_views();
84
108
  slice_view.draw();
85
109
  metrics.get('sim').record(performance.now() - t0);
86
110
  }))
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Trilinearly sample a cell-centered scalar at a fractional position,
3
+ * EXCLUDING solid cells from the interpolation: solid taps contribute zero
4
+ * weight and the remaining weights are renormalized. This is the
5
+ * free-boundary-aware sampling of Bridson §3.x — without it, a back-trace
6
+ * landing NEAR a wall blends the frozen in-wall value into the fluid, which
7
+ * reads as dye bleeding out of (or being eaten by) walls.
8
+ *
9
+ * When every tap is solid (a trace clipped into a fully walled corner) there
10
+ * is nothing valid to interpolate; `fallback` — typically the destination
11
+ * cell's own previous value, i.e. "no transport this step" — is returned.
12
+ *
13
+ * Positions clamp to the cell lattice exactly like
14
+ * {@link scs3d_sample_linear}; with a zero-filled `solid` grid the result
15
+ * matches that sampler up to floating-point reassociation (the weights sum
16
+ * to 1 and renormalization divides by it).
17
+ *
18
+ * @param {Float32Array} data Cell-centered scalar field.
19
+ * @param {Uint8Array} solid Cell-centered solid flags.
20
+ * @param {number} res_x
21
+ * @param {number} res_y
22
+ * @param {number} res_z
23
+ * @param {number} x
24
+ * @param {number} y
25
+ * @param {number} z
26
+ * @param {number} fallback Returned when no fluid tap is available.
27
+ * @return {number}
28
+ */
29
+ export function v3_grid_sample_scalar_masked(data: Float32Array, solid: Uint8Array, res_x: number, res_y: number, res_z: number, x: number, y: number, z: number, fallback: number): number;
30
+ //# sourceMappingURL=v3_grid_sample_scalar_masked.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v3_grid_sample_scalar_masked.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,mDAXW,YAAY,SACZ,UAAU,SACV,MAAM,SACN,MAAM,SACN,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,YACN,MAAM,GACL,MAAM,CA+DjB"}