1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! Grid fluid solver: FLIP (fluid implicit particle, Zhu and Bridson 2005)
//! on a staggered grid over the container box, for large water volumes.
//!
//! Particles carry the water; each step moves their velocities onto the grid,
//! makes the grid velocity free of divergence with a pressure solve, and
//! gives the particles the change (95 % FLIP, 5 % PIC). It costs one pass
//! over the particles and a few dozen over the cells, with no neighbor
//! search, so it scales to far more particles than the position based solver.
//!
//! Determinism: particles are splatted in index order, the pressure solve
//! sweeps cells in index order, and nothing uses atomics or randomness.
// The per-axis loops index several small arrays at once; iterators read worse.
#![allow(clippy::needless_range_loop)]
use super::fluid::{Fluid, FluidSettings};
/// Share of the grid's velocity change a particle takes on top of its own
/// velocity; the rest of its new velocity is the grid velocity itself (PIC).
/// Lower is calmer and more viscous.
const FLIP_RATIO: f32 = 0.95;
/// How hard a crowded cell pushes its particles apart, as a share of one
/// cell per step at twice the rest count.
const DRIFT: f32 = 0.3;
/// Gauss-Seidel sweeps of the pressure solve per density iteration of the
/// settings.
const SWEEPS_PER_ITERATION: u32 = 15;
/// Most cells a grid holds, whatever the box and spacing ask.
const MAX_CELLS: usize = 1 << 21;
/// The grid over the container: cells of twice the particle spacing, so a
/// settled cell holds about eight particles.
struct Grid {
min: [f32; 3],
size: f32,
cells: [usize; 3],
}
impl Grid {
fn new(settings: &FluidSettings) -> Self {
let (min, max) = settings.bounds;
// Scene data sets the box and the spacing; past MAX_CELLS the cells
// grow instead, so a huge box cannot allocate without bound.
let extent: [f64; 3] = std::array::from_fn(|axis| {
let side = f64::from(max[axis]) - f64::from(min[axis]);
if side.is_finite() {
side.max(0.0)
} else {
0.0
}
});
let mut size = f64::from(2.0 * settings.spacing).max(1e-3);
let volume = extent.iter().map(|side| side / size).product::<f64>();
if volume > MAX_CELLS as f64 {
size *= (volume / MAX_CELLS as f64).cbrt();
}
let cells = std::array::from_fn(|axis| {
((extent[axis] / size).ceil() as usize).clamp(1, MAX_CELLS)
});
let mut grid = Self {
min,
size: size as f32,
cells,
};
// Rounding up each axis can still pass the cap; shrink the largest.
while grid.cells.iter().product::<usize>() > MAX_CELLS {
let largest =
(0..3).max_by_key(|&axis| grid.cells[axis]).unwrap_or(0);
grid.cells[largest] -= 1;
}
grid
}
/// Size of the face array holding velocity component `axis`.
fn faces(&self, axis: usize) -> [usize; 3] {
let mut dims = self.cells;
dims[axis] += 1;
dims
}
fn cell_of(&self, p: [f32; 3]) -> usize {
let at = |axis: usize| {
let x = ((p[axis] - self.min[axis]) / self.size).floor().max(0.0);
(x as usize).min(self.cells[axis] - 1)
};
index(self.cells, [at(0), at(1), at(2)])
}
/// The eight faces of component `axis` around `p` and their trilinear
/// weights. Faces of `axis` sit on cell walls along it and at cell
/// centers along the other two axes.
fn stencil(&self, axis: usize, p: [f32; 3]) -> [(usize, f32); 8] {
let dims = self.faces(axis);
let mut base = [0usize; 3];
let mut frac = [0.0f32; 3];
for b in 0..3 {
let shift = if b == axis { 0.0 } else { 0.5 };
let top = (dims[b] - 1) as f32;
let x = ((p[b] - self.min[b]) / self.size - shift).clamp(0.0, top);
let i = (x.floor() as usize).min(dims[b].saturating_sub(2));
base[b] = i;
frac[b] = x - i as f32;
}
std::array::from_fn(|corner| {
let mut at = [0usize; 3];
let mut weight = 1.0;
for b in 0..3 {
let up = (corner >> b) & 1 == 1;
at[b] = (base[b] + usize::from(up)).min(dims[b] - 1);
weight *= if up { frac[b] } else { 1.0 - frac[b] };
}
(index(dims, at), weight)
})
}
fn sample(&self, axis: usize, field: &[f32], p: [f32; 3]) -> f32 {
self.stencil(axis, p)
.iter()
.map(|&(i, w)| field[i] * w)
.sum()
}
}
fn index(dims: [usize; 3], at: [usize; 3]) -> usize {
at[0] + dims[0] * (at[1] + dims[1] * at[2])
}
/// Advances `fluid` by `dt` seconds with the grid solver.
pub(super) fn step(fluid: &mut Fluid, settings: &FluidSettings, dt: f32) {
let grid = Grid::new(settings);
for v in &mut fluid.velocities {
for axis in 0..3 {
v[axis] += settings.gravity[axis] * dt;
}
}
// Particles to grid, in particle order.
let mut velocity: [Vec<f32>; 3] = std::array::from_fn(|axis| {
let dims = grid.faces(axis);
let mut sum = vec![0.0f32; dims[0] * dims[1] * dims[2]];
let mut weight = vec![0.0f32; sum.len()];
for (p, v) in fluid.positions.iter().zip(&fluid.velocities) {
for (i, w) in grid.stencil(axis, *p) {
sum[i] += w * v[axis];
weight[i] += w;
}
}
for (s, w) in sum.iter_mut().zip(&weight) {
if *w > 0.0 {
*s /= w;
}
}
// The container walls let nothing through.
for (i, s) in sum.iter_mut().enumerate() {
let along =
(i / [1, dims[0], dims[0] * dims[1]][axis]) % dims[axis];
if along == 0 || along == dims[axis] - 1 {
*s = 0.0;
}
}
sum
});
let before = velocity.clone();
let cells = grid.cells;
let mut crowd = vec![0u32; cells[0] * cells[1] * cells[2]];
for p in &fluid.positions {
crowd[grid.cell_of(*p)] += 1;
}
let water: Vec<bool> = crowd.iter().map(|&n| n > 0).collect();
// The grid alone cannot tell a crowded cell from a sparse one, so the
// particles drift together. A cell holding more than its rest share of
// eight particles asks for outflow in proportion (drift correction).
let push = DRIFT * grid.size / dt;
let target: Vec<f32> = (0..water.len())
.map(|c| {
let crowded = crowd[c] as f32 / 8.0 - 1.0;
push * crowded.max(0.0)
})
.collect();
// Pressure: make every water cell's outflow zero. Air cells hold zero
// pressure; the walls are closed. `pressure` is in velocity units, so a
// face loses the difference of its two cells.
let mut pressure = vec![0.0f32; water.len()];
let sweeps = settings
.iterations
.clamp(1, super::fluid::MAX_FLUID_ITERATIONS)
* SWEEPS_PER_ITERATION;
let face = |axis: usize, at: [usize; 3]| index(grid.faces(axis), at);
for _ in 0..sweeps {
for k in 0..cells[2] {
for j in 0..cells[1] {
for i in 0..cells[0] {
let at = [i, j, k];
let c = index(cells, at);
if !water[c] {
continue;
}
let mut outflow = 0.0;
let mut sum = 0.0;
let mut open = 0.0;
for axis in 0..3 {
let mut up = at;
up[axis] += 1;
outflow += velocity[axis][face(axis, up)]
- velocity[axis][face(axis, at)];
if at[axis] > 0 {
let mut n = at;
n[axis] -= 1;
sum += pressure[index(cells, n)];
open += 1.0;
}
if at[axis] + 1 < cells[axis] {
sum += pressure[index(cells, up)];
open += 1.0;
}
}
// Outflow is measured on the unprojected velocity, so the
// neighbors' pressure is the only thing that changes.
if open > 0.0 {
pressure[c] = (sum - outflow + target[c]) / open;
}
}
}
}
}
project(&grid, &water, &pressure, &mut velocity);
// Grid to particles, then move.
let (min, max) = settings.bounds;
for (p, v) in fluid.positions.iter_mut().zip(&mut fluid.velocities) {
for axis in 0..3 {
let pic = grid.sample(axis, &velocity[axis], *p);
let change = pic - grid.sample(axis, &before[axis], *p);
v[axis] =
FLIP_RATIO * (v[axis] + change) + (1.0 - FLIP_RATIO) * pic;
}
for axis in 0..3 {
let moved = p[axis] + v[axis] * dt;
p[axis] = moved.clamp(min[axis], max[axis]);
// A wall stops the particle along it.
if p[axis] != moved {
v[axis] = 0.0;
}
}
}
}
/// Takes the pressure difference off every inner face next to water.
fn project(
grid: &Grid,
water: &[bool],
pressure: &[f32],
velocity: &mut [Vec<f32>; 3],
) {
let cells = grid.cells;
for k in 0..cells[2] {
for j in 0..cells[1] {
for i in 0..cells[0] {
let at = [i, j, k];
for axis in 0..3 {
if at[axis] == 0 {
continue;
}
let mut below = at;
below[axis] -= 1;
let (a, b) = (index(cells, below), index(cells, at));
if water[a] || water[b] {
velocity[axis][index(grid.faces(axis), at)] -=
pressure[b] - pressure[a];
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::fluid::{Fluid, FluidSettings, FluidSolver};
fn settings() -> FluidSettings {
FluidSettings {
spacing: 0.05,
bounds: ([-0.5, 0.0, -0.5], [0.5, 2.0, 0.5]),
solver: FluidSolver::Grid,
..FluidSettings::default()
}
}
/// A 0.4 m wide, 1 m tall column on the floor of a 1 m wide box.
fn column() -> Fluid {
Fluid::block([-0.2, 0.025, -0.2], [8, 20, 8], 0.05)
}
#[test]
fn a_grid_column_spreads_over_the_floor_and_keeps_its_volume() {
let settings = settings();
let mut fluid = column();
for _ in 0..240 {
fluid.step(&settings, 1.0 / 60.0);
}
let (min, max) = settings.bounds;
let mut speed = 0.0f32;
for (p, v) in fluid.positions.iter().zip(&fluid.velocities) {
for axis in 0..3 {
assert!(p[axis].is_finite() && v[axis].is_finite());
assert!(p[axis] >= min[axis] && p[axis] <= max[axis]);
}
speed = speed.max((v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt());
}
// 1280 particles of 0.05 m fill 0.16 m of the 1 m square floor, so
// the mean height of a settled pool is about 0.08 m.
let mean = fluid.positions.iter().map(|p| p[1]).sum::<f32>()
/ fluid.positions.len() as f32;
assert!((0.06..0.12).contains(&mean), "mean height {mean}");
assert!(speed < 1.5, "still moving at {speed} m/s");
}
#[test]
fn a_huge_box_gets_a_coarser_grid_not_a_huge_one() {
for bounds in [
([-1e4, -1e4, -1e4], [1e4, 1e4, 1e4]),
([f32::NEG_INFINITY, 0.0, 0.0], [f32::INFINITY, 1.0, 1.0]),
] {
let grid = super::Grid::new(&FluidSettings {
bounds,
..settings()
});
assert!(grid.cells.iter().product::<usize>() <= super::MAX_CELLS);
assert!(grid.cells.iter().all(|&n| n >= 1));
}
let mut fluid = column();
let huge = FluidSettings {
bounds: ([-1e4, 0.0, -1e4], [1e4, 1e4, 1e4]),
..settings()
};
fluid.step(&huge, 1.0 / 60.0);
assert!(fluid.positions.iter().flatten().all(|x| x.is_finite()));
}
#[test]
fn the_same_grid_start_gives_the_same_bits() {
let run = || {
let mut fluid = column();
for _ in 0..30 {
fluid.step(&settings(), 1.0 / 60.0);
}
fluid.state_hash()
};
assert_eq!(run(), run());
}
}