proof_engine/worldgen/
climate.rs1use super::{Grid2D, Rng};
7use std::f32::consts::PI;
8
9#[derive(Debug, Clone)]
11pub struct ClimateParams {
12 pub equator_temp: f32,
14 pub polar_temp: f32,
16 pub lapse_rate: f32,
18 pub ocean_moderation: f32,
20 pub prevailing_wind: f32,
22 pub wind_speed: f32,
24 pub moisture_rate: f32,
26 pub orographic_factor: f32,
28 pub diffusion: f32,
30 pub sea_level: f32,
32}
33
34impl Default for ClimateParams {
35 fn default() -> Self {
36 Self {
37 equator_temp: 30.0,
38 polar_temp: -20.0,
39 lapse_rate: 40.0,
40 ocean_moderation: 0.3,
41 prevailing_wind: 0.0,
42 wind_speed: 1.0,
43 moisture_rate: 0.05,
44 orographic_factor: 3.0,
45 diffusion: 0.1,
46 sea_level: 0.4,
47 }
48 }
49}
50
51pub fn simulate(heightmap: &Grid2D, iterations: usize, rng: &mut Rng) -> (Grid2D, Grid2D) {
53 let params = ClimateParams::default();
54 simulate_with_params(heightmap, iterations, ¶ms, rng)
55}
56
57pub fn simulate_with_params(
59 heightmap: &Grid2D,
60 iterations: usize,
61 params: &ClimateParams,
62 _rng: &mut Rng,
63) -> (Grid2D, Grid2D) {
64 let w = heightmap.width;
65 let h = heightmap.height;
66
67 let mut temperature = Grid2D::new(w, h);
69 for y in 0..h {
70 let latitude = (y as f32 / h as f32 - 0.5).abs() * 2.0; let lat_temp = params.equator_temp + (params.polar_temp - params.equator_temp) * latitude;
72
73 for x in 0..w {
74 let elevation = heightmap.get(x, y);
75 let alt_offset = if elevation > params.sea_level {
76 -(elevation - params.sea_level) * params.lapse_rate
77 } else {
78 0.0
79 };
80
81 let is_ocean = elevation < params.sea_level;
83 let base = lat_temp + alt_offset;
84 let temp = if is_ocean {
85 base * (1.0 - params.ocean_moderation) + lat_temp * params.ocean_moderation
86 } else {
87 base
88 };
89
90 temperature.set(x, y, temp);
91 }
92 }
93
94 for _ in 0..iterations {
96 diffuse_temperature(&mut temperature, params.diffusion);
97 }
98
99 let mut precipitation = Grid2D::new(w, h);
101
102 let wind_dx = params.prevailing_wind.cos();
104 let wind_dy = params.prevailing_wind.sin();
105
106 for wind_band in 0..3 {
108 let band_angle = match wind_band {
109 0 => params.prevailing_wind, 1 => params.prevailing_wind + PI, _ => params.prevailing_wind + PI * 0.5, };
113 let wdx = band_angle.cos() * params.wind_speed;
114 let wdy = band_angle.sin() * params.wind_speed;
115
116 let (y_min, y_max) = match wind_band {
118 0 => (h / 3, 2 * h / 3), 1 => (0, h / 3), _ => (2 * h / 3, h), };
122
123 let mut moisture = Grid2D::new(w, h);
124
125 for step in 0..w.max(h) {
127 for y in y_min..y_max {
128 for x in 0..w {
129 let sx = (x as f32 - wdx * step as f32).rem_euclid(w as f32) as usize;
130 let sy = (y as f32 - wdy * step as f32).clamp(0.0, h as f32 - 1.0) as usize;
131
132 let elev = heightmap.get(x, y);
133 let is_ocean = elev < params.sea_level;
134
135 if is_ocean {
136 moisture.add(x, y, params.moisture_rate);
138 } else {
139 let (gx, gy) = heightmap.gradient(x, y);
141 let upslope = gx * wdx + gy * wdy;
142 if upslope > 0.0 {
143 let rain = moisture.get(x, y) * upslope * params.orographic_factor;
144 let rain = rain.min(moisture.get(x, y));
145 precipitation.add(x, y, rain);
146 moisture.add(x, y, -rain);
147 }
148 }
149 }
150 }
151 }
152
153 for y in y_min..y_max {
155 for x in 0..w {
156 precipitation.add(x, y, moisture.get(x, y) * 0.1);
157 }
158 }
159 }
160
161 for y in 0..h {
163 let lat = (y as f32 / h as f32 - 0.5).abs() * 2.0;
164 let itcz = (1.0 - lat * 3.0).max(0.0); for x in 0..w {
166 precipitation.add(x, y, itcz * 0.5);
167 }
168 }
169
170 precipitation.normalize();
172
173 (temperature, precipitation)
174}
175
176fn diffuse_temperature(temp: &mut Grid2D, rate: f32) {
178 let w = temp.width;
179 let h = temp.height;
180 let old = temp.data.clone();
181
182 for y in 1..h - 1 {
183 for x in 1..w - 1 {
184 let idx = y * w + x;
185 let laplacian = old[idx - 1] + old[idx + 1]
186 + old[idx - w] + old[idx + w]
187 - 4.0 * old[idx];
188 temp.data[idx] += laplacian * rate;
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_temperature_latitude() {
199 let hm = Grid2D::filled(32, 32, 0.5); let mut rng = Rng::new(42);
201 let (temp, _) = simulate(&hm, 10, &mut rng);
202 let equator_t = temp.get(16, 16);
204 let pole_t = temp.get(16, 0);
205 assert!(equator_t > pole_t, "equator should be warmer: eq={equator_t}, pole={pole_t}");
206 }
207
208 #[test]
209 fn test_precipitation_exists() {
210 let hm = Grid2D::filled(32, 32, 0.3); let mut rng = Rng::new(42);
212 let (_, precip) = simulate(&hm, 10, &mut rng);
213 let max_p = precip.max_value();
214 assert!(max_p > 0.0, "should have some precipitation");
215 }
216}