1use std::collections::HashMap;
7use super::{Vec3, lerp, smoothstep, fbm_2d, value_noise_2d};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum PrecipitationType {
13 None,
14 Drizzle,
15 Rain,
16 Snow,
17 Sleet,
18 Hail,
19}
20
21impl PrecipitationType {
22 pub fn terminal_velocity(self) -> f32 {
24 match self {
25 Self::None => 0.0,
26 Self::Drizzle => 1.0,
27 Self::Rain => 6.5,
28 Self::Snow => 1.2,
29 Self::Sleet => 4.0,
30 Self::Hail => 20.0,
31 }
32 }
33
34 pub fn typical_radius_m(self) -> f32 {
36 match self {
37 Self::None => 0.0,
38 Self::Drizzle => 0.000_2,
39 Self::Rain => 0.001_5,
40 Self::Snow => 0.003,
41 Self::Sleet => 0.002,
42 Self::Hail => 0.015,
43 }
44 }
45
46 pub fn can_ice(self) -> bool {
48 matches!(self, Self::Sleet | Self::Hail)
49 }
50
51 pub fn is_soft_accumulation(self) -> bool {
53 matches!(self, Self::Snow | Self::Drizzle)
54 }
55}
56
57#[derive(Debug, Clone)]
60pub struct PrecipitationConfig {
61 pub max_droplets: usize,
63 pub grid_width: usize,
65 pub grid_depth: usize,
67 pub cell_size: f32,
69 pub rain_humidity_threshold: f32,
71 pub snow_threshold_c: f32,
73 pub hail_threshold_c: f32,
75 pub puddle_evap_rate: f32,
77 pub snow_settle_rate: f32,
79}
80
81impl Default for PrecipitationConfig {
82 fn default() -> Self {
83 Self {
84 max_droplets: 4096,
85 grid_width: 64,
86 grid_depth: 64,
87 cell_size: 10.0,
88 rain_humidity_threshold: 0.75,
89 snow_threshold_c: 2.0,
90 hail_threshold_c: -5.0,
91 puddle_evap_rate: 0.001,
92 snow_settle_rate: 0.0005,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy)]
101pub struct DropletPhysics {
102 pub radius_m: f32,
103 pub density_kg_m3: f32, pub drag_coeff: f32,
105 pub air_density: f32,
106}
107
108impl DropletPhysics {
109 pub fn water_drop(radius_m: f32) -> Self {
110 Self { radius_m, density_kg_m3: 1000.0, drag_coeff: 0.47, air_density: 1.225 }
111 }
112 pub fn ice_particle(radius_m: f32) -> Self {
113 Self { radius_m, density_kg_m3: 917.0, drag_coeff: 0.55, air_density: 1.225 }
114 }
115 pub fn snowflake(radius_m: f32) -> Self {
116 Self { radius_m, density_kg_m3: 100.0, drag_coeff: 1.5, air_density: 1.225 }
117 }
118
119 pub fn mass(&self) -> f32 {
121 let vol = (4.0 / 3.0) * std::f32::consts::PI * self.radius_m.powi(3);
122 vol * self.density_kg_m3
123 }
124
125 pub fn area(&self) -> f32 {
127 std::f32::consts::PI * self.radius_m * self.radius_m
128 }
129
130 pub fn terminal_velocity(&self) -> f32 {
132 let m = self.mass();
133 let a = self.area();
134 let cd = self.drag_coeff;
135 let rho = self.air_density;
136 let g = 9.807_f32;
137 (2.0 * m * g / (cd * rho * a)).sqrt()
138 }
139
140 pub fn acceleration(&self, vel: Vec3, wind: Vec3) -> Vec3 {
142 let rel = vel.sub(wind); let speed = rel.length();
144 let m = self.mass();
145 let a = self.area();
146 let drag_mag = 0.5 * self.air_density * self.drag_coeff * a * speed * speed;
148 let drag_dir = if speed > 1e-6 { rel.scale(-1.0 / speed) } else { Vec3::ZERO };
149 let drag = drag_dir.scale(drag_mag / m);
150 let gravity = Vec3::new(0.0, -9.807, 0.0);
152 gravity.add(drag)
153 }
154}
155
156#[derive(Debug, Clone)]
160pub struct Droplet {
161 pub position: Vec3,
162 pub velocity: Vec3,
163 pub kind: PrecipitationType,
164 pub physics: DropletPhysics,
165 pub lifetime: f32,
167 pub landed: bool,
169 pub coalescence: f32,
171}
172
173impl Droplet {
174 pub fn new_rain(pos: Vec3) -> Self {
175 let r = 0.001 + value_noise_2d(pos.x * 0.1, pos.z * 0.1) * 0.002;
176 let phys = DropletPhysics::water_drop(r);
177 let vt = -phys.terminal_velocity();
178 Self {
179 position: pos,
180 velocity: Vec3::new(0.0, vt * 0.5, 0.0),
181 kind: PrecipitationType::Rain,
182 physics: phys,
183 lifetime: 10.0,
184 landed: false,
185 coalescence: 0.0,
186 }
187 }
188
189 pub fn new_snow(pos: Vec3) -> Self {
190 let r = 0.002 + value_noise_2d(pos.x * 0.05, pos.z * 0.05) * 0.003;
191 let phys = DropletPhysics::snowflake(r);
192 Self {
193 position: pos,
194 velocity: Vec3::new(0.0, -0.8, 0.0),
195 kind: PrecipitationType::Snow,
196 physics: phys,
197 lifetime: 20.0,
198 landed: false,
199 coalescence: 0.0,
200 }
201 }
202
203 pub fn new_hail(pos: Vec3) -> Self {
204 let r = 0.008 + value_noise_2d(pos.x * 0.02, pos.z * 0.02) * 0.015;
205 let phys = DropletPhysics::ice_particle(r);
206 let vt = -phys.terminal_velocity();
207 Self {
208 position: pos,
209 velocity: Vec3::new(0.0, vt * 0.3, 0.0),
210 kind: PrecipitationType::Hail,
211 physics: phys,
212 lifetime: 15.0,
213 landed: false,
214 coalescence: 0.0,
215 }
216 }
217
218 pub fn new_sleet(pos: Vec3) -> Self {
219 let r = 0.0015;
220 let phys = DropletPhysics::ice_particle(r);
221 let vt = -phys.terminal_velocity();
222 Self {
223 position: pos,
224 velocity: Vec3::new(0.0, vt * 0.6, 0.0),
225 kind: PrecipitationType::Sleet,
226 physics: phys,
227 lifetime: 8.0,
228 landed: false,
229 coalescence: 0.0,
230 }
231 }
232
233 pub fn tick(&mut self, dt: f32, wind: Vec3, surface_y: f32) {
235 if self.landed { return; }
236 let acc = self.physics.acceleration(self.velocity, wind);
237 self.velocity = self.velocity.add(acc.scale(dt));
238 self.position = self.position.add(self.velocity.scale(dt));
239 self.lifetime -= dt;
240 if self.position.y <= surface_y {
242 self.position.y = surface_y;
243 self.landed = true;
244 }
245 }
246
247 pub fn is_alive(&self) -> bool {
248 !self.landed && self.lifetime > 0.0
249 }
250}
251
252#[derive(Debug, Clone)]
256pub struct RainBand {
257 pub centre: [f32; 2],
259 pub radius: f32,
261 pub intensity: f32,
263 pub drift: [f32; 2],
265 pub kind: PrecipitationType,
267 pub lifetime: f32,
269}
270
271impl RainBand {
272 pub fn new(cx: f32, cz: f32, radius: f32, intensity: f32, kind: PrecipitationType) -> Self {
273 Self {
274 centre: [cx, cz],
275 radius,
276 intensity,
277 drift: [2.0, 1.0],
278 kind,
279 lifetime: 3600.0,
280 }
281 }
282
283 pub fn tick(&mut self, dt: f32) {
284 self.centre[0] += self.drift[0] * dt;
285 self.centre[1] += self.drift[1] * dt;
286 self.lifetime -= dt;
287 if self.lifetime < 300.0 {
289 self.intensity *= 1.0 - dt / 300.0;
290 }
291 }
292
293 pub fn intensity_at(&self, x: f32, z: f32) -> f32 {
295 let dx = x - self.centre[0];
296 let dz = z - self.centre[1];
297 let dist = (dx * dx + dz * dz).sqrt();
298 if dist >= self.radius { return 0.0; }
299 let t = smoothstep(self.radius, 0.0, dist);
300 t * (self.intensity / 100.0)
301 }
302
303 pub fn is_alive(&self) -> bool {
304 self.lifetime > 0.0 && self.intensity > 0.01
305 }
306}
307
308#[derive(Debug, Clone)]
312pub struct SurfaceAccumulation {
313 pub width: usize,
314 pub depth: usize,
315 pub cell_size: f32,
316 pub water_depth: Vec<f32>,
318 pub snow_depth: Vec<f32>,
320 pub ice_depth: Vec<f32>,
322}
323
324impl SurfaceAccumulation {
325 pub fn new(width: usize, depth: usize, cell_size: f32) -> Self {
326 let n = width * depth;
327 Self {
328 width,
329 depth,
330 cell_size,
331 water_depth: vec![0.0; n],
332 snow_depth: vec![0.0; n],
333 ice_depth: vec![0.0; n],
334 }
335 }
336
337 fn idx(&self, gx: usize, gz: usize) -> usize {
338 gz * self.width + gx
339 }
340
341 fn world_to_grid(&self, wx: f32, wz: f32) -> (usize, usize) {
342 let gx = (wx / self.cell_size).floor() as i32;
343 let gz = (wz / self.cell_size).floor() as i32;
344 (
345 gx.rem_euclid(self.width as i32) as usize,
346 gz.rem_euclid(self.depth as i32) as usize,
347 )
348 }
349
350 pub fn add_water(&mut self, wx: f32, wz: f32, depth_m: f32) {
352 let (gx, gz) = self.world_to_grid(wx, wz);
353 let i = self.idx(gx, gz);
354 self.water_depth[i] = (self.water_depth[i] + depth_m).min(1.0);
355 }
356
357 pub fn add_snow(&mut self, wx: f32, wz: f32, depth_m: f32) {
359 let (gx, gz) = self.world_to_grid(wx, wz);
360 let i = self.idx(gx, gz);
361 self.snow_depth[i] = (self.snow_depth[i] + depth_m).min(2.0);
362 }
363
364 pub fn add_ice(&mut self, wx: f32, wz: f32, thickness_m: f32) {
366 let (gx, gz) = self.world_to_grid(wx, wz);
367 let i = self.idx(gx, gz);
368 self.ice_depth[i] = (self.ice_depth[i] + thickness_m).min(0.1);
369 }
370
371 pub fn water_at(&self, wx: f32, wz: f32) -> f32 {
373 let (gx, gz) = self.world_to_grid(wx, wz);
374 self.water_depth[self.idx(gx, gz)]
375 }
376
377 pub fn snow_at(&self, wx: f32, wz: f32) -> f32 {
379 let (gx, gz) = self.world_to_grid(wx, wz);
380 self.snow_depth[self.idx(gx, gz)]
381 }
382
383 pub fn ice_at(&self, wx: f32, wz: f32) -> f32 {
385 let (gx, gz) = self.world_to_grid(wx, wz);
386 self.ice_depth[self.idx(gx, gz)]
387 }
388
389 pub fn evaporate(&mut self, rate: f32) {
391 for v in &mut self.water_depth {
392 *v = (*v - rate).max(0.0);
393 }
394 }
395
396 pub fn melt_snow(&mut self, temp_above_zero_c: f32, dt: f32) {
398 let melt = (temp_above_zero_c * 3e-3 / 3600.0) * dt;
400 for i in 0..self.snow_depth.len() {
401 let melted = self.snow_depth[i].min(melt);
402 self.snow_depth[i] -= melted;
403 self.water_depth[i] = (self.water_depth[i] + melted * 0.3).min(1.0);
404 }
405 }
406
407 pub fn freeze_water(&mut self, temp_below_zero_c: f32, dt: f32) {
409 let freeze_rate = temp_below_zero_c * 1e-4 * dt;
410 for i in 0..self.water_depth.len() {
411 let frozen = self.water_depth[i].min(freeze_rate);
412 self.water_depth[i] -= frozen;
413 self.ice_depth[i] = (self.ice_depth[i] + frozen * 0.9).min(0.1);
414 }
415 }
416
417 pub fn flow_water(&mut self, dt: f32) {
419 let flow_rate = 0.05 * dt;
421 let old = self.water_depth.clone();
422 for gz in 0..self.depth {
423 for gx in 0..self.width {
424 let i = self.idx(gx, gz);
425 let h = old[i];
426 if h < 1e-5 { continue; }
427 let neighbours = [
429 if gx > 0 { Some(self.idx(gx - 1, gz)) } else { None },
430 if gx + 1 < self.width { Some(self.idx(gx + 1, gz)) } else { None },
431 if gz > 0 { Some(self.idx(gx, gz - 1)) } else { None },
432 if gz + 1 < self.depth { Some(self.idx(gx, gz + 1)) } else { None },
433 ];
434 for nb in neighbours.iter().flatten() {
435 if old[*nb] < h {
436 let transfer = (h - old[*nb]) * flow_rate * 0.25;
437 self.water_depth[i] -= transfer;
438 self.water_depth[*nb] += transfer;
439 }
440 }
441 }
442 }
443 for v in &mut self.water_depth {
444 *v = v.clamp(0.0, 1.0);
445 }
446 }
447}
448
449#[derive(Debug, Clone)]
453pub struct Puddle {
454 pub centre: [f32; 2],
455 pub radius: f32,
456 pub volume: f32,
458 pub area: f32,
460 pub evap_rate: f32,
462 pub age: f32,
464 pub frozen: bool,
466}
467
468impl Puddle {
469 pub fn new(cx: f32, cz: f32, initial_volume: f32) -> Self {
470 let area = (initial_volume / 0.01).sqrt() * std::f32::consts::PI;
471 let radius = (area / std::f32::consts::PI).sqrt();
472 Self {
473 centre: [cx, cz],
474 radius,
475 volume: initial_volume,
476 area,
477 evap_rate: 1e-6,
478 age: 0.0,
479 frozen: false,
480 }
481 }
482
483 pub fn tick(&mut self, dt: f32, temp_c: f32, wind_speed: f32) {
484 self.age += dt;
485 if self.frozen { return; }
486
487 if temp_c < 0.0 {
488 self.frozen = true;
489 return;
490 }
491
492 let evap = self.evap_rate * (1.0 + wind_speed * 0.1) * (1.0 + temp_c * 0.02) * dt;
494 self.volume = (self.volume - evap).max(0.0);
495 self.area = (self.volume / 0.01).sqrt() * std::f32::consts::PI;
497 self.radius = (self.area / std::f32::consts::PI).sqrt();
498 }
499
500 pub fn add_water(&mut self, volume: f32) {
501 if self.frozen { return; }
502 self.volume += volume;
503 self.area = (self.volume / 0.01).sqrt() * std::f32::consts::PI;
504 self.radius = (self.area / std::f32::consts::PI).sqrt();
505 }
506
507 pub fn is_alive(&self) -> bool {
508 self.volume > 1e-8
509 }
510
511 pub fn contains(&self, x: f32, z: f32) -> bool {
512 let dx = x - self.centre[0];
513 let dz = z - self.centre[1];
514 (dx * dx + dz * dz) <= self.radius * self.radius
515 }
516
517 pub fn centre_depth_m(&self) -> f32 {
519 if self.area < 1e-8 { return 0.0; }
520 self.volume / self.area
521 }
522}
523
524#[derive(Debug, Clone)]
528pub struct SnowpackLayer {
529 pub age: f32,
531 pub depth_m: f32,
533 pub density: f32,
535 pub temp_k: f32,
537 pub liquid_water: f32,
539 pub is_ice_layer: bool,
541}
542
543impl SnowpackLayer {
544 pub fn fresh(depth_m: f32, temp_k: f32) -> Self {
545 Self {
546 age: 0.0,
547 depth_m,
548 density: 50.0,
549 temp_k,
550 liquid_water: 0.0,
551 is_ice_layer: false,
552 }
553 }
554
555 pub fn tick(&mut self, dt: f32, surface_temp_k: f32) {
556 self.age += dt;
557 self.temp_k = lerp(self.temp_k, surface_temp_k, 0.0001 * dt);
559 let settle_rate = 5e-6 * dt * (self.density / 50.0).sqrt();
561 let new_density = (self.density + settle_rate * 300.0).min(917.0);
562 if new_density > self.density + 1e-6 {
564 self.depth_m *= self.density / new_density;
565 self.density = new_density;
566 }
567 if self.temp_k > 273.15 {
569 let melt = (self.temp_k - 273.15) * 0.001 * dt;
570 let melted = self.depth_m.min(melt);
571 self.depth_m -= melted;
572 self.liquid_water = (self.liquid_water + melted * 0.5 / self.depth_m.max(0.01)).min(1.0);
573 }
574 if self.temp_k < 271.0 && self.liquid_water > 0.0 {
576 self.is_ice_layer = true;
577 }
578 }
579
580 pub fn swe(&self) -> f32 {
582 self.depth_m * self.density / 1000.0
583 }
584}
585
586#[derive(Debug, Clone)]
588pub struct Snowpack {
589 pub layers: Vec<SnowpackLayer>,
590 pub max_layers: usize,
591}
592
593impl Snowpack {
594 pub fn new() -> Self {
595 Self { layers: Vec::new(), max_layers: 16 }
596 }
597
598 pub fn total_depth(&self) -> f32 {
600 self.layers.iter().map(|l| l.depth_m).sum()
601 }
602
603 pub fn total_swe(&self) -> f32 {
605 self.layers.iter().map(|l| l.swe()).sum()
606 }
607
608 pub fn deposit(&mut self, depth_m: f32, temp_k: f32) {
610 if depth_m < 1e-6 { return; }
611 self.layers.push(SnowpackLayer::fresh(depth_m, temp_k));
612 if self.layers.len() > self.max_layers {
614 let a = self.layers.remove(0);
615 let b = &mut self.layers[0];
616 let total_mass = a.depth_m * a.density + b.depth_m * b.density;
617 let total_depth = a.depth_m + b.depth_m;
618 b.depth_m = total_depth;
619 b.density = if total_depth > 0.0 { total_mass / total_depth } else { 100.0 };
620 b.age = b.age.min(a.age);
621 }
622 }
623
624 pub fn tick(&mut self, dt: f32, surface_temp_k: f32) {
625 for layer in &mut self.layers {
626 layer.tick(dt, surface_temp_k);
627 }
628 self.layers.retain(|l| l.depth_m > 1e-6);
629 }
630}
631
632impl Default for Snowpack {
633 fn default() -> Self { Self::new() }
634}
635
636#[derive(Debug, Clone)]
640pub struct IceSheet {
641 pub thickness_m: f32,
642 pub surface_temp_k: f32,
643 pub age: f32,
644 pub is_black_ice: bool,
646 pub is_rime: bool,
648}
649
650impl IceSheet {
651 pub fn new(thickness_m: f32, temp_k: f32, is_black_ice: bool) -> Self {
652 Self {
653 thickness_m,
654 surface_temp_k: temp_k,
655 age: 0.0,
656 is_black_ice,
657 is_rime: false,
658 }
659 }
660
661 pub fn rime(thickness_m: f32) -> Self {
662 Self {
663 thickness_m,
664 surface_temp_k: 268.0,
665 age: 0.0,
666 is_black_ice: false,
667 is_rime: true,
668 }
669 }
670
671 pub fn tick(&mut self, dt: f32, surface_temp_k: f32) {
672 self.age += dt;
673 self.surface_temp_k = surface_temp_k;
674 if surface_temp_k > 273.15 {
675 let melt = (surface_temp_k - 273.15) * 5e-5 * dt;
676 self.thickness_m = (self.thickness_m - melt).max(0.0);
677 } else {
678 self.thickness_m = (self.thickness_m + 1e-7 * dt).min(0.05);
680 }
681 }
682
683 pub fn friction_coefficient(&self) -> f32 {
684 if self.is_black_ice {
685 0.05 + self.thickness_m * 2.0 } else if self.is_rime {
687 0.15 + self.thickness_m * 5.0
688 } else {
689 0.1 + self.thickness_m * 3.0
690 }
691 }
692
693 pub fn is_alive(&self) -> bool { self.thickness_m > 1e-7 }
694}
695
696#[derive(Debug, Clone, Copy)]
700pub struct SnowCrystal {
701 pub habit: u8,
703 pub size_m: f32,
705 pub mass_kg: f32,
707 pub growth_rate: f32,
709 pub formation_temp_k: f32,
711}
712
713impl SnowCrystal {
714 pub fn form(temp_k: f32, supersaturation: f32) -> Self {
715 let tc = temp_k - 273.15;
716 let habit = if tc > -2.0 { 0 } else if tc > -5.0 { 3 } else if tc > -10.0 { 2 } else if tc > -22.0 { 1 } else { 4 }; let base_size = 0.001 + supersaturation * 0.003;
722 let mass = match habit {
723 0 => base_size.powi(2) * 1e-6 * 900.0,
724 1 => base_size.powi(3) * 1e-9 * 900.0,
725 _ => base_size.powi(2) * 3e-7 * 200.0,
726 };
727 Self {
728 habit,
729 size_m: base_size,
730 mass_kg: mass,
731 growth_rate: supersaturation * 1e-5,
732 formation_temp_k: temp_k,
733 }
734 }
735
736 pub fn grow(&mut self, dt: f32, supersaturation: f32) {
737 self.size_m += self.growth_rate * supersaturation * dt;
738 self.mass_kg += self.growth_rate * supersaturation * dt * 1e-4;
739 }
740
741 pub fn habit_name(&self) -> &'static str {
742 match self.habit {
743 0 => "Thin plate",
744 1 => "Column",
745 2 => "Stellar dendrite",
746 3 => "Needle",
747 4 => "Spatial dendrite",
748 _ => "Unknown",
749 }
750 }
751}
752
753#[derive(Debug, Clone)]
757pub struct HailStone {
758 pub radius_m: f32,
759 pub mass_kg: f32,
760 pub position: Vec3,
761 pub velocity: Vec3,
762 pub cycles: u32,
764 pub ice_mass: f32,
766 pub liquid_coat: f32,
768}
769
770impl HailStone {
771 pub fn new(pos: Vec3) -> Self {
772 Self {
773 radius_m: 0.002,
774 mass_kg: 1e-5,
775 position: pos,
776 velocity: Vec3::new(0.0, -5.0, 0.0),
777 cycles: 0,
778 ice_mass: 0.0,
779 liquid_coat: 0.0,
780 }
781 }
782
783 pub fn accrete(&mut self, liquid_water_content: f32, dt: f32) {
785 let cross_section = std::f32::consts::PI * self.radius_m * self.radius_m;
786 let collection_efficiency = 0.8_f32;
787 let rel_speed = self.velocity.length();
788 let added_mass = liquid_water_content * cross_section * collection_efficiency * rel_speed * dt;
789 self.ice_mass += added_mass;
790 self.mass_kg += added_mass;
791 let vol = self.mass_kg / 917.0;
793 self.radius_m = (3.0 * vol / (4.0 * std::f32::consts::PI)).cbrt();
794 }
795
796 pub fn tick(&mut self, dt: f32, updraft: f32, wind: Vec3) {
797 let phys = DropletPhysics::ice_particle(self.radius_m);
798 let acc = phys.acceleration(self.velocity, wind.add(Vec3::new(0.0, updraft, 0.0)));
799 self.velocity = self.velocity.add(acc.scale(dt));
800 self.position = self.position.add(self.velocity.scale(dt));
801 if self.velocity.y > 0.0 { self.cycles += 0; } }
804
805 pub fn diameter_mm(&self) -> f32 {
806 self.radius_m * 2000.0
807 }
808
809 pub fn severity_class(&self) -> &'static str {
810 let d = self.diameter_mm();
811 if d < 5.0 { "Pea" }
812 else if d < 19.0 { "Marble" }
813 else if d < 38.0 { "Golf ball" }
814 else if d < 64.0 { "Tennis ball" }
815 else { "Softball" }
816 }
817}
818
819#[derive(Debug, Clone, Copy)]
823pub struct SleetParticle {
824 pub position: Vec3,
825 pub velocity: Vec3,
826 pub radius_m: f32,
827 pub ice_fraction: f32,
829 pub lifetime: f32,
830}
831
832impl SleetParticle {
833 pub fn new(pos: Vec3) -> Self {
834 Self {
835 position: pos,
836 velocity: Vec3::new(0.0, -4.0, 0.0),
837 radius_m: 0.002,
838 ice_fraction: 0.5,
839 lifetime: 10.0,
840 }
841 }
842
843 pub fn tick(&mut self, dt: f32, temp_k: f32, wind: Vec3) {
844 let phys = DropletPhysics::ice_particle(self.radius_m);
845 let acc = phys.acceleration(self.velocity, wind);
846 self.velocity = self.velocity.add(acc.scale(dt));
847 self.position = self.position.add(self.velocity.scale(dt));
848 self.lifetime -= dt;
849 if temp_k < 270.0 {
851 self.ice_fraction = (self.ice_fraction + 0.1 * dt).min(1.0);
852 }
853 }
854}
855
856#[derive(Debug, Clone)]
860pub struct ThunderCell {
861 pub position: [f32; 2],
863 pub charge_altitude: f32,
865 pub cloud_base_m: f32,
867 pub cloud_top_m: f32,
869 pub updraft: f32,
871 pub charge: f32,
873 pub discharge_threshold: f32,
875 pub bolts: Vec<LightningBolt>,
877 pub time_since_discharge: f32,
879 pub mean_discharge_interval: f32,
881 pub lifetime: f32,
883 pub intensity: f32,
885 cell_seed: f32,
887}
888
889impl ThunderCell {
890 pub fn new(cx: f32, cz: f32) -> Self {
891 Self {
892 position: [cx, cz],
893 charge_altitude: 6_000.0,
894 cloud_base_m: 1_500.0,
895 cloud_top_m: 12_000.0,
896 updraft: 25.0,
897 charge: 0.0,
898 discharge_threshold: 1.0,
899 bolts: Vec::new(),
900 time_since_discharge: 0.0,
901 mean_discharge_interval: 30.0,
902 lifetime: 7200.0,
903 intensity: 0.0,
904 cell_seed: cx.abs().fract() + cz.abs().fract(),
905 }
906 }
907
908 pub fn tick(&mut self, dt: f32, humidity: f32, updraft_forcing: f32) {
909 self.lifetime -= dt;
910 self.time_since_discharge += dt;
911
912 let charge_rate = self.updraft * humidity * 0.002;
914 self.charge += charge_rate * dt;
915 self.intensity = (self.charge / self.discharge_threshold).clamp(0.0, 1.0);
916
917 if self.charge >= self.discharge_threshold {
919 self.discharge();
920 }
921
922 let noise = value_noise_2d(self.cell_seed + self.time_since_discharge * 0.001, 0.0);
924 self.updraft = (10.0 + updraft_forcing + noise * 20.0).max(0.0);
925
926 self.bolts.retain(|b| b.is_alive());
928 for bolt in &mut self.bolts {
929 bolt.tick(dt);
930 }
931
932 let age_fraction = 1.0 - self.lifetime / 7200.0;
934 if age_fraction > 0.7 {
935 self.updraft *= 0.999;
936 }
937 }
938
939 fn discharge(&mut self) {
940 self.charge = 0.0;
941 self.time_since_discharge = 0.0;
942 let bolt = LightningBolt::new(
944 Vec3::new(self.position[0], self.charge_altitude, self.position[1]),
945 self.cloud_base_m,
946 self.intensity,
947 );
948 self.bolts.push(bolt);
949 }
950
951 pub fn nearest_strike_dist(&self, x: f32, z: f32) -> Option<f32> {
953 self.bolts.iter()
954 .filter(|b| b.is_alive() && b.struck)
955 .map(|b| {
956 let dx = b.strike_point.x - x;
957 let dz = b.strike_point.z - z;
958 (dx * dx + dz * dz).sqrt()
959 })
960 .reduce(f32::min)
961 }
962
963 pub fn thunder_delay_s(strike_x: f32, strike_z: f32, obs_x: f32, obs_z: f32) -> f32 {
965 let dx = strike_x - obs_x;
966 let dz = strike_z - obs_z;
967 let dist = (dx * dx + dz * dz).sqrt();
968 dist / 343.0 }
970
971 pub fn is_alive(&self) -> bool { self.lifetime > 0.0 }
972}
973
974#[derive(Debug, Clone)]
978pub struct LightningBolt {
979 pub origin: Vec3,
981 pub strike_point: Vec3,
983 pub channel: Vec<Vec3>,
985 pub struck: bool,
987 pub visible_time: f32,
989 pub peak_current_ka: f32,
991 pub return_strokes: u32,
993 pub thunder_intensity: f32,
995}
996
997impl LightningBolt {
998 pub fn new(origin: Vec3, cloud_base: f32, intensity: f32) -> Self {
999 let mut channel = Vec::new();
1000 let steps = 12usize;
1002 let mut pt = origin;
1003 let strike = Vec3::new(
1004 origin.x + (value_noise_2d(origin.x * 0.001, 0.5) * 2.0 - 1.0) * 2_000.0,
1005 cloud_base - 20.0,
1006 origin.z + (value_noise_2d(0.5, origin.z * 0.001) * 2.0 - 1.0) * 2_000.0,
1007 );
1008 channel.push(pt);
1009 for i in 1..steps {
1010 let t = i as f32 / steps as f32;
1011 let noise_x = (value_noise_2d(pt.x * 0.0001 + t, pt.z * 0.0001) * 2.0 - 1.0) * 300.0;
1012 let noise_z = (value_noise_2d(pt.z * 0.0001 + t + 7.0, pt.x * 0.0001) * 2.0 - 1.0) * 300.0;
1013 let next = Vec3::new(
1014 lerp(origin.x, strike.x, t) + noise_x,
1015 lerp(origin.y, strike.y, t),
1016 lerp(origin.z, strike.z, t) + noise_z,
1017 );
1018 channel.push(next);
1019 pt = next;
1020 }
1021 channel.push(strike);
1022 Self {
1023 origin,
1024 strike_point: strike,
1025 channel,
1026 struck: true,
1027 visible_time: 0.3,
1028 peak_current_ka: 20.0 + intensity * 80.0,
1029 return_strokes: 1 + (intensity * 3.0) as u32,
1030 thunder_intensity: intensity,
1031 }
1032 }
1033
1034 pub fn tick(&mut self, dt: f32) {
1035 self.visible_time -= dt;
1036 }
1037
1038 pub fn is_alive(&self) -> bool { self.visible_time > 0.0 }
1039
1040 pub fn danger_radius_m(&self) -> f32 {
1042 self.peak_current_ka * 0.5
1043 }
1044}
1045
1046#[derive(Debug, Clone)]
1050pub struct PrecipitationEvent {
1051 pub kind: PrecipitationType,
1052 pub intensity: f32,
1053 pub duration_s: f32,
1054 pub elapsed_s: f32,
1055 pub centre: [f32; 2],
1056 pub radius: f32,
1057}
1058
1059impl PrecipitationEvent {
1060 pub fn progress(&self) -> f32 {
1061 (self.elapsed_s / self.duration_s).clamp(0.0, 1.0)
1062 }
1063 pub fn is_finished(&self) -> bool { self.elapsed_s >= self.duration_s }
1064}
1065
1066#[derive(Debug, Clone)]
1070pub struct PrecipitationSystem {
1071 pub config: PrecipitationConfig,
1072 pub droplets: Vec<Droplet>,
1073 pub rain_bands: Vec<RainBand>,
1074 pub surface: SurfaceAccumulation,
1075 pub puddles: Vec<Puddle>,
1076 pub snowpacks: HashMap<(i32, i32), Snowpack>,
1077 pub ice_sheets: HashMap<(i32, i32), IceSheet>,
1078 pub hailstones: Vec<HailStone>,
1079 pub sleet_particles: Vec<SleetParticle>,
1080 pub thunder_cells: Vec<ThunderCell>,
1081 pub events: Vec<PrecipitationEvent>,
1082 pub dominant_kind: PrecipitationType,
1083 pub global_intensity: f32, spawn_accum: f32,
1086 pub total_precip_m: f32,
1088 noise_t: f32,
1090}
1091
1092impl PrecipitationSystem {
1093 pub fn new() -> Self {
1094 Self::with_config(PrecipitationConfig::default())
1095 }
1096
1097 pub fn with_config(config: PrecipitationConfig) -> Self {
1098 let surface = SurfaceAccumulation::new(config.grid_width, config.grid_depth, config.cell_size);
1099 Self {
1100 config,
1101 droplets: Vec::new(),
1102 rain_bands: Vec::new(),
1103 surface,
1104 puddles: Vec::new(),
1105 snowpacks: HashMap::new(),
1106 ice_sheets: HashMap::new(),
1107 hailstones: Vec::new(),
1108 sleet_particles: Vec::new(),
1109 thunder_cells: Vec::new(),
1110 events: Vec::new(),
1111 dominant_kind: PrecipitationType::None,
1112 global_intensity: 0.0,
1113 spawn_accum: 0.0,
1114 total_precip_m: 0.0,
1115 noise_t: 0.0,
1116 }
1117 }
1118
1119 pub fn tick(&mut self, dt: f32, temp_c: f32, humidity: f32, wind: Vec3, pressure_pa: f32) {
1122 self.noise_t += dt * 0.01;
1123
1124 let kind = self.classify_precip(temp_c, humidity, pressure_pa);
1126 self.dominant_kind = kind;
1127
1128 let excess = (humidity - self.config.rain_humidity_threshold).max(0.0);
1130 let target_intensity = smoothstep(0.0, 0.3, excess);
1131 self.global_intensity = lerp(self.global_intensity, target_intensity, dt * 0.1);
1132
1133 self.update_rain_bands(dt, kind);
1135
1136 if self.global_intensity > 0.05 {
1138 self.spawn_droplets(dt, kind, wind, temp_c);
1139 }
1140
1141 let surface_y = 0.0_f32;
1143 let mut land_events: Vec<(Vec3, PrecipitationType)> = Vec::new();
1144 for drop in &mut self.droplets {
1145 drop.tick(dt, wind, surface_y);
1146 if drop.landed {
1147 land_events.push((drop.position, drop.kind));
1148 }
1149 }
1150 self.droplets.retain(|d| d.is_alive());
1151
1152 for (pos, kind) in land_events {
1154 self.handle_landing(pos, kind, temp_c);
1155 }
1156
1157 for h in &mut self.hailstones {
1159 h.tick(dt, 15.0, wind);
1160 }
1161 self.hailstones.retain(|h| h.position.y > 0.0);
1162
1163 for s in &mut self.sleet_particles {
1165 let temp_k = temp_c + 273.15;
1166 s.tick(dt, temp_k, wind);
1167 }
1168 self.sleet_particles.retain(|s| s.position.y > 0.0 && s.lifetime > 0.0);
1169
1170 let updraft = self.global_intensity * 20.0;
1172 for cell in &mut self.thunder_cells {
1173 cell.tick(dt, humidity, updraft);
1174 }
1175 self.thunder_cells.retain(|c| c.is_alive());
1176
1177 self.surface.flow_water(dt);
1179 let evap = self.config.puddle_evap_rate * (1.0 + (temp_c * 0.05).max(0.0)) * dt;
1180 self.surface.evaporate(evap);
1181
1182 if temp_c > 0.0 {
1184 self.surface.melt_snow(temp_c, dt);
1185 } else {
1186 self.surface.freeze_water(-temp_c, dt);
1187 }
1188
1189 let temp_k = temp_c + 273.15;
1191 for sp in self.snowpacks.values_mut() {
1192 sp.tick(dt, temp_k);
1193 }
1194 self.snowpacks.retain(|_, sp| sp.total_depth() > 1e-6);
1195
1196 for ice in self.ice_sheets.values_mut() {
1198 ice.tick(dt, temp_k);
1199 }
1200 self.ice_sheets.retain(|_, ice| ice.is_alive());
1201
1202 let wind_spd = wind.length();
1204 for puddle in &mut self.puddles {
1205 puddle.tick(dt, temp_c, wind_spd);
1206 }
1207 self.puddles.retain(|p| p.is_alive());
1208
1209 for ev in &mut self.events {
1211 ev.elapsed_s += dt;
1212 }
1213 self.events.retain(|e| !e.is_finished());
1214
1215 self.total_precip_m += self.global_intensity * 5e-6 * dt;
1217 }
1218
1219 fn classify_precip(&self, temp_c: f32, humidity: f32, pressure_pa: f32) -> PrecipitationType {
1220 if humidity < self.config.rain_humidity_threshold { return PrecipitationType::None; }
1221 if pressure_pa > 102_500.0 { return PrecipitationType::None; }
1223 if temp_c < self.config.hail_threshold_c { return PrecipitationType::Hail; }
1224 if temp_c < 0.0 { return PrecipitationType::Sleet; }
1225 if temp_c < self.config.snow_threshold_c { return PrecipitationType::Snow; }
1226 if humidity < 0.85 { return PrecipitationType::Drizzle; }
1227 PrecipitationType::Rain
1228 }
1229
1230 fn update_rain_bands(&mut self, dt: f32, kind: PrecipitationType) {
1231 for band in &mut self.rain_bands {
1232 band.tick(dt);
1233 }
1234 self.rain_bands.retain(|b| b.is_alive());
1235
1236 if self.global_intensity > 0.2 && self.rain_bands.len() < 4 {
1238 let cx = (value_noise_2d(self.noise_t, 0.3) * 2.0 - 1.0) * 5000.0;
1239 let cz = (value_noise_2d(0.7, self.noise_t + 1.0) * 2.0 - 1.0) * 5000.0;
1240 let band = RainBand::new(cx, cz, 3_000.0, self.global_intensity * 80.0, kind);
1241 self.rain_bands.push(band);
1242 }
1243
1244 if kind == PrecipitationType::Rain && self.global_intensity > 0.6 && self.thunder_cells.is_empty() {
1246 let cx = (value_noise_2d(self.noise_t * 1.3, 0.0) * 2.0 - 1.0) * 8000.0;
1247 let cz = (value_noise_2d(0.0, self.noise_t * 1.3 + 2.0) * 2.0 - 1.0) * 8000.0;
1248 self.thunder_cells.push(ThunderCell::new(cx, cz));
1249 }
1250 }
1251
1252 fn spawn_droplets(&mut self, dt: f32, kind: PrecipitationType, wind: Vec3, temp_c: f32) {
1253 if self.droplets.len() >= self.config.max_droplets { return; }
1254 let spawn_rate = self.global_intensity * 50.0;
1255 self.spawn_accum += spawn_rate * dt;
1256 let to_spawn = self.spawn_accum as usize;
1257 self.spawn_accum -= to_spawn as f32;
1258
1259 for i in 0..to_spawn {
1260 if self.droplets.len() >= self.config.max_droplets { break; }
1261 let offset_x = (value_noise_2d(self.noise_t + i as f32 * 0.1, 0.0) * 2.0 - 1.0) * 200.0;
1262 let offset_z = (value_noise_2d(0.0, self.noise_t + i as f32 * 0.1 + 50.0) * 2.0 - 1.0) * 200.0;
1263 let spawn_y = 300.0 + value_noise_2d(self.noise_t, i as f32) * 200.0;
1264 let pos = Vec3::new(offset_x + wind.x * 2.0, spawn_y, offset_z + wind.z * 2.0);
1265
1266 let drop = match kind {
1267 PrecipitationType::Rain | PrecipitationType::Drizzle => Droplet::new_rain(pos),
1268 PrecipitationType::Snow => Droplet::new_snow(pos),
1269 PrecipitationType::Hail => {
1270 self.hailstones.push(HailStone::new(pos));
1271 continue;
1272 }
1273 PrecipitationType::Sleet => {
1274 self.sleet_particles.push(SleetParticle::new(pos));
1275 continue;
1276 }
1277 PrecipitationType::None => continue,
1278 };
1279 self.droplets.push(drop);
1280 }
1281 }
1282
1283 fn handle_landing(&mut self, pos: Vec3, kind: PrecipitationType, temp_c: f32) {
1284 match kind {
1285 PrecipitationType::Rain | PrecipitationType::Drizzle => {
1286 let depth = 1e-5;
1287 self.surface.add_water(pos.x, pos.z, depth);
1288 self.total_precip_m += depth;
1289 let cell_water = self.surface.water_at(pos.x, pos.z);
1291 if cell_water > 0.005 {
1292 let mut found = false;
1294 for puddle in &mut self.puddles {
1295 if puddle.contains(pos.x, pos.z) {
1296 puddle.add_water(1e-6);
1297 found = true;
1298 break;
1299 }
1300 }
1301 if !found && self.puddles.len() < 256 {
1302 self.puddles.push(Puddle::new(pos.x, pos.z, 1e-4));
1303 }
1304 }
1305 }
1306 PrecipitationType::Snow => {
1307 let key = (
1308 (pos.x / self.config.cell_size).floor() as i32,
1309 (pos.z / self.config.cell_size).floor() as i32,
1310 );
1311 let sp = self.snowpacks.entry(key).or_insert_with(Snowpack::new);
1312 sp.deposit(5e-6, temp_c + 273.15);
1313 self.surface.add_snow(pos.x, pos.z, 5e-6);
1314 self.total_precip_m += 5e-6 * 0.1;
1315 }
1316 PrecipitationType::Sleet => {
1317 self.surface.add_ice(pos.x, pos.z, 1e-6);
1318 let key = (
1319 (pos.x / self.config.cell_size).floor() as i32,
1320 (pos.z / self.config.cell_size).floor() as i32,
1321 );
1322 self.ice_sheets.entry(key)
1323 .or_insert_with(|| IceSheet::new(0.0, temp_c + 273.15, temp_c < -1.0))
1324 .thickness_m += 1e-6;
1325 }
1326 _ => {}
1327 }
1328 }
1329
1330 pub fn intensity_at(&self, x: f32, z: f32) -> f32 {
1334 let base = self.global_intensity;
1335 let band_contrib: f32 = self.rain_bands.iter()
1336 .map(|b| b.intensity_at(x, z))
1337 .fold(0.0_f32, f32::max);
1338 let noise = fbm_2d(x * 0.0001 + self.noise_t, z * 0.0001, 3, 2.0, 0.5) * 0.2;
1339 (base * 0.5 + band_contrib * 0.5 + noise).clamp(0.0, 1.0)
1340 }
1341
1342 pub fn dominant_type(&self) -> PrecipitationType {
1343 self.dominant_kind
1344 }
1345
1346 pub fn nearest_thunder_cell(&self, x: f32, z: f32, max_dist: f32) -> Option<&ThunderCell> {
1348 self.thunder_cells.iter().filter(|c| {
1349 let dx = c.position[0] - x;
1350 let dz = c.position[1] - z;
1351 (dx * dx + dz * dz).sqrt() < max_dist
1352 }).min_by(|a, b| {
1353 let da = { let dx = a.position[0]-x; let dz = a.position[1]-z; dx*dx+dz*dz };
1354 let db = { let dx = b.position[0]-x; let dz = b.position[1]-z; dx*dx+dz*dz };
1355 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1356 })
1357 }
1358
1359 pub fn trigger_event(&mut self, kind: PrecipitationType, intensity: f32, duration_s: f32, cx: f32, cz: f32, radius: f32) {
1361 self.events.push(PrecipitationEvent {
1362 kind,
1363 intensity,
1364 duration_s,
1365 elapsed_s: 0.0,
1366 centre: [cx, cz],
1367 radius,
1368 });
1369 self.rain_bands.push(RainBand::new(cx, cz, radius, intensity * 80.0, kind));
1370 }
1371
1372 pub fn snow_depth_at(&self, x: f32, z: f32) -> f32 {
1374 self.surface.snow_at(x, z)
1375 }
1376
1377 pub fn ice_thickness_at(&self, x: f32, z: f32) -> f32 {
1379 self.surface.ice_at(x, z)
1380 }
1381
1382 pub fn water_depth_at(&self, x: f32, z: f32) -> f32 {
1384 self.surface.water_at(x, z)
1385 }
1386}
1387
1388impl Default for PrecipitationSystem {
1389 fn default() -> Self { Self::new() }
1390}