Skip to main content

pfx_physics/
particles.rs

1use crate::math::{add3, scale3, sub3};
2use crate::noise::{Rng, noise3};
3use crate::wind::Wind;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum ParticleKind {
7    Petal,
8    Dust,
9    Steam,
10}
11
12#[derive(Clone, Copy, Debug)]
13pub struct Speck {
14    pub pos: [f32; 3],
15    pub vel: [f32; 3],
16    pub age: f32,
17    pub life: f32,
18    pub seed: u32,
19}
20
21pub struct Emitter {
22    kind: ParticleKind,
23    origin: [f32; 3],
24    rate: f32,
25    carry: f32,
26    max: usize,
27    rng: Rng,
28    specks: Vec<Speck>,
29}
30
31impl Emitter {
32    pub fn new(kind: ParticleKind, origin: [f32; 3], rate: f32, seed: u64) -> Self {
33        Self {
34            kind,
35            origin,
36            rate,
37            carry: 0.0,
38            max: 256,
39            rng: Rng::new(seed),
40            specks: Vec::new(),
41        }
42    }
43
44    pub fn specks(&self) -> &[Speck] {
45        &self.specks
46    }
47
48    pub fn origin(&self) -> [f32; 3] {
49        self.origin
50    }
51
52    pub fn set_origin(&mut self, origin: [f32; 3]) {
53        if origin.iter().all(|value| value.is_finite()) {
54            self.origin = origin;
55        }
56    }
57
58    pub fn step(&mut self, dt: f32, time: f32, wind: &Wind) {
59        if !dt.is_finite() || dt <= 0.0 {
60            return;
61        }
62        self.carry += self.rate * dt;
63        while self.carry >= 1.0 && self.specks.len() < self.max {
64            self.carry -= 1.0;
65            self.spawn();
66        }
67        let kind = self.kind;
68        for speck in &mut self.specks {
69            let blow = wind.sample(speck.pos, time);
70            let (drag, gravity) = match kind {
71                ParticleKind::Petal => (1.6, [0.0, -1.4, 0.0]),
72                ParticleKind::Dust => (6.0, [0.0, -0.02, 0.0]),
73                ParticleKind::Steam => (2.2, [0.0, 0.9, 0.0]),
74            };
75            let mut vel = pull(speck.vel, blow, drag, dt);
76            vel = add3(vel, scale3(gravity, dt));
77            if kind == ParticleKind::Petal {
78                let n = noise3([
79                    speck.pos[0] * 3.0 + speck.seed as f32 * 0.001,
80                    time * 2.0,
81                    speck.pos[2] * 3.0,
82                ]);
83                let m = noise3([
84                    speck.pos[2] * 3.0,
85                    time * 1.7 + 4.0,
86                    speck.pos[0] * 3.0 + speck.seed as f32 * 0.002,
87                ]);
88                vel[0] += (n - 0.5) * dt;
89                vel[2] += (m - 0.5) * dt;
90            }
91            speck.vel = vel;
92            speck.pos = add3(speck.pos, scale3(vel, dt));
93            speck.age += dt;
94        }
95        self.specks.retain(|speck| speck.age < speck.life);
96    }
97
98    fn spawn(&mut self) {
99        let (life, spread, vel) = match self.kind {
100            ParticleKind::Petal => (
101                self.rng.range(4.0, 7.0),
102                0.15,
103                [
104                    self.rng.range(-0.2, 0.2),
105                    self.rng.range(-0.15, 0.05),
106                    self.rng.range(-0.2, 0.2),
107                ],
108            ),
109            ParticleKind::Dust => (
110                self.rng.range(8.0, 12.0),
111                0.4,
112                [
113                    self.rng.range(-0.05, 0.05),
114                    self.rng.range(-0.02, 0.04),
115                    self.rng.range(-0.05, 0.05),
116                ],
117            ),
118            ParticleKind::Steam => (
119                self.rng.range(1.4, 2.8),
120                0.04,
121                [
122                    self.rng.range(-0.05, 0.05),
123                    self.rng.range(0.15, 0.35),
124                    self.rng.range(-0.05, 0.05),
125                ],
126            ),
127        };
128        let seed = self.rng.next_u32();
129        let pos = [
130            self.origin[0] + self.rng.range(-spread, spread),
131            self.origin[1] + self.rng.range(-spread * 0.25, spread * 0.25),
132            self.origin[2] + self.rng.range(-spread, spread),
133        ];
134        self.specks.push(Speck {
135            pos,
136            vel,
137            age: 0.0,
138            life,
139            seed,
140        });
141    }
142}
143
144fn pull(vel: [f32; 3], toward: [f32; 3], drag: f32, dt: f32) -> [f32; 3] {
145    let k = 1.0 - (-drag * dt).exp();
146    add3(vel, scale3(sub3(toward, vel), k))
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    fn run(kind: ParticleKind, seed: u64) -> Vec<Speck> {
154        let wind = Wind::new(11, [1.0, 0.0, 0.2], 1.2);
155        let mut emitter = Emitter::new(kind, [0.0, 2.0, 0.0], 8.0, seed);
156        let dt = 1.0 / 60.0;
157        let mut time = 0.0;
158        for _ in 0..240 {
159            emitter.step(dt, time, &wind);
160            time += dt;
161        }
162        emitter.specks().to_vec()
163    }
164
165    #[test]
166    fn the_same_seed_repeats_every_kind() {
167        for kind in [ParticleKind::Petal, ParticleKind::Dust, ParticleKind::Steam] {
168            let a = run(kind, 99);
169            let b = run(kind, 99);
170            assert!(!a.is_empty());
171            assert_eq!(a.len(), b.len());
172            for (left, right) in a.iter().zip(&b) {
173                assert_eq!(left.pos, right.pos);
174                assert_eq!(left.vel, right.vel);
175                assert_eq!(left.age, right.age);
176                assert!(left.pos.iter().all(|v| v.is_finite()));
177            }
178        }
179    }
180
181    #[test]
182    fn moved_origin_spawns_at_the_new_place() {
183        let wind = Wind::new(4, [1.0, 0.0, 0.0], 0.0);
184        let mut emitter = Emitter::new(ParticleKind::Petal, [0.0, 2.0, 0.0], 60.0, 8);
185        emitter.step(0.1, 0.0, &wind);
186        let before = emitter.specks().len();
187        emitter.set_origin([5.0, 2.0, -3.0]);
188        emitter.set_origin([f32::NAN, 0.0, 0.0]);
189        assert_eq!(emitter.origin(), [5.0, 2.0, -3.0]);
190        emitter.step(0.1, 0.1, &wind);
191        let fresh = &emitter.specks()[before..];
192        assert!(!fresh.is_empty());
193        for speck in fresh {
194            assert!((speck.pos[0] - 5.0).abs() < 0.5);
195            assert!((speck.pos[2] + 3.0).abs() < 0.5);
196        }
197    }
198
199    #[test]
200    fn petals_fall_and_steam_rises() {
201        let wind = Wind::new(4, [1.0, 0.0, 0.0], 0.4);
202        let origin = [0.0, 3.0, 0.0];
203        let mut petals = Emitter::new(ParticleKind::Petal, origin, 6.0, 5);
204        let mut steam = Emitter::new(ParticleKind::Steam, origin, 10.0, 5);
205        let dt = 1.0 / 60.0;
206        let mut time = 0.0;
207        for _ in 0..180 {
208            petals.step(dt, time, &wind);
209            steam.step(dt, time, &wind);
210            time += dt;
211        }
212        let petal = petals
213            .specks()
214            .iter()
215            .max_by(|a, b| a.age.total_cmp(&b.age))
216            .unwrap();
217        let puff = steam
218            .specks()
219            .iter()
220            .max_by(|a, b| a.age.total_cmp(&b.age))
221            .unwrap();
222        assert!(petal.pos[1] < origin[1], "{}", petal.pos[1]);
223        assert!(puff.pos[1] > origin[1], "{}", puff.pos[1]);
224    }
225}