Skip to main content

viewport_lib_wind/
field.rs

1//! CPU-side wind authoring and sampling.
2
3use glam::Vec3;
4
5use crate::globals::WindGlobals;
6
7/// Authoring knobs for a [`WindField`]. These describe what the field looks
8/// like at rest; the field itself owns the time clock that ticks forward.
9#[derive(Clone, Copy, Debug)]
10pub struct WindAuthoring {
11    /// Prevailing wind direction. Need not be unit length.
12    pub direction: Vec3,
13    /// Steady-state magnitude applied along `direction`.
14    pub base_strength: f32,
15    /// Amplitude of the gust envelope.
16    pub gust_strength: f32,
17    /// Gust cycles per second.
18    pub gust_frequency: f32,
19    /// Spatial wavelength factor for gust noise. Larger means smaller gusts.
20    pub spatial_density: f32,
21}
22
23impl Default for WindAuthoring {
24    fn default() -> Self {
25        Self {
26            direction: Vec3::new(1.0, 0.0, 0.0),
27            base_strength: 1.0,
28            gust_strength: 0.3,
29            gust_frequency: 0.5,
30            spatial_density: 0.25,
31        }
32    }
33}
34
35/// The CPU-side wind field. Holds the authoring values plus a running clock.
36///
37/// `sample(world_pos)` returns the same vector the GPU helper produces for a
38/// vertex at the same position, so spring bones, particles, and cloth see a
39/// consistent field.
40#[derive(Clone, Debug)]
41pub struct WindField {
42    authoring: WindAuthoring,
43    time: f32,
44}
45
46impl WindField {
47    /// Build a field from authoring values, starting the clock at zero.
48    pub fn new(authoring: WindAuthoring) -> Self {
49        Self {
50            authoring,
51            time: 0.0,
52        }
53    }
54
55    /// Replace the authoring values. The clock is preserved so a runtime
56    /// tweak does not snap the field discontinuously.
57    pub fn set_authoring(&mut self, authoring: WindAuthoring) {
58        self.authoring = authoring;
59    }
60
61    /// Current authoring values.
62    pub fn authoring(&self) -> WindAuthoring {
63        self.authoring
64    }
65
66    /// Advance the clock by `dt` seconds.
67    pub fn advance(&mut self, dt: f32) {
68        self.time += dt;
69    }
70
71    /// Reset the clock to zero. Authoring values are preserved.
72    pub fn reset(&mut self) {
73        self.time = 0.0;
74    }
75
76    /// Current clock value.
77    pub fn time(&self) -> f32 {
78        self.time
79    }
80
81    /// Sample the wind at a world-space position. Matches the WGSL helper
82    /// formula in [`crate::SHARED_WIND_WGSL`].
83    pub fn sample(&self, world_pos: Vec3) -> Vec3 {
84        let dir = match self.authoring.direction.try_normalize() {
85            Some(d) => d,
86            None => return Vec3::ZERO,
87        };
88        let phase = self.time * self.authoring.gust_frequency
89            + (world_pos.x + world_pos.z) * self.authoring.spatial_density;
90        let gust = phase.sin() * self.authoring.gust_strength;
91        dir * (self.authoring.base_strength + gust)
92    }
93
94    /// Pack the field into the GPU uniform layout.
95    pub fn to_globals(&self) -> WindGlobals {
96        WindGlobals::new(
97            self.authoring.direction.to_array(),
98            self.authoring.base_strength,
99            self.authoring.gust_strength,
100            self.authoring.gust_frequency,
101            self.authoring.spatial_density,
102            self.time,
103        )
104    }
105
106    /// Pack the field into the four `vec4<f32>` slot-params words that the
107    /// deformer body reads from `deform_header.slot_params[slot * 4 .. + 4]`.
108    ///
109    /// Layout:
110    /// - `[0]` = `(direction.x, direction.y, direction.z, base_strength)`
111    /// - `[1]` = `(gust_strength, gust_frequency, spatial_density, time)`
112    /// - `[2]` / `[3]` are reserved for future use.
113    pub fn to_slot_params(&self) -> [[f32; 4]; 4] {
114        let d = self.authoring.direction;
115        [
116            [d.x, d.y, d.z, self.authoring.base_strength],
117            [
118                self.authoring.gust_strength,
119                self.authoring.gust_frequency,
120                self.authoring.spatial_density,
121                self.time,
122            ],
123            [0.0; 4],
124            [0.0; 4],
125        ]
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn approx(a: Vec3, b: Vec3, eps: f32) -> bool {
134        (a - b).length() < eps
135    }
136
137    #[test]
138    fn advance_accumulates_time() {
139        let mut f = WindField::new(WindAuthoring::default());
140        f.advance(0.5);
141        f.advance(0.25);
142        assert!((f.time() - 0.75).abs() < 1e-6);
143    }
144
145    #[test]
146    fn reset_clears_time_only() {
147        let auth = WindAuthoring {
148            base_strength: 2.0,
149            ..WindAuthoring::default()
150        };
151        let mut f = WindField::new(auth);
152        f.advance(10.0);
153        f.reset();
154        assert_eq!(f.time(), 0.0);
155        assert_eq!(f.authoring().base_strength, 2.0);
156    }
157
158    #[test]
159    fn zero_direction_samples_zero() {
160        let auth = WindAuthoring {
161            direction: Vec3::ZERO,
162            ..WindAuthoring::default()
163        };
164        let f = WindField::new(auth);
165        assert_eq!(f.sample(Vec3::new(1.0, 2.0, 3.0)), Vec3::ZERO);
166    }
167
168    #[test]
169    fn steady_field_returns_direction_times_strength() {
170        let auth = WindAuthoring {
171            direction: Vec3::X,
172            base_strength: 2.0,
173            gust_strength: 0.0,
174            gust_frequency: 0.0,
175            spatial_density: 0.0,
176        };
177        let f = WindField::new(auth);
178        assert!(approx(
179            f.sample(Vec3::new(7.0, 0.0, 4.0)),
180            Vec3::new(2.0, 0.0, 0.0),
181            1e-6,
182        ));
183    }
184
185    #[test]
186    fn to_globals_round_trips_fields() {
187        let auth = WindAuthoring {
188            direction: Vec3::new(1.0, 0.0, 2.0),
189            base_strength: 1.5,
190            gust_strength: 0.4,
191            gust_frequency: 0.3,
192            spatial_density: 0.5,
193        };
194        let mut f = WindField::new(auth);
195        f.advance(1.25);
196        let g = f.to_globals();
197        assert_eq!(g.direction, [1.0, 0.0, 2.0]);
198        assert_eq!(g.base_strength, 1.5);
199        assert_eq!(g.gust_strength, 0.4);
200        assert_eq!(g.gust_frequency, 0.3);
201        assert_eq!(g.spatial_density, 0.5);
202        assert!((g.time - 1.25).abs() < 1e-6);
203    }
204}