1use std::collections::HashMap;
7use super::{Vec3, lerp, smoothstep, fbm_2d, value_noise_2d};
8
9pub const ISA_PRESSURE_PA: f32 = 101_325.0;
13pub const ISA_TEMP_K: f32 = 288.15;
15pub const LAPSE_RATE: f32 = 0.006_5;
17pub const R_DRY: f32 = 287.058;
19pub const GRAVITY: f32 = 9.807;
21pub const TROPOPAUSE_ALT: f32 = 11_000.0;
23pub const STRATO_TEMP_K: f32 = 216.65;
25
26#[derive(Debug, Clone)]
30pub struct AtmosphereConfig {
31 pub layer_count: usize,
33 pub max_altitude_m: f32,
35 pub grid_resolution: f32,
37 pub grid_width: usize,
39 pub grid_depth: usize,
41 pub turbulence_scale: f32,
43 pub enable_jet_streams: bool,
45 pub fog_scatter_coeff: f32,
47}
48
49impl Default for AtmosphereConfig {
50 fn default() -> Self {
51 Self {
52 layer_count: 8,
53 max_altitude_m: 12_000.0,
54 grid_resolution: 200.0,
55 grid_width: 32,
56 grid_depth: 32,
57 turbulence_scale: 0.4,
58 enable_jet_streams: true,
59 fog_scatter_coeff: 0.02,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
68pub struct PressureCell {
69 pub centre: [f32; 2],
71 pub radius: f32,
73 pub pressure_pa: f32,
75 pub kind: PressureCellKind,
77 pub drift: [f32; 2],
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum PressureCellKind {
83 HighPressure,
84 LowPressure,
85 Neutral,
86}
87
88impl PressureCell {
89 pub fn new_high(cx: f32, cz: f32, radius: f32) -> Self {
90 Self {
91 centre: [cx, cz],
92 radius,
93 pressure_pa: ISA_PRESSURE_PA + 2_000.0,
94 kind: PressureCellKind::HighPressure,
95 drift: [1.5, 0.5],
96 }
97 }
98
99 pub fn new_low(cx: f32, cz: f32, radius: f32) -> Self {
100 Self {
101 centre: [cx, cz],
102 radius,
103 pressure_pa: ISA_PRESSURE_PA - 3_000.0,
104 kind: PressureCellKind::LowPressure,
105 drift: [2.0, -0.3],
106 }
107 }
108
109 pub fn sample(&self, x: f32, z: f32) -> f32 {
111 let dx = x - self.centre[0];
112 let dz = z - self.centre[1];
113 let dist = (dx * dx + dz * dz).sqrt();
114 if dist >= self.radius { return ISA_PRESSURE_PA; }
115 let t = smoothstep(self.radius, 0.0, dist);
116 lerp(ISA_PRESSURE_PA, self.pressure_pa, t)
117 }
118
119 pub fn tick(&mut self, dt: f32) {
121 self.centre[0] += self.drift[0] * dt;
122 self.centre[1] += self.drift[1] * dt;
123 }
124}
125
126#[derive(Debug, Clone, Copy)]
128pub struct BarometricGradient {
129 pub grad_x: f32,
131 pub grad_z: f32,
132}
133
134impl BarometricGradient {
135 pub fn magnitude(&self) -> f32 {
136 (self.grad_x * self.grad_x + self.grad_z * self.grad_z).sqrt()
137 }
138
139 pub fn geostrophic_wind(&self, air_density: f32, coriolis: f32) -> Vec3 {
141 let factor = 1.0 / (air_density * coriolis.max(1e-5));
143 Vec3::new(
144 self.grad_z * factor,
145 0.0,
146 -self.grad_x * factor,
147 )
148 }
149}
150
151#[derive(Debug, Clone)]
155pub struct HumidityMap {
156 pub width: usize,
157 pub depth: usize,
158 pub data: Vec<f32>,
160}
161
162impl HumidityMap {
163 pub fn new(width: usize, depth: usize, base: f32) -> Self {
164 Self {
165 width,
166 depth,
167 data: vec![base; width * depth],
168 }
169 }
170
171 pub fn sample(&self, gx: f32, gz: f32) -> f32 {
173 let x0 = (gx.floor() as usize).min(self.width.saturating_sub(2));
174 let z0 = (gz.floor() as usize).min(self.depth.saturating_sub(2));
175 let fx = gx - gx.floor();
176 let fz = gz - gz.floor();
177 let v00 = self.data[z0 * self.width + x0 ];
178 let v10 = self.data[z0 * self.width + x0 + 1];
179 let v01 = self.data[(z0 + 1) * self.width + x0 ];
180 let v11 = self.data[(z0 + 1) * self.width + x0 + 1];
181 lerp(lerp(v00, v10, fx), lerp(v01, v11, fx), fz)
182 }
183
184 pub fn set(&mut self, gx: usize, gz: usize, val: f32) {
186 if gx < self.width && gz < self.depth {
187 self.data[gz * self.width + gx] = val.clamp(0.0, 1.0);
188 }
189 }
190
191 pub fn advect(&mut self, dx: f32, dz: f32) {
193 let old = self.data.clone();
194 for z in 0..self.depth {
195 for x in 0..self.width {
196 let src_x = x as f32 - dx;
197 let src_z = z as f32 - dz;
198 let clamped_x = src_x.clamp(0.0, (self.width - 1) as f32);
199 let clamped_z = src_z.clamp(0.0, (self.depth - 1) as f32);
200 let x0 = clamped_x.floor() as usize;
201 let z0 = clamped_z.floor() as usize;
202 let x1 = (x0 + 1).min(self.width - 1);
203 let z1 = (z0 + 1).min(self.depth - 1);
204 let fx = clamped_x - clamped_x.floor();
205 let fz = clamped_z - clamped_z.floor();
206 let v = lerp(
207 lerp(old[z0 * self.width + x0], old[z0 * self.width + x1], fx),
208 lerp(old[z1 * self.width + x0], old[z1 * self.width + x1], fx),
209 fz,
210 );
211 self.data[z * self.width + x] = v.clamp(0.0, 1.0);
212 }
213 }
214 }
215
216 pub fn evaporate(&mut self, rate: f32) {
218 for v in &mut self.data {
219 *v = (*v + rate).min(1.0);
220 }
221 }
222
223 pub fn precipitate(&mut self, sat: f32, coeff: f32) {
225 for v in &mut self.data {
226 if *v > sat {
227 *v -= (*v - sat) * coeff;
228 }
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
237pub struct TemperatureProfile {
238 pub altitudes: Vec<f32>,
240 pub temps_k: Vec<f32>,
242}
243
244impl TemperatureProfile {
245 pub fn isa(layer_count: usize, max_alt_m: f32, surface_temp_k: f32) -> Self {
247 let mut altitudes = Vec::with_capacity(layer_count);
248 let mut temps_k = Vec::with_capacity(layer_count);
249 for i in 0..layer_count {
250 let alt = i as f32 * max_alt_m / (layer_count - 1).max(1) as f32;
251 let t = if alt <= TROPOPAUSE_ALT {
252 surface_temp_k - LAPSE_RATE * alt
253 } else {
254 STRATO_TEMP_K
255 };
256 altitudes.push(alt);
257 temps_k.push(t);
258 }
259 Self { altitudes, temps_k }
260 }
261
262 pub fn sample_at(&self, alt_m: f32) -> f32 {
264 if alt_m <= self.altitudes[0] { return self.temps_k[0]; }
265 let last = self.altitudes.len() - 1;
266 if alt_m >= self.altitudes[last] { return self.temps_k[last]; }
267 for i in 0..last {
268 if alt_m >= self.altitudes[i] && alt_m <= self.altitudes[i + 1] {
269 let t = (alt_m - self.altitudes[i])
270 / (self.altitudes[i + 1] - self.altitudes[i]);
271 return lerp(self.temps_k[i], self.temps_k[i + 1], t);
272 }
273 }
274 self.temps_k[last]
275 }
276
277 pub fn density_at(&self, alt_m: f32) -> f32 {
279 let temp = self.sample_at(alt_m);
280 let pressure = isa_pressure_at_altitude(alt_m);
281 pressure / (R_DRY * temp)
282 }
283
284 pub fn set_surface_temp(&mut self, new_temp_k: f32) {
286 if self.temps_k.is_empty() { return; }
287 let old_surface = self.temps_k[0];
288 let delta = new_temp_k - old_surface;
289 for (i, t) in self.temps_k.iter_mut().enumerate() {
291 let alt = self.altitudes[i];
292 let decay = (-alt / 3_000.0_f32).exp();
293 *t += delta * decay;
294 }
295 }
296}
297
298pub fn isa_pressure_at_altitude(alt_m: f32) -> f32 {
300 if alt_m <= TROPOPAUSE_ALT {
301 let base_ratio = 1.0 - LAPSE_RATE * alt_m / ISA_TEMP_K;
302 ISA_PRESSURE_PA * base_ratio.powf(GRAVITY / (LAPSE_RATE * R_DRY))
303 } else {
304 let p_tropo = isa_pressure_at_altitude(TROPOPAUSE_ALT);
305 let delta_alt = alt_m - TROPOPAUSE_ALT;
306 p_tropo * (-(GRAVITY * delta_alt) / (R_DRY * STRATO_TEMP_K)).exp()
307 }
308}
309
310#[derive(Debug, Clone)]
314pub struct AtmosphericLayer {
315 pub altitude_base_m: f32,
316 pub altitude_top_m: f32,
317 pub pressure_pa: f32,
318 pub temperature_k: f32,
319 pub density_kg_m3: f32,
320 pub humidity: f32, pub cloud_fraction: f32, pub wind: Vec3,
323}
324
325impl AtmosphericLayer {
326 pub fn from_isa(alt_base: f32, alt_top: f32, surface_temp_k: f32, humidity: f32) -> Self {
327 let mid = (alt_base + alt_top) * 0.5;
328 let temp_k = if mid <= TROPOPAUSE_ALT {
329 surface_temp_k - LAPSE_RATE * mid
330 } else {
331 STRATO_TEMP_K
332 };
333 let pressure = isa_pressure_at_altitude(mid);
334 let density = pressure / (R_DRY * temp_k.max(1.0));
335 Self {
336 altitude_base_m: alt_base,
337 altitude_top_m: alt_top,
338 pressure_pa: pressure,
339 temperature_k: temp_k,
340 density_kg_m3: density,
341 humidity,
342 cloud_fraction: 0.0,
343 wind: Vec3::ZERO,
344 }
345 }
346
347 pub fn thickness(&self) -> f32 {
348 self.altitude_top_m - self.altitude_base_m
349 }
350
351 pub fn dew_point_k(&self) -> f32 {
353 let tc = self.temperature_k - 273.15;
354 let rh = self.humidity.clamp(0.001, 1.0);
355 let a = 17.625_f32;
356 let b = 243.04_f32;
357 let alpha = (a * tc / (b + tc)) + rh.ln();
358 let dp_c = b * alpha / (a - alpha);
359 dp_c + 273.15
360 }
361
362 pub fn is_saturated(&self) -> bool {
364 self.temperature_k <= self.dew_point_k() + 0.5
365 }
366
367 pub fn update_cloud_fraction(&mut self) {
369 if self.is_saturated() {
370 self.cloud_fraction = (self.cloud_fraction + 0.1).min(1.0);
371 } else {
372 self.cloud_fraction = (self.cloud_fraction - 0.05).max(0.0);
373 }
374 }
375}
376
377#[derive(Debug, Clone, Copy)]
381pub struct WindVector {
382 pub velocity: Vec3, pub turbulence: f32, }
385
386#[derive(Debug, Clone)]
388pub struct WindField {
389 pub grid_w: usize,
390 pub grid_h: usize, pub grid_d: usize,
392 pub resolution: f32, pub layer_height: f32, pub data: Vec<WindVector>,
395}
396
397impl WindField {
398 pub fn new(w: usize, h: usize, d: usize, resolution: f32, layer_height: f32) -> Self {
399 let default_wv = WindVector { velocity: Vec3::ZERO, turbulence: 0.0 };
400 Self {
401 grid_w: w,
402 grid_h: h,
403 grid_d: d,
404 resolution,
405 layer_height,
406 data: vec![default_wv; w * h * d],
407 }
408 }
409
410 fn index(&self, x: usize, y: usize, z: usize) -> usize {
411 (y * self.grid_d + z) * self.grid_w + x
412 }
413
414 pub fn set(&mut self, x: usize, y: usize, z: usize, wv: WindVector) {
415 if x < self.grid_w && y < self.grid_h && z < self.grid_d {
416 let i = self.index(x, y, z);
417 self.data[i] = wv;
418 }
419 }
420
421 pub fn sample_world(&self, wx: f32, wy: f32, wz: f32) -> WindVector {
423 let gx = wx / self.resolution;
424 let gy = wy / self.layer_height;
425 let gz = wz / self.resolution;
426
427 let x0 = (gx.floor() as usize).min(self.grid_w.saturating_sub(2));
428 let y0 = (gy.floor() as usize).min(self.grid_h.saturating_sub(2));
429 let z0 = (gz.floor() as usize).min(self.grid_d.saturating_sub(2));
430 let x1 = (x0 + 1).min(self.grid_w - 1);
431 let y1 = (y0 + 1).min(self.grid_h - 1);
432 let z1 = (z0 + 1).min(self.grid_d - 1);
433
434 let fx = gx - gx.floor();
435 let fy = gy - gy.floor();
436 let fz = gz - gz.floor();
437
438 let lerp_wv = |a: WindVector, b: WindVector, t: f32| -> WindVector {
439 WindVector {
440 velocity: a.velocity.lerp(b.velocity, t),
441 turbulence: lerp(a.turbulence, b.turbulence, t),
442 }
443 };
444
445 let c000 = self.data[self.index(x0, y0, z0)];
446 let c100 = self.data[self.index(x1, y0, z0)];
447 let c010 = self.data[self.index(x0, y1, z0)];
448 let c110 = self.data[self.index(x1, y1, z0)];
449 let c001 = self.data[self.index(x0, y0, z1)];
450 let c101 = self.data[self.index(x1, y0, z1)];
451 let c011 = self.data[self.index(x0, y1, z1)];
452 let c111 = self.data[self.index(x1, y1, z1)];
453
454 let c00 = lerp_wv(c000, c100, fx);
455 let c10 = lerp_wv(c010, c110, fx);
456 let c01 = lerp_wv(c001, c101, fx);
457 let c11 = lerp_wv(c011, c111, fx);
458
459 let c0 = lerp_wv(c00, c10, fy);
460 let c1 = lerp_wv(c01, c11, fy);
461
462 lerp_wv(c0, c1, fz)
463 }
464
465 pub fn fill_from_layers(&mut self, layers: &[AtmosphericLayer]) {
467 for y in 0..self.grid_h {
468 let alt = y as f32 * self.layer_height;
469 let layer_wind = layers.iter()
471 .find(|l| alt >= l.altitude_base_m && alt < l.altitude_top_m)
472 .map(|l| l.wind)
473 .unwrap_or(Vec3::ZERO);
474
475 for z in 0..self.grid_d {
476 for x in 0..self.grid_w {
477 let turb_noise = fbm_2d(
478 x as f32 * 0.3 + alt * 0.001,
479 z as f32 * 0.3,
480 3, 2.0, 0.5,
481 ) * 0.5 - 0.25;
482 let turb_vec = Vec3::new(turb_noise, 0.0, turb_noise * 0.7);
483 let wv = WindVector {
484 velocity: layer_wind.add(turb_vec),
485 turbulence: turb_noise.abs() * 2.0,
486 };
487 self.set(x, y, z, wv);
488 }
489 }
490 }
491 }
492}
493
494#[derive(Debug, Clone)]
498pub struct JetStream {
499 pub altitude_m: f32,
501 pub core_speed: f32,
503 pub direction: f32,
505 pub vertical_width_m: f32,
507 pub wave_amplitude: f32,
509 pub wave_number: f32,
511 pub wave_phase: f32,
513 pub wave_phase_speed: f32,
515}
516
517impl JetStream {
518 pub fn polar(northern_hemisphere: bool) -> Self {
519 Self {
520 altitude_m: 10_000.0,
521 core_speed: if northern_hemisphere { 60.0 } else { 55.0 },
522 direction: if northern_hemisphere { 0.0 } else { std::f32::consts::PI },
523 vertical_width_m: 2_000.0,
524 wave_amplitude: 400_000.0,
525 wave_number: 1.0 / 6_000_000.0,
526 wave_phase: 0.0,
527 wave_phase_speed: 1e-5,
528 }
529 }
530
531 pub fn subtropical() -> Self {
532 Self {
533 altitude_m: 12_000.0,
534 core_speed: 45.0,
535 direction: 0.1,
536 vertical_width_m: 1_500.0,
537 wave_amplitude: 200_000.0,
538 wave_number: 1.0 / 4_000_000.0,
539 wave_phase: 1.0,
540 wave_phase_speed: 8e-6,
541 }
542 }
543
544 pub fn tick(&mut self, dt: f32) {
545 self.wave_phase += self.wave_phase_speed * dt;
546 }
547
548 pub fn sample(&self, wx: f32, wy: f32, wz: f32) -> Vec3 {
550 let dalt = wy - self.altitude_m;
552 let vert_factor = (-0.5 * (dalt / self.vertical_width_m).powi(2)).exp();
553 if vert_factor < 1e-4 { return Vec3::ZERO; }
554
555 let undulation = (wx * self.wave_number + self.wave_phase).sin() * self.wave_amplitude;
557 let _effective_z = wz - undulation;
558
559 let speed = self.core_speed * vert_factor;
560 Vec3::new(
561 speed * self.direction.cos(),
562 0.0,
563 speed * self.direction.sin(),
564 )
565 }
566}
567
568#[derive(Debug, Clone)]
572pub struct FogLayer {
573 pub base_m: f32,
575 pub top_m: f32,
577 pub extinction: f32,
579 pub kind: FogKind,
581 pub visibility_m: f32,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586pub enum FogKind {
587 Radiation,
588 Advection,
589 Upslope,
590 Freezing,
591}
592
593impl FogLayer {
594 pub fn radiation(base: f32, top: f32, extinction: f32) -> Self {
595 let vis = if extinction > 0.0 { 3.912 / extinction } else { 50_000.0 };
596 Self { base_m: base, top_m: top, extinction, kind: FogKind::Radiation, visibility_m: vis }
597 }
598
599 pub fn advection(base: f32, top: f32, extinction: f32) -> Self {
600 let vis = if extinction > 0.0 { 3.912 / extinction } else { 50_000.0 };
601 Self { base_m: base, top_m: top, extinction, kind: FogKind::Advection, visibility_m: vis }
602 }
603
604 pub fn extinction_at(&self, y: f32) -> f32 {
606 if y < self.base_m || y > self.top_m { return 0.0; }
607 let t = (y - self.base_m) / (self.top_m - self.base_m).max(0.1);
608 let bell = smoothstep(0.0, 0.5, t) * smoothstep(1.0, 0.5, t) * 2.0;
610 self.extinction * bell
611 }
612
613 pub fn dissipate(&mut self, rate: f32) {
615 self.extinction = (self.extinction - rate).max(0.0);
616 self.visibility_m = if self.extinction > 0.0 { 3.912 / self.extinction } else { 50_000.0 };
617 }
618
619 pub fn intensify(&mut self, rate: f32) {
621 self.extinction = (self.extinction + rate).min(0.5);
622 self.visibility_m = if self.extinction > 0.0 { 3.912 / self.extinction } else { 50_000.0 };
623 }
624}
625
626#[derive(Debug, Clone, Copy)]
630pub struct VisibilityResult {
631 pub distance_m: f32,
633 pub optical_depth: f32,
635 pub limiting_factor: VisibilityLimiter,
637}
638
639#[derive(Debug, Clone, Copy, PartialEq, Eq)]
640pub enum VisibilityLimiter {
641 Clear,
642 Fog,
643 Precipitation,
644 Haze,
645 Smoke,
646 Dust,
647}
648
649#[derive(Debug, Clone)]
653pub struct Atmosphere {
654 pub config: AtmosphereConfig,
655 pub layers: Vec<AtmosphericLayer>,
656 pub temperature_profile: TemperatureProfile,
657 pub humidity_map: HumidityMap,
658 pub wind_field: WindField,
659 pub pressure_cells: Vec<PressureCell>,
660 pub jet_streams: Vec<JetStream>,
661 pub fog_layers: Vec<FogLayer>,
662 pub surface_altitude_m: f32,
663 pub haze_extinction: f32,
665 time_accum: f32,
667 noise_offset: f32,
669}
670
671impl Atmosphere {
672 pub fn new(surface_altitude_m: f32) -> Self {
673 Self::with_config(surface_altitude_m, AtmosphereConfig::default())
674 }
675
676 pub fn with_config(surface_altitude_m: f32, config: AtmosphereConfig) -> Self {
677 let n = config.layer_count;
678 let max_alt = config.max_altitude_m;
679 let surface_temp_k = ISA_TEMP_K;
680
681 let layers: Vec<AtmosphericLayer> = (0..n).map(|i| {
683 let base = i as f32 * max_alt / n as f32;
684 let top = (i + 1) as f32 * max_alt / n as f32;
685 AtmosphericLayer::from_isa(base, top, surface_temp_k, 0.5)
686 }).collect();
687
688 let temp_profile = TemperatureProfile::isa(n, max_alt, surface_temp_k);
689
690 let humidity_map = HumidityMap::new(
691 config.grid_width,
692 config.grid_depth,
693 0.55,
694 );
695
696 let layer_height = max_alt / n as f32;
697 let mut wind_field = WindField::new(
698 config.grid_width,
699 n,
700 config.grid_depth,
701 config.grid_resolution,
702 layer_height,
703 );
704 wind_field.fill_from_layers(&layers);
705
706 let pressure_cells = vec![
707 PressureCell::new_high(0.0, 0.0, 500_000.0),
708 PressureCell::new_low(200_000.0, 100_000.0, 300_000.0),
709 ];
710
711 let jet_streams = if config.enable_jet_streams {
712 vec![JetStream::polar(true), JetStream::subtropical()]
713 } else {
714 vec![]
715 };
716
717 Self {
718 config,
719 layers,
720 temperature_profile: temp_profile,
721 humidity_map,
722 wind_field,
723 pressure_cells,
724 jet_streams,
725 fog_layers: vec![],
726 surface_altitude_m,
727 haze_extinction: 0.002,
728 time_accum: 0.0,
729 noise_offset: 0.0,
730 }
731 }
732
733 pub fn tick(&mut self, dt: f32, surface_temp_k: f32, time_of_day: f32) {
736 self.time_accum += dt;
737 self.noise_offset += dt * 0.003;
738
739 self.temperature_profile.set_surface_temp(surface_temp_k);
741
742 for layer in &mut self.layers {
744 let mid = (layer.altitude_base_m + layer.altitude_top_m) * 0.5;
745 layer.temperature_k = self.temperature_profile.sample_at(mid);
746 layer.pressure_pa = isa_pressure_at_altitude(mid);
747 layer.density_kg_m3 = layer.pressure_pa / (R_DRY * layer.temperature_k.max(1.0));
748 layer.update_cloud_fraction();
749 }
750
751 for cell in &mut self.pressure_cells {
753 cell.tick(dt);
754 }
755
756 for js in &mut self.jet_streams {
758 js.tick(dt);
759 }
760
761 self.rebuild_layer_winds(time_of_day);
763
764 if self.time_accum >= 5.0 {
766 self.time_accum = 0.0;
767 let layers_snapshot = self.layers.clone();
768 self.wind_field.fill_from_layers(&layers_snapshot);
769 }
770
771 let surf_wind = self.surface_wind();
773 let adv_scale = dt / self.config.grid_resolution;
774 self.humidity_map.advect(surf_wind.x * adv_scale, surf_wind.z * adv_scale);
775
776 let day_factor = smoothstep(6.0, 12.0, time_of_day) * smoothstep(20.0, 15.0, time_of_day);
778 self.humidity_map.evaporate(0.00002 * day_factor * dt);
779
780 self.humidity_map.precipitate(0.85, 0.001 * dt);
782
783 self.update_fog(surface_temp_k, dt);
785 }
786
787 fn rebuild_layer_winds(&mut self, _time_of_day: f32) {
788 let sample_x = 0.0_f32;
789 let sample_z = 0.0_f32;
790
791 let dp_dx = self.pressure_gradient_x(sample_x, sample_z);
793 let dp_dz = self.pressure_gradient_z(sample_x, sample_z);
794 let grad = BarometricGradient { grad_x: dp_dx, grad_z: dp_dz };
795
796 for layer in &mut self.layers {
797 let alt = (layer.altitude_base_m + layer.altitude_top_m) * 0.5;
798 let density = layer.density_kg_m3.max(0.01);
799 let f_cor = 1e-4_f32;
801 let geo_wind = grad.geostrophic_wind(density, f_cor);
802
803 let t_scale = alt * 0.0001 + 0.5;
805 let turb = value_noise_2d(t_scale, 0.1) * 2.0 - 1.0;
806
807 layer.wind = Vec3::new(
808 geo_wind.x + turb * 1.5,
809 0.0,
810 geo_wind.z + turb * 0.8,
811 );
812 }
813
814 for layer in &mut self.layers {
816 let alt = (layer.altitude_base_m + layer.altitude_top_m) * 0.5;
817 let mut js_contrib = Vec3::ZERO;
818 for js in &self.jet_streams {
819 js_contrib = js_contrib.add(js.sample(sample_x, alt, sample_z));
820 }
821 layer.wind = layer.wind.add(js_contrib);
822 }
823 }
824
825 fn pressure_gradient_x(&self, x: f32, z: f32) -> f32 {
826 let dx = 1_000.0_f32;
827 let p1 = self.total_pressure_at(x + dx, z);
828 let p0 = self.total_pressure_at(x - dx, z);
829 (p1 - p0) / (2.0 * dx)
830 }
831
832 fn pressure_gradient_z(&self, x: f32, z: f32) -> f32 {
833 let dz = 1_000.0_f32;
834 let p1 = self.total_pressure_at(x, z + dz);
835 let p0 = self.total_pressure_at(x, z - dz);
836 (p1 - p0) / (2.0 * dz)
837 }
838
839 fn total_pressure_at(&self, x: f32, z: f32) -> f32 {
840 let mut p = ISA_PRESSURE_PA;
841 for cell in &self.pressure_cells {
842 let sample = cell.sample(x, z);
843 p += sample - ISA_PRESSURE_PA;
844 }
845 p += (fbm_2d(x * 0.000_01, z * 0.000_01, 3, 2.0, 0.5) - 0.5) * 400.0;
847 p
848 }
849
850 fn update_fog(&mut self, surface_temp_k: f32, dt: f32) {
851 let surface_humidity = self.surface_humidity();
852 let dew_point = {
853 let tc = surface_temp_k - 273.15;
854 let rh = surface_humidity.clamp(0.001, 1.0);
855 let a = 17.625_f32;
856 let b = 243.04_f32;
857 let alpha = (a * tc / (b + tc)) + rh.ln();
858 b * alpha / (a - alpha) + 273.15
859 };
860
861 let temp_spread = surface_temp_k - dew_point;
863 if temp_spread < 2.5 && surface_humidity > 0.8 {
864 if self.fog_layers.is_empty() {
865 self.fog_layers.push(FogLayer::radiation(0.0, 150.0, 0.01));
866 } else {
867 for fog in &mut self.fog_layers {
868 fog.intensify(0.0005 * dt);
869 }
870 }
871 } else {
872 for fog in &mut self.fog_layers {
873 fog.dissipate(0.0003 * dt);
874 }
875 self.fog_layers.retain(|f| f.extinction > 1e-5);
876 }
877 }
878
879 pub fn surface_humidity(&self) -> f32 {
883 self.humidity_map.sample(
884 (self.config.grid_width / 2) as f32,
885 (self.config.grid_depth / 2) as f32,
886 )
887 }
888
889 pub fn surface_pressure(&self) -> f32 {
891 self.total_pressure_at(0.0, 0.0)
892 }
893
894 pub fn surface_wind(&self) -> Vec3 {
896 if self.layers.is_empty() { return Vec3::ZERO; }
897 self.layers[0].wind
898 }
899
900 pub fn wind_at(&self, wx: f32, wy: f32, wz: f32) -> [f32; 3] {
902 let base_wv = self.wind_field.sample_world(wx, wy, wz);
903 let mut wind = base_wv.velocity;
904 for js in &self.jet_streams {
906 wind = wind.add(js.sample(wx, wy, wz));
907 }
908 let t = self.noise_offset;
910 let turb = Vec3::new(
911 (fbm_2d(wx * 0.0005 + t, wz * 0.0005, 3, 2.0, 0.5) * 2.0 - 1.0) * self.config.turbulence_scale,
912 0.0,
913 (fbm_2d(wz * 0.0005 + t + 100.0, wx * 0.0005, 3, 2.0, 0.5) * 2.0 - 1.0) * self.config.turbulence_scale * 0.5,
914 );
915 wind = wind.add(turb);
916 [wind.x, wind.y, wind.z]
917 }
918
919 pub fn compute_visibility(&self) -> VisibilityResult {
921 let mut ext = self.haze_extinction;
923 let mut limiter = VisibilityLimiter::Haze;
924
925 for fog in &self.fog_layers {
926 let fog_ext = fog.extinction_at(self.surface_altitude_m + 1.5); if fog_ext > ext {
928 ext = fog_ext;
929 limiter = VisibilityLimiter::Fog;
930 }
931 }
932
933 if ext < 1e-8 {
934 return VisibilityResult {
935 distance_m: 50_000.0,
936 optical_depth: 0.0,
937 limiting_factor: VisibilityLimiter::Clear,
938 };
939 }
940
941 let dist = (3.912 / ext).min(50_000.0);
943 let opt_depth = ext * dist;
944 VisibilityResult { distance_m: dist, optical_depth: opt_depth, limiting_factor: limiter }
945 }
946
947 pub fn cloud_fraction_at(&self, alt_m: f32) -> f32 {
949 for layer in &self.layers {
950 if alt_m >= layer.altitude_base_m && alt_m < layer.altitude_top_m {
951 return layer.cloud_fraction;
952 }
953 }
954 0.0
955 }
956
957 pub fn optical_depth_between(&self, alt_low: f32, alt_high: f32) -> f32 {
959 let steps = 8usize;
960 let dh = (alt_high - alt_low) / steps as f32;
961 let mut od = 0.0_f32;
962 for i in 0..steps {
963 let h = alt_low + (i as f32 + 0.5) * dh;
964 let cloud = self.cloud_fraction_at(h) * 0.05;
965 let haze = self.haze_extinction * (-h / 8_500.0_f32).exp();
966 let fog_ext: f32 = self.fog_layers.iter().map(|f| f.extinction_at(h)).sum();
967 od += (cloud + haze + fog_ext) * dh;
968 }
969 od
970 }
971
972 pub fn temperature_at(&self, alt_m: f32) -> f32 {
974 self.temperature_profile.sample_at(alt_m + self.surface_altitude_m)
975 }
976}
977
978#[derive(Debug, Clone)]
982pub struct AtmosphereSimulator {
983 pub atmo: Atmosphere,
984 pub last_visibility: VisibilityResult,
986 pub precip_potential: f32,
988 stations: HashMap<String, [f32; 3]>,
990}
991
992impl AtmosphereSimulator {
993 pub fn new(surface_alt: f32) -> Self {
994 let atmo = Atmosphere::new(surface_alt);
995 let vis = atmo.compute_visibility();
996 Self {
997 atmo,
998 last_visibility: vis,
999 precip_potential: 0.0,
1000 stations: HashMap::new(),
1001 }
1002 }
1003
1004 pub fn tick(&mut self, dt: f32, surface_temp_k: f32, time_of_day: f32) {
1005 self.atmo.tick(dt, surface_temp_k, time_of_day);
1006 self.last_visibility = self.atmo.compute_visibility();
1007 let hum = self.atmo.surface_humidity();
1009 if hum > 0.85 {
1010 self.precip_potential = (self.precip_potential + (hum - 0.85) * dt * 0.1).min(1.0);
1011 } else {
1012 self.precip_potential = (self.precip_potential - dt * 0.005).max(0.0);
1013 }
1014 }
1015
1016 pub fn add_station(&mut self, name: impl Into<String>, x: f32, y: f32, z: f32) {
1018 self.stations.insert(name.into(), [x, y, z]);
1019 }
1020
1021 pub fn station_report(&self, name: &str) -> Option<StationReport> {
1023 let pos = self.stations.get(name)?;
1024 let [x, y, z] = *pos;
1025 let wind = self.atmo.wind_at(x, y, z);
1026 let temp_k = self.atmo.temperature_at(y);
1027 let pressure = isa_pressure_at_altitude(y + self.atmo.surface_altitude_m);
1028 let vis = self.last_visibility.distance_m;
1029 let cloud = self.atmo.cloud_fraction_at(y);
1030 Some(StationReport { x, y, z, wind, temp_k, pressure_pa: pressure, visibility_m: vis, cloud_fraction: cloud })
1031 }
1032
1033 pub fn precipitation_likely(&self) -> bool {
1035 self.precip_potential > 0.3 && self.atmo.surface_humidity() > 0.75
1036 }
1037}
1038
1039#[derive(Debug, Clone, Copy)]
1041pub struct StationReport {
1042 pub x: f32, pub y: f32, pub z: f32,
1043 pub wind: [f32; 3],
1044 pub temp_k: f32,
1045 pub pressure_pa: f32,
1046 pub visibility_m: f32,
1047 pub cloud_fraction: f32,
1048}
1049
1050impl StationReport {
1051 pub fn temp_celsius(&self) -> f32 { self.temp_k - 273.15 }
1052 pub fn wind_speed_ms(&self) -> f32 {
1053 let [wx, wy, wz] = self.wind;
1054 (wx * wx + wy * wy + wz * wz).sqrt()
1055 }
1056 pub fn beaufort_number(&self) -> u8 {
1057 let spd = self.wind_speed_ms();
1058 match spd as u32 {
1059 0 => 0,
1060 1..=5 => 1,
1061 6..=11 => 2,
1062 12..=19 => 3,
1063 20..=28 => 4,
1064 29..=38 => 5,
1065 39..=49 => 6,
1066 50..=61 => 7,
1067 62..=74 => 8,
1068 75..=88 => 9,
1069 89..=102 => 10,
1070 103..=117 => 11,
1071 _ => 12,
1072 }
1073 }
1074}