viewport_lib_wind/
field.rs1use glam::Vec3;
4
5use crate::globals::WindGlobals;
6
7#[derive(Clone, Copy, Debug)]
10pub struct WindAuthoring {
11 pub direction: Vec3,
13 pub base_strength: f32,
15 pub gust_strength: f32,
17 pub gust_frequency: f32,
19 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#[derive(Clone, Debug)]
41pub struct WindField {
42 authoring: WindAuthoring,
43 time: f32,
44}
45
46impl WindField {
47 pub fn new(authoring: WindAuthoring) -> Self {
49 Self {
50 authoring,
51 time: 0.0,
52 }
53 }
54
55 pub fn set_authoring(&mut self, authoring: WindAuthoring) {
58 self.authoring = authoring;
59 }
60
61 pub fn authoring(&self) -> WindAuthoring {
63 self.authoring
64 }
65
66 pub fn advance(&mut self, dt: f32) {
68 self.time += dt;
69 }
70
71 pub fn reset(&mut self) {
73 self.time = 0.0;
74 }
75
76 pub fn time(&self) -> f32 {
78 self.time
79 }
80
81 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 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 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}