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}