@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
@@ -0,0 +1,348 @@
1
+ import { PosedShape3D } from "../../../../core/geom/3d/shape/PosedShape3D.js";
2
+ import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
3
+ import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
4
+ import { System } from "../../../ecs/System.js";
5
+ import { Transform } from "../../../ecs/transform/Transform.js";
6
+ import { Collider } from "../../ecs/Collider.js";
7
+ import { RigidBody } from "../../ecs/RigidBody.js";
8
+ import { FluidComponent } from "./FluidComponent.js";
9
+ import { FluidObstacle } from "./FluidObstacle.js";
10
+
11
+ /**
12
+ * ECS system that voxelizes collider geometry into fluid solid masks and
13
+ * stamps rigid-body velocities onto the walls as moving-wall boundary
14
+ * conditions.
15
+ *
16
+ * Every fixed update, each entity carrying ({@link FluidObstacle},
17
+ * {@link Collider}, {@link RigidBody}, {@link Transform}) — the same tuple
18
+ * {@link PhysicsSystem} simulates, plus the marker — is swept against every
19
+ * {@link FluidComponent}: the collider's shape — posed rigidly at the
20
+ * transform's position/rotation via {@link PosedShape3D} — is sampled at the
21
+ * centre of every fluid cell inside its world AABB, and cells whose signed
22
+ * distance is `<= obstacle.inflation` become solid. The fluid then treats
23
+ * those cells exactly like hand-authored walls: faces pinned, pressure
24
+ * Neumann, traces clipped.
25
+ *
26
+ * **Ownership contract**: while at least one obstacle exists, this system
27
+ * OWNS the solid mask of every fluid field — masks are cleared and rebuilt
28
+ * from scratch each tick, so moving obstacles leave no stale walls behind.
29
+ * Hand-written `setSolidAt` state will be wiped; mix the two styles only by
30
+ * expressing the static geometry as obstacle entities too (a `BoxShape3D`
31
+ * collider is cheaper to author than a splat loop anyway). With NO obstacles
32
+ * present the system leaves every mask untouched.
33
+ *
34
+ * **Moving walls — the wall-velocity specification**:
35
+ *
36
+ * - The velocity source is `RigidBody.linearVelocity` (world m/s) — the
37
+ * SAME value the contact solver resolves against, so air and contacts
38
+ * always agree on how fast a wall moves. It is stamped onto every face
39
+ * of every cell the obstacle voxelized (converted to grid units per
40
+ * field); the projection's divergence reads pinned faces at face value,
41
+ * so a moving collider genuinely pushes and drags the air around it.
42
+ * - Pose is still read from `Transform`; velocity is NOT derived from
43
+ * pose deltas. A teleported body (spawn snap, network correction)
44
+ * relocates its walls but stamps no velocity — no one-tick air blast.
45
+ * This mirrors the engine's own contact-solver rule: see
46
+ * {@link BodyKind}.KinematicPosition, deferred for exactly this reason.
47
+ * Consequently pose-animated colliders read as stationary walls that
48
+ * teleport, for air just as for contacts — drive movers through
49
+ * `BodyKind.KinematicVelocity` (or Dynamic) to displace air.
50
+ * - `BodyKind.Static` bodies have zero velocity and stamp nothing.
51
+ * Sleeping bodies stamp nothing either — the solver zeroes velocities
52
+ * on sleep.
53
+ * - A body that stops stamps ONE extra pass of zeros
54
+ * (`FluidObstacle._was_moving`): pinned faces preserve their values
55
+ * across mask recomputes by design, so without it a stale moving BC
56
+ * would drive phantom inflow forever.
57
+ * - A retreating wall leaves its velocity behind on unpinned faces as the
58
+ * wake seed for the fluid that takes its place.
59
+ * - Rotation-induced surface velocity (ω × r from
60
+ * `RigidBody.angularVelocity`) is not modelled yet — fast spinners read
61
+ * as translating walls only (follow-up in REVIEW_02_PLAN.md).
62
+ *
63
+ * **Ordering**: register AFTER {@link PhysicsSystem} (so this tick's solved
64
+ * velocities and integrated poses are read) and BEFORE {@link FluidSystem}
65
+ * (so the simulation step sees this tick's walls and wall velocities).
66
+ * Engine systems run in registration order.
67
+ *
68
+ * Cost: clearing is O(cells) per field; voxelization is one
69
+ * `signed_distance_at_point` per cell inside each obstacle's AABB ∩ field.
70
+ * Static scenes pay the same as moving ones — change detection is a
71
+ * follow-up (REVIEW_02_PLAN.md) if profiling ever points here.
72
+ */
73
+ export class FluidObstacleSystem extends System {
74
+
75
+ dependencies = [FluidObstacle];
76
+
77
+ components_used = [
78
+ ResourceAccessSpecification.from(FluidObstacle, ResourceAccessKind.Read),
79
+ ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
80
+ ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read),
81
+ ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read),
82
+ // Solid masks are rewritten each tick.
83
+ ResourceAccessSpecification.from(FluidComponent, ResourceAccessKind.Read | ResourceAccessKind.Write),
84
+ ];
85
+
86
+ /**
87
+ * Reusable rigid-pose adapter, rebound per obstacle per tick.
88
+ * @type {PosedShape3D}
89
+ */
90
+ #posed = new PosedShape3D();
91
+
92
+ /**
93
+ * World AABB scratch for the current posed shape (x0,y0,z0,x1,y1,z1).
94
+ * @type {Float64Array}
95
+ */
96
+ #aabb = new Float64Array(6);
97
+
98
+ /**
99
+ * World-space point scratch for per-cell SDF queries.
100
+ * @type {Float64Array}
101
+ */
102
+ #point = new Float64Array(3);
103
+
104
+ /**
105
+ * @param {number} time_delta_seconds
106
+ */
107
+ fixedUpdate(time_delta_seconds) {
108
+ const em = this.entityManager;
109
+ if (em === null) {
110
+ return;
111
+ }
112
+ const ecd = em.dataset;
113
+ if (ecd === null) {
114
+ return;
115
+ }
116
+
117
+ // With no obstacles the system must not touch any mask — fields with
118
+ // hand-authored solids keep them.
119
+ let obstacle_count = 0;
120
+ ecd.traverseEntities([FluidObstacle, Collider, RigidBody, Transform], function () {
121
+ obstacle_count++;
122
+ });
123
+ if (obstacle_count === 0) {
124
+ return;
125
+ }
126
+
127
+ // Clear every field's mask, then accumulate every obstacle into every
128
+ // overlapping field. Solids are world-anchored; rebuilding from
129
+ // scratch each tick is what keeps moving obstacles honest.
130
+ ecd.traverseComponents(FluidComponent, FluidObstacleSystem.#clear_field);
131
+
132
+ const posed = this.#posed;
133
+ const aabb = this.#aabb;
134
+ const point = this.#point;
135
+
136
+ ecd.traverseEntities([FluidObstacle, Collider, RigidBody, Transform], function (obstacle, collider, body, transform) {
137
+ posed.setup(collider.shape, transform.position, transform.rotation);
138
+ posed.compute_bounding_box(aabb);
139
+
140
+ ecd.traverseComponents(FluidComponent, function (fluid) {
141
+ FluidObstacleSystem.#voxelize(fluid, posed, aabb, obstacle.inflation, point);
142
+ });
143
+ });
144
+
145
+ // Refresh each field's derived masks NOW so the unpinned→pinned
146
+ // transitions are consumed here (zeroing those faces to the static
147
+ // default). The moving-wall velocities written below then survive the
148
+ // simulator's own per-step refresh — already-pinned faces keep their
149
+ // values (see v3_mac_compute_face_solid).
150
+ ecd.traverseComponents(FluidComponent, FluidObstacleSystem.#refresh_masks);
151
+
152
+ // Second pass: moving bodies stamp their velocity onto the faces of
153
+ // the cells they voxelized — the MAC moving-wall boundary condition.
154
+ // The divergence reads these values, so the wall genuinely pushes
155
+ // (and pulls) the surrounding fluid. The source is the solver's own
156
+ // linearVelocity, NOT a pose delta — teleports stamp nothing.
157
+ ecd.traverseEntities([FluidObstacle, Collider, RigidBody, Transform], function (obstacle, collider, body, transform) {
158
+ const lv = body.linearVelocity;
159
+ const vx = lv.x;
160
+ const vy = lv.y;
161
+ const vz = lv.z;
162
+
163
+ const moving = vx !== 0 || vy !== 0 || vz !== 0;
164
+
165
+ // Stamp while moving, plus ONE extra pass on the tick the body
166
+ // stops (or falls asleep — the solver zeroes velocities on
167
+ // sleep): pinned faces preserve their values across mask
168
+ // recomputes, so a stopped wall must overwrite its stale moving
169
+ // BC with zeros or the projection keeps seeing phantom inflow.
170
+ // Bodies that never move never pay for this pass.
171
+ if (moving || obstacle._was_moving) {
172
+ posed.setup(collider.shape, transform.position, transform.rotation);
173
+ posed.compute_bounding_box(aabb);
174
+ ecd.traverseComponents(FluidComponent, function (fluid) {
175
+ FluidObstacleSystem.#stamp_wall_velocity(
176
+ fluid, posed, aabb, obstacle.inflation, point, vx, vy, vz);
177
+ });
178
+ }
179
+
180
+ obstacle._was_moving = moving;
181
+ });
182
+ }
183
+
184
+ /**
185
+ * @param {FluidComponent} fluid
186
+ */
187
+ static #refresh_masks(fluid) {
188
+ fluid.field.recomputeSolidNeighbourMask();
189
+ }
190
+
191
+ /**
192
+ * @param {FluidComponent} fluid
193
+ */
194
+ static #clear_field(fluid) {
195
+ const solid = fluid.field.solid;
196
+ if (solid !== null) {
197
+ solid.fill(0);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Mark every cell of `fluid` whose centre lies within `inflation` of the
203
+ * posed shape as solid. Iteration is clipped to the shape's world AABB
204
+ * (grown by `inflation`), so far-away obstacles cost nothing.
205
+ *
206
+ * @param {FluidComponent} fluid
207
+ * @param {PosedShape3D} posed
208
+ * @param {Float64Array} aabb world AABB of the posed shape
209
+ * @param {number} inflation world-units SDF threshold
210
+ * @param {Float64Array} point length-3 scratch
211
+ */
212
+ static #voxelize(fluid, posed, aabb, inflation, point) {
213
+ const field = fluid.field;
214
+ const solid = field.solid;
215
+ if (solid === null) {
216
+ return; // field not built yet
217
+ }
218
+
219
+ const res = field.getResolution();
220
+ const cs = fluid.cell_size;
221
+ const inv_cs = 1 / cs;
222
+ const origin = fluid.origin;
223
+
224
+ // Index range of cell centres inside the inflated AABB, clamped.
225
+ let x_min = Math.ceil((aabb[0] - inflation - origin[0]) * inv_cs);
226
+ let y_min = Math.ceil((aabb[1] - inflation - origin[1]) * inv_cs);
227
+ let z_min = Math.ceil((aabb[2] - inflation - origin[2]) * inv_cs);
228
+ let x_max = Math.floor((aabb[3] + inflation - origin[0]) * inv_cs);
229
+ let y_max = Math.floor((aabb[4] + inflation - origin[1]) * inv_cs);
230
+ let z_max = Math.floor((aabb[5] + inflation - origin[2]) * inv_cs);
231
+
232
+ if (x_min < 0) x_min = 0;
233
+ if (y_min < 0) y_min = 0;
234
+ if (z_min < 0) z_min = 0;
235
+ if (x_max >= res[0]) x_max = res[0] - 1;
236
+ if (y_max >= res[1]) y_max = res[1] - 1;
237
+ if (z_max >= res[2]) z_max = res[2] - 1;
238
+
239
+ if (x_min > x_max || y_min > y_max || z_min > z_max) {
240
+ return; // no overlap with this field
241
+ }
242
+
243
+ const slice = res[0] * res[1];
244
+
245
+ for (let z = z_min; z <= z_max; z++) {
246
+ const z_off = z * slice;
247
+ point[2] = origin[2] + z * cs;
248
+ for (let y = y_min; y <= y_max; y++) {
249
+ const y_off = z_off + y * res[0];
250
+ point[1] = origin[1] + y * cs;
251
+ for (let x = x_min; x <= x_max; x++) {
252
+ point[0] = origin[0] + x * cs;
253
+ if (posed.signed_distance_at_point(point) <= inflation) {
254
+ solid[y_off + x] = 1;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Write the obstacle's translation velocity onto every face of every cell
263
+ * it voxelized — the moving-wall boundary condition. Runs AFTER the mask
264
+ * refresh, so the values stick (already-pinned faces keep their stored
265
+ * velocity through subsequent recomputes).
266
+ *
267
+ * Velocity is converted to grid units (cells/second) per field. Where
268
+ * obstacles overlap, the later-visited one wins on shared faces — both
269
+ * claims are walls, the disagreement is sub-cell.
270
+ *
271
+ * @param {FluidComponent} fluid
272
+ * @param {PosedShape3D} posed
273
+ * @param {Float64Array} aabb
274
+ * @param {number} inflation
275
+ * @param {Float64Array} point
276
+ * @param {number} wvx world-units-per-second obstacle velocity
277
+ * @param {number} wvy
278
+ * @param {number} wvz
279
+ */
280
+ static #stamp_wall_velocity(fluid, posed, aabb, inflation, point, wvx, wvy, wvz) {
281
+ const field = fluid.field;
282
+ const solid = field.solid;
283
+ if (solid === null) {
284
+ return;
285
+ }
286
+
287
+ const res = field.getResolution();
288
+ const cs = fluid.cell_size;
289
+ const inv_cs = 1 / cs;
290
+ const origin = fluid.origin;
291
+
292
+ // Grid-units wall velocity (cells per second).
293
+ const gvx = wvx * inv_cs;
294
+ const gvy = wvy * inv_cs;
295
+ const gvz = wvz * inv_cs;
296
+
297
+ let x_min = Math.ceil((aabb[0] - inflation - origin[0]) * inv_cs);
298
+ let y_min = Math.ceil((aabb[1] - inflation - origin[1]) * inv_cs);
299
+ let z_min = Math.ceil((aabb[2] - inflation - origin[2]) * inv_cs);
300
+ let x_max = Math.floor((aabb[3] + inflation - origin[0]) * inv_cs);
301
+ let y_max = Math.floor((aabb[4] + inflation - origin[1]) * inv_cs);
302
+ let z_max = Math.floor((aabb[5] + inflation - origin[2]) * inv_cs);
303
+
304
+ if (x_min < 0) x_min = 0;
305
+ if (y_min < 0) y_min = 0;
306
+ if (z_min < 0) z_min = 0;
307
+ if (x_max >= res[0]) x_max = res[0] - 1;
308
+ if (y_max >= res[1]) y_max = res[1] - 1;
309
+ if (z_max >= res[2]) z_max = res[2] - 1;
310
+
311
+ if (x_min > x_max || y_min > y_max || z_min > z_max) {
312
+ return;
313
+ }
314
+
315
+ const rx = res[0];
316
+ const ry = res[1];
317
+ const slice = rx * ry;
318
+ const sx = rx + 1;
319
+ const vel_u = field.velocity_x;
320
+ const vel_v = field.velocity_y;
321
+ const vel_w = field.velocity_z;
322
+
323
+ for (let z = z_min; z <= z_max; z++) {
324
+ point[2] = origin[2] + z * cs;
325
+ for (let y = y_min; y <= y_max; y++) {
326
+ point[1] = origin[1] + y * cs;
327
+ for (let x = x_min; x <= x_max; x++) {
328
+ point[0] = origin[0] + x * cs;
329
+ // Re-test ownership: only this obstacle's own cells get
330
+ // its velocity (the solid mask may contain other walls).
331
+ if (posed.signed_distance_at_point(point) > inflation) {
332
+ continue;
333
+ }
334
+ // All six faces of a solid cell are pinned by definition.
335
+ const u = z * sx * ry + y * sx + x;
336
+ vel_u[u] = gvx;
337
+ vel_u[u + 1] = gvx;
338
+ const v = z * rx * (ry + 1) + y * rx + x;
339
+ vel_v[v] = gvy;
340
+ vel_v[v + rx] = gvy;
341
+ const w = z * slice + y * rx + x;
342
+ vel_w[w] = gvz;
343
+ vel_w[w + slice] = gvz;
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
@@ -38,7 +38,7 @@ export class FluidSystem extends System<any, any, any, any, any> {
38
38
  * @param {FluidEffectorsComponent} effectors_component
39
39
  * @param {Transform} transform
40
40
  */
41
- static "__#141@#sync_effectors_from_transform"(effectors_component: FluidEffectorsComponent, transform: Transform): void;
41
+ static "__#142@#sync_effectors_from_transform"(effectors_component: FluidEffectorsComponent, transform: Transform): void;
42
42
  /**
43
43
  * Visitor for the (FluidComponent, Transform) traversal — keeps the field's
44
44
  * grid origin locked to a cell-aligned position near the transform.
@@ -61,7 +61,7 @@ export class FluidSystem extends System<any, any, any, any, any> {
61
61
  * @param {FluidComponent} component
62
62
  * @param {Transform} transform
63
63
  */
64
- static "__#141@#reanchor_field"(component: FluidComponent, transform: Transform): void;
64
+ static "__#142@#reanchor_field"(component: FluidComponent, transform: Transform): void;
65
65
  /**
66
66
  * Write the world-to-grid affine for a FluidComponent into `out`. Axis-aligned,
67
67
  * uniform-scale, so the matrix is sparse:
@@ -76,7 +76,7 @@ export class FluidSystem extends System<any, any, any, any, any> {
76
76
  * @param {Float32Array} out length-16
77
77
  * @param {FluidComponent} component
78
78
  */
79
- static "__#141@#build_world_to_grid"(out: Float32Array, component: FluidComponent): void;
79
+ static "__#142@#build_world_to_grid"(out: Float32Array, component: FluidComponent): void;
80
80
  constructor();
81
81
  dependencies: (typeof FluidComponent)[];
82
82
  /**
@@ -1,25 +1,75 @@
1
1
  /**
2
- * Adds the same acceleration to every fluid cell each step (gravity, ambient wind).
2
+ * The global atmosphere: every fluid face in every field integrates the same
3
+ * linear flow model each step,
3
4
  *
4
- * `force` is in WORLD units per second squared. The `world_to_grid` matrix is applied
5
- * (as a direction transform — translation ignored) inside {@link apply} to convert
6
- * the force into the target field's grid-cell units before depositing. The same
7
- * GlobalFluidEffector applied to fields with different `cell_size` therefore produces
8
- * the same world-space acceleration on each — gravity stays gravity.
5
+ * dv/dt = force + drag · (wind v)
9
6
  *
10
- * Solid cells are left untouched — the projection step would zero them anyway, but
11
- * skipping the write is slightly cheaper and keeps the solid mask authoritative for
12
- * effectors as well as the solver.
7
+ * One effector covers the whole family of "push everything" behaviours
8
+ * pick the regime by which fields you set:
9
+ *
10
+ * - **`force` alone** — constant body force (gravity, buoyancy). NOTE:
11
+ * incompressible projection cannot oppose a uniform force (a uniform
12
+ * field is divergence-free), so without `drag`, a sealed container, or
13
+ * {@link FluidSimulator#velocity_damping}, the velocity grows without
14
+ * bound. This is the legacy GlobalFluidEffector behaviour.
15
+ *
16
+ * - **`wind` + `drag`** — prevailing wind: the air relaxes toward the wind
17
+ * velocity at `drag` per second; gusts, wakes and splats decay back to
18
+ * the ambient flow. The fixed point is exactly `wind`.
19
+ *
20
+ * - **`force` + `drag` (+ `wind`)** — bounded forcing with a terminal
21
+ * velocity of `wind + force / drag`: falling smoke, drifting ash, rain
22
+ * sheets in a breeze.
23
+ *
24
+ * - **`drag` alone** — pure exponential calming toward still air. (The
25
+ * scene-content twin of {@link FluidSimulator#velocity_damping}, which
26
+ * is the solver-level stability knob that exists even when no effectors
27
+ * are wired.)
28
+ *
29
+ * `wind` only enters through the drag term — with `drag = 0` it is inert by
30
+ * construction (the model degenerates to dv/dt = force).
31
+ *
32
+ * The step uses the EXACT solution of the linear ODE, not an Euler step:
33
+ *
34
+ * v ← v∞ + (v − v∞) · e^(−drag·dt), v∞ = wind + force / drag
35
+ *
36
+ * (and plain `v += force·dt`, also exact, when `drag = 0`). Exactness makes
37
+ * the effector frame-rate independent — two half-steps land exactly where
38
+ * one full step does, including with both terms active.
39
+ *
40
+ * All quantities are WORLD-space: `force` in units/s², `wind` in units/s,
41
+ * `drag` in 1/s. The `world_to_grid` matrix's linear part converts the two
42
+ * vectors at apply time, so one instance produces the same world-space
43
+ * atmosphere on fields of any `cell_size` — gravity stays gravity.
44
+ *
45
+ * Pinned faces (either adjacent cell solid) are skipped — the projection
46
+ * owns those as boundary conditions, and skipping keeps the solid mask
47
+ * authoritative for effectors as well as the solver.
13
48
  */
14
49
  export class GlobalFluidEffector extends AbstractFluidEffector {
15
50
  /**
16
- * Acceleration vector in WORLD units per second squared (e.g. `[0, -9.8, 0]` for
17
- * Earth gravity in metres-and-seconds). Per-step deposit into the velocity field
18
- * is `world_to_grid · force × dt`.
51
+ * Body-force acceleration in WORLD units per second squared (e.g.
52
+ * `[0, -9.8, 0]` for Earth gravity in metres-and-seconds).
19
53
  *
20
54
  * @type {[number, number, number]}
21
55
  */
22
56
  force: [number, number, number];
57
+ /**
58
+ * Ambient wind velocity in WORLD units per second — the velocity the air
59
+ * relaxes toward. Only effective when {@link drag} is non-zero.
60
+ *
61
+ * @type {[number, number, number]}
62
+ */
63
+ wind: [number, number, number];
64
+ /**
65
+ * Relaxation rate toward the ambient flow, per second. Higher = stiffer
66
+ * atmosphere that re-asserts itself quickly after disturbances; lower =
67
+ * gusts and wakes linger. A disturbance decays to ~37% in `1/drag`
68
+ * seconds. `0` disables the relaxation term entirely.
69
+ *
70
+ * @type {number}
71
+ */
72
+ drag: number;
23
73
  }
24
74
  import { AbstractFluidEffector } from "./AbstractFluidEffector.js";
25
75
  //# sourceMappingURL=GlobalFluidEffector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GlobalFluidEffector.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/effector/GlobalFluidEffector.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH;IAEI;;;;;;OAMG;IACH,OAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEhB;CA8ErB;sCAtGqC,4BAA4B"}
1
+ {"version":3,"file":"GlobalFluidEffector.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/effector/GlobalFluidEffector.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH;IAEI;;;;;OAKG;IACH,OAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEhB;IAElB;;;;;OAKG;IACH,MAFU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAEjB;IAEjB;;;;;;;OAOG;IACH,MAFU,MAAM,CAEP;CA2HZ;sCAvMqC,4BAA4B"}