Skip to main content

pfx_physics/
fluid.rs

1use crate::math::{add2, clamp2, dot2, len2, scale2, sign, smoothstep, sub2};
2use crate::shape::{
3    DRAIN_TIME, KERNEL, Look, MAX_PARTICLES, MAX_TARGETS, Params, Particle, Pour, SLOTS, SPACING,
4    SUBSTEP, Target, along, cells_for, poly6, region, rest_density, spawn, spiky,
5};
6
7#[path = "fluid_pipeline.rs"]
8mod pipeline;
9use pfx_gpu::GpuProfiler;
10use pipeline::Pipeline;
11#[path = "fluid_inspect.rs"]
12mod inspect;
13
14pub const SPH_WGSL: &str = include_str!("sph.wgsl");
15pub const BLUR_WGSL: &str = include_str!("fluid_blur.wgsl");
16pub const JFA_WGSL: &str = include_str!("fluid_jfa.wgsl");
17pub const EDGE_WGSL: &str = include_str!("fluid_edge.wgsl");
18pub const RAISE_WGSL: &str = include_str!("fluid_raise.wgsl");
19
20const BLOCK: u32 = 256;
21const SETTLE: f32 = 2.0;
22
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct Smoothing {
25    pub wide: f32,
26    pub fine: f32,
27    pub pool: f32,
28}
29
30impl Default for Smoothing {
31    fn default() -> Self {
32        Self {
33            wide: 10.0,
34            fine: 2.0,
35            pool: 20.0,
36        }
37    }
38}
39
40impl Smoothing {
41    pub fn residuals(self) -> [f32; 2] {
42        [
43            ((self.wide * self.wide - self.fine * self.fine).max(0.25)).sqrt(),
44            ((self.pool * self.pool - self.fine * self.fine).max(0.25)).sqrt(),
45        ]
46    }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct Wire {
51    pub radius: f32,
52    pub lift: f32,
53    pub period: f32,
54}
55
56impl Default for Wire {
57    fn default() -> Self {
58        Self {
59            radius: 2.0,
60            lift: 4.0,
61            period: 3.0,
62        }
63    }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct Outline {
68    pub shape: [f32; 4],
69    pub size: [f32; 4],
70}
71
72impl Default for Outline {
73    fn default() -> Self {
74        Self {
75            shape: [8.0, 16.0, 0.0, 4.0],
76            size: [6.0, 16.0, 0.3, 0.0],
77        }
78    }
79}
80
81#[derive(Clone, Debug, PartialEq)]
82pub struct FluidState {
83    pub live: u32,
84    pub asleep: u32,
85    pub bounds: Option<([f32; 2], [f32; 2])>,
86    pub height_min: f32,
87    pub height_max: f32,
88    pub height_mean: f32,
89    pub channel_histogram: [std::collections::BTreeMap<i32, u32>; 3],
90    pub resting: bool,
91    pub row: Option<Vec<f32>>,
92}
93
94#[repr(C)]
95#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
96struct Uniform {
97    grid: [f32; 4],
98    table: [f32; 4],
99    k: [f32; 4],
100    misc: [f32; 4],
101    apart: [f32; 4],
102    tension: [f32; 4],
103    clock: [f32; 4],
104}
105
106pub struct Reference {
107    particles: Vec<Particle>,
108    targets: Vec<Target>,
109    table: [f32; 2],
110    params: Params,
111    rest: f32,
112    clock: f32,
113}
114
115impl Reference {
116    pub fn bead(center: [f32; 2], radius: f32, table: [f32; 2]) -> Self {
117        let look = Look {
118            tall: 1.0,
119            active: 0.0,
120            weight_b: 0.0,
121            weight_a: 0.0,
122        };
123        let target = Target::bead(center, radius, &look);
124        Self {
125            particles: spawn(&target, 0, None, 1),
126            targets: vec![target],
127            table,
128            params: Params::default(),
129            rest: rest_density(),
130            clock: 0.0,
131        }
132    }
133
134    pub fn count(&self) -> usize {
135        self.particles.iter().filter(|p| p.born >= 0.0).count()
136    }
137
138    pub fn particles(&self) -> &[Particle] {
139        &self.particles
140    }
141
142    pub fn step(&mut self, dt: f32, time: f32, calm: f32) {
143        if !dt.is_finite() || dt <= 0.0 {
144            return;
145        }
146        self.clock = (self.clock + dt).min(SUBSTEP * 4.0);
147        let steps = (self.clock / SUBSTEP).floor() as usize;
148        self.clock -= steps as f32 * SUBSTEP;
149        for _ in 0..steps {
150            step_once(
151                &mut self.particles,
152                &self.targets,
153                self.table,
154                &self.params,
155                self.rest,
156                time,
157                calm,
158            );
159        }
160    }
161}
162
163fn step_once(
164    particles: &mut [Particle],
165    targets: &[Target],
166    table: [f32; 2],
167    params: &Params,
168    rest: f32,
169    time: f32,
170    calm: f32,
171) {
172    let cells = cells_for(table);
173    let Binned {
174        counts,
175        starts,
176        items,
177    } = bin(particles, cells);
178    let neighbors = Neighbors {
179        counts: &counts,
180        starts: &starts,
181        items: &items,
182        cells,
183    };
184    for i in 0..particles.len() {
185        if particles[i].born < 0.0 {
186            continue;
187        }
188        particles[i].rho = density(i, particles, &neighbors).max(rest * 0.05);
189    }
190    let accel: Vec<[f32; 2]> = (0..particles.len())
191        .map(|i| {
192            if particles[i].born < 0.0 {
193                [0.0; 2]
194            } else {
195                force(i, particles, targets, &neighbors, params, time, calm)
196            }
197        })
198        .collect();
199    for (i, particle) in particles.iter_mut().enumerate() {
200        if particle.born < 0.0 {
201            continue;
202        }
203        let Some(target) = targets.get(particle.owner as usize) else {
204            continue;
205        };
206        let mut vel = scale2(particle.vel, (-params.damping * SUBSTEP).exp());
207        vel = add2(vel, scale2(accel[i], SUBSTEP));
208        let speed = len2(vel);
209        if speed > 900.0 {
210            vel = scale2(vel, 900.0 / speed);
211        }
212        let pos = clamp2(
213            add2(
214                particle.pos,
215                scale2(add2(vel, [target.carry[0], target.carry[1]]), SUBSTEP),
216            ),
217            [1.0, 1.0],
218            [table[0] - 1.0, table[1] - 1.0],
219        );
220        let mut goal = if target.flow[3] > 0.5 {
221            target.flow[0]
222        } else {
223            0.0
224        };
225        if target.kind() == 2 && target.carry[3] > 0.5 {
226            let curve = along(target, pos);
227            goal *= smoothstep(target.carry[2] + 0.05, target.carry[2] - 0.05, curve[0]);
228        }
229        let rate = params.life_rate * SUBSTEP;
230        let life = (particle.life + (goal - particle.life).clamp(-rate, rate)).clamp(0.0, 4.0);
231        particle.vel = vel;
232        particle.pos = pos;
233        particle.life = life;
234    }
235}
236
237fn cell_of(p: [f32; 2], cells: [usize; 2]) -> [usize; 2] {
238    [
239        (p[0] / KERNEL).floor().clamp(0.0, cells[0] as f32 - 1.0) as usize,
240        (p[1] / KERNEL).floor().clamp(0.0, cells[1] as f32 - 1.0) as usize,
241    ]
242}
243
244fn binned_cell(particle: &Particle, cells: [usize; 2]) -> Option<usize> {
245    if particle.born < 0.0 || particle.life <= 1e-3 {
246        return None;
247    }
248    let c = cell_of(particle.pos, cells);
249    Some(c[1] * cells[0] + c[0])
250}
251
252fn exclusive_scan(counts: &[u32]) -> Vec<u32> {
253    let mut total = 0;
254    counts
255        .iter()
256        .map(|count| {
257            let start = total;
258            total += count;
259            start
260        })
261        .collect()
262}
263
264struct Binned {
265    counts: Vec<u32>,
266    starts: Vec<u32>,
267    items: Vec<u32>,
268}
269
270fn bin(particles: &[Particle], cells: [usize; 2]) -> Binned {
271    let mut counts = vec![0u32; cells[0] * cells[1]];
272    for particle in particles {
273        if let Some(id) = binned_cell(particle, cells) {
274            counts[id] += 1;
275        }
276    }
277    let starts = exclusive_scan(&counts);
278    let mut items = vec![0u32; counts.iter().sum::<u32>() as usize];
279    let mut next = starts.clone();
280    for (i, particle) in particles.iter().enumerate() {
281        if let Some(id) = binned_cell(particle, cells) {
282            items[next[id] as usize] = i as u32;
283            next[id] += 1;
284        }
285    }
286    Binned {
287        counts,
288        starts,
289        items,
290    }
291}
292
293struct Neighbors<'a> {
294    counts: &'a [u32],
295    starts: &'a [u32],
296    items: &'a [u32],
297    cells: [usize; 2],
298}
299
300fn visit_cell(home: [usize; 2], span: i32, neighbors: &Neighbors, mut body: impl FnMut(usize)) {
301    for dy in -span..=span {
302        for dx in -span..=span {
303            let c = [home[0] as i32 + dx, home[1] as i32 + dy];
304            if c[0] < 0
305                || c[1] < 0
306                || c[0] >= neighbors.cells[0] as i32
307                || c[1] >= neighbors.cells[1] as i32
308            {
309                continue;
310            }
311            let id = c[1] as usize * neighbors.cells[0] + c[0] as usize;
312            let start = neighbors.starts[id] as usize;
313            let n = neighbors.counts[id].min(SLOTS) as usize;
314            for &item in &neighbors.items[start..start + n] {
315                body(item as usize);
316            }
317        }
318    }
319}
320
321fn density(i: usize, particles: &[Particle], neighbors: &Neighbors) -> f32 {
322    let home = cell_of(particles[i].pos, neighbors.cells);
323    let mut rho = 0.0;
324    visit_cell(home, 1, neighbors, |j| {
325        let d = sub2(particles[i].pos, particles[j].pos);
326        rho += particles[j].life * poly6(dot2(d, d));
327    });
328    rho
329}
330
331fn force(
332    i: usize,
333    particles: &[Particle],
334    targets: &[Target],
335    neighbors: &Neighbors,
336    params: &Params,
337    time: f32,
338    calm: f32,
339) -> [f32; 2] {
340    let p = particles[i];
341    let Some(target) = targets.get(p.owner as usize).copied() else {
342        return [0.0; 2];
343    };
344    let home = cell_of(p.pos, neighbors.cells);
345    let mut accel = [0.0; 2];
346    let mut drag = [0.0; 2];
347    let card = target.kind() == 1;
348    let film = KERNEL * params.apart[1].max(1.0);
349    let span = if card {
350        params.apart[1].max(1.0).ceil() as i32
351    } else {
352        1
353    };
354    visit_cell(home, span, neighbors, |j| {
355        if j == i {
356            return;
357        }
358        let q = particles[j];
359        let d = sub2(p.pos, q.pos);
360        let r = len2(d);
361        if card
362            && q.owner != p.owner
363            && targets
364                .get(q.owner as usize)
365                .is_some_and(|other| other.kind() == 1)
366        {
367            if r < film && r > 1e-4 {
368                let falloff = 1.0 - r / film;
369                let push = params.apart[0] * q.life * p.life * falloff * falloff / r;
370                accel = add2(accel, scale2(d, push));
371            }
372            return;
373        }
374        if r >= KERNEL * 1.5 {
375            return;
376        }
377        if r < 1e-4 {
378            accel[0] += ((i as f32 * 0.618).fract() - 0.5) * 50.0;
379            accel[1] += ((j as f32 * 0.618).fract() - 0.5) * 50.0;
380            return;
381        }
382        let press = params.pressure * q.life * (1.0 / p.rho + 1.0 / q.rho) * spiky(r) / r;
383        accel = sub2(accel, scale2(d, press));
384        drag = add2(
385            drag,
386            scale2(sub2(q.vel, p.vel), q.life * poly6(r * r) / q.rho),
387        );
388    });
389    let shape = region(&target, p.pos, time, calm);
390    let out = [p.pos[0] - shape[1], p.pos[1] - shape[2]];
391    let dir = scale2(out, 1.0 / len2(out).max(1e-3));
392    if target.c[3] > 0.5 {
393        accel = sub2(
394            accel,
395            scale2(dir, params.contain * 0.35 * (shape[0] + target.c[3] - 1.0)),
396        );
397        if target.kind() == 2 {
398            let curve = along(&target, p.pos);
399            let keep = curve[0].clamp(target.form[2], 1.0 - target.form[2]);
400            accel = add2(
401                accel,
402                scale2(
403                    [curve[1], curve[2]],
404                    params.contain * 0.5 * (keep - curve[0]) * curve[3],
405                ),
406            );
407        }
408        if target.kind() == 0 && target.b[3] > 0.0 {
409            let rel = sub2(p.pos, [target.a[0], target.a[1]]);
410            let delta = rel[1].atan2(rel[0]) - target.b[2];
411            let off = delta.sin().atan2(delta.cos());
412            if off.abs() < target.b[3] {
413                let tangent = scale2([-rel[1], rel[0]], 1.0 / len2(rel).max(1.0));
414                let push = params.contain * 2.0 * sign(off) * (target.b[3] - off.abs()) * len2(rel);
415                accel = add2(accel, scale2(tangent, push));
416            }
417        }
418    } else {
419        if shape[0] > 0.0 {
420            accel = sub2(accel, scale2(dir, params.contain * shape[0]));
421        }
422        let mut hold = 1.0;
423        if target.kind() == 0 && params.tension[0] > 0.0 {
424            hold = params.tension[2]
425                + smoothstep(-params.tension[0], 0.0, shape[0]) * params.tension[1];
426        }
427        accel = sub2(accel, scale2(out, params.cohesion * target.flow[1] * hold));
428        if target.kind() == 2 && target.form[1] != 0.0 {
429            let curve = along(&target, p.pos);
430            let push = params.cohesion
431                * target.flow[1]
432                * target.form[1]
433                * (2.0 * curve[0] - 1.0)
434                * curve[3]
435                * 0.5;
436            accel = add2(accel, scale2([curve[1], curve[2]], push));
437        }
438    }
439    add2(accel, scale2(drag, params.viscosity))
440}
441
442pub struct FluidDesc {
443    pub width: u32,
444    pub height: u32,
445    pub max_particles: u32,
446    pub max_targets: usize,
447    pub params: Params,
448    pub settle: f32,
449}
450
451impl FluidDesc {
452    pub fn new(width: u32, height: u32) -> Self {
453        Self {
454            width,
455            height,
456            ..Self::default()
457        }
458    }
459}
460
461impl Default for FluidDesc {
462    fn default() -> Self {
463        Self {
464            width: 1024,
465            height: 1024,
466            max_particles: MAX_PARTICLES,
467            max_targets: MAX_TARGETS,
468            params: Params::default(),
469            settle: SETTLE,
470        }
471    }
472}
473
474struct Body {
475    key: String,
476    slot: usize,
477    start: u32,
478    count: u32,
479    seen: bool,
480    gone: f32,
481    quiet: f32,
482}
483
484struct Pipes {
485    clear_cells: wgpu::ComputePipeline,
486    count: wgpu::ComputePipeline,
487    scan_cells: wgpu::ComputePipeline,
488    scan_blocks: wgpu::ComputePipeline,
489    scatter: wgpu::ComputePipeline,
490    order: wgpu::ComputePipeline,
491    density: wgpu::ComputePipeline,
492    forces: wgpu::ComputePipeline,
493    integrate: wgpu::ComputePipeline,
494    gather: wgpu::ComputePipeline,
495}
496
497struct Grid {
498    counts: wgpu::Buffer,
499    starts: wgpu::Buffer,
500    items: wgpu::Buffer,
501    ranks: wgpu::Buffer,
502}
503
504pub struct Fluid {
505    particles: wgpu::Buffer,
506    grid: Grid,
507    targets_buffer: wgpu::Buffer,
508    uniform: wgpu::Buffer,
509    bind: wgpu::BindGroup,
510    pipes: Pipes,
511    pipeline: Pipeline,
512    height_texture: wgpu::Texture,
513    height_view: wgpu::TextureView,
514    width: u32,
515    height: u32,
516    cells: [u32; 2],
517    params: Params,
518    bodies: Vec<Body>,
519    free_slots: Vec<usize>,
520    free_ranges: Vec<(u32, u32)>,
521    high: u32,
522    targets: Vec<Target>,
523    clock: f32,
524    rest: f32,
525    still: f32,
526    settle: f32,
527    pub moved: bool,
528    stale: bool,
529    wires: Vec<[f32; 4]>,
530    last_wires: Vec<[f32; 4]>,
531    wire: Wire,
532    smoothing: Smoothing,
533    outline: Outline,
534    wire_changed: bool,
535}
536
537impl Fluid {
538    pub fn new(device: &wgpu::Device, desc: FluidDesc) -> Self {
539        let width = desc.width.max(1);
540        let height = desc.height.max(1);
541        let max_particles = desc.max_particles.max(1);
542        let max_targets = desc.max_targets.max(1);
543        let cells_usize = cells_for([width as f32, height as f32]);
544        let cells = [cells_usize[0] as u32, cells_usize[1] as u32];
545        let cell_total = (cells[0] * cells[1]) as u64;
546        let block_total = cell_total.div_ceil(BLOCK as u64);
547        let storage = |label: &str, size: u64, mapped: bool| {
548            device.create_buffer(&wgpu::BufferDescriptor {
549                label: Some(label),
550                size,
551                usage: wgpu::BufferUsages::STORAGE
552                    | wgpu::BufferUsages::COPY_DST
553                    | wgpu::BufferUsages::COPY_SRC,
554                mapped_at_creation: mapped,
555            })
556        };
557        let particles = storage(
558            "Particles",
559            max_particles as u64 * std::mem::size_of::<Particle>() as u64,
560            true,
561        );
562        particles
563            .slice(..)
564            .get_mapped_range_mut()
565            .expect("a mapped range")
566            .slice(..)
567            .fill(0);
568        particles.unmap();
569        let targets_buffer = storage(
570            "Targets",
571            max_targets as u64 * std::mem::size_of::<Target>() as u64,
572            true,
573        );
574        targets_buffer
575            .slice(..)
576            .get_mapped_range_mut()
577            .expect("a mapped range")
578            .slice(..)
579            .fill(0);
580        targets_buffer.unmap();
581        let grid = Grid {
582            counts: storage("Cell counts", cell_total * 4, false),
583            starts: storage("Cell starts", (cell_total + block_total) * 4, false),
584            items: storage("Cell items", max_particles as u64 * 4, false),
585            ranks: storage("Cell ranks", max_particles as u64 * 4, false),
586        };
587        let accel = storage("Accelerations", max_particles as u64 * 8, false);
588        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
589            label: Some("Sim"),
590            size: std::mem::size_of::<Uniform>() as u64,
591            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
592            mapped_at_creation: false,
593        });
594        let texture = |label| {
595            device.create_texture(&wgpu::TextureDescriptor {
596                label: Some(label),
597                size: wgpu::Extent3d {
598                    width,
599                    height,
600                    depth_or_array_layers: 1,
601                },
602                mip_level_count: 1,
603                sample_count: 1,
604                dimension: wgpu::TextureDimension::D2,
605                format: wgpu::TextureFormat::Rgba16Float,
606                usage: wgpu::TextureUsages::STORAGE_BINDING
607                    | wgpu::TextureUsages::TEXTURE_BINDING
608                    | wgpu::TextureUsages::COPY_SRC,
609                view_formats: &[],
610            })
611        };
612        let height_texture = texture("Liquid height");
613        let height_view = height_texture.create_view(&Default::default());
614        let raw_texture = texture("Liquid raw");
615        let raw_view = raw_texture.create_view(&Default::default());
616        let buffer_entry = |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry {
617            binding,
618            visibility: wgpu::ShaderStages::COMPUTE,
619            ty: wgpu::BindingType::Buffer {
620                ty: wgpu::BufferBindingType::Storage { read_only },
621                has_dynamic_offset: false,
622                min_binding_size: None,
623            },
624            count: None,
625        };
626        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
627            label: Some("Sim"),
628            entries: &[
629                buffer_entry(0, false),
630                buffer_entry(1, true),
631                buffer_entry(2, false),
632                buffer_entry(3, false),
633                wgpu::BindGroupLayoutEntry {
634                    binding: 4,
635                    visibility: wgpu::ShaderStages::COMPUTE,
636                    ty: wgpu::BindingType::Buffer {
637                        ty: wgpu::BufferBindingType::Uniform,
638                        has_dynamic_offset: false,
639                        min_binding_size: None,
640                    },
641                    count: None,
642                },
643                buffer_entry(5, false),
644                wgpu::BindGroupLayoutEntry {
645                    binding: 6,
646                    visibility: wgpu::ShaderStages::COMPUTE,
647                    ty: wgpu::BindingType::StorageTexture {
648                        access: wgpu::StorageTextureAccess::WriteOnly,
649                        format: wgpu::TextureFormat::Rgba16Float,
650                        view_dimension: wgpu::TextureViewDimension::D2,
651                    },
652                    count: None,
653                },
654                buffer_entry(7, false),
655                buffer_entry(8, false),
656            ],
657        });
658        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
659            label: Some("Sim"),
660            layout: &layout,
661            entries: &[
662                wgpu::BindGroupEntry {
663                    binding: 0,
664                    resource: particles.as_entire_binding(),
665                },
666                wgpu::BindGroupEntry {
667                    binding: 1,
668                    resource: targets_buffer.as_entire_binding(),
669                },
670                wgpu::BindGroupEntry {
671                    binding: 2,
672                    resource: grid.counts.as_entire_binding(),
673                },
674                wgpu::BindGroupEntry {
675                    binding: 3,
676                    resource: grid.items.as_entire_binding(),
677                },
678                wgpu::BindGroupEntry {
679                    binding: 4,
680                    resource: uniform.as_entire_binding(),
681                },
682                wgpu::BindGroupEntry {
683                    binding: 5,
684                    resource: accel.as_entire_binding(),
685                },
686                wgpu::BindGroupEntry {
687                    binding: 6,
688                    resource: wgpu::BindingResource::TextureView(&raw_view),
689                },
690                wgpu::BindGroupEntry {
691                    binding: 7,
692                    resource: grid.starts.as_entire_binding(),
693                },
694                wgpu::BindGroupEntry {
695                    binding: 8,
696                    resource: grid.ranks.as_entire_binding(),
697                },
698            ],
699        });
700        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
701            label: Some("Sim"),
702            source: wgpu::ShaderSource::Wgsl(SPH_WGSL.into()),
703        });
704        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
705            label: Some("Sim"),
706            bind_group_layouts: &[Some(&layout)],
707            immediate_size: 0,
708        });
709        let pipe = |name: &str| {
710            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
711                label: Some(name),
712                layout: Some(&pipeline_layout),
713                module: &module,
714                entry_point: Some(name),
715                compilation_options: Default::default(),
716                cache: None,
717            })
718        };
719        let pipeline = Pipeline::new(device, width, height, &raw_view, &height_view);
720        Self {
721            particles,
722            grid,
723            targets_buffer,
724            uniform,
725            bind,
726            pipes: Pipes {
727                clear_cells: pipe("clear_cells"),
728                count: pipe("count"),
729                scan_cells: pipe("scan_cells"),
730                scan_blocks: pipe("scan_blocks"),
731                scatter: pipe("scatter"),
732                order: pipe("order"),
733                density: pipe("density"),
734                forces: pipe("forces"),
735                integrate: pipe("integrate"),
736                gather: pipe("gather"),
737            },
738            pipeline,
739            height_texture,
740            height_view,
741            width,
742            height,
743            cells,
744            params: desc.params,
745            bodies: Vec::new(),
746            free_slots: (0..max_targets).rev().collect(),
747            free_ranges: vec![(0, max_particles)],
748            high: 0,
749            targets: vec![Target::default(); max_targets],
750            clock: 0.0,
751            rest: rest_density(),
752            still: 0.0,
753            settle: if desc.settle.is_finite() {
754                desc.settle.max(0.0)
755            } else {
756                SETTLE
757            },
758            moved: true,
759            stale: true,
760            wires: Vec::new(),
761            last_wires: Vec::new(),
762            wire: Wire::default(),
763            smoothing: Smoothing::default(),
764            outline: Outline::default(),
765            wire_changed: false,
766        }
767    }
768
769    pub fn height(&self) -> &wgpu::TextureView {
770        &self.height_view
771    }
772
773    pub fn edge_view(&self) -> &wgpu::TextureView {
774        self.pipeline.edge_view()
775    }
776
777    pub fn edge_texture(&self) -> &wgpu::Texture {
778        self.pipeline.edge_texture()
779    }
780
781    pub fn settle(&self) -> f32 {
782        self.settle
783    }
784
785    pub fn height_texture(&self) -> &wgpu::Texture {
786        &self.height_texture
787    }
788
789    pub fn particles_buffer(&self) -> &wgpu::Buffer {
790        &self.particles
791    }
792
793    pub fn cell_counts(&self) -> &wgpu::Buffer {
794        &self.grid.counts
795    }
796
797    pub fn cell_starts(&self) -> &wgpu::Buffer {
798        &self.grid.starts
799    }
800
801    pub fn cell_items(&self) -> &wgpu::Buffer {
802        &self.grid.items
803    }
804
805    pub fn particle_count(&self) -> u32 {
806        self.bodies.iter().map(|body| body.count).sum()
807    }
808
809    pub fn set_wires(&mut self, segments: &[[f32; 4]], wire: Wire) {
810        let next = &segments[..segments.len().min(256)];
811        if self.last_wires.len() != next.len()
812            || self
813                .last_wires
814                .iter()
815                .zip(next)
816                .any(|(left, right)| left.iter().zip(right).any(|(a, b)| (a - b).abs() > 0.05))
817        {
818            self.still = 0.0;
819            self.wire_changed = true;
820            self.last_wires = next.to_vec();
821        }
822        if self.wire != wire {
823            self.still = 0.0;
824            self.wire_changed = true;
825        }
826        self.wires.clear();
827        self.wires.extend_from_slice(next);
828        self.wire = wire;
829    }
830
831    pub fn set_smoothing(&mut self, smoothing: Smoothing) {
832        if self.smoothing != smoothing {
833            self.still = 0.0;
834            self.wire_changed = true;
835        }
836        self.smoothing = smoothing;
837    }
838
839    pub fn set_outline(&mut self, outline: Outline) {
840        if self.outline != outline {
841            self.still = 0.0;
842            self.wire_changed = true;
843        }
844        self.outline = outline;
845    }
846
847    pub fn set_field(
848        &mut self,
849        segments: &[[f32; 4]],
850        wire: [f32; 3],
851        smoothing: [f32; 3],
852        outline: [f32; 8],
853    ) {
854        self.set_wires(
855            segments,
856            Wire {
857                radius: wire[0],
858                lift: wire[1],
859                period: wire[2],
860            },
861        );
862        self.set_smoothing(Smoothing {
863            wide: smoothing[0],
864            fine: smoothing[1],
865            pool: smoothing[2],
866        });
867        self.set_outline(Outline {
868            shape: outline[..4].try_into().expect("outline shape"),
869            size: outline[4..].try_into().expect("outline size"),
870        });
871    }
872
873    pub fn sync(&mut self, queue: &wgpu::Queue, pours: &[Pour], dt: f32) {
874        let before = self.targets.clone();
875        let count = self.bodies.len();
876        for body in &mut self.bodies {
877            body.seen = false;
878        }
879        for pour in pours {
880            if let Some(index) = self.bodies.iter().position(|body| body.key == pour.key) {
881                let slot = self.bodies[index].slot;
882                let next = pour.target.carried(&self.targets[slot], dt);
883                let moved = target_drift(&self.targets[slot], &next) > 0.05;
884                let body = &mut self.bodies[index];
885                body.seen = true;
886                body.gone = 0.0;
887                body.quiet = if moved { 0.0 } else { body.quiet + dt };
888                self.targets[slot] = next;
889                continue;
890            }
891            let Some(slot) = self.free_slots.pop() else {
892                continue;
893            };
894            let seed = pour.key.bytes().fold(2_166_136_261u32, |hash, byte| {
895                (hash ^ byte as u32).wrapping_mul(16_777_619)
896            });
897            let particles = spawn(
898                pour.fill.as_ref().unwrap_or(&pour.target),
899                slot,
900                pour.from,
901                seed,
902            );
903            if particles.is_empty() {
904                self.free_slots.push(slot);
905                continue;
906            }
907            let count = particles.len() as u32;
908            let Some(start) = self.allocate(count) else {
909                self.free_slots.push(slot);
910                continue;
911            };
912            queue.write_buffer(
913                &self.particles,
914                start as u64 * std::mem::size_of::<Particle>() as u64,
915                bytemuck::cast_slice(&particles),
916            );
917            self.targets[slot] = pour.target;
918            self.bodies.push(Body {
919                key: pour.key.clone(),
920                slot,
921                start,
922                count,
923                seen: true,
924                gone: 0.0,
925                quiet: 0.0,
926            });
927        }
928        let mut freed = Vec::new();
929        for body in &mut self.bodies {
930            if body.seen {
931                continue;
932            }
933            self.targets[body.slot].flow[3] = 0.0;
934            self.targets[body.slot].carry[0] = 0.0;
935            self.targets[body.slot].carry[1] = 0.0;
936            body.gone += dt;
937            if body.gone > DRAIN_TIME {
938                freed.push(body.key.clone());
939            }
940        }
941        for key in freed {
942            let Some(index) = self.bodies.iter().position(|body| body.key == key) else {
943                continue;
944            };
945            let body = self.bodies.remove(index);
946            let dead = vec![
947                Particle {
948                    born: -1.0,
949                    ..Particle::default()
950                };
951                body.count as usize
952            ];
953            queue.write_buffer(
954                &self.particles,
955                body.start as u64 * std::mem::size_of::<Particle>() as u64,
956                bytemuck::cast_slice(&dead),
957            );
958            self.release(body.start, body.count);
959            self.free_slots.push(body.slot);
960        }
961        let changed = self.bodies.len() != count
962            || self.bodies.iter().any(|body| body.gone > 0.0)
963            || target_drift_all(&before, &self.targets) > 0.05
964            || self.wire_changed;
965        self.still = if changed { 0.0 } else { self.still + dt };
966        self.wire_changed = false;
967        queue.write_buffer(&self.targets_buffer, 0, bytemuck::cast_slice(&self.targets));
968    }
969
970    pub fn step(
971        &mut self,
972        device: &wgpu::Device,
973        queue: &wgpu::Queue,
974        dt: f32,
975        time: f32,
976        calm: f32,
977        field: bool,
978    ) {
979        let mut encoder =
980            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Sim") });
981        self.encode(queue, &mut encoder, dt, time, calm, field, None);
982        queue.submit(Some(encoder.finish()));
983    }
984
985    #[allow(clippy::too_many_arguments)]
986    pub fn encode(
987        &mut self,
988        queue: &wgpu::Queue,
989        encoder: &mut wgpu::CommandEncoder,
990        dt: f32,
991        time: f32,
992        calm: f32,
993        field: bool,
994        mut profiler: Option<&mut GpuProfiler>,
995    ) {
996        if !dt.is_finite() || dt < 0.0 {
997            return;
998        }
999        let resting = self.still > self.settle;
1000        if resting {
1001            self.clock = 0.0;
1002            if !(field && self.stale) {
1003                self.moved = false;
1004                return;
1005            }
1006        }
1007        self.moved = field;
1008        let asleep = self.asleep();
1009        let uniform = self.sim_uniform(asleep, time, calm);
1010        queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&uniform));
1011        if field {
1012            self.pipeline
1013                .update(queue, self.smoothing, self.outline, &self.wires, self.wire);
1014        }
1015        self.clock = (self.clock + dt).min(SUBSTEP * 4.0);
1016        let particle_groups = self.high.div_ceil(256).max(1);
1017        let awake_groups = self.high.saturating_sub(asleep).div_ceil(256).max(1);
1018        let cell_groups = (self.cells[0] * self.cells[1]).div_ceil(BLOCK).max(1);
1019        {
1020            let stamp = profiler
1021                .as_deref_mut()
1022                .and_then(|profile| profile.pass("fluid sim"));
1023            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1024                label: Some("Sim"),
1025                timestamp_writes: profiler
1026                    .as_deref()
1027                    .and_then(|profile| profile.compute_writes(stamp)),
1028            });
1029            pass.set_bind_group(0, &self.bind, &[]);
1030            while self.clock >= SUBSTEP {
1031                self.clock -= SUBSTEP;
1032                self.bin(&mut pass, particle_groups, cell_groups);
1033                dispatch(&mut pass, &self.pipes.density, awake_groups, 1);
1034                dispatch(&mut pass, &self.pipes.forces, awake_groups, 1);
1035                dispatch(&mut pass, &self.pipes.integrate, awake_groups, 1);
1036            }
1037        }
1038        if !field {
1039            self.stale = true;
1040            return;
1041        }
1042        self.stale = false;
1043        {
1044            let stamp = profiler
1045                .as_deref_mut()
1046                .and_then(|profile| profile.pass("fluid gather"));
1047            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1048                label: Some("Gather"),
1049                timestamp_writes: profiler
1050                    .as_deref()
1051                    .and_then(|profile| profile.compute_writes(stamp)),
1052            });
1053            pass.set_bind_group(0, &self.bind, &[]);
1054            self.bin(&mut pass, particle_groups, cell_groups);
1055            dispatch(
1056                &mut pass,
1057                &self.pipes.gather,
1058                self.width.div_ceil(8),
1059                self.height.div_ceil(8),
1060            );
1061        }
1062        self.pipeline.encode(encoder, profiler);
1063    }
1064
1065    fn sim_uniform(&self, asleep: u32, time: f32, calm: f32) -> Uniform {
1066        let mut apart = self.params.apart;
1067        apart[2] = asleep as f32;
1068        Uniform {
1069            grid: [
1070                self.cells[0] as f32,
1071                self.cells[1] as f32,
1072                KERNEL,
1073                self.high as f32,
1074            ],
1075            table: [self.width as f32, self.height as f32, self.rest, SUBSTEP],
1076            k: [
1077                self.params.pressure,
1078                self.params.contain,
1079                self.params.cohesion,
1080                self.params.damping,
1081            ],
1082            misc: [
1083                self.params.life_rate,
1084                SPACING,
1085                self.params.gather.clamp(1.0, 2.0),
1086                self.params.viscosity,
1087            ],
1088            apart,
1089            tension: self.params.tension,
1090            clock: [time, calm, 0.0, 0.0],
1091        }
1092    }
1093
1094    fn bin(&self, pass: &mut wgpu::ComputePass, particle_groups: u32, cell_groups: u32) {
1095        dispatch(pass, &self.pipes.clear_cells, cell_groups, 1);
1096        dispatch(pass, &self.pipes.count, particle_groups, 1);
1097        dispatch(pass, &self.pipes.scan_cells, cell_groups, 1);
1098        dispatch(pass, &self.pipes.scan_blocks, 1, 1);
1099        dispatch(pass, &self.pipes.scatter, particle_groups, 1);
1100        dispatch(pass, &self.pipes.order, cell_groups, 1);
1101    }
1102
1103    fn asleep(&self) -> u32 {
1104        self.bodies
1105            .iter()
1106            .find(|body| body.start == 0 && body.seen && body.quiet > self.settle)
1107            .map_or(0, |body| body.count)
1108    }
1109
1110    fn allocate(&mut self, count: u32) -> Option<u32> {
1111        let at = self.free_ranges.iter().position(|(_, len)| *len >= count)?;
1112        let (start, len) = self.free_ranges[at];
1113        if len == count {
1114            self.free_ranges.remove(at);
1115        } else {
1116            self.free_ranges[at] = (start + count, len - count);
1117        }
1118        self.high = self.high.max(start + count);
1119        Some(start)
1120    }
1121
1122    fn release(&mut self, start: u32, count: u32) {
1123        self.free_ranges.push((start, count));
1124        self.free_ranges.sort_by_key(|range| range.0);
1125        let mut merged: Vec<(u32, u32)> = Vec::new();
1126        for (start, len) in self.free_ranges.drain(..) {
1127            if let Some(last) = merged.last_mut()
1128                && last.0 + last.1 == start
1129            {
1130                last.1 += len;
1131                continue;
1132            }
1133            merged.push((start, len));
1134        }
1135        self.free_ranges = merged;
1136    }
1137}
1138
1139fn dispatch(pass: &mut wgpu::ComputePass, pipeline: &wgpu::ComputePipeline, x: u32, y: u32) {
1140    pass.set_pipeline(pipeline);
1141    pass.dispatch_workgroups(x.max(1), y.max(1), 1);
1142}
1143
1144fn target_drift(a: &Target, b: &Target) -> f32 {
1145    let left: &[f32] = bytemuck::cast_slice(std::slice::from_ref(a));
1146    let right: &[f32] = bytemuck::cast_slice(std::slice::from_ref(b));
1147    left.iter()
1148        .zip(right)
1149        .map(|(x, y)| (x - y).abs())
1150        .fold(0.0, f32::max)
1151}
1152
1153fn target_drift_all(a: &[Target], b: &[Target]) -> f32 {
1154    let left: &[f32] = bytemuck::cast_slice(a);
1155    let right: &[f32] = bytemuck::cast_slice(b);
1156    left.iter()
1157        .zip(right)
1158        .map(|(x, y)| (x - y).abs())
1159        .fold(0.0, f32::max)
1160}
1161
1162#[cfg(test)]
1163#[path = "fluid_seams_test.rs"]
1164mod seams;
1165
1166#[cfg(test)]
1167#[path = "fluid_determinism_test.rs"]
1168mod determinism;
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173    use crate::shape::Form;
1174
1175    fn reference_raise(
1176        p: [f32; 2],
1177        value: [f32; 4],
1178        segments: &[[f32; 4]],
1179        wire: Wire,
1180    ) -> [f32; 4] {
1181        let mut h = value[0];
1182        let mut ch = [
1183            value[1] / h.max(1e-3),
1184            value[2] / h.max(1e-3),
1185            value[3] / h.max(1e-3),
1186        ];
1187        let mut near = 1e9f32;
1188        let mut along = 0.0;
1189        let mut side = 0.0;
1190        let mut run = 0.0;
1191        for segment in segments.iter().take(256) {
1192            let a = [segment[0], segment[1]];
1193            let ba = [segment[2] - a[0], segment[3] - a[1]];
1194            let span = ba[0].hypot(ba[1]);
1195            let t = (dot2(sub2(p, a), ba) / (span * span).max(1e-4)).clamp(0.0, 1.0);
1196            let off = sub2(sub2(p, a), scale2(ba, t));
1197            let d = len2(off);
1198            if d < near {
1199                near = d;
1200                along = run + t * span;
1201                side = (ba[0] * off[1] - ba[1] * off[0]).signum() * d;
1202            }
1203            run += span;
1204        }
1205        if near < wire.radius + 1.0 {
1206            let across = (near / wire.radius).clamp(0.0, 1.0);
1207            let strands = 0.5
1208                + 0.5
1209                    * ((along / wire.period + side / wire.radius * 0.9) * std::f32::consts::TAU)
1210                        .cos();
1211            let lift =
1212                wire.lift * (1.0 - across * across).max(0.0).sqrt() * (0.82 + 0.18 * strands);
1213            let edge = 1.0 - smoothstep(wire.radius - 0.6, wire.radius + 0.6, near);
1214            if lift > h {
1215                for (channel, target) in ch.iter_mut().zip([0.0, 1.0, 12.0]) {
1216                    *channel += (target - *channel) * edge;
1217                }
1218                h += (lift - h) * edge;
1219            }
1220        }
1221        [h, ch[0] * h, ch[1] * h, ch[2] * h]
1222    }
1223
1224    #[test]
1225    fn wire_raise_single_segment_and_strands() {
1226        let wire = Wire::default();
1227        let segment = [[0.0, 0.0, 20.0, 0.0]];
1228        let crest = reference_raise([0.0, 0.0], [0.0; 4], &segment, wire);
1229        let trough = reference_raise([1.6, 0.0], [0.0; 4], &segment, wire);
1230        let strands = 0.5 + 0.5 * (1.6 / wire.period * std::f32::consts::TAU).cos();
1231        assert!((crest[0] - wire.lift).abs() < 1e-5);
1232        assert!((trough[0] - wire.lift * (0.82 + 0.18 * strands)).abs() < 1e-5);
1233        assert!((crest[3] / crest[0] - 12.0).abs() < 1e-5);
1234        let edge = reference_raise([5.0, 1.8], [0.2, 0.0, 0.0, 0.0], &segment, wire);
1235        assert!(edge[0] > 0.2 && edge[0] < crest[0]);
1236        let high = reference_raise([5.0, 0.0], [10.0, 1.0, 2.0, 3.0], &segment, wire);
1237        assert_eq!(high, [10.0, 1.0, 2.0, 3.0]);
1238    }
1239
1240    #[test]
1241    fn dual_sigma_and_taps() {
1242        let smoothing = Smoothing::default();
1243        let [wide, pool] = smoothing.residuals();
1244        assert!((wide - (100.0f32 - 4.0).sqrt()).abs() < 0.0001);
1245        assert!((pool - (400.0f32 - 4.0).sqrt()).abs() < 0.0001);
1246        let sigma = 3.0f32;
1247        let reach = (sigma * 2.5).ceil() as i32;
1248        let weights: Vec<f32> = (-reach..=reach)
1249            .map(|k| (-(k * k) as f32 / (2.0 * sigma * sigma)).exp())
1250            .collect();
1251        let total: f32 = weights.iter().sum();
1252        let normalized: f32 = weights.iter().map(|w| w / total).sum();
1253        assert!((normalized - 1.0).abs() < 1e-6);
1254        let analytic: f32 = weights.iter().map(|w| w / (sigma * 2.5066283)).sum();
1255        assert!((analytic - 1.0).abs() < 0.02);
1256    }
1257
1258    #[test]
1259    fn field_shaders_validate() {
1260        for source in [SPH_WGSL, BLUR_WGSL, JFA_WGSL, EDGE_WGSL, RAISE_WGSL] {
1261            let module = naga::front::wgsl::parse_str(source).expect("parse field shader");
1262            naga::valid::Validator::new(
1263                naga::valid::ValidationFlags::all(),
1264                naga::valid::Capabilities::all(),
1265            )
1266            .validate(&module)
1267            .expect("validate field shader");
1268        }
1269    }
1270
1271    #[test]
1272    #[ignore = "needs a GPU; run under pgpu"]
1273    fn two_wires_and_small_pool_inspect() {
1274        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1275            backends: wgpu::Backends::VULKAN,
1276            ..wgpu::InstanceDescriptor::new_without_display_handle()
1277        });
1278        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1279            power_preference: wgpu::PowerPreference::HighPerformance,
1280            compatible_surface: None,
1281            force_fallback_adapter: false,
1282            apply_limit_buckets: false,
1283        }))
1284        .expect("adapter");
1285        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1286            label: Some("fluid wire test"),
1287            required_features: wgpu::Features::empty(),
1288            ..Default::default()
1289        }))
1290        .expect("device");
1291        let mut fluid = Fluid::new(
1292            &device,
1293            FluidDesc {
1294                width: 64,
1295                height: 64,
1296                max_particles: 512,
1297                max_targets: 4,
1298                params: Params::default(),
1299                settle: SETTLE,
1300            },
1301        );
1302        let look = Look {
1303            tall: 1.0,
1304            active: 0.0,
1305            weight_b: 0.0,
1306            weight_a: 0.0,
1307        };
1308        let pour = Pour {
1309            key: "pool".into(),
1310            target: Target::bead([32.0, 32.0], 8.0, &look),
1311            from: None,
1312            fill: None,
1313        };
1314        let wires = [[8.0, 32.5, 56.0, 32.5], [32.5, 8.0, 32.5, 56.0]];
1315        let dt = SUBSTEP * 4.0;
1316        fluid.sync(&queue, std::slice::from_ref(&pour), dt);
1317        let wire = Wire::default();
1318        let smoothing = Smoothing::default();
1319        let outline = Outline::default();
1320        fluid.set_field(
1321            &wires,
1322            [wire.radius, wire.lift, wire.period],
1323            [smoothing.wide, smoothing.fine, smoothing.pool],
1324            [
1325                outline.shape[0],
1326                outline.shape[1],
1327                outline.shape[2],
1328                outline.shape[3],
1329                outline.size[0],
1330                outline.size[1],
1331                outline.size[2],
1332                outline.size[3],
1333            ],
1334        );
1335        for frame in 0..10 {
1336            fluid.step(&device, &queue, dt, frame as f32 * dt, 1.0, true);
1337        }
1338        let state = fluid.inspect_row(&device, &queue, 32);
1339        assert_eq!(state.live, fluid.particle_count());
1340        let (lo, hi) = state.bounds.expect("pool bounds");
1341        assert!(lo[0] > 16.0 && lo[1] > 16.0 && hi[0] < 48.0 && hi[1] < 48.0);
1342        assert!(
1343            state.height_min >= 0.0
1344                && state.height_max > wire.lift - 1.0
1345                && state.height_max < wire.lift + 1.0
1346        );
1347        assert!(state.height_mean > 0.0 && state.height_mean < 2.0);
1348        assert_eq!(state.row.as_ref().map(Vec::len), Some(64));
1349        assert!(state.channel_histogram[2].contains_key(&192));
1350        fluid.sync(&queue, std::slice::from_ref(&pour), 3.0);
1351        fluid.sync(&queue, &[pour], 3.0);
1352        let rested = fluid.inspect(&device, &queue);
1353        assert!(rested.resting);
1354        assert_eq!(rested.asleep, rested.live);
1355    }
1356
1357    #[test]
1358    fn a_bead_keeps_every_particle() {
1359        let mut fluid = Reference::bead([40.0, 40.0], 10.0, [80.0, 80.0]);
1360        let n = fluid.count();
1361        assert!(n > 8, "{n}");
1362        assert!(rest_density() > 0.0 && rest_density().is_finite());
1363        let start: Vec<[f32; 2]> = fluid.particles().iter().map(|p| p.pos).collect();
1364        for frame in 0..60 {
1365            fluid.step(SUBSTEP * 4.0, frame as f32 * SUBSTEP * 4.0, 1.0);
1366        }
1367        assert_eq!(fluid.count(), n);
1368        assert_eq!(fluid.particles().len(), n);
1369        let mut moved = false;
1370        for (particle, from) in fluid.particles().iter().zip(&start) {
1371            assert!(particle.pos[0].is_finite() && particle.pos[1].is_finite());
1372            assert!(particle.born > 0.0);
1373            let away = (particle.pos[0] - 40.0).hypot(particle.pos[1] - 40.0);
1374            assert!(away < 16.0, "{away}");
1375            if (particle.pos[0] - from[0]).abs() > 1e-3 || (particle.pos[1] - from[1]).abs() > 1e-3
1376            {
1377                moved = true;
1378            }
1379        }
1380        assert!(moved);
1381    }
1382
1383    #[test]
1384    fn the_same_bead_repeats() {
1385        let mut left = Reference::bead([40.0, 40.0], 8.0, [80.0, 80.0]);
1386        let mut right = Reference::bead([40.0, 40.0], 8.0, [80.0, 80.0]);
1387        for frame in 0..20 {
1388            let time = frame as f32 * SUBSTEP * 4.0;
1389            left.step(SUBSTEP * 4.0, time, 1.0);
1390            right.step(SUBSTEP * 4.0, time, 1.0);
1391        }
1392        for (a, b) in left.particles().iter().zip(right.particles()) {
1393            assert_eq!(a.pos, b.pos);
1394            assert_eq!(a.vel, b.vel);
1395            assert_eq!(a.life, b.life);
1396        }
1397    }
1398
1399    #[test]
1400    fn a_drop_spawns_inside_itself() {
1401        let look = Look {
1402            tall: 1.0,
1403            active: 0.2,
1404            weight_b: 0.0,
1405            weight_a: 0.1,
1406        };
1407        let form = Form {
1408            tilt: 0.2,
1409            irregular: 0.15,
1410            seed: 1.7,
1411            rim: 0.0,
1412            weight: [0.3, 0.2],
1413        };
1414        let drop = Target::drop([48.0, 48.0], [14.0, 11.0], &form, &look).organic(0.1, 0.05, 2.0);
1415        let particles = spawn(&drop, 0, None, 9);
1416        assert!(particles.len() > 4, "{}", particles.len());
1417        assert!(particles.iter().all(|particle| drop.inside(particle.pos)));
1418        let bead = Target::bead([20.0, 20.0], 6.0, &look)
1419            .pull(0.5)
1420            .mass(1.2)
1421            .shifted([1.0, -1.0]);
1422        assert_eq!(bead.kind(), 3);
1423        assert!(bead.inside(bead.origin()));
1424        assert_eq!(
1425            Target::capsule([10.0, 12.0], [28.0, 16.0], [4.0, 3.0], &look)
1426                .drifting(0.2)
1427                .kind(),
1428            1
1429        );
1430        assert_eq!(
1431            Target::curve(
1432                [[8.0, 8.0], [16.0, 18.0], [28.0, 16.0], [36.0, 10.0]],
1433                [3.0, 4.0, 3.0],
1434                &look,
1435            )
1436            .flare(1.4)
1437            .revealed(0.5)
1438            .kind(),
1439            2
1440        );
1441        assert_eq!(
1442            Target::outline(
1443                [24.0, 24.0],
1444                9.0,
1445                [
1446                    [0.04, 0.2],
1447                    [0.02, 0.4],
1448                    [0.01, 1.0],
1449                    [0.02, 0.6],
1450                    [0.01, 0.3],
1451                ],
1452                &look,
1453            )
1454            .squash(0.85)
1455            .gathered([24.0, 24.0], 0.1, 0.9)
1456            .kind(),
1457            4
1458        );
1459    }
1460}