1#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct WindConfig {
12 pub base_direction: [f32; 3],
14 pub base_speed: f32,
16 pub turbulence: f32,
18 pub gust_frequency: f32,
20 pub vortex_strength: f32,
22 pub seed: u64,
24}
25
26impl Default for WindConfig {
27 fn default() -> Self {
28 Self {
29 base_direction: [1.0, 0.0, 0.0],
30 base_speed: 5.0,
31 turbulence: 0.3,
32 gust_frequency: 0.5,
33 vortex_strength: 0.2,
34 seed: 42,
35 }
36 }
37}
38
39#[allow(dead_code)]
41#[derive(Debug, Clone)]
42pub struct WindField {
43 pub config: WindConfig,
44}
45
46#[allow(dead_code)]
48#[derive(Debug, Clone)]
49pub struct WindSample {
50 pub velocity: [f32; 3],
52 pub pressure: f32,
54}
55
56#[inline]
60fn lcg_hash(v: u64) -> u64 {
61 v.wrapping_mul(6_364_136_223_846_793_005)
62 .wrapping_add(1_442_695_040_888_963_407)
63}
64
65#[inline]
67fn hash_corner(ix: i32, iy: i32, iz: i32, seed: u64) -> f32 {
68 let h = lcg_hash(lcg_hash(lcg_hash(seed ^ (ix as u64)) ^ (iy as u64)) ^ (iz as u64));
69 (h as f32 / u64::MAX as f32) * 2.0 - 1.0
71}
72
73#[inline]
75fn smoothstep(t: f32) -> f32 {
76 t * t * (3.0 - 2.0 * t)
77}
78
79#[allow(clippy::too_many_arguments)]
81#[inline]
82fn trilerp(
83 c000: f32,
84 c100: f32,
85 c010: f32,
86 c110: f32,
87 c001: f32,
88 c101: f32,
89 c011: f32,
90 c111: f32,
91 tx: f32,
92 ty: f32,
93 tz: f32,
94) -> f32 {
95 let c00 = c000 + tx * (c100 - c000);
96 let c10 = c010 + tx * (c110 - c010);
97 let c01 = c001 + tx * (c101 - c001);
98 let c11 = c011 + tx * (c111 - c011);
99 let c0 = c00 + ty * (c10 - c00);
100 let c1 = c01 + ty * (c11 - c01);
101 c0 + tz * (c1 - c0)
102}
103
104#[allow(dead_code)]
107pub fn value_noise_3d(x: f32, y: f32, z: f32, seed: u64) -> f32 {
108 let ix = x.floor() as i32;
109 let iy = y.floor() as i32;
110 let iz = z.floor() as i32;
111 let fx = smoothstep(x - ix as f32);
112 let fy = smoothstep(y - iy as f32);
113 let fz = smoothstep(z - iz as f32);
114
115 trilerp(
116 hash_corner(ix, iy, iz, seed),
117 hash_corner(ix + 1, iy, iz, seed),
118 hash_corner(ix, iy + 1, iz, seed),
119 hash_corner(ix + 1, iy + 1, iz, seed),
120 hash_corner(ix, iy, iz + 1, seed),
121 hash_corner(ix + 1, iy, iz + 1, seed),
122 hash_corner(ix, iy + 1, iz + 1, seed),
123 hash_corner(ix + 1, iy + 1, iz + 1, seed),
124 fx,
125 fy,
126 fz,
127 )
128}
129
130#[allow(dead_code)]
134pub fn dynamic_pressure(speed: f32) -> f32 {
135 0.5 * 1.225 * speed * speed
136}
137
138#[allow(dead_code)]
140pub fn wind_speed_beaufort(speed_ms: f32) -> u8 {
141 const THRESHOLDS: [f32; 13] = [
143 0.3,
144 1.6,
145 3.4,
146 5.5,
147 8.0,
148 10.8,
149 13.9,
150 17.2,
151 20.8,
152 24.5,
153 28.5,
154 32.7,
155 f32::INFINITY,
156 ];
157 for (i, &upper) in THRESHOLDS.iter().enumerate() {
158 if speed_ms < upper {
159 return i as u8;
160 }
161 }
162 12
163}
164
165impl WindField {
168 #[allow(dead_code)]
170 pub fn new(config: WindConfig) -> Self {
171 Self { config }
172 }
173
174 #[allow(dead_code)]
179 pub fn sample(&self, position: [f32; 3], time: f32) -> WindSample {
180 let cfg = &self.config;
181
182 let scale = 0.5_f32;
184 let tx = value_noise_3d(
185 position[0] * scale + time * 0.1,
186 position[1] * scale,
187 position[2] * scale,
188 cfg.seed,
189 );
190 let ty = value_noise_3d(
191 position[0] * scale,
192 position[1] * scale + time * 0.1,
193 position[2] * scale,
194 cfg.seed.wrapping_add(1),
195 );
196 let tz = value_noise_3d(
197 position[0] * scale,
198 position[1] * scale,
199 position[2] * scale + time * 0.1,
200 cfg.seed.wrapping_add(2),
201 );
202
203 let gust =
205 1.0 + cfg.turbulence * (2.0 * std::f32::consts::PI * cfg.gust_frequency * time).sin();
206 let speed = cfg.base_speed * gust;
207
208 let bd = cfg.base_direction;
210 let up = if bd[1].abs() < 0.9 {
212 [0.0_f32, 1.0, 0.0]
213 } else {
214 [1.0_f32, 0.0, 0.0]
215 };
216 let vortex = cross3(bd, up);
217
218 let vx = bd[0] * speed
219 + cfg.turbulence * tx * cfg.base_speed
220 + cfg.vortex_strength * vortex[0] * cfg.base_speed;
221 let vy = bd[1] * speed
222 + cfg.turbulence * ty * cfg.base_speed
223 + cfg.vortex_strength * vortex[1] * cfg.base_speed;
224 let vz = bd[2] * speed
225 + cfg.turbulence * tz * cfg.base_speed
226 + cfg.vortex_strength * vortex[2] * cfg.base_speed;
227
228 let velocity = [vx, vy, vz];
229 let spd = (vx * vx + vy * vy + vz * vz).sqrt();
230 let pressure = dynamic_pressure(spd);
231
232 WindSample { velocity, pressure }
233 }
234}
235
236#[inline]
239fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
240 [
241 a[1] * b[2] - a[2] * b[1],
242 a[2] * b[0] - a[0] * b[2],
243 a[0] * b[1] - a[1] * b[0],
244 ]
245}
246
247#[inline]
248fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
249 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
250}
251
252#[inline]
253#[allow(dead_code)]
254fn len3(a: [f32; 3]) -> f32 {
255 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
256}
257
258#[allow(dead_code)]
263pub fn wind_force_on_face(wind: &WindSample, face_normal: [f32; 3], face_area: f32) -> [f32; 3] {
264 let spd =
265 (wind.velocity[0].powi(2) + wind.velocity[1].powi(2) + wind.velocity[2].powi(2)).sqrt();
266 if spd < 1e-9 {
267 return [0.0; 3];
268 }
269 let v_norm = [
270 wind.velocity[0] / spd,
271 wind.velocity[1] / spd,
272 wind.velocity[2] / spd,
273 ];
274 let d = dot3(v_norm, face_normal);
275 if d <= 0.0 {
276 return [0.0; 3];
277 }
278 let mag = wind.pressure * d * face_area;
279 [
280 mag * face_normal[0],
281 mag * face_normal[1],
282 mag * face_normal[2],
283 ]
284}
285
286#[allow(dead_code)]
292pub fn apply_wind_to_cloth(
293 cloth: &mut super::cloth::ClothSim,
294 wind: &WindField,
295 time: f32,
296 dt: f32,
297) {
298 for particle in cloth.particles.iter_mut() {
299 if particle.pinned {
300 continue;
301 }
302 let ws = wind.sample(particle.position, time);
303 let normal = [0.0_f32, 1.0, 0.0];
305 let force = wind_force_on_face(&ws, normal, 1.0);
306 let inv_m = if particle.mass > 1e-9 {
307 1.0 / particle.mass
308 } else {
309 0.0
310 };
311 particle.position[0] += force[0] * dt * inv_m;
312 particle.position[1] += force[1] * dt * inv_m;
313 particle.position[2] += force[2] * dt * inv_m;
314 }
315}
316
317#[cfg(test)]
320mod tests {
321 use super::*;
322 use crate::cloth::{ClothParticle, ClothSim, Spring};
323
324 fn default_config() -> WindConfig {
325 WindConfig::default()
326 }
327
328 #[test]
330 fn noise_deterministic() {
331 let a = value_noise_3d(1.5, 2.3, 0.7, 42);
332 let b = value_noise_3d(1.5, 2.3, 0.7, 42);
333 assert_eq!(a, b);
334 }
335
336 #[test]
338 fn noise_seed_differs() {
339 let a = value_noise_3d(1.0, 1.0, 1.0, 0);
340 let b = value_noise_3d(1.0, 1.0, 1.0, 99);
341 assert_ne!(a, b);
342 }
343
344 #[test]
346 fn noise_range() {
347 for i in 0..20 {
348 let v = value_noise_3d(i as f32 * 0.37, i as f32 * 0.13, i as f32 * 0.71, 7);
349 assert!((-1.0..=1.0).contains(&v), "out of range: {v}");
350 }
351 }
352
353 #[test]
355 fn noise_smooth() {
356 let a = value_noise_3d(1.0, 1.0, 1.0, 1);
357 let b = value_noise_3d(1.001, 1.0, 1.0, 1);
358 assert!((a - b).abs() < 0.01, "not smooth: diff={}", (a - b).abs());
359 }
360
361 #[test]
363 fn dynamic_pressure_formula() {
364 let p = dynamic_pressure(10.0);
365 let expected = 0.5 * 1.225 * 100.0;
366 assert!((p - expected).abs() < 1e-4, "got {p}, expected {expected}");
367 }
368
369 #[test]
371 fn dynamic_pressure_zero() {
372 assert_eq!(dynamic_pressure(0.0), 0.0);
373 }
374
375 #[test]
377 fn beaufort_calm() {
378 assert_eq!(wind_speed_beaufort(0.0), 0);
379 assert_eq!(wind_speed_beaufort(0.2), 0);
380 }
381
382 #[test]
384 fn beaufort_breeze() {
385 assert_eq!(wind_speed_beaufort(2.5), 2);
386 }
387
388 #[test]
390 fn beaufort_gale() {
391 assert_eq!(wind_speed_beaufort(18.0), 8);
392 }
393
394 #[test]
396 fn beaufort_max() {
397 assert_eq!(wind_speed_beaufort(33.0), 12);
398 }
399
400 #[test]
402 fn wind_force_backfacing_zero() {
403 let ws = WindSample {
404 velocity: [1.0, 0.0, 0.0],
405 pressure: dynamic_pressure(1.0),
406 };
407 let f = wind_force_on_face(&ws, [-1.0, 0.0, 0.0], 1.0);
409 assert_eq!(f, [0.0; 3]);
410 }
411
412 #[test]
414 fn wind_force_proportional_to_area() {
415 let ws = WindSample {
416 velocity: [1.0, 0.0, 0.0],
417 pressure: dynamic_pressure(1.0),
418 };
419 let f1 = wind_force_on_face(&ws, [1.0, 0.0, 0.0], 1.0);
420 let f2 = wind_force_on_face(&ws, [1.0, 0.0, 0.0], 2.0);
421 let mag1 = len3(f1);
422 let mag2 = len3(f2);
423 assert!((mag2 - 2.0 * mag1).abs() < 1e-5, "mag1={mag1}, mag2={mag2}");
424 }
425
426 #[test]
428 fn wind_sample_pressure_matches_speed() {
429 let cfg = default_config();
430 let field = WindField::new(cfg);
431 let ws = field.sample([0.0, 0.0, 0.0], 0.0);
432 let spd = len3(ws.velocity);
433 let expected = dynamic_pressure(spd);
434 assert!((ws.pressure - expected).abs() < 1e-3, "pressure mismatch");
435 }
436
437 #[test]
439 fn sample_time_varying() {
440 let field = WindField::new(WindConfig {
441 turbulence: 0.5,
442 gust_frequency: 1.0,
443 ..WindConfig::default()
444 });
445 let s0 = field.sample([0.0; 3], 0.0);
446 let s1 = field.sample([0.0; 3], 1.0);
447 let diff = len3([
449 s0.velocity[0] - s1.velocity[0],
450 s0.velocity[1] - s1.velocity[1],
451 s0.velocity[2] - s1.velocity[2],
452 ]);
453 assert!(diff > 1e-4, "time-varying expected, diff={diff}");
454 }
455
456 #[test]
458 fn apply_wind_no_panic() {
459 let mut sim = ClothSim {
460 particles: vec![
461 ClothParticle::new([0.0, 0.0, 0.0], 0.1),
462 ClothParticle::new([0.1, 0.0, 0.0], 0.1),
463 ],
464 springs: vec![Spring {
465 a: 0,
466 b: 1,
467 rest_length: 0.1,
468 stiffness: 100.0,
469 kind: crate::cloth::SpringKind::Structural,
470 }],
471 gravity: [0.0, -9.81, 0.0],
472 damping: 0.99,
473 };
474 let field = WindField::new(WindConfig::default());
475 apply_wind_to_cloth(&mut sim, &field, 0.0, 0.016);
476 for p in &sim.particles {
478 assert!(p.position[0].is_finite());
479 }
480 }
481}