Skip to main content

pfx_physics/
wind.rs

1use crate::math::{norm3, scale3, smoothstep};
2use crate::noise::{fbm, pcg};
3
4pub const BEND_WGSL: &str = include_str!("bend.wgsl");
5
6#[derive(Clone, Copy, Debug)]
7pub struct Wind {
8    seed: u32,
9    direction: [f32; 3],
10    strength: f32,
11}
12
13impl Wind {
14    pub fn new(seed: u32, direction: [f32; 3], strength: f32) -> Self {
15        let len = crate::math::len3(direction);
16        let direction = if len < 1e-8 {
17            [1.0, 0.0, 0.0]
18        } else {
19            scale3(direction, 1.0 / len)
20        };
21        Self {
22            seed,
23            direction,
24            strength,
25        }
26    }
27
28    pub fn direction(&self) -> [f32; 3] {
29        self.direction
30    }
31
32    pub fn strength(&self) -> f32 {
33        self.strength
34    }
35
36    pub fn gust_at(&self, p: [f32; 3], time: f32) -> f32 {
37        let envelope = 0.55 + 0.45 * (time * 0.21 + 0.4).sin() * (time * 0.13 + 1.7).sin();
38        let n = fbm([
39            p[0] * 0.37 + time * 0.05,
40            p[1] * 0.37 + 2.0,
41            p[2] * 0.37 + self.seed as f32 * 0.13,
42        ]);
43        (envelope * (0.35 + 0.65 * n)).max(0.0)
44    }
45
46    pub fn sample(&self, p: [f32; 3], time: f32) -> [f32; 3] {
47        scale3(self.direction, self.strength * self.gust_at(p, time))
48    }
49}
50
51pub fn sway(p: [f32; 3], obj: u32, time: f32, wind: &Wind) -> [f32; 3] {
52    let h = (pcg(obj.wrapping_mul(747796405).wrapping_add(11)) >> 8) as f32 / 16_777_216.0;
53    let reach = 0.4
54        + 0.6
55            * smoothstep(
56                0.0,
57                0.35,
58                ((p[0] - 0.4).powi(2) + (p[2] + 0.12).powi(2)).sqrt(),
59            );
60    let gust = wind.gust_at(p, time);
61    let bough = [
62        (time * 0.8 + p[0] * 3.0).sin() * 0.006,
63        0.35 * (time * 0.6 + p[0] * 2.0).sin() * 0.006,
64        0.5 * (time * 0.7 + p[0] * 2.5).cos() * 0.006,
65    ];
66    let flutter = [
67        (time * 3.1 + h * 37.0).sin() * 0.0015,
68        (time * 3.7 + h * 21.0).sin() * 0.0015,
69        (time * 2.9 + h * 29.0).cos() * 0.0015,
70    ];
71    let amp = wind.strength * gust;
72    let local = [
73        (bough[0] * reach + flutter[0]) * amp,
74        (bough[1] * reach + flutter[1]) * amp,
75        (bough[2] * reach + flutter[2]) * amp,
76    ];
77    let flat = [wind.direction[0], 0.0, wind.direction[2]];
78    let dir = norm3([flat[0] + 1e-5, 0.0, flat[2]]);
79    let side = [-dir[2], 0.0, dir[0]];
80    [
81        dir[0] * local[0] + side[0] * local[2],
82        local[1],
83        dir[2] * local[0] + side[2] * local[2],
84    ]
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn gust_is_deterministic_and_sway_follows_strength() {
93        let wind = Wind::new(3, [2.0, 0.0, 0.4], 2.0);
94        let p = [0.5, 0.3, -0.1];
95        assert_eq!(wind.gust_at(p, 1.25), wind.gust_at(p, 1.25));
96        let once = sway(p, 4, 1.25, &wind);
97        let twice = sway(p, 4, 1.25, &wind);
98        assert_eq!(once, twice);
99        assert!(once.iter().any(|v| v.abs() > 0.0));
100        let still = Wind::new(3, [2.0, 0.0, 0.4], 0.0);
101        assert_eq!(sway(p, 4, 1.25, &still), [0.0; 3]);
102        let blow = wind.sample(p, 1.25);
103        let along = blow[0] * wind.direction()[0] + blow[2] * wind.direction()[2];
104        assert!(along > 0.0);
105        assert!(blow[1].abs() < 1e-6);
106        assert!(BEND_WGSL.contains("fn bend("));
107    }
108}