1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6pub const PI: f32 = std::f32::consts::PI;
11pub const TWO_PI: f32 = 2.0 * PI;
12pub const HALF_PI: f32 = PI * 0.5;
13pub const DEG2RAD: f32 = PI / 180.0;
14pub const RAD2DEG: f32 = 180.0 / PI;
15pub const SQRT3: f32 = 1.732_050_8;
16pub const F3: f32 = 1.0 / 3.0;
17pub const G3: f32 = 1.0 / 6.0;
18
19pub const RAYLEIGH_SCALE_HEIGHT: f64 = 8.5; pub const MIE_SCALE_HEIGHT: f64 = 1.2; pub const RAYLEIGH_R: f64 = 5.8e-6;
23pub const RAYLEIGH_G: f64 = 13.5e-6;
24pub const RAYLEIGH_B: f64 = 33.1e-6;
25pub const MIE_COEFF: f64 = 21.0e-6;
26pub const MIE_G: f64 = 0.758; pub const EARTH_RADIUS: f64 = 6371.0; pub const ATMO_RADIUS: f64 = 6471.0; pub const SOLAR_OBLIQUITY: f64 = 23.45; pub const EROSION_INERTIA: f32 = 0.05;
35pub const EROSION_CAPACITY: f32 = 4.0;
36pub const EROSION_DEPOSITION: f32 = 0.3;
37pub const EROSION_EROSION_SPEED: f32 = 0.3;
38pub const EROSION_EVAPORATION: f32 = 0.02;
39pub const EROSION_MIN_SLOPE: f32 = 0.01;
40pub const EROSION_GRAVITY: f32 = 4.0;
41pub const EROSION_MAX_STEPS: usize = 64;
42
43pub const PERM: [u8; 512] = [
48 151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233, 7,225,
49 140, 36,103, 30, 69,142, 8, 99, 37,240, 21, 10, 23,190, 6,148,
50 247,120,234, 75, 0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
51 57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
52 74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
53 60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
54 65, 25, 63,161, 1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
55 200,196,135,130,116,188,159, 86,164,100,109,198,173,186, 3, 64,
56 52,217,226,250,124,123, 5,202, 38,147,118,126,255, 82, 85,212,
57 207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
58 119,248,152, 2, 44,154,163, 70,221,153,101,155,167, 43,172, 9,
59 129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
60 218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
61 81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
62 184, 84,204,176,115,121, 50, 45,127, 4,150,254,138,236,205, 93,
63 222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
64 151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233, 7,225,
65 140, 36,103, 30, 69,142, 8, 99, 37,240, 21, 10, 23,190, 6,148,
66 247,120,234, 75, 0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
67 57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
68 74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
69 60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
70 65, 25, 63,161, 1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
71 200,196,135,130,116,188,159, 86,164,100,109,198,173,186, 3, 64,
72 52,217,226,250,124,123, 5,202, 38,147,118,126,255, 82, 85,212,
73 207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
74 119,248,152, 2, 44,154,163, 70,221,153,101,155,167, 43,172, 9,
75 129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
76 218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
77 81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
78 184, 84,204,176,115,121, 50, 45,127, 4,150,254,138,236,205, 93,
79 222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
80];
81
82pub const GRAD3: [[f32; 3]; 16] = [
84 [ 1.0, 1.0, 0.0], [-1.0, 1.0, 0.0], [ 1.0,-1.0, 0.0], [-1.0,-1.0, 0.0],
85 [ 1.0, 0.0, 1.0], [-1.0, 0.0, 1.0], [ 1.0, 0.0,-1.0], [-1.0, 0.0,-1.0],
86 [ 0.0, 1.0, 1.0], [ 0.0,-1.0, 1.0], [ 0.0, 1.0,-1.0], [ 0.0,-1.0,-1.0],
87 [ 1.0, 1.0, 0.0], [-1.0, 1.0, 0.0], [ 0.0,-1.0, 1.0], [ 0.0,-1.0,-1.0],
88];
89
90pub const GRAD4: [[f32; 4]; 32] = [
92 [ 0.0, 1.0, 1.0, 1.0],[ 0.0, 1.0, 1.0,-1.0],[ 0.0, 1.0,-1.0, 1.0],[ 0.0, 1.0,-1.0,-1.0],
93 [ 0.0,-1.0, 1.0, 1.0],[ 0.0,-1.0, 1.0,-1.0],[ 0.0,-1.0,-1.0, 1.0],[ 0.0,-1.0,-1.0,-1.0],
94 [ 1.0, 0.0, 1.0, 1.0],[ 1.0, 0.0, 1.0,-1.0],[ 1.0, 0.0,-1.0, 1.0],[ 1.0, 0.0,-1.0,-1.0],
95 [-1.0, 0.0, 1.0, 1.0],[-1.0, 0.0, 1.0,-1.0],[-1.0, 0.0,-1.0, 1.0],[-1.0, 0.0,-1.0,-1.0],
96 [ 1.0, 1.0, 0.0, 1.0],[ 1.0, 1.0, 0.0,-1.0],[ 1.0,-1.0, 0.0, 1.0],[ 1.0,-1.0, 0.0,-1.0],
97 [-1.0, 1.0, 0.0, 1.0],[-1.0, 1.0, 0.0,-1.0],[-1.0,-1.0, 0.0, 1.0],[-1.0,-1.0, 0.0,-1.0],
98 [ 1.0, 1.0, 1.0, 0.0],[ 1.0, 1.0,-1.0, 0.0],[ 1.0,-1.0, 1.0, 0.0],[ 1.0,-1.0,-1.0, 0.0],
99 [-1.0, 1.0, 1.0, 0.0],[-1.0, 1.0,-1.0, 0.0],[-1.0,-1.0, 1.0, 0.0],[-1.0,-1.0,-1.0, 0.0],
100];
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum BiomeId {
108 TropicalRainforest = 0,
109 TropicalSavanna = 1,
110 HotDesert = 2,
111 ColdDesert = 3,
112 XericShrubland = 4,
113 MediterraneanShrub = 5,
114 TemperateGrassland = 6,
115 TemperateRainforest = 7,
116 TemperateDeciduous = 8,
117 BorealForest = 9,
118 TaigaSpruce = 10,
119 Tundra = 11,
120 ArcticDesert = 12,
121 AlpineMeadow = 13,
122 AlpineTundra = 14,
123 PolarIceCap = 15,
124 Mangrove = 16,
125 Wetland = 17,
126 FloodPlain = 18,
127 VolcanicLandscape = 19,
128 SaltFlat = 20,
129 GlacialValley = 21,
130 CoastalDunes = 22,
131 DeepOceanFloor = 23,
132 CoralReef = 24,
133}
134
135#[derive(Clone, Debug)]
136pub struct BiomeDescriptor {
137 pub id: BiomeId,
138 pub name: &'static str,
139 pub temp_min: f32,
140 pub temp_max: f32,
141 pub humidity_min: f32,
142 pub humidity_max: f32,
143 pub alt_min: f32,
144 pub alt_max: f32,
145 pub ground_color: Vec3,
146 pub tree_density: f32,
147 pub grass_density: f32,
148 pub rock_density: f32,
149 pub snow_coverage: f32,
150 pub rainfall_mm: f32,
151 pub wind_speed_ms: f32,
152 pub fog_density: f32,
153}
154
155impl BiomeDescriptor {
156 pub fn classify_point(temp: f32, humidity: f32, altitude: f32) -> BiomeId {
157 if altitude > 0.88 {
159 return BiomeId::PolarIceCap;
160 }
161 if altitude > 0.75 {
162 return BiomeId::AlpineTundra;
163 }
164 if altitude > 0.62 {
165 return BiomeId::AlpineMeadow;
166 }
167
168 if temp > 24.0 {
170 if humidity > 0.80 {
171 return BiomeId::TropicalRainforest;
172 } else if humidity > 0.50 {
173 return BiomeId::TropicalSavanna;
174 } else if humidity > 0.25 {
175 return BiomeId::XericShrubland;
176 } else {
177 return BiomeId::HotDesert;
178 }
179 } else if temp > 10.0 {
180 if humidity > 0.70 {
181 return BiomeId::TemperateRainforest;
182 } else if humidity > 0.50 {
183 return BiomeId::TemperateDeciduous;
184 } else if humidity > 0.28 {
185 return BiomeId::MediterraneanShrub;
186 } else {
187 return BiomeId::XericShrubland;
188 }
189 } else if temp > 0.0 {
190 if humidity > 0.65 {
191 return BiomeId::BorealForest;
192 } else if humidity > 0.40 {
193 return BiomeId::TemperateGrassland;
194 } else {
195 return BiomeId::ColdDesert;
196 }
197 } else if temp > -10.0 {
198 if humidity > 0.50 {
199 return BiomeId::TaigaSpruce;
200 } else {
201 return BiomeId::Tundra;
202 }
203 } else {
204 if humidity > 0.30 {
205 return BiomeId::Tundra;
206 } else {
207 return BiomeId::ArcticDesert;
208 }
209 }
210 }
211
212 pub fn blend_weight(&self, temp: f32, humidity: f32, altitude: f32) -> f32 {
214 let temp_w = gaussian_falloff(temp, (self.temp_min + self.temp_max) * 0.5, (self.temp_max - self.temp_min) * 0.5 + 0.5);
215 let hum_w = gaussian_falloff(humidity, (self.humidity_min + self.humidity_max) * 0.5, (self.humidity_max - self.humidity_min) * 0.5 + 0.05);
216 let alt_w = gaussian_falloff(altitude, (self.alt_min + self.alt_max) * 0.5, (self.alt_max - self.alt_min) * 0.5 + 0.05);
217 (temp_w * hum_w * alt_w).max(0.0)
218 }
219}
220
221fn gaussian_falloff(x: f32, center: f32, sigma: f32) -> f32 {
222 let diff = x - center;
223 (-(diff * diff) / (2.0 * sigma * sigma)).exp()
224}
225
226pub fn build_biome_table() -> Vec<BiomeDescriptor> {
228 vec![
229 BiomeDescriptor {
230 id: BiomeId::TropicalRainforest, name: "Tropical Rainforest",
231 temp_min: 24.0, temp_max: 36.0, humidity_min: 0.80, humidity_max: 1.00,
232 alt_min: 0.00, alt_max: 0.30, ground_color: Vec3::new(0.04, 0.35, 0.06),
233 tree_density: 0.95, grass_density: 0.60, rock_density: 0.03,
234 snow_coverage: 0.00, rainfall_mm: 3000.0, wind_speed_ms: 2.0, fog_density: 0.15,
235 },
236 BiomeDescriptor {
237 id: BiomeId::TropicalSavanna, name: "Tropical Savanna",
238 temp_min: 20.0, temp_max: 35.0, humidity_min: 0.25, humidity_max: 0.55,
239 alt_min: 0.00, alt_max: 0.30, ground_color: Vec3::new(0.62, 0.55, 0.14),
240 tree_density: 0.18, grass_density: 0.85, rock_density: 0.10,
241 snow_coverage: 0.00, rainfall_mm: 900.0, wind_speed_ms: 4.0, fog_density: 0.02,
242 },
243 BiomeDescriptor {
244 id: BiomeId::HotDesert, name: "Hot Desert",
245 temp_min: 20.0, temp_max: 52.0, humidity_min: 0.00, humidity_max: 0.18,
246 alt_min: 0.00, alt_max: 0.35, ground_color: Vec3::new(0.87, 0.79, 0.41),
247 tree_density: 0.01, grass_density: 0.04, rock_density: 0.40,
248 snow_coverage: 0.00, rainfall_mm: 80.0, wind_speed_ms: 7.0, fog_density: 0.00,
249 },
250 BiomeDescriptor {
251 id: BiomeId::ColdDesert, name: "Cold Desert",
252 temp_min: -10.0, temp_max: 15.0, humidity_min: 0.00, humidity_max: 0.20,
253 alt_min: 0.00, alt_max: 0.45, ground_color: Vec3::new(0.70, 0.65, 0.50),
254 tree_density: 0.02, grass_density: 0.10, rock_density: 0.50,
255 snow_coverage: 0.10, rainfall_mm: 150.0, wind_speed_ms: 8.0, fog_density: 0.01,
256 },
257 BiomeDescriptor {
258 id: BiomeId::XericShrubland, name: "Xeric Shrubland",
259 temp_min: 10.0, temp_max: 30.0, humidity_min: 0.10, humidity_max: 0.30,
260 alt_min: 0.00, alt_max: 0.40, ground_color: Vec3::new(0.70, 0.65, 0.30),
261 tree_density: 0.05, grass_density: 0.40, rock_density: 0.30,
262 snow_coverage: 0.00, rainfall_mm: 300.0, wind_speed_ms: 5.0, fog_density: 0.01,
263 },
264 BiomeDescriptor {
265 id: BiomeId::MediterraneanShrub, name: "Mediterranean Shrubland",
266 temp_min: 5.0, temp_max: 28.0, humidity_min: 0.25, humidity_max: 0.50,
267 alt_min: 0.00, alt_max: 0.40, ground_color: Vec3::new(0.55, 0.62, 0.20),
268 tree_density: 0.25, grass_density: 0.55, rock_density: 0.20,
269 snow_coverage: 0.00, rainfall_mm: 600.0, wind_speed_ms: 4.0, fog_density: 0.03,
270 },
271 BiomeDescriptor {
272 id: BiomeId::TemperateGrassland, name: "Temperate Grassland",
273 temp_min: -5.0, temp_max: 20.0, humidity_min: 0.20, humidity_max: 0.50,
274 alt_min: 0.00, alt_max: 0.45, ground_color: Vec3::new(0.50, 0.70, 0.15),
275 tree_density: 0.05, grass_density: 0.90, rock_density: 0.05,
276 snow_coverage: 0.05, rainfall_mm: 500.0, wind_speed_ms: 5.5, fog_density: 0.05,
277 },
278 BiomeDescriptor {
279 id: BiomeId::TemperateRainforest, name: "Temperate Rainforest",
280 temp_min: 5.0, temp_max: 20.0, humidity_min: 0.70, humidity_max: 1.00,
281 alt_min: 0.00, alt_max: 0.50, ground_color: Vec3::new(0.10, 0.40, 0.10),
282 tree_density: 0.85, grass_density: 0.50, rock_density: 0.08,
283 snow_coverage: 0.00, rainfall_mm: 2500.0, wind_speed_ms: 3.0, fog_density: 0.20,
284 },
285 BiomeDescriptor {
286 id: BiomeId::TemperateDeciduous, name: "Temperate Deciduous Forest",
287 temp_min: 5.0, temp_max: 22.0, humidity_min: 0.50, humidity_max: 0.75,
288 alt_min: 0.00, alt_max: 0.50, ground_color: Vec3::new(0.20, 0.50, 0.10),
289 tree_density: 0.70, grass_density: 0.35, rock_density: 0.10,
290 snow_coverage: 0.05, rainfall_mm: 1100.0, wind_speed_ms: 3.5, fog_density: 0.08,
291 },
292 BiomeDescriptor {
293 id: BiomeId::BorealForest, name: "Boreal Forest",
294 temp_min: -10.0, temp_max: 10.0, humidity_min: 0.45, humidity_max: 0.70,
295 alt_min: 0.00, alt_max: 0.55, ground_color: Vec3::new(0.15, 0.35, 0.12),
296 tree_density: 0.75, grass_density: 0.20, rock_density: 0.12,
297 snow_coverage: 0.25, rainfall_mm: 700.0, wind_speed_ms: 4.0, fog_density: 0.10,
298 },
299 BiomeDescriptor {
300 id: BiomeId::TaigaSpruce, name: "Taiga Spruce",
301 temp_min: -20.0, temp_max: 5.0, humidity_min: 0.40, humidity_max: 0.65,
302 alt_min: 0.00, alt_max: 0.60, ground_color: Vec3::new(0.12, 0.28, 0.12),
303 tree_density: 0.65, grass_density: 0.15, rock_density: 0.15,
304 snow_coverage: 0.45, rainfall_mm: 550.0, wind_speed_ms: 5.0, fog_density: 0.12,
305 },
306 BiomeDescriptor {
307 id: BiomeId::Tundra, name: "Tundra",
308 temp_min: -25.0, temp_max: 0.0, humidity_min: 0.20, humidity_max: 0.55,
309 alt_min: 0.00, alt_max: 0.65, ground_color: Vec3::new(0.45, 0.50, 0.30),
310 tree_density: 0.02, grass_density: 0.50, rock_density: 0.30,
311 snow_coverage: 0.60, rainfall_mm: 280.0, wind_speed_ms: 8.0, fog_density: 0.15,
312 },
313 BiomeDescriptor {
314 id: BiomeId::ArcticDesert, name: "Arctic Desert",
315 temp_min: -40.0, temp_max: -10.0, humidity_min: 0.00, humidity_max: 0.20,
316 alt_min: 0.00, alt_max: 0.70, ground_color: Vec3::new(0.80, 0.85, 0.90),
317 tree_density: 0.00, grass_density: 0.02, rock_density: 0.20,
318 snow_coverage: 0.90, rainfall_mm: 100.0, wind_speed_ms: 12.0, fog_density: 0.10,
319 },
320 BiomeDescriptor {
321 id: BiomeId::AlpineMeadow, name: "Alpine Meadow",
322 temp_min: -5.0, temp_max: 12.0, humidity_min: 0.40, humidity_max: 0.75,
323 alt_min: 0.58, alt_max: 0.75, ground_color: Vec3::new(0.35, 0.60, 0.20),
324 tree_density: 0.10, grass_density: 0.75, rock_density: 0.25,
325 snow_coverage: 0.20, rainfall_mm: 800.0, wind_speed_ms: 6.0, fog_density: 0.08,
326 },
327 BiomeDescriptor {
328 id: BiomeId::AlpineTundra, name: "Alpine Tundra",
329 temp_min: -15.0, temp_max: 5.0, humidity_min: 0.20, humidity_max: 0.60,
330 alt_min: 0.72, alt_max: 0.88, ground_color: Vec3::new(0.40, 0.42, 0.38),
331 tree_density: 0.00, grass_density: 0.30, rock_density: 0.60,
332 snow_coverage: 0.50, rainfall_mm: 500.0, wind_speed_ms: 10.0, fog_density: 0.12,
333 },
334 BiomeDescriptor {
335 id: BiomeId::PolarIceCap, name: "Polar Ice Cap",
336 temp_min: -50.0, temp_max: -5.0, humidity_min: 0.00, humidity_max: 0.30,
337 alt_min: 0.85, alt_max: 1.00, ground_color: Vec3::new(0.92, 0.95, 1.00),
338 tree_density: 0.00, grass_density: 0.00, rock_density: 0.05,
339 snow_coverage: 1.00, rainfall_mm: 50.0, wind_speed_ms: 15.0, fog_density: 0.20,
340 },
341 BiomeDescriptor {
342 id: BiomeId::Mangrove, name: "Mangrove",
343 temp_min: 20.0, temp_max: 35.0, humidity_min: 0.70, humidity_max: 1.00,
344 alt_min: 0.00, alt_max: 0.08, ground_color: Vec3::new(0.20, 0.35, 0.10),
345 tree_density: 0.70, grass_density: 0.30, rock_density: 0.02,
346 snow_coverage: 0.00, rainfall_mm: 2000.0, wind_speed_ms: 2.0, fog_density: 0.25,
347 },
348 BiomeDescriptor {
349 id: BiomeId::Wetland, name: "Wetland",
350 temp_min: 0.0, temp_max: 25.0, humidity_min: 0.75, humidity_max: 1.00,
351 alt_min: 0.00, alt_max: 0.15, ground_color: Vec3::new(0.18, 0.32, 0.10),
352 tree_density: 0.30, grass_density: 0.80, rock_density: 0.02,
353 snow_coverage: 0.00, rainfall_mm: 1400.0, wind_speed_ms: 2.0, fog_density: 0.30,
354 },
355 BiomeDescriptor {
356 id: BiomeId::FloodPlain, name: "Flood Plain",
357 temp_min: 10.0, temp_max: 30.0, humidity_min: 0.55, humidity_max: 0.85,
358 alt_min: 0.00, alt_max: 0.12, ground_color: Vec3::new(0.40, 0.55, 0.15),
359 tree_density: 0.15, grass_density: 0.85, rock_density: 0.03,
360 snow_coverage: 0.00, rainfall_mm: 1200.0, wind_speed_ms: 3.0, fog_density: 0.12,
361 },
362 BiomeDescriptor {
363 id: BiomeId::VolcanicLandscape, name: "Volcanic Landscape",
364 temp_min: 5.0, temp_max: 40.0, humidity_min: 0.10, humidity_max: 0.60,
365 alt_min: 0.10, alt_max: 0.70, ground_color: Vec3::new(0.12, 0.10, 0.10),
366 tree_density: 0.05, grass_density: 0.10, rock_density: 0.85,
367 snow_coverage: 0.00, rainfall_mm: 400.0, wind_speed_ms: 6.0, fog_density: 0.20,
368 },
369 BiomeDescriptor {
370 id: BiomeId::SaltFlat, name: "Salt Flat",
371 temp_min: 15.0, temp_max: 45.0, humidity_min: 0.00, humidity_max: 0.12,
372 alt_min: 0.00, alt_max: 0.10, ground_color: Vec3::new(0.95, 0.95, 0.92),
373 tree_density: 0.00, grass_density: 0.03, rock_density: 0.05,
374 snow_coverage: 0.00, rainfall_mm: 50.0, wind_speed_ms: 8.0, fog_density: 0.00,
375 },
376 BiomeDescriptor {
377 id: BiomeId::GlacialValley, name: "Glacial Valley",
378 temp_min: -20.0, temp_max: 2.0, humidity_min: 0.30, humidity_max: 0.70,
379 alt_min: 0.30, alt_max: 0.80, ground_color: Vec3::new(0.55, 0.65, 0.70),
380 tree_density: 0.05, grass_density: 0.15, rock_density: 0.60,
381 snow_coverage: 0.70, rainfall_mm: 600.0, wind_speed_ms: 7.0, fog_density: 0.15,
382 },
383 BiomeDescriptor {
384 id: BiomeId::CoastalDunes, name: "Coastal Dunes",
385 temp_min: 10.0, temp_max: 35.0, humidity_min: 0.15, humidity_max: 0.45,
386 alt_min: 0.00, alt_max: 0.10, ground_color: Vec3::new(0.90, 0.85, 0.65),
387 tree_density: 0.05, grass_density: 0.30, rock_density: 0.10,
388 snow_coverage: 0.00, rainfall_mm: 350.0, wind_speed_ms: 9.0, fog_density: 0.08,
389 },
390 BiomeDescriptor {
391 id: BiomeId::DeepOceanFloor, name: "Deep Ocean Floor",
392 temp_min: 2.0, temp_max: 8.0, humidity_min: 1.00, humidity_max: 1.00,
393 alt_min: 0.00, alt_max: 0.05, ground_color: Vec3::new(0.05, 0.06, 0.15),
394 tree_density: 0.00, grass_density: 0.05, rock_density: 0.20,
395 snow_coverage: 0.00, rainfall_mm: 0.0, wind_speed_ms: 0.0, fog_density: 0.90,
396 },
397 BiomeDescriptor {
398 id: BiomeId::CoralReef, name: "Coral Reef",
399 temp_min: 22.0, temp_max: 32.0, humidity_min: 0.90, humidity_max: 1.00,
400 alt_min: 0.00, alt_max: 0.06, ground_color: Vec3::new(0.90, 0.60, 0.40),
401 tree_density: 0.00, grass_density: 0.60, rock_density: 0.30,
402 snow_coverage: 0.00, rainfall_mm: 0.0, wind_speed_ms: 0.0, fog_density: 0.30,
403 },
404 ]
405}
406
407#[derive(Clone, Debug)]
408pub struct BiomeBlendSample {
409 pub weights: [f32; 25],
410 pub dominant: BiomeId,
411 pub blended_color: Vec3,
412 pub blended_tree_density: f32,
413 pub blended_grass_density: f32,
414 pub blended_rock_density: f32,
415 pub blended_snow: f32,
416}
417
418pub struct BiomeSystem {
419 pub descriptors: Vec<BiomeDescriptor>,
420}
421
422impl BiomeSystem {
423 pub fn new() -> Self {
424 Self { descriptors: build_biome_table() }
425 }
426
427 pub fn sample(&self, temp: f32, humidity: f32, altitude: f32) -> BiomeBlendSample {
429 let mut weights = [0.0f32; 25];
430 let mut weight_sum = 0.0f32;
431
432 for (i, desc) in self.descriptors.iter().enumerate() {
433 let w = desc.blend_weight(temp, humidity, altitude);
434 weights[i] = w;
435 weight_sum += w;
436 }
437
438 if weight_sum < 1e-10 {
440 let id = BiomeDescriptor::classify_point(temp, humidity, altitude) as usize;
442 weights[id] = 1.0;
443 weight_sum = 1.0;
444 }
445 for w in weights.iter_mut() {
446 *w /= weight_sum;
447 }
448
449 let dominant_idx = weights.iter().enumerate()
451 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
452 .map(|(i, _)| i)
453 .unwrap_or(0);
454
455 let mut blended_color = Vec3::ZERO;
457 let mut blended_tree = 0.0f32;
458 let mut blended_grass = 0.0f32;
459 let mut blended_rock = 0.0f32;
460 let mut blended_snow = 0.0f32;
461
462 for (i, desc) in self.descriptors.iter().enumerate() {
463 let w = weights[i];
464 blended_color += desc.ground_color * w;
465 blended_tree += desc.tree_density * w;
466 blended_grass += desc.grass_density * w;
467 blended_rock += desc.rock_density * w;
468 blended_snow += desc.snow_coverage * w;
469 }
470
471 let dominant_id = self.descriptors[dominant_idx].id;
472 BiomeBlendSample {
473 weights,
474 dominant: dominant_id,
475 blended_color,
476 blended_tree_density: blended_tree,
477 blended_grass_density: blended_grass,
478 blended_rock_density: blended_rock,
479 blended_snow,
480 }
481 }
482
483 pub fn transition_factor(&self, biome_a: BiomeId, biome_b: BiomeId,
485 temp: f32, humidity: f32, altitude: f32) -> f32 {
486 let wa = self.descriptors[biome_a as usize].blend_weight(temp, humidity, altitude);
487 let wb = self.descriptors[biome_b as usize].blend_weight(temp, humidity, altitude);
488 if wa + wb < 1e-10 { return 0.5; }
489 wa / (wa + wb)
490 }
491
492 pub fn wind_speed(&self, sample: &BiomeBlendSample) -> f32 {
494 let mut speed = 0.0f32;
495 for (i, desc) in self.descriptors.iter().enumerate() {
496 speed += desc.wind_speed_ms * sample.weights[i];
497 }
498 speed
499 }
500}
501
502#[inline]
509fn fade(t: f32) -> f32 {
510 t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
512}
513
514#[inline]
515fn lerp_f(a: f32, b: f32, t: f32) -> f32 {
516 a + t * (b - a)
517}
518
519#[inline]
520fn grad3(hash: u8, x: f32, y: f32, z: f32) -> f32 {
521 let h = (hash & 15) as usize;
522 let g = &GRAD3[h];
523 g[0] * x + g[1] * y + g[2] * z
524}
525
526pub fn perlin_noise_3d(x: f32, y: f32, z: f32) -> f32 {
527 let xi = x.floor() as i32;
528 let yi = y.floor() as i32;
529 let zi = z.floor() as i32;
530
531 let xf = x - xi as f32;
532 let yf = y - yi as f32;
533 let zf = z - zi as f32;
534
535 let u = fade(xf);
536 let v = fade(yf);
537 let w = fade(zf);
538
539 let xi = (xi & 255) as usize;
540 let yi = (yi & 255) as usize;
541 let zi = (zi & 255) as usize;
542
543 let aaa = PERM[PERM[PERM[xi] as usize + yi] as usize + zi] as u8;
544 let aba = PERM[PERM[PERM[xi] as usize + yi + 1] as usize + zi] as u8;
545 let aab = PERM[PERM[PERM[xi] as usize + yi] as usize + zi + 1] as u8;
546 let abb = PERM[PERM[PERM[xi] as usize + yi + 1] as usize + zi + 1] as u8;
547 let baa = PERM[PERM[PERM[xi + 1] as usize + yi] as usize + zi] as u8;
548 let bba = PERM[PERM[PERM[xi + 1] as usize + yi + 1] as usize + zi] as u8;
549 let bab = PERM[PERM[PERM[xi + 1] as usize + yi] as usize + zi + 1] as u8;
550 let bbb = PERM[PERM[PERM[xi + 1] as usize + yi + 1] as usize + zi + 1] as u8;
551
552 let x1 = lerp_f(grad3(aaa, xf, yf, zf), grad3(baa, xf - 1.0, yf, zf), u);
553 let x2 = lerp_f(grad3(aba, xf, yf - 1.0, zf), grad3(bba, xf - 1.0, yf - 1.0, zf), u);
554 let y1 = lerp_f(x1, x2, v);
555
556 let x3 = lerp_f(grad3(aab, xf, yf, zf - 1.0), grad3(bab, xf - 1.0, yf, zf - 1.0), u);
557 let x4 = lerp_f(grad3(abb, xf, yf - 1.0, zf - 1.0), grad3(bbb, xf - 1.0, yf - 1.0, zf - 1.0), u);
558 let y2 = lerp_f(x3, x4, v);
559
560 lerp_f(y1, y2, w)
561}
562
563pub fn perlin_noise_2d(x: f32, y: f32) -> f32 {
564 perlin_noise_3d(x, y, 0.0)
565}
566
567#[inline]
570fn simplex_grad3(hash: u8, x: f32, y: f32, z: f32) -> f32 {
571 let h = (hash & 15) as usize;
572 let g = &GRAD3[h];
573 g[0] * x + g[1] * y + g[2] * z
574}
575
576pub fn simplex_noise_3d(xin: f32, yin: f32, zin: f32) -> f32 {
577 let f3 = 1.0 / 3.0_f32;
579 let g3 = 1.0 / 6.0_f32;
580
581 let s = (xin + yin + zin) * f3;
582 let i = (xin + s).floor() as i32;
583 let j = (yin + s).floor() as i32;
584 let k = (zin + s).floor() as i32;
585
586 let t = (i + j + k) as f32 * g3;
587 let x0 = xin - (i as f32 - t);
588 let y0 = yin - (j as f32 - t);
589 let z0 = zin - (k as f32 - t);
590
591 let (i1, j1, k1, i2, j2, k2);
593 if x0 >= y0 {
594 if y0 >= z0 { i1=1;j1=0;k1=0; i2=1;j2=1;k2=0; }
595 else if x0 >= z0 { i1=1;j1=0;k1=0; i2=1;j2=0;k2=1; }
596 else { i1=0;j1=0;k1=1; i2=1;j2=0;k2=1; }
597 } else {
598 if y0 < z0 { i1=0;j1=0;k1=1; i2=0;j2=1;k2=1; }
599 else if x0 < z0 { i1=0;j1=1;k1=0; i2=0;j2=1;k2=1; }
600 else { i1=0;j1=1;k1=0; i2=1;j2=1;k2=0; }
601 }
602
603 let x1 = x0 - i1 as f32 + g3;
604 let y1 = y0 - j1 as f32 + g3;
605 let z1 = z0 - k1 as f32 + g3;
606 let x2 = x0 - i2 as f32 + 2.0 * g3;
607 let y2 = y0 - j2 as f32 + 2.0 * g3;
608 let z2 = z0 - k2 as f32 + 2.0 * g3;
609 let x3 = x0 - 1.0 + 3.0 * g3;
610 let y3 = y0 - 1.0 + 3.0 * g3;
611 let z3 = z0 - 1.0 + 3.0 * g3;
612
613 let ii = (i & 255) as usize;
614 let jj = (j & 255) as usize;
615 let kk = (k & 255) as usize;
616
617 let gi0 = PERM[ii + PERM[jj + PERM[kk ] as usize] as usize] & 15;
618 let gi1 = PERM[ii + i1 as usize + PERM[jj + j1 as usize + PERM[(kk + k1 as usize) & 255] as usize] as usize] & 15;
619 let gi2 = PERM[ii + i2 as usize + PERM[jj + j2 as usize + PERM[(kk + k2 as usize) & 255] as usize] as usize] & 15;
620 let gi3 = PERM[(ii+1)&255 + PERM[(jj+1)&255 + PERM[(kk+1)&255] as usize] as usize] & 15;
621
622 let t0 = 0.6 - x0*x0 - y0*y0 - z0*z0;
623 let n0 = if t0 < 0.0 { 0.0 } else { t0*t0*t0*t0 * simplex_grad3(gi0, x0, y0, z0) };
624
625 let t1 = 0.6 - x1*x1 - y1*y1 - z1*z1;
626 let n1 = if t1 < 0.0 { 0.0 } else { t1*t1*t1*t1 * simplex_grad3(gi1, x1, y1, z1) };
627
628 let t2 = 0.6 - x2*x2 - y2*y2 - z2*z2;
629 let n2 = if t2 < 0.0 { 0.0 } else { t2*t2*t2*t2 * simplex_grad3(gi2, x2, y2, z2) };
630
631 let t3 = 0.6 - x3*x3 - y3*y3 - z3*z3;
632 let n3 = if t3 < 0.0 { 0.0 } else { t3*t3*t3*t3 * simplex_grad3(gi3, x3, y3, z3) };
633
634 32.0 * (n0 + n1 + n2 + n3)
635}
636
637pub fn worley_noise_2d(x: f32, y: f32) -> (f32, f32) {
641 let cx = x.floor() as i32;
642 let cy = y.floor() as i32;
643
644 let mut f1 = f32::MAX;
645 let mut f2 = f32::MAX;
646
647 for dx in -2..=2i32 {
648 for dy in -2..=2i32 {
649 let nx = cx + dx;
650 let ny = cy + dy;
651 let hash = worley_hash(nx, ny);
653 let fx = nx as f32 + ((hash & 0xFFFF) as f32 / 65535.0);
654 let fy = ny as f32 + (((hash >> 16) & 0xFFFF) as f32 / 65535.0);
655 let dist = ((fx - x) * (fx - x) + (fy - y) * (fy - y)).sqrt();
656 if dist < f1 { f2 = f1; f1 = dist; }
657 else if dist < f2 { f2 = dist; }
658 }
659 }
660 (f1, f2)
661}
662
663pub fn worley_noise_3d(x: f32, y: f32, z: f32) -> (f32, f32) {
664 let cx = x.floor() as i32;
665 let cy = y.floor() as i32;
666 let cz = z.floor() as i32;
667
668 let mut f1 = f32::MAX;
669 let mut f2 = f32::MAX;
670
671 for dx in -1..=1i32 {
672 for dy in -1..=1i32 {
673 for dz in -1..=1i32 {
674 let nx = cx + dx;
675 let ny = cy + dy;
676 let nz = cz + dz;
677 let h = worley_hash_3d(nx, ny, nz);
678 let fx = nx as f32 + ((h & 0x3FF) as f32 / 1023.0);
679 let fy = ny as f32 + (((h >> 10) & 0x3FF) as f32 / 1023.0);
680 let fz = nz as f32 + (((h >> 20) & 0x3FF) as f32 / 1023.0);
681 let dist = ((fx-x)*(fx-x) + (fy-y)*(fy-y) + (fz-z)*(fz-z)).sqrt();
682 if dist < f1 { f2 = f1; f1 = dist; }
683 else if dist < f2 { f2 = dist; }
684 }
685 }
686 }
687 (f1, f2)
688}
689
690#[inline]
691fn worley_hash(x: i32, y: i32) -> u32 {
692 let mut h = (x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337))) as u32;
693 h ^= h >> 16;
694 h = h.wrapping_mul(0x45d9f3b);
695 h ^= h >> 16;
696 h
697}
698
699#[inline]
700fn worley_hash_3d(x: i32, y: i32, z: i32) -> u32 {
701 let mut h = (x.wrapping_mul(1619)
702 .wrapping_add(y.wrapping_mul(31337))
703 .wrapping_add(z.wrapping_mul(1013))) as u32;
704 h ^= h >> 16;
705 h = h.wrapping_mul(0x45d9f3b);
706 h ^= h >> 16;
707 h = h.wrapping_mul(0xd7e7f3b);
708 h ^= h >> 16;
709 h
710}
711
712#[derive(Clone, Debug)]
715pub struct FbmParams {
716 pub octaves: usize,
717 pub frequency: f32,
718 pub lacunarity: f32,
719 pub gain: f32,
720 pub amplitude: f32,
721 pub offset: f32,
722 pub ridge: bool,
723}
724
725impl FbmParams {
726 pub fn default_terrain() -> Self {
727 FbmParams { octaves: 8, frequency: 1.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 1.0, ridge: false }
728 }
729 pub fn default_ridge() -> Self {
730 FbmParams { octaves: 6, frequency: 1.0, lacunarity: 2.2, gain: 0.6, amplitude: 1.0, offset: 1.0, ridge: true }
731 }
732 pub fn default_cloud() -> Self {
733 FbmParams { octaves: 5, frequency: 2.0, lacunarity: 2.0, gain: 0.45, amplitude: 0.8, offset: 0.0, ridge: false }
734 }
735}
736
737pub fn fbm_3d(x: f32, y: f32, z: f32, params: &FbmParams) -> f32 {
738 let mut freq = params.frequency;
739 let mut amp = params.amplitude;
740 let mut value = 0.0f32;
741 let mut weight = 1.0f32;
742 let mut prev = 1.0f32;
743
744 for i in 0..params.octaves {
745 let n = perlin_noise_3d(x * freq, y * freq, z * freq);
746
747 if params.ridge {
748 let ridged = (params.offset - n.abs()).abs();
749 let signal = ridged * ridged * weight;
750 weight = (signal * 2.0).clamp(0.0, 1.0);
751 value += signal * amp;
752 } else {
753 value += n * amp;
754 }
755
756 freq *= params.lacunarity;
757 amp *= params.gain;
758 }
759 value
760}
761
762pub fn fbm_2d(x: f32, y: f32, params: &FbmParams) -> f32 {
763 fbm_3d(x, y, 0.0, params)
764}
765
766pub fn turbulence_2d(x: f32, y: f32, octaves: usize, freq: f32, gain: f32, lacunarity: f32) -> f32 {
768 let mut f = freq;
769 let mut amp = 1.0f32;
770 let mut v = 0.0f32;
771 let mut max = 0.0f32;
772 for _ in 0..octaves {
773 v += perlin_noise_2d(x * f, y * f).abs() * amp;
774 max += amp;
775 f *= lacunarity;
776 amp *= gain;
777 }
778 if max > 0.0 { v / max } else { 0.0 }
779}
780
781pub fn domain_warp_fbm_2d(x: f32, y: f32, warp_strength: f32, params: &FbmParams) -> f32 {
783 let q_x = fbm_2d(x, y, params);
784 let q_y = fbm_2d(x + 5.2, y + 1.3, params);
785 let r_x = fbm_2d(x + warp_strength * q_x + 1.7, y + warp_strength * q_y + 9.2, params);
786 let r_y = fbm_2d(x + warp_strength * q_x + 8.3, y + warp_strength * q_y + 2.8, params);
787 fbm_2d(x + warp_strength * r_x, y + warp_strength * r_y, params)
788}
789
790#[derive(Clone, Debug)]
795pub struct Heightmap {
796 pub width: usize,
797 pub height: usize,
798 pub data: Vec<f32>, pub min_h: f32,
800 pub max_h: f32,
801}
802
803impl Heightmap {
804 pub fn new(width: usize, height: usize) -> Self {
805 Self {
806 width,
807 height,
808 data: vec![0.0; width * height],
809 min_h: 0.0,
810 max_h: 1.0,
811 }
812 }
813
814 #[inline]
815 pub fn index(&self, x: usize, y: usize) -> usize {
816 y * self.width + x
817 }
818
819 #[inline]
820 pub fn get(&self, x: usize, y: usize) -> f32 {
821 self.data[self.index(x, y)]
822 }
823
824 #[inline]
825 pub fn set(&mut self, x: usize, y: usize, v: f32) {
826 let idx = self.index(x, y);
827 self.data[idx] = v;
828 }
829
830 #[inline]
831 pub fn get_clamped(&self, x: i32, y: i32) -> f32 {
832 let cx = x.clamp(0, self.width as i32 - 1) as usize;
833 let cy = y.clamp(0, self.height as i32 - 1) as usize;
834 self.get(cx, cy)
835 }
836
837 pub fn sample_bilinear(&self, u: f32, v: f32) -> f32 {
838 let px = u * (self.width - 1) as f32;
839 let py = v * (self.height - 1) as f32;
840 let x0 = px.floor() as i32;
841 let y0 = py.floor() as i32;
842 let x1 = x0 + 1;
843 let y1 = y0 + 1;
844 let tx = px - x0 as f32;
845 let ty = py - y0 as f32;
846 let a = self.get_clamped(x0, y0);
847 let b = self.get_clamped(x1, y0);
848 let c = self.get_clamped(x0, y1);
849 let d = self.get_clamped(x1, y1);
850 lerp_f(lerp_f(a, b, tx), lerp_f(c, d, tx), ty)
851 }
852
853 pub fn normal_at(&self, x: usize, y: usize, cell_size: f32) -> Vec3 {
855 let xi = x as i32;
856 let yi = y as i32;
857 let hL = self.get_clamped(xi - 1, yi);
858 let hR = self.get_clamped(xi + 1, yi);
859 let hD = self.get_clamped(xi, yi - 1);
860 let hU = self.get_clamped(xi, yi + 1);
861 let dx = (hR - hL) / (2.0 * cell_size);
862 let dz = (hU - hD) / (2.0 * cell_size);
863 Vec3::new(-dx, 1.0, -dz).normalize()
864 }
865
866 pub fn slope_at(&self, x: usize, y: usize, cell_size: f32) -> f32 {
868 let n = self.normal_at(x, y, cell_size);
869 n.y.acos()
870 }
871
872 pub fn gradient_at(&self, x: usize, y: usize) -> Vec2 {
874 let xi = x as i32;
875 let yi = y as i32;
876 let dx = (self.get_clamped(xi + 1, yi) - self.get_clamped(xi - 1, yi)) * 0.5;
877 let dy = (self.get_clamped(xi, yi + 1) - self.get_clamped(xi, yi - 1)) * 0.5;
878 Vec2::new(dx, dy)
879 }
880
881 pub fn recompute_minmax(&mut self) {
882 self.min_h = self.data.iter().cloned().fold(f32::MAX, f32::min);
883 self.max_h = self.data.iter().cloned().fold(f32::MIN, f32::max);
884 }
885
886 pub fn normalize_to_01(&mut self) {
887 self.recompute_minmax();
888 let range = self.max_h - self.min_h;
889 if range < 1e-10 { return; }
890 for v in self.data.iter_mut() {
891 *v = (*v - self.min_h) / range;
892 }
893 self.min_h = 0.0;
894 self.max_h = 1.0;
895 }
896
897 pub fn generate_fbm(&mut self, params: &FbmParams, seed_offset: Vec2) {
899 for y in 0..self.height {
900 for x in 0..self.width {
901 let nx = x as f32 / self.width as f32 + seed_offset.x;
902 let ny = y as f32 / self.height as f32 + seed_offset.y;
903 let h = fbm_2d(nx, ny, params) * 0.5 + 0.5;
904 self.set(x, y, h.clamp(0.0, 1.0));
905 }
906 }
907 self.recompute_minmax();
908 }
909
910 pub fn generate_domain_warp(&mut self, params: &FbmParams, warp: f32, seed_offset: Vec2) {
912 for y in 0..self.height {
913 for x in 0..self.width {
914 let nx = x as f32 / self.width as f32 + seed_offset.x;
915 let ny = y as f32 / self.height as f32 + seed_offset.y;
916 let h = domain_warp_fbm_2d(nx, ny, warp, params) * 0.5 + 0.5;
917 self.set(x, y, h.clamp(0.0, 1.0));
918 }
919 }
920 self.recompute_minmax();
921 }
922}
923
924#[derive(Clone, Debug)]
927pub struct ErosionParams {
928 pub num_particles: usize,
929 pub inertia: f32, pub capacity: f32, pub deposition: f32, pub erosion_speed: f32, pub evaporation: f32, pub min_slope: f32, pub gravity: f32,
936 pub max_steps: usize,
937 pub erosion_radius: f32, pub seed: u64,
939}
940
941impl Default for ErosionParams {
942 fn default() -> Self {
943 ErosionParams {
944 num_particles: 50_000,
945 inertia: EROSION_INERTIA,
946 capacity: EROSION_CAPACITY,
947 deposition: EROSION_DEPOSITION,
948 erosion_speed: EROSION_EROSION_SPEED,
949 evaporation: EROSION_EVAPORATION,
950 min_slope: EROSION_MIN_SLOPE,
951 gravity: EROSION_GRAVITY,
952 max_steps: EROSION_MAX_STEPS,
953 erosion_radius: 3.0,
954 seed: 0xDEAD_BEEF_1234,
955 }
956 }
957}
958
959struct LcgRng { state: u64 }
960impl LcgRng {
961 fn new(seed: u64) -> Self { Self { state: seed ^ 0x123456789ABCDEF } }
962 fn next_u64(&mut self) -> u64 {
963 self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
964 self.state
965 }
966 fn next_f32(&mut self) -> f32 { (self.next_u64() >> 32) as f32 / u32::MAX as f32 }
967 fn next_f32_range(&mut self, min: f32, max: f32) -> f32 { min + self.next_f32() * (max - min) }
968}
969
970fn hmap_height_bilinear(data: &[f32], width: usize, height: usize, x: f32, y: f32) -> f32 {
972 let x0 = x.floor() as i32;
973 let y0 = y.floor() as i32;
974 let x1 = x0 + 1;
975 let y1 = y0 + 1;
976 let tx = x - x0 as f32;
977 let ty = y - y0 as f32;
978
979 let clamp_x = |v: i32| -> usize { v.clamp(0, width as i32 - 1) as usize };
980 let clamp_y = |v: i32| -> usize { v.clamp(0, height as i32 - 1) as usize };
981
982 let a = data[clamp_y(y0) * width + clamp_x(x0)];
983 let b = data[clamp_y(y0) * width + clamp_x(x1)];
984 let c = data[clamp_y(y1) * width + clamp_x(x0)];
985 let d = data[clamp_y(y1) * width + clamp_x(x1)];
986
987 lerp_f(lerp_f(a, b, tx), lerp_f(c, d, tx), ty)
988}
989
990fn hmap_gradient(data: &[f32], width: usize, height: usize, x: f32, y: f32) -> Vec2 {
992 let gx = hmap_height_bilinear(data, width, height, x + 0.5, y)
993 - hmap_height_bilinear(data, width, height, x - 0.5, y);
994 let gy = hmap_height_bilinear(data, width, height, x, y + 0.5)
995 - hmap_height_bilinear(data, width, height, x, y - 0.5);
996 Vec2::new(gx, gy)
997}
998
999pub fn hydraulic_erosion(hmap: &mut Heightmap, params: &ErosionParams) {
1001 let w = hmap.width;
1002 let h = hmap.height;
1003 let mut rng = LcgRng::new(params.seed);
1004
1005 let radius = params.erosion_radius;
1007 let brush_radius = radius.ceil() as i32;
1008 let mut brush_offsets: Vec<(i32, i32, f32)> = Vec::new();
1009 let mut brush_weight_sum = 0.0f32;
1010 for dy in -brush_radius..=brush_radius {
1011 for dx in -brush_radius..=brush_radius {
1012 let dist = ((dx*dx + dy*dy) as f32).sqrt();
1013 if dist <= radius {
1014 let w_val = 1.0 - dist / radius;
1015 brush_offsets.push((dx, dy, w_val));
1016 brush_weight_sum += w_val;
1017 }
1018 }
1019 }
1020 for b in brush_offsets.iter_mut() { b.2 /= brush_weight_sum; }
1022
1023 let data = &mut hmap.data;
1024
1025 for _particle in 0..params.num_particles {
1026 let mut pos_x = rng.next_f32_range(0.0, (w - 1) as f32);
1028 let mut pos_y = rng.next_f32_range(0.0, (h - 1) as f32);
1029 let mut vel_x = 0.0f32;
1030 let mut vel_y = 0.0f32;
1031 let mut speed = 0.0f32;
1032 let mut water = 1.0f32;
1033 let mut sediment = 0.0f32;
1034
1035 for _step in 0..params.max_steps {
1036 let node_x = pos_x.floor() as i32;
1037 let node_y = pos_y.floor() as i32;
1038
1039 if node_x < 0 || node_x >= w as i32 - 1 || node_y < 0 || node_y >= h as i32 - 1 {
1040 break;
1041 }
1042
1043 let grad = hmap_gradient(data, w, h, pos_x, pos_y);
1044 vel_x = vel_x * params.inertia - grad.x * (1.0 - params.inertia);
1046 vel_y = vel_y * params.inertia - grad.y * (1.0 - params.inertia);
1047
1048 let vel_len = (vel_x * vel_x + vel_y * vel_y).sqrt();
1049 if vel_len < 1e-6 {
1050 break; }
1052 vel_x /= vel_len;
1053 vel_y /= vel_len;
1054
1055 let new_x = pos_x + vel_x;
1056 let new_y = pos_y + vel_y;
1057
1058 let old_h = hmap_height_bilinear(data, w, h, pos_x, pos_y);
1060 let new_h = hmap_height_bilinear(data, w, h, new_x, new_y);
1061 let delta_h = new_h - old_h;
1062
1063 let slope = (-delta_h).max(params.min_slope);
1065 let carry_capacity = slope * vel_len * water * params.capacity;
1066
1067 if sediment > carry_capacity || delta_h > 0.0 {
1068 let amount = if delta_h > 0.0 {
1070 sediment.min(delta_h)
1071 } else {
1072 (sediment - carry_capacity) * params.deposition
1073 };
1074 sediment -= amount;
1075
1076 for &(bdx, bdy, bw) in &brush_offsets {
1078 let bx = node_x + bdx;
1079 let by = node_y + bdy;
1080 if bx >= 0 && bx < w as i32 && by >= 0 && by < h as i32 {
1081 let idx = by as usize * w + bx as usize;
1082 data[idx] += amount * bw;
1083 }
1084 }
1085 } else {
1086 let erode_amount = ((carry_capacity - sediment) * params.erosion_speed)
1088 .min(-delta_h);
1089 let erode_amount = erode_amount.max(0.0);
1090 sediment += erode_amount;
1091
1092 for &(bdx, bdy, bw) in &brush_offsets {
1093 let bx = node_x + bdx;
1094 let by = node_y + bdy;
1095 if bx >= 0 && bx < w as i32 && by >= 0 && by < h as i32 {
1096 let idx = by as usize * w + bx as usize;
1097 data[idx] -= erode_amount * bw;
1098 if data[idx] < 0.0 { data[idx] = 0.0; }
1099 }
1100 }
1101 }
1102
1103 speed = ((speed * speed + delta_h * params.gravity).max(0.0)).sqrt();
1104 water *= 1.0 - params.evaporation;
1105 pos_x = new_x;
1106 pos_y = new_y;
1107
1108 if water < 0.01 { break; }
1109 }
1110 }
1111
1112 hmap.recompute_minmax();
1113}
1114
1115pub fn thermal_erosion(hmap: &mut Heightmap, iterations: usize, talus_angle: f32) {
1117 let w = hmap.width;
1118 let h = hmap.height;
1119 let talus = talus_angle.tan(); for _iter in 0..iterations {
1122 let data_copy = hmap.data.clone();
1123 for y in 1..h-1 {
1124 for x in 1..w-1 {
1125 let center = data_copy[y * w + x];
1126 let neighbours = [
1127 (x+1, y), (x-1, y), (x, y+1), (x, y-1),
1128 (x+1, y+1), (x-1, y+1), (x+1, y-1), (x-1, y-1),
1129 ];
1130 let mut total_diff = 0.0f32;
1131 let mut max_diff = 0.0f32;
1132 let mut count = 0usize;
1133 for &(nx, ny) in &neighbours {
1134 let diff = center - data_copy[ny * w + nx];
1135 if diff > talus {
1136 total_diff += diff;
1137 if diff > max_diff { max_diff = diff; }
1138 count += 1;
1139 }
1140 }
1141 if count == 0 || total_diff < 1e-8 { continue; }
1142 let move_frac = 0.5 * (max_diff - talus) / total_diff;
1143 for &(nx, ny) in &neighbours {
1144 let diff = center - data_copy[ny * w + nx];
1145 if diff > talus {
1146 let transfer = move_frac * diff;
1147 hmap.data[y * w + x] -= transfer;
1148 hmap.data[ny * w + nx] += transfer;
1149 }
1150 }
1151 }
1152 }
1153 }
1154 hmap.recompute_minmax();
1155}
1156
1157#[derive(Clone, Debug)]
1162pub struct RiverPath {
1163 pub points: Vec<Vec2>, pub widths: Vec<f32>,
1165 pub depths: Vec<f32>,
1166 pub flow_rates: Vec<f32>,
1167 pub source: Vec2,
1168 pub mouth: Vec2,
1169 pub total_length: f32,
1170}
1171
1172impl RiverPath {
1173 pub fn new() -> Self {
1174 RiverPath {
1175 points: Vec::new(),
1176 widths: Vec::new(),
1177 depths: Vec::new(),
1178 flow_rates: Vec::new(),
1179 source: Vec2::ZERO,
1180 mouth: Vec2::ZERO,
1181 total_length: 0.0,
1182 }
1183 }
1184
1185 pub fn compute_total_length(&mut self) {
1186 let mut len = 0.0f32;
1187 for i in 1..self.points.len() {
1188 len += (self.points[i] - self.points[i-1]).length();
1189 }
1190 self.total_length = len;
1191 }
1192}
1193
1194pub fn simulate_river(hmap: &Heightmap, start: Vec2, min_height: f32) -> RiverPath {
1196 let mut path = RiverPath::new();
1197 path.source = start;
1198
1199 let mut pos = start;
1200 let mut flow = 1.0f32;
1201 let mut prev_dir = Vec2::ZERO;
1202
1203 path.points.push(pos);
1204 path.widths.push(0.5);
1205 path.depths.push(0.1);
1206 path.flow_rates.push(flow);
1207
1208 let w = hmap.width as f32;
1209 let h = hmap.height as f32;
1210
1211 let mut visited: HashSet<(i32, i32)> = HashSet::new();
1212
1213 for step in 0..16384usize {
1214 let ux = (pos.x / w).clamp(0.0, 1.0);
1215 let uy = (pos.y / h).clamp(0.0, 1.0);
1216 let current_h = hmap.sample_bilinear(ux, uy);
1217
1218 if current_h <= min_height { break; }
1219
1220 let step_size = 0.5f32;
1222 let mut best_dir = Vec2::ZERO;
1223 let mut best_drop = 0.0f32;
1224
1225 let angles: [f32; 16] = [
1226 0.0, PI/8.0, PI/4.0, 3.0*PI/8.0, PI/2.0, 5.0*PI/8.0, 3.0*PI/4.0, 7.0*PI/8.0,
1227 PI, 9.0*PI/8.0, 5.0*PI/4.0, 11.0*PI/8.0, 3.0*PI/2.0, 13.0*PI/8.0, 7.0*PI/4.0, 15.0*PI/8.0,
1228 ];
1229
1230 for &angle in &angles {
1231 let dir = Vec2::new(angle.cos(), angle.sin());
1232 let weighted_dir = if prev_dir.length() > 0.01 {
1234 (dir * 0.7 + prev_dir * 0.3).normalize()
1235 } else {
1236 dir
1237 };
1238 let npos = pos + weighted_dir * step_size;
1239 let nu = (npos.x / w).clamp(0.0, 1.0);
1240 let nv = (npos.y / h).clamp(0.0, 1.0);
1241 let nh = hmap.sample_bilinear(nu, nv);
1242 let drop = current_h - nh;
1243 if drop > best_drop {
1244 best_drop = drop;
1245 best_dir = weighted_dir;
1246 }
1247 }
1248
1249 if best_drop < 0.0001 && step > 10 {
1250 break;
1252 }
1253
1254 if best_dir.length() < 0.01 { break; }
1255 best_dir = best_dir.normalize();
1256
1257 pos = pos + best_dir * step_size;
1258 prev_dir = best_dir;
1259
1260 flow += 0.005 * best_drop;
1262 let width = (flow * 0.3).clamp(0.2, 20.0);
1263 let depth = (flow * 0.05).clamp(0.05, 5.0);
1264
1265 path.points.push(pos);
1266 path.widths.push(width);
1267 path.depths.push(depth);
1268 path.flow_rates.push(flow);
1269
1270 let cell = (pos.x as i32, pos.y as i32);
1271 if visited.contains(&cell) { break; } visited.insert(cell);
1273
1274 if pos.x < 0.5 || pos.y < 0.5 || pos.x > w - 0.5 || pos.y > h - 0.5 {
1276 break;
1277 }
1278 }
1279
1280 path.mouth = pos;
1281 path.compute_total_length();
1282 path
1283}
1284
1285#[derive(Clone, Debug)]
1287pub struct LakeBody {
1288 pub cells: Vec<(usize, usize)>,
1289 pub water_level: f32,
1290 pub surface_area: f32,
1291 pub volume: f32,
1292 pub centroid: Vec2,
1293}
1294
1295pub fn fill_lake(hmap: &Heightmap, seed_x: usize, seed_y: usize, max_water_level: f32) -> LakeBody {
1296 let w = hmap.width;
1297 let h = hmap.height;
1298 let mut visited = vec![false; w * h];
1299 let mut cells = Vec::new();
1300 let mut queue = VecDeque::new();
1301
1302 let seed_h = hmap.get(seed_x, seed_y);
1303 let water_level = seed_h.max(max_water_level);
1304
1305 queue.push_back((seed_x, seed_y));
1306 visited[seed_y * w + seed_x] = true;
1307
1308 while let Some((cx, cy)) = queue.pop_front() {
1309 let ch = hmap.get(cx, cy);
1310 if ch <= water_level {
1311 cells.push((cx, cy));
1312 let neighbours = [
1313 (cx.wrapping_sub(1), cy), (cx+1, cy),
1314 (cx, cy.wrapping_sub(1)), (cx, cy+1),
1315 ];
1316 for &(nx, ny) in &neighbours {
1317 if nx < w && ny < h && !visited[ny * w + nx] {
1318 visited[ny * w + nx] = true;
1319 queue.push_back((nx, ny));
1320 }
1321 }
1322 }
1323 }
1324
1325 let surface_area = cells.len() as f32;
1326 let mut cx_sum = 0.0f32;
1327 let mut cy_sum = 0.0f32;
1328 let mut volume = 0.0f32;
1329 for &(x, y) in &cells {
1330 cx_sum += x as f32;
1331 cy_sum += y as f32;
1332 volume += water_level - hmap.get(x, y);
1333 }
1334 let count = cells.len() as f32;
1335 let centroid = if count > 0.0 {
1336 Vec2::new(cx_sum / count, cy_sum / count)
1337 } else {
1338 Vec2::new(seed_x as f32, seed_y as f32)
1339 };
1340
1341 LakeBody { cells, water_level, surface_area, volume, centroid }
1342}
1343
1344#[derive(Clone, Debug)]
1346pub struct OceanShore {
1347 pub shore_cells: Vec<(usize, usize)>,
1348 pub sea_level: f32,
1349 pub beach_width: f32,
1350}
1351
1352pub fn generate_ocean_shore(hmap: &Heightmap, sea_level: f32, beach_width: f32) -> OceanShore {
1353 let w = hmap.width;
1354 let h = hmap.height;
1355 let mut shore_cells = Vec::new();
1356
1357 for y in 1..h-1 {
1358 for x in 1..w-1 {
1359 let ch = hmap.get(x, y);
1360 if ch <= sea_level { continue; } let neighbours = [(x+1,y),(x-1,y),(x,y+1),(x,y-1)];
1363 let has_water_nb = neighbours.iter().any(|&(nx, ny)| {
1364 nx < w && ny < h && hmap.get(nx, ny) <= sea_level
1365 });
1366 if has_water_nb {
1367 shore_cells.push((x, y));
1368 }
1369 }
1370 }
1371
1372 OceanShore { shore_cells, sea_level, beach_width }
1373}
1374
1375pub fn is_beach(shore: &OceanShore, hmap: &Heightmap, x: usize, y: usize) -> bool {
1377 let h_val = hmap.get(x, y);
1378 h_val > shore.sea_level && h_val < shore.sea_level + shore.beach_width
1379}
1380
1381#[derive(Clone, Debug)]
1386pub struct FoliageInstance {
1387 pub position: Vec3,
1388 pub rotation: Quat,
1389 pub scale: Vec3,
1390 pub asset_id: u32,
1391 pub biome_id: u8,
1392 pub lod_factor: f32,
1393}
1394
1395#[derive(Clone, Debug)]
1396pub struct FoliagePlacementParams {
1397 pub min_radius: f32, pub max_instances: usize,
1399 pub max_slope_rad: f32, pub min_altitude: f32, pub max_altitude: f32,
1402 pub density_scale: f32,
1403 pub use_density_map: bool,
1404 pub random_rotation: bool,
1405 pub scale_variance: f32,
1406 pub base_scale: Vec3,
1407 pub asset_id: u32,
1408 pub biome_id: u8,
1409 pub align_to_normal: bool,
1410}
1411
1412impl Default for FoliagePlacementParams {
1413 fn default() -> Self {
1414 FoliagePlacementParams {
1415 min_radius: 2.0,
1416 max_instances: 100_000,
1417 max_slope_rad: 0.7,
1418 min_altitude: 0.05,
1419 max_altitude: 0.75,
1420 density_scale: 1.0,
1421 use_density_map: false,
1422 random_rotation: true,
1423 scale_variance: 0.25,
1424 base_scale: Vec3::ONE,
1425 asset_id: 0,
1426 biome_id: 0,
1427 align_to_normal: false,
1428 }
1429 }
1430}
1431
1432pub fn poisson_disk_2d(
1434 width: f32,
1435 height: f32,
1436 min_dist: f32,
1437 max_attempts: usize,
1438 seed: u64,
1439) -> Vec<Vec2> {
1440 let cell_size = min_dist / (2.0_f32).sqrt();
1441 let grid_w = (width / cell_size).ceil() as usize + 1;
1442 let grid_h = (height / cell_size).ceil() as usize + 1;
1443
1444 let mut grid: Vec<Option<Vec2>> = vec![None; grid_w * grid_h];
1445 let mut active_list: Vec<Vec2> = Vec::new();
1446 let mut samples: Vec<Vec2> = Vec::new();
1447 let mut rng = LcgRng::new(seed ^ 0xF00D);
1448
1449 let grid_idx = |p: Vec2| -> usize {
1450 let gx = (p.x / cell_size) as usize;
1451 let gy = (p.y / cell_size) as usize;
1452 gy * grid_w + gx
1453 };
1454
1455 let first = Vec2::new(
1457 rng.next_f32() * width,
1458 rng.next_f32() * height,
1459 );
1460 active_list.push(first);
1461 samples.push(first);
1462 grid[grid_idx(first)] = Some(first);
1463
1464 while !active_list.is_empty() {
1465 let rand_idx = (rng.next_f32() * active_list.len() as f32) as usize;
1466 let rand_idx = rand_idx.min(active_list.len() - 1);
1467 let base = active_list[rand_idx];
1468
1469 let mut found = false;
1470 for _ in 0..max_attempts {
1471 let angle = rng.next_f32() * TWO_PI;
1473 let rad = min_dist + rng.next_f32() * min_dist;
1474 let candidate = Vec2::new(
1475 base.x + angle.cos() * rad,
1476 base.y + angle.sin() * rad,
1477 );
1478
1479 if candidate.x < 0.0 || candidate.x >= width
1480 || candidate.y < 0.0 || candidate.y >= height {
1481 continue;
1482 }
1483
1484 let gx0 = ((candidate.x - min_dist) / cell_size).floor() as i32;
1486 let gy0 = ((candidate.y - min_dist) / cell_size).floor() as i32;
1487 let gx1 = ((candidate.x + min_dist) / cell_size).ceil() as i32;
1488 let gy1 = ((candidate.y + min_dist) / cell_size).ceil() as i32;
1489
1490 let gx0u = gx0.max(0) as usize;
1491 let gy0u = gy0.max(0) as usize;
1492 let gx1u = (gx1 as usize).min(grid_w - 1);
1493 let gy1u = (gy1 as usize).min(grid_h - 1);
1494
1495 let mut ok = true;
1496 'outer: for gy in gy0u..=gy1u {
1497 for gx in gx0u..=gx1u {
1498 if let Some(p) = grid[gy * grid_w + gx] {
1499 if (p - candidate).length() < min_dist {
1500 ok = false;
1501 break 'outer;
1502 }
1503 }
1504 }
1505 }
1506
1507 if ok {
1508 active_list.push(candidate);
1509 samples.push(candidate);
1510 grid[grid_idx(candidate)] = Some(candidate);
1511 found = true;
1512 break;
1513 }
1514 }
1515
1516 if !found {
1517 active_list.swap_remove(rand_idx);
1518 }
1519 }
1520
1521 samples
1522}
1523
1524pub fn place_foliage(
1525 hmap: &Heightmap,
1526 density_map: Option<&Vec<f32>>,
1527 params: &FoliagePlacementParams,
1528 seed: u64,
1529 cell_size: f32,
1530) -> Vec<FoliageInstance> {
1531 let w = hmap.width as f32;
1532 let h = hmap.height as f32;
1533
1534 let candidates = poisson_disk_2d(w, h, params.min_radius, 30, seed);
1535 let mut rng = LcgRng::new(seed ^ 0xFACE);
1536 let mut result = Vec::new();
1537
1538 for pos2d in &candidates {
1539 if result.len() >= params.max_instances { break; }
1540
1541 let ux = (pos2d.x / w).clamp(0.0, 1.0);
1542 let uy = (pos2d.y / h).clamp(0.0, 1.0);
1543 let altitude = hmap.sample_bilinear(ux, uy);
1544
1545 if altitude < params.min_altitude || altitude > params.max_altitude { continue; }
1546
1547 let xi = pos2d.x as usize;
1548 let yi = pos2d.y as usize;
1549 let slope = if xi < hmap.width && yi < hmap.height {
1550 hmap.slope_at(xi.min(hmap.width-1), yi.min(hmap.height-1), cell_size)
1551 } else { 0.0 };
1552
1553 if slope > params.max_slope_rad { continue; }
1554
1555 if params.use_density_map {
1557 if let Some(dmap) = density_map {
1558 let di = (uy * (hmap.height - 1) as f32) as usize * hmap.width
1559 + (ux * (hmap.width - 1) as f32) as usize;
1560 let di = di.min(dmap.len() - 1);
1561 let density = dmap[di] * params.density_scale;
1562 if rng.next_f32() > density { continue; }
1563 }
1564 }
1565
1566 let rotation = if params.align_to_normal {
1568 let xi_c = xi.min(hmap.width - 1);
1569 let yi_c = yi.min(hmap.height - 1);
1570 let normal = hmap.normal_at(xi_c, yi_c, cell_size);
1571 let up = Vec3::Y;
1572 let axis = up.cross(normal);
1573 let angle = up.dot(normal).acos();
1574 if axis.length() > 1e-6 {
1575 Quat::from_axis_angle(axis.normalize(), angle)
1576 } else {
1577 Quat::IDENTITY
1578 }
1579 } else if params.random_rotation {
1580 let angle = rng.next_f32() * TWO_PI;
1581 Quat::from_rotation_y(angle)
1582 } else {
1583 Quat::IDENTITY
1584 };
1585
1586 let sv = 1.0 + (rng.next_f32() * 2.0 - 1.0) * params.scale_variance;
1588 let scale = params.base_scale * sv;
1589
1590 let world_y = altitude * hmap.max_h;
1591 let position = Vec3::new(pos2d.x * cell_size, world_y, pos2d.y * cell_size);
1592
1593 result.push(FoliageInstance {
1594 position,
1595 rotation,
1596 scale,
1597 asset_id: params.asset_id,
1598 biome_id: params.biome_id,
1599 lod_factor: 1.0,
1600 });
1601 }
1602
1603 result
1604}
1605
1606#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1611pub struct GridNode {
1612 pub x: i32,
1613 pub y: i32,
1614}
1615
1616impl GridNode {
1617 pub fn new(x: i32, y: i32) -> Self { GridNode { x, y } }
1618 pub fn to_vec2(&self, cell_size: f32) -> Vec2 {
1619 Vec2::new(self.x as f32 * cell_size, self.y as f32 * cell_size)
1620 }
1621}
1622
1623#[derive(Clone, Debug)]
1624pub struct AStarNode {
1625 pub pos: GridNode,
1626 pub g_cost: f32,
1627 pub h_cost: f32,
1628 pub parent: Option<GridNode>,
1629}
1630
1631impl AStarNode {
1632 pub fn f_cost(&self) -> f32 { self.g_cost + self.h_cost }
1633}
1634
1635fn astar_heuristic(a: &GridNode, b: &GridNode) -> f32 {
1636 let dx = (a.x - b.x).abs() as f32;
1638 let dy = (a.y - b.y).abs() as f32;
1639 let (min_d, max_d) = if dx < dy { (dx, dy) } else { (dy, dx) };
1640 max_d + (1.41421356 - 1.0) * min_d
1641}
1642
1643pub struct RoadCostParams {
1644 pub slope_weight: f32,
1645 pub height_weight: f32,
1646 pub water_penalty: f32,
1647 pub sea_level: f32,
1648}
1649
1650impl Default for RoadCostParams {
1651 fn default() -> Self {
1652 RoadCostParams { slope_weight: 5.0, height_weight: 2.0, water_penalty: 100.0, sea_level: 0.1 }
1653 }
1654}
1655
1656fn road_move_cost(hmap: &Heightmap, from: &GridNode, to: &GridNode, cost_params: &RoadCostParams) -> f32 {
1657 let w = hmap.width as i32;
1658 let h = hmap.height as i32;
1659 if to.x < 0 || to.y < 0 || to.x >= w || to.y >= h { return f32::MAX; }
1660
1661 let diagonal = from.x != to.x && from.y != to.y;
1662 let base_cost = if diagonal { 1.41421356 } else { 1.0 };
1663
1664 let h_from = hmap.get_clamped(from.x, from.y);
1665 let h_to = hmap.get_clamped(to.x, to.y);
1666
1667 if h_to <= cost_params.sea_level { return base_cost + cost_params.water_penalty; }
1669
1670 let slope = (h_to - h_from).abs();
1671 let cost = base_cost
1672 + slope * cost_params.slope_weight
1673 + (h_to - 0.3).abs() * cost_params.height_weight;
1674 cost
1675}
1676
1677pub fn astar_path(
1679 hmap: &Heightmap,
1680 start: GridNode,
1681 goal: GridNode,
1682 cost_params: &RoadCostParams,
1683) -> Option<Vec<GridNode>> {
1684 use std::collections::BinaryHeap;
1685 use std::cmp::Ordering;
1686
1687 #[derive(Clone)]
1688 struct Entry { cost: f32, node: GridNode }
1689 impl PartialEq for Entry { fn eq(&self, o: &Self) -> bool { self.cost == o.cost } }
1690 impl Eq for Entry {}
1691 impl PartialOrd for Entry {
1692 fn partial_cmp(&self, o: &Self) -> Option<Ordering> { Some(self.cmp(o)) }
1693 }
1694 impl Ord for Entry {
1695 fn cmp(&self, o: &Self) -> Ordering {
1696 o.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal)
1697 }
1698 }
1699
1700 let mut open_heap: BinaryHeap<Entry> = BinaryHeap::new();
1701 let mut g_score: HashMap<(i32,i32), f32> = HashMap::new();
1702 let mut came_from: HashMap<(i32,i32), GridNode> = HashMap::new();
1703 let mut closed_set: HashSet<(i32,i32)> = HashSet::new();
1704
1705 let start_key = (start.x, start.y);
1706 g_score.insert(start_key, 0.0);
1707 open_heap.push(Entry { cost: astar_heuristic(&start, &goal), node: start.clone() });
1708
1709 let directions: [(i32,i32); 8] = [
1710 (1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)
1711 ];
1712
1713 let mut iterations = 0usize;
1714 const MAX_ITER: usize = 200_000;
1715
1716 while let Some(Entry { node: current, .. }) = open_heap.pop() {
1717 iterations += 1;
1718 if iterations > MAX_ITER { return None; }
1719
1720 let cur_key = (current.x, current.y);
1721
1722 if current.x == goal.x && current.y == goal.y {
1723 let mut path = vec![current.clone()];
1725 let mut cur = cur_key;
1726 while let Some(parent) = came_from.get(&cur) {
1727 path.push(parent.clone());
1728 cur = (parent.x, parent.y);
1729 }
1730 path.reverse();
1731 return Some(path);
1732 }
1733
1734 if closed_set.contains(&cur_key) { continue; }
1735 closed_set.insert(cur_key);
1736
1737 let cur_g = *g_score.get(&cur_key).unwrap_or(&f32::MAX);
1738
1739 for &(dx, dy) in &directions {
1740 let nb = GridNode::new(current.x + dx, current.y + dy);
1741 let nb_key = (nb.x, nb.y);
1742 if closed_set.contains(&nb_key) { continue; }
1743
1744 let move_c = road_move_cost(hmap, ¤t, &nb, cost_params);
1745 if move_c >= f32::MAX * 0.5 { continue; }
1746
1747 let tentative_g = cur_g + move_c;
1748 let old_g = *g_score.get(&nb_key).unwrap_or(&f32::MAX);
1749 if tentative_g < old_g {
1750 g_score.insert(nb_key, tentative_g);
1751 came_from.insert(nb_key, current.clone());
1752 let f = tentative_g + astar_heuristic(&nb, &goal);
1753 open_heap.push(Entry { cost: f, node: nb });
1754 }
1755 }
1756 }
1757 None
1758}
1759
1760pub fn catmull_rom(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
1762 let t2 = t * t;
1763 let t3 = t2 * t;
1764 let q = 0.5 * (
1766 (p1 * 2.0)
1767 + (-p0 + p2) * t
1768 + (p0 * 2.0 - p1 * 5.0 + p2 * 4.0 - p3) * t2
1769 + (-p0 + p1 * 3.0 - p2 * 3.0 + p3) * t3
1770 );
1771 q
1772}
1773
1774pub fn smooth_road_path(
1776 raw_nodes: &[GridNode],
1777 cell_size: f32,
1778 samples_per_segment: usize,
1779) -> Vec<Vec2> {
1780 if raw_nodes.len() < 2 { return Vec::new(); }
1781 let pts: Vec<Vec2> = raw_nodes.iter().map(|n| n.to_vec2(cell_size)).collect();
1782 let n = pts.len();
1783 let mut result = Vec::with_capacity(n * samples_per_segment);
1784
1785 for i in 0..n - 1 {
1786 let p0 = if i == 0 { pts[0] + (pts[0] - pts[1]) } else { pts[i-1] };
1787 let p1 = pts[i];
1788 let p2 = pts[i+1];
1789 let p3 = if i+2 >= n { pts[n-1] + (pts[n-1] - pts[n-2]) } else { pts[i+2] };
1790
1791 for s in 0..samples_per_segment {
1792 let t = s as f32 / samples_per_segment as f32;
1793 result.push(catmull_rom(p0, p1, p2, p3, t));
1794 }
1795 }
1796 result.push(*pts.last().unwrap());
1797 result
1798}
1799
1800#[derive(Clone, Debug)]
1801pub struct RoadSegment {
1802 pub id: u32,
1803 pub control_pts: Vec<Vec2>,
1804 pub smoothed_pts: Vec<Vec2>,
1805 pub width: f32,
1806 pub road_type: RoadType,
1807 pub start_node: u32,
1808 pub end_node: u32,
1809 pub length: f32,
1810}
1811
1812#[derive(Clone, Debug, PartialEq, Eq)]
1813pub enum RoadType {
1814 Dirt,
1815 Gravel,
1816 Paved,
1817 Highway,
1818 Trail,
1819}
1820
1821impl RoadSegment {
1822 pub fn compute_length(&mut self) {
1823 let mut len = 0.0f32;
1824 for i in 1..self.smoothed_pts.len() {
1825 len += (self.smoothed_pts[i] - self.smoothed_pts[i-1]).length();
1826 }
1827 self.length = len;
1828 }
1829}
1830
1831#[derive(Clone, Debug)]
1832pub struct RoadNetwork {
1833 pub segments: Vec<RoadSegment>,
1834 pub nodes: HashMap<u32, Vec2>,
1835 pub next_node_id: u32,
1836 pub next_seg_id: u32,
1837}
1838
1839impl RoadNetwork {
1840 pub fn new() -> Self {
1841 RoadNetwork { segments: Vec::new(), nodes: HashMap::new(), next_node_id: 0, next_seg_id: 0 }
1842 }
1843
1844 pub fn add_node(&mut self, pos: Vec2) -> u32 {
1845 let id = self.next_node_id;
1846 self.nodes.insert(id, pos);
1847 self.next_node_id += 1;
1848 id
1849 }
1850
1851 pub fn build_road(
1852 &mut self,
1853 hmap: &Heightmap,
1854 start_world: Vec2,
1855 end_world: Vec2,
1856 cell_size: f32,
1857 road_type: RoadType,
1858 cost_params: &RoadCostParams,
1859 ) -> Option<u32> {
1860 let start_node = GridNode::new(
1861 (start_world.x / cell_size) as i32,
1862 (start_world.y / cell_size) as i32,
1863 );
1864 let end_node = GridNode::new(
1865 (end_world.x / cell_size) as i32,
1866 (end_world.y / cell_size) as i32,
1867 );
1868
1869 let path = astar_path(hmap, start_node, end_node, cost_params)?;
1870 let smoothed = smooth_road_path(&path, cell_size, 8);
1871 let control_pts: Vec<Vec2> = path.iter().map(|n| n.to_vec2(cell_size)).collect();
1872
1873 let width = match road_type {
1874 RoadType::Dirt => 3.0,
1875 RoadType::Gravel => 4.5,
1876 RoadType::Paved => 6.0,
1877 RoadType::Highway => 12.0,
1878 RoadType::Trail => 1.5,
1879 };
1880
1881 let sid = self.next_seg_id;
1882 self.next_seg_id += 1;
1883
1884 let start_nid = self.add_node(start_world);
1885 let end_nid = self.add_node(end_world);
1886
1887 let mut seg = RoadSegment {
1888 id: sid,
1889 control_pts,
1890 smoothed_pts: smoothed,
1891 width,
1892 road_type,
1893 start_node: start_nid,
1894 end_node: end_nid,
1895 length: 0.0,
1896 };
1897 seg.compute_length();
1898 self.segments.push(seg);
1899 Some(sid)
1900 }
1901}
1902
1903#[derive(Clone, Debug)]
1908pub struct AtmosphereParams {
1909 pub rayleigh_scale_height: f64,
1910 pub mie_scale_height: f64,
1911 pub rayleigh_coeff: [f64; 3], pub mie_coeff: f64,
1913 pub mie_asymmetry: f64, pub sun_intensity: f64,
1915 pub num_view_samples: usize,
1916 pub num_light_samples: usize,
1917 pub planet_radius: f64, pub atmo_radius: f64, }
1920
1921impl Default for AtmosphereParams {
1922 fn default() -> Self {
1923 AtmosphereParams {
1924 rayleigh_scale_height: RAYLEIGH_SCALE_HEIGHT,
1925 mie_scale_height: MIE_SCALE_HEIGHT,
1926 rayleigh_coeff: [RAYLEIGH_R, RAYLEIGH_G, RAYLEIGH_B],
1927 mie_coeff: MIE_COEFF,
1928 mie_asymmetry: MIE_G,
1929 sun_intensity: 20.0,
1930 num_view_samples: 16,
1931 num_light_samples: 8,
1932 planet_radius: EARTH_RADIUS,
1933 atmo_radius: ATMO_RADIUS,
1934 }
1935 }
1936}
1937
1938fn ray_sphere_intersection(
1939 ray_origin: [f64; 3],
1940 ray_dir: [f64; 3],
1941 sphere_radius: f64,
1942) -> Option<(f64, f64)> {
1943 let a = dot3(ray_dir, ray_dir);
1944 let b = 2.0 * dot3(ray_origin, ray_dir);
1945 let c = dot3(ray_origin, ray_origin) - sphere_radius * sphere_radius;
1946 let disc = b * b - 4.0 * a * c;
1947 if disc < 0.0 { return None; }
1948 let sqrt_disc = disc.sqrt();
1949 let t0 = (-b - sqrt_disc) / (2.0 * a);
1950 let t1 = (-b + sqrt_disc) / (2.0 * a);
1951 Some((t0, t1))
1952}
1953
1954#[inline]
1955fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
1956 a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
1957}
1958
1959#[inline]
1960fn normalize3(v: [f64; 3]) -> [f64; 3] {
1961 let len = (v[0]*v[0] + v[1]*v[1] + v[2]*v[2]).sqrt();
1962 if len < 1e-15 { return [0.0, 1.0, 0.0]; }
1963 [v[0]/len, v[1]/len, v[2]/len]
1964}
1965
1966fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { [a[0]+b[0], a[1]+b[1], a[2]+b[2]] }
1967fn scale3(a: [f64; 3], s: f64) -> [f64; 3] { [a[0]*s, a[1]*s, a[2]*s] }
1968fn mul3_elem(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { [a[0]*b[0], a[1]*b[1], a[2]*b[2]] }
1969fn exp3(a: [f64; 3]) -> [f64; 3] { [a[0].exp(), a[1].exp(), a[2].exp()] }
1970fn neg3(a: [f64; 3]) -> [f64; 3] { [-a[0], -a[1], -a[2]] }
1971
1972fn phase_rayleigh(cos_theta: f64) -> f64 {
1974 (3.0 / (16.0 * std::f64::consts::PI)) * (1.0 + cos_theta * cos_theta)
1975}
1976
1977fn phase_mie(cos_theta: f64, g: f64) -> f64 {
1979 let g2 = g * g;
1980 (1.0 - g2) / (4.0 * std::f64::consts::PI * (1.0 + g2 - 2.0 * g * cos_theta).powf(1.5))
1981}
1982
1983pub fn compute_sky_color(
1986 view_dir: Vec3,
1987 sun_dir: Vec3,
1988 params: &AtmosphereParams,
1989) -> Vec3 {
1990 let planet_r = params.planet_radius;
1991 let atmo_r = params.atmo_radius;
1992
1993 let camera_pos = [0.0f64, planet_r + 0.1, 0.0f64]; let vd = [view_dir.x as f64, view_dir.y as f64, view_dir.z as f64];
1996 let vd = normalize3(vd);
1997 let sd = [sun_dir.x as f64, sun_dir.y as f64, sun_dir.z as f64];
1998 let sd = normalize3(sd);
1999
2000 let (_, t_max_opt) = match ray_sphere_intersection(camera_pos, vd, atmo_r) {
2002 Some(v) => v,
2003 None => return Vec3::ZERO,
2004 };
2005 let t_max_opt = t_max_opt.max(0.0);
2006
2007 let t_max = if let Some((t0, _)) = ray_sphere_intersection(camera_pos, vd, planet_r + 0.001) {
2009 if t0 > 0.0 { t0 } else { t_max_opt }
2010 } else {
2011 t_max_opt
2012 };
2013
2014 let cos_theta = dot3(vd, sd);
2015 let phase_r = phase_rayleigh(cos_theta);
2016 let phase_m = phase_mie(cos_theta, params.mie_asymmetry);
2017
2018 let mut total_rayleigh = [0.0f64; 3];
2019 let mut total_mie = [0.0f64; 3];
2020
2021 let ns = params.num_view_samples;
2022 let seg_len = t_max / ns as f64;
2023
2024 let mut optical_depth_r = 0.0f64;
2025 let mut optical_depth_m = 0.0f64;
2026
2027 for i in 0..ns {
2028 let t_mid = (i as f64 + 0.5) * seg_len;
2029 let sample_pos = add3(camera_pos, scale3(vd, t_mid));
2030 let sample_r = (dot3(sample_pos, sample_pos)).sqrt();
2031 let height = (sample_r - planet_r).max(0.0);
2032
2033 let hr = (-(height / params.rayleigh_scale_height)).exp();
2034 let hm = (-(height / params.mie_scale_height)).exp();
2035
2036 optical_depth_r += hr * seg_len;
2037 optical_depth_m += hm * seg_len;
2038
2039 let (_, t_light) = match ray_sphere_intersection(sample_pos, sd, atmo_r) {
2041 Some(v) => v,
2042 None => continue,
2043 };
2044 let t_light = t_light.max(0.0);
2045 let nl = params.num_light_samples;
2046 let light_seg = t_light / nl as f64;
2047 let mut od_lr = 0.0f64;
2048 let mut od_lm = 0.0f64;
2049 let mut above_planet = true;
2050 for j in 0..nl {
2051 let lt = (j as f64 + 0.5) * light_seg;
2052 let lpos = add3(sample_pos, scale3(sd, lt));
2053 let lr = (dot3(lpos, lpos)).sqrt();
2054 if lr < planet_r { above_planet = false; break; }
2055 let lh = (lr - planet_r).max(0.0);
2056 od_lr += (-(lh / params.rayleigh_scale_height)).exp() * light_seg;
2057 od_lm += (-(lh / params.mie_scale_height)).exp() * light_seg;
2058 }
2059 if !above_planet { continue; }
2060
2061 let tau_r = [
2062 params.rayleigh_coeff[0] * (optical_depth_r + od_lr),
2063 params.rayleigh_coeff[1] * (optical_depth_r + od_lr),
2064 params.rayleigh_coeff[2] * (optical_depth_r + od_lr),
2065 ];
2066 let tau_m_val = 1.1 * params.mie_coeff * (optical_depth_m + od_lm);
2067 let tau_m = [tau_m_val; 3];
2068
2069 let attenuation_r = exp3(neg3(tau_r));
2070 let attenuation_m = exp3(neg3(tau_m));
2071
2072 let contrib_r = scale3(attenuation_r, hr * seg_len);
2073 let contrib_m = scale3(attenuation_m, hm * seg_len);
2074
2075 for k in 0..3 {
2076 total_rayleigh[k] += contrib_r[k] * params.rayleigh_coeff[k];
2077 total_mie[k] += contrib_m[k] * params.mie_coeff;
2078 }
2079 }
2080
2081 let sun_intensity = params.sun_intensity;
2082 let color = [
2083 sun_intensity * (phase_r * total_rayleigh[0] + phase_m * total_mie[0]),
2084 sun_intensity * (phase_r * total_rayleigh[1] + phase_m * total_mie[1]),
2085 sun_intensity * (phase_r * total_rayleigh[2] + phase_m * total_mie[2]),
2086 ];
2087
2088 Vec3::new(color[0] as f32, color[1] as f32, color[2] as f32)
2089}
2090
2091pub fn sun_disk_color(view_dir: Vec3, sun_dir: Vec3, disk_size: f32, sun_color: Vec3) -> Vec3 {
2093 let cos_angle = view_dir.dot(sun_dir).clamp(-1.0, 1.0);
2094 let angle = cos_angle.acos();
2095 if angle < disk_size {
2096 let t = (angle / disk_size).clamp(0.0, 1.0);
2098 let limb = 1.0 - 0.6 * t.sqrt(); sun_color * limb
2100 } else {
2101 Vec3::ZERO
2102 }
2103}
2104
2105pub fn aces_tonemap(color: Vec3) -> Vec3 {
2107 let a = 2.51f32;
2108 let b = 0.03f32;
2109 let c = 2.43f32;
2110 let d = 0.59f32;
2111 let e = 0.14f32;
2112 let result = (color * (color * a + Vec3::splat(b)))
2113 / (color * (color * c + Vec3::splat(d)) + Vec3::splat(e));
2114 result.clamp(Vec3::ZERO, Vec3::ONE)
2115}
2116
2117pub fn reinhard_tonemap(color: Vec3) -> Vec3 {
2119 color / (Vec3::ONE + color)
2120}
2121
2122pub fn solar_declination(day_of_year: f64) -> f64 {
2129 let b = 360.0 / 365.0 * (day_of_year - 81.0);
2131 let b_rad = b * std::f64::consts::PI / 180.0;
2132 SOLAR_OBLIQUITY * b_rad.sin()
2133}
2134
2135pub fn equation_of_time(day_of_year: f64) -> f64 {
2137 let b = 360.0 / 365.0 * (day_of_year - 81.0);
2138 let b_rad = b * std::f64::consts::PI / 180.0;
2139 9.87 * (2.0 * b_rad).sin() - 7.53 * b_rad.cos() - 1.5 * b_rad.sin()
2140}
2141
2142pub fn hour_angle(longitude_deg: f64, solar_time_hours: f64) -> f64 {
2144 15.0 * (solar_time_hours - 12.0)
2145}
2146
2147pub fn solar_position(
2150 latitude_deg: f64,
2151 longitude_deg: f64,
2152 day_of_year: f64,
2153 utc_hour: f64,
2154) -> (f64, f64) {
2155 let decl = solar_declination(day_of_year) * std::f64::consts::PI / 180.0;
2156 let lat = latitude_deg * std::f64::consts::PI / 180.0;
2157 let eot = equation_of_time(day_of_year);
2158 let solar_time = utc_hour + longitude_deg / 15.0 + eot / 60.0;
2159 let ha = hour_angle(longitude_deg, solar_time) * std::f64::consts::PI / 180.0;
2160
2161 let sin_alt = decl.sin() * lat.sin() + decl.cos() * lat.cos() * ha.cos();
2162 let altitude = sin_alt.asin() * 180.0 / std::f64::consts::PI;
2163
2164 let cos_az = (decl.sin() * lat.cos() - decl.cos() * lat.sin() * ha.cos())
2165 / (1.0 - sin_alt * sin_alt).sqrt().max(1e-10);
2166 let azimuth_rad = cos_az.acos();
2167 let azimuth = if ha > 0.0 { 360.0 - azimuth_rad * 180.0 / std::f64::consts::PI }
2168 else { azimuth_rad * 180.0 / std::f64::consts::PI };
2169
2170 (altitude, azimuth)
2171}
2172
2173pub fn sunrise_sunset(
2176 latitude_deg: f64,
2177 longitude_deg: f64,
2178 day_of_year: f64,
2179) -> Option<(f64, f64)> {
2180 let decl = solar_declination(day_of_year) * std::f64::consts::PI / 180.0;
2181 let lat = latitude_deg * std::f64::consts::PI / 180.0;
2182 let eot = equation_of_time(day_of_year);
2183
2184 let h_arg = (-0.01454 - decl.sin() * lat.sin()) / (decl.cos() * lat.cos());
2186 if h_arg.abs() > 1.0 { return None; } let ha0_deg = h_arg.acos() * 180.0 / std::f64::consts::PI;
2188
2189 let solar_noon_utc = 12.0 - longitude_deg / 15.0 - eot / 60.0;
2190 let half_day = ha0_deg / 15.0;
2191 Some((solar_noon_utc - half_day, solar_noon_utc + half_day))
2192}
2193
2194pub fn solar_direction_vec(altitude_deg: f64, azimuth_deg: f64) -> Vec3 {
2196 let alt = (altitude_deg as f32) * DEG2RAD;
2197 let az = (azimuth_deg as f32) * DEG2RAD;
2198 let cos_alt = alt.cos();
2199 Vec3::new(cos_alt * az.sin(), alt.sin(), cos_alt * az.cos())
2200}
2201
2202#[derive(Clone, Debug)]
2203pub struct SolarState {
2204 pub altitude_deg: f64,
2205 pub azimuth_deg: f64,
2206 pub direction: Vec3,
2207 pub is_day: bool,
2208 pub sun_color: Vec3,
2209 pub sun_intensity: f32,
2210 pub sky_color: Vec3,
2211 pub ambient_color: Vec3,
2212}
2213
2214impl SolarState {
2215 pub fn compute(
2216 latitude: f64,
2217 longitude: f64,
2218 doy: f64,
2219 utc_hour: f64,
2220 atmo: &AtmosphereParams,
2221 ) -> Self {
2222 let (alt, az) = solar_position(latitude, longitude, doy, utc_hour);
2223 let dir = solar_direction_vec(alt, az);
2224 let is_day = alt > -0.833;
2225
2226 let elevation_factor = (alt as f32 / 90.0 + 0.1).clamp(0.0, 1.0);
2228 let sun_color = Vec3::new(
2229 1.0,
2230 0.8 + 0.2 * elevation_factor,
2231 0.5 + 0.5 * elevation_factor,
2232 );
2233 let sun_intensity = if is_day {
2234 ((alt as f32 * DEG2RAD).sin().max(0.0)).sqrt() * 100.0
2235 } else {
2236 0.0
2237 };
2238
2239 let sky_color = if is_day {
2240 let sky = compute_sky_color(dir * -1.0, dir, atmo); let view_up = Vec3::Y;
2242 let raw = compute_sky_color(view_up, dir, atmo);
2243 aces_tonemap(raw)
2244 } else {
2245 Vec3::new(0.005, 0.005, 0.02)
2246 };
2247
2248 let ambient_color = sky_color * 0.3 + Vec3::new(0.02, 0.02, 0.04);
2249
2250 SolarState { altitude_deg: alt, azimuth_deg: az, direction: dir, is_day, sun_color, sun_intensity, sky_color, ambient_color }
2251 }
2252}
2253
2254#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2259pub enum WeatherState {
2260 Clear = 0,
2261 Cloudy = 1,
2262 Rain = 2,
2263 Storm = 3,
2264 Snow = 4,
2265}
2266
2267impl WeatherState {
2268 pub fn from_index(i: usize) -> Self {
2269 match i {
2270 0 => WeatherState::Clear,
2271 1 => WeatherState::Cloudy,
2272 2 => WeatherState::Rain,
2273 3 => WeatherState::Storm,
2274 4 => WeatherState::Snow,
2275 _ => WeatherState::Clear,
2276 }
2277 }
2278 pub fn name(&self) -> &'static str {
2279 match self {
2280 WeatherState::Clear => "Clear",
2281 WeatherState::Cloudy => "Cloudy",
2282 WeatherState::Rain => "Rain",
2283 WeatherState::Storm => "Storm",
2284 WeatherState::Snow => "Snow",
2285 }
2286 }
2287}
2288
2289pub const WEATHER_TRANSITION_MATRIX: [[f32; 5]; 5] = [
2292 [0.60, 0.28, 0.08, 0.02, 0.02],
2294 [0.25, 0.40, 0.25, 0.07, 0.03],
2296 [0.10, 0.30, 0.40, 0.15, 0.05],
2298 [0.05, 0.20, 0.35, 0.30, 0.10],
2300 [0.08, 0.25, 0.10, 0.05, 0.52],
2302];
2303
2304#[derive(Clone, Debug)]
2305pub struct WeatherSnapshot {
2306 pub state: WeatherState,
2307 pub temperature_c: f32,
2308 pub wind_speed_ms: f32,
2309 pub wind_dir_deg: f32,
2310 pub precipitation_mm: f32,
2311 pub cloud_cover: f32, pub visibility_km: f32,
2313 pub humidity: f32,
2314 pub pressure_hpa: f32,
2315 pub fog_density: f32,
2316 pub lightning_chance: f32,
2317}
2318
2319impl WeatherSnapshot {
2320 pub fn clear(temp: f32) -> Self {
2321 WeatherSnapshot {
2322 state: WeatherState::Clear,
2323 temperature_c: temp,
2324 wind_speed_ms: 2.0,
2325 wind_dir_deg: 0.0,
2326 precipitation_mm: 0.0,
2327 cloud_cover: 0.05,
2328 visibility_km: 50.0,
2329 humidity: 0.30,
2330 pressure_hpa: 1013.25,
2331 fog_density: 0.0,
2332 lightning_chance: 0.0,
2333 }
2334 }
2335}
2336
2337pub struct WeatherSystem {
2338 pub current: WeatherSnapshot,
2339 pub history: VecDeque<WeatherSnapshot>,
2340 pub max_history: usize,
2341 pub rng: LcgRng,
2342 pub base_temp: f32,
2343 pub season: f32, pub latitude: f32,
2345}
2346
2347impl WeatherSystem {
2348 pub fn new(seed: u64, base_temp: f32, latitude: f32) -> Self {
2349 WeatherSystem {
2350 current: WeatherSnapshot::clear(base_temp),
2351 history: VecDeque::with_capacity(256),
2352 max_history: 256,
2353 rng: LcgRng::new(seed),
2354 base_temp,
2355 season: 0.25, latitude,
2357 }
2358 }
2359
2360 pub fn step(&mut self, hours_elapsed: f32) {
2362 let steps = (hours_elapsed as usize).max(1);
2364
2365 for _ in 0..steps {
2366 let cur_idx = self.current.state as usize;
2367 let row = &WEATHER_TRANSITION_MATRIX[cur_idx];
2368
2369 let r = self.rng.next_f32();
2371 let mut cum = 0.0f32;
2372 let mut next_state = self.current.state;
2373 for (i, &p) in row.iter().enumerate() {
2374 cum += p;
2375 if r < cum {
2376 next_state = WeatherState::from_index(i);
2377 break;
2378 }
2379 }
2380
2381 let temp = self.compute_temperature(next_state);
2383 let snap = self.generate_snapshot(next_state, temp);
2384
2385 self.history.push_back(self.current.clone());
2386 if self.history.len() > self.max_history {
2387 self.history.pop_front();
2388 }
2389 self.current = snap;
2390 }
2391 }
2392
2393 fn compute_temperature(&mut self, state: WeatherState) -> f32 {
2394 let seasonal_bias = (self.season * TWO_PI).sin() * 15.0;
2396 let lat_bias = -(self.latitude.abs() * 0.5);
2398 let weather_bias = match state {
2399 WeatherState::Clear => 2.0,
2400 WeatherState::Cloudy => -1.0,
2401 WeatherState::Rain => -3.0,
2402 WeatherState::Storm => -5.0,
2403 WeatherState::Snow => -8.0,
2404 };
2405 let noise = (self.rng.next_f32() - 0.5) * 4.0;
2406 self.base_temp + seasonal_bias + lat_bias + weather_bias + noise
2407 }
2408
2409 fn generate_snapshot(&mut self, state: WeatherState, temp: f32) -> WeatherSnapshot {
2410 let r = |rng: &mut LcgRng| rng.next_f32();
2411 match state {
2412 WeatherState::Clear => WeatherSnapshot {
2413 state,
2414 temperature_c: temp,
2415 wind_speed_ms: r(&mut self.rng) * 5.0,
2416 wind_dir_deg: r(&mut self.rng) * 360.0,
2417 precipitation_mm: 0.0,
2418 cloud_cover: r(&mut self.rng) * 0.15,
2419 visibility_km: 40.0 + r(&mut self.rng) * 30.0,
2420 humidity: 0.20 + r(&mut self.rng) * 0.25,
2421 pressure_hpa: 1015.0 + r(&mut self.rng) * 10.0,
2422 fog_density: 0.0,
2423 lightning_chance: 0.0,
2424 },
2425 WeatherState::Cloudy => WeatherSnapshot {
2426 state,
2427 temperature_c: temp,
2428 wind_speed_ms: 2.0 + r(&mut self.rng) * 8.0,
2429 wind_dir_deg: r(&mut self.rng) * 360.0,
2430 precipitation_mm: 0.0,
2431 cloud_cover: 0.50 + r(&mut self.rng) * 0.40,
2432 visibility_km: 15.0 + r(&mut self.rng) * 25.0,
2433 humidity: 0.50 + r(&mut self.rng) * 0.25,
2434 pressure_hpa: 1005.0 + r(&mut self.rng) * 10.0,
2435 fog_density: r(&mut self.rng) * 0.1,
2436 lightning_chance: 0.0,
2437 },
2438 WeatherState::Rain => WeatherSnapshot {
2439 state,
2440 temperature_c: temp,
2441 wind_speed_ms: 5.0 + r(&mut self.rng) * 10.0,
2442 wind_dir_deg: r(&mut self.rng) * 360.0,
2443 precipitation_mm: 1.0 + r(&mut self.rng) * 8.0,
2444 cloud_cover: 0.75 + r(&mut self.rng) * 0.25,
2445 visibility_km: 3.0 + r(&mut self.rng) * 7.0,
2446 humidity: 0.75 + r(&mut self.rng) * 0.20,
2447 pressure_hpa: 995.0 + r(&mut self.rng) * 10.0,
2448 fog_density: 0.1 + r(&mut self.rng) * 0.2,
2449 lightning_chance: 0.05,
2450 },
2451 WeatherState::Storm => WeatherSnapshot {
2452 state,
2453 temperature_c: temp,
2454 wind_speed_ms: 15.0 + r(&mut self.rng) * 30.0,
2455 wind_dir_deg: r(&mut self.rng) * 360.0,
2456 precipitation_mm: 8.0 + r(&mut self.rng) * 25.0,
2457 cloud_cover: 0.90 + r(&mut self.rng) * 0.10,
2458 visibility_km: 0.2 + r(&mut self.rng) * 2.0,
2459 humidity: 0.90 + r(&mut self.rng) * 0.10,
2460 pressure_hpa: 975.0 + r(&mut self.rng) * 15.0,
2461 fog_density: 0.3 + r(&mut self.rng) * 0.4,
2462 lightning_chance: 0.40 + r(&mut self.rng) * 0.40,
2463 },
2464 WeatherState::Snow => WeatherSnapshot {
2465 state,
2466 temperature_c: temp.min(-1.0),
2467 wind_speed_ms: 3.0 + r(&mut self.rng) * 15.0,
2468 wind_dir_deg: r(&mut self.rng) * 360.0,
2469 precipitation_mm: 0.5 + r(&mut self.rng) * 4.0,
2470 cloud_cover: 0.70 + r(&mut self.rng) * 0.30,
2471 visibility_km: 0.5 + r(&mut self.rng) * 4.0,
2472 humidity: 0.60 + r(&mut self.rng) * 0.30,
2473 pressure_hpa: 1000.0 + r(&mut self.rng) * 15.0,
2474 fog_density: 0.15 + r(&mut self.rng) * 0.25,
2475 lightning_chance: 0.02,
2476 },
2477 }
2478 }
2479
2480 pub fn interpolate_snapshots(a: &WeatherSnapshot, b: &WeatherSnapshot, t: f32) -> WeatherSnapshot {
2482 let lerp = |x: f32, y: f32| x + (y - x) * t;
2483 WeatherSnapshot {
2484 state: if t < 0.5 { a.state } else { b.state },
2485 temperature_c: lerp(a.temperature_c, b.temperature_c),
2486 wind_speed_ms: lerp(a.wind_speed_ms, b.wind_speed_ms),
2487 wind_dir_deg: lerp(a.wind_dir_deg, b.wind_dir_deg),
2488 precipitation_mm: lerp(a.precipitation_mm, b.precipitation_mm),
2489 cloud_cover: lerp(a.cloud_cover, b.cloud_cover),
2490 visibility_km: lerp(a.visibility_km, b.visibility_km),
2491 humidity: lerp(a.humidity, b.humidity),
2492 pressure_hpa: lerp(a.pressure_hpa, b.pressure_hpa),
2493 fog_density: lerp(a.fog_density, b.fog_density),
2494 lightning_chance: lerp(a.lightning_chance, b.lightning_chance),
2495 }
2496 }
2497
2498 pub fn wind_vector(&self) -> Vec2 {
2500 let dir_rad = self.current.wind_dir_deg * DEG2RAD;
2501 Vec2::new(dir_rad.cos(), dir_rad.sin()) * self.current.wind_speed_ms
2502 }
2503
2504 pub fn is_snowing(&self) -> bool {
2506 self.current.state == WeatherState::Snow
2507 || (self.current.temperature_c < 0.0 && self.current.precipitation_mm > 0.5)
2508 }
2509
2510 pub fn advance_season(&mut self, delta_fraction: f32) {
2512 self.season = (self.season + delta_fraction) % 1.0;
2513 }
2514}
2515
2516#[derive(Debug)]
2521pub enum EditAction {
2522 SetHeightRegion {
2523 x: usize, y: usize,
2524 width: usize, height: usize,
2525 old_data: Vec<f32>,
2526 new_data: Vec<f32>,
2527 },
2528 PlaceFoliageInstances {
2529 instances: Vec<FoliageInstance>,
2530 indices: Vec<usize>,
2531 },
2532 RemoveFoliageInstances {
2533 indices: Vec<usize>,
2534 instances: Vec<FoliageInstance>,
2535 },
2536 AddRoadSegment {
2537 segment_id: u32,
2538 segment: RoadSegment,
2539 },
2540 RemoveRoadSegment {
2541 segment_id: u32,
2542 segment: RoadSegment,
2543 },
2544 SetBiomeOverride {
2545 x: usize, y: usize,
2546 old_biome: Option<BiomeId>,
2547 new_biome: Option<BiomeId>,
2548 },
2549 AddWaterBody {
2550 lake: LakeBody,
2551 index: usize,
2552 },
2553 RemoveWaterBody {
2554 lake: LakeBody,
2555 index: usize,
2556 },
2557 CompoundAction {
2558 actions: Vec<EditAction>,
2559 description: String,
2560 },
2561}
2562
2563pub struct UndoRedoStack {
2564 pub undo_stack: Vec<EditAction>,
2565 pub redo_stack: Vec<EditAction>,
2566 pub max_depth: usize,
2567}
2568
2569impl UndoRedoStack {
2570 pub fn new(max_depth: usize) -> Self {
2571 UndoRedoStack { undo_stack: Vec::new(), redo_stack: Vec::new(), max_depth }
2572 }
2573
2574 pub fn push(&mut self, action: EditAction) {
2575 self.redo_stack.clear();
2576 if self.undo_stack.len() >= self.max_depth {
2577 self.undo_stack.remove(0);
2578 }
2579 self.undo_stack.push(action);
2580 }
2581
2582 pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
2583 pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
2584
2585 pub fn pop_undo(&mut self) -> Option<EditAction> {
2586 let a = self.undo_stack.pop()?;
2587 Some(a)
2588 }
2589
2590 pub fn push_redo(&mut self, action: EditAction) {
2591 self.redo_stack.push(action);
2592 }
2593
2594 pub fn pop_redo(&mut self) -> Option<EditAction> {
2595 self.redo_stack.pop()
2596 }
2597}
2598
2599#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2604pub enum SelectionItem {
2605 TerrainCell(usize, usize),
2606 FoliageInstance(usize),
2607 RoadSegment(u32),
2608 WaterBody(usize),
2609 RiverPath(usize),
2610 BiomeZone(BiomeId),
2611}
2612
2613#[derive(Clone, Debug)]
2614pub struct SelectionState {
2615 pub items: HashSet<SelectionItem>,
2616 pub pivot: Option<Vec3>,
2617 pub aabb_min: Vec3,
2618 pub aabb_max: Vec3,
2619 pub mode: SelectionMode,
2620}
2621
2622#[derive(Clone, Debug, PartialEq, Eq)]
2623pub enum SelectionMode {
2624 Single,
2625 Multi,
2626 Box,
2627 Paint,
2628}
2629
2630impl SelectionState {
2631 pub fn new() -> Self {
2632 SelectionState {
2633 items: HashSet::new(),
2634 pivot: None,
2635 aabb_min: Vec3::splat(f32::MAX),
2636 aabb_max: Vec3::splat(f32::MIN),
2637 mode: SelectionMode::Single,
2638 }
2639 }
2640
2641 pub fn select(&mut self, item: SelectionItem) {
2642 if self.mode == SelectionMode::Single { self.items.clear(); }
2643 self.items.insert(item);
2644 }
2645
2646 pub fn deselect(&mut self, item: &SelectionItem) {
2647 self.items.remove(item);
2648 }
2649
2650 pub fn toggle(&mut self, item: SelectionItem) {
2651 if self.items.contains(&item) { self.items.remove(&item); }
2652 else { self.items.insert(item); }
2653 }
2654
2655 pub fn clear(&mut self) {
2656 self.items.clear();
2657 self.pivot = None;
2658 }
2659
2660 pub fn is_empty(&self) -> bool { self.items.is_empty() }
2661 pub fn len(&self) -> usize { self.items.len() }
2662
2663 pub fn box_select_terrain(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
2665 let (lx, rx) = if x0 < x1 { (x0, x1) } else { (x1, x0) };
2666 let (ly, ry) = if y0 < y1 { (y0, y1) } else { (y1, y0) };
2667 for y in ly..=ry {
2668 for x in lx..=rx {
2669 self.items.insert(SelectionItem::TerrainCell(x, y));
2670 }
2671 }
2672 }
2673
2674 pub fn count_terrain_cells(&self) -> usize {
2675 self.items.iter().filter(|i| matches!(i, SelectionItem::TerrainCell(..)) ).count()
2676 }
2677}
2678
2679#[derive(Clone, Debug)]
2684pub struct SerializedWorld {
2685 pub version: u32,
2686 pub width: usize,
2687 pub height: usize,
2688 pub cell_size: f32,
2689 pub heightmap: Vec<f32>,
2690 pub biome_map: Vec<u8>,
2691 pub rivers: Vec<SerializedRiver>,
2692 pub lakes: Vec<SerializedLake>,
2693 pub roads: Vec<SerializedRoad>,
2694 pub foliage: Vec<SerializedFoliage>,
2695 pub sea_level: f32,
2696 pub world_name: String,
2697 pub metadata: HashMap<String, String>,
2698}
2699
2700#[derive(Clone, Debug)]
2701pub struct SerializedRiver {
2702 pub points: Vec<[f32; 2]>,
2703 pub widths: Vec<f32>,
2704 pub depths: Vec<f32>,
2705}
2706
2707#[derive(Clone, Debug)]
2708pub struct SerializedLake {
2709 pub water_level: f32,
2710 pub centroid: [f32; 2],
2711 pub volume: f32,
2712 pub surface_area: f32,
2713}
2714
2715#[derive(Clone, Debug)]
2716pub struct SerializedRoad {
2717 pub id: u32,
2718 pub road_type: u8,
2719 pub width: f32,
2720 pub points: Vec<[f32; 2]>,
2721 pub length: f32,
2722}
2723
2724#[derive(Clone, Debug)]
2725pub struct SerializedFoliage {
2726 pub asset_id: u32,
2727 pub biome_id: u8,
2728 pub position: [f32; 3],
2729 pub rotation: [f32; 4],
2730 pub scale: [f32; 3],
2731}
2732
2733impl SerializedWorld {
2734 pub fn from_editor(editor: &WorldEditor) -> Self {
2735 let heightmap = editor.heightmap.data.clone();
2736 let w = editor.heightmap.width;
2737 let h = editor.heightmap.height;
2738
2739 let mut biome_map = vec![0u8; w * h];
2740 for y in 0..h {
2741 for x in 0..w {
2742 let temp = editor.temperature_map[y * w + x];
2743 let humidity = editor.humidity_map[y * w + x];
2744 let altitude = editor.heightmap.get(x, y);
2745 let biome = BiomeDescriptor::classify_point(temp, humidity, altitude);
2746 biome_map[y * w + x] = biome as u8;
2747 }
2748 }
2749
2750 let rivers: Vec<SerializedRiver> = editor.rivers.iter().map(|r| {
2751 SerializedRiver {
2752 points: r.points.iter().map(|p| [p.x, p.y]).collect(),
2753 widths: r.widths.clone(),
2754 depths: r.depths.clone(),
2755 }
2756 }).collect();
2757
2758 let lakes: Vec<SerializedLake> = editor.lakes.iter().map(|l| {
2759 SerializedLake {
2760 water_level: l.water_level,
2761 centroid: [l.centroid.x, l.centroid.y],
2762 volume: l.volume,
2763 surface_area: l.surface_area,
2764 }
2765 }).collect();
2766
2767 let roads: Vec<SerializedRoad> = editor.road_network.segments.iter().map(|seg| {
2768 SerializedRoad {
2769 id: seg.id,
2770 road_type: seg.road_type.clone() as u8,
2771 width: seg.width,
2772 points: seg.smoothed_pts.iter().map(|p| [p.x, p.y]).collect(),
2773 length: seg.length,
2774 }
2775 }).collect();
2776
2777 let foliage: Vec<SerializedFoliage> = editor.foliage.iter().map(|fi| {
2778 SerializedFoliage {
2779 asset_id: fi.asset_id,
2780 biome_id: fi.biome_id,
2781 position: [fi.position.x, fi.position.y, fi.position.z],
2782 rotation: [fi.rotation.x, fi.rotation.y, fi.rotation.z, fi.rotation.w],
2783 scale: [fi.scale.x, fi.scale.y, fi.scale.z],
2784 }
2785 }).collect();
2786
2787 SerializedWorld {
2788 version: 1,
2789 width: w,
2790 height: h,
2791 cell_size: editor.cell_size,
2792 heightmap,
2793 biome_map,
2794 rivers,
2795 lakes,
2796 roads,
2797 foliage,
2798 sea_level: editor.sea_level,
2799 world_name: editor.world_name.clone(),
2800 metadata: editor.metadata.clone(),
2801 }
2802 }
2803
2804 pub fn to_bytes(&self) -> Vec<u8> {
2806 let mut buf = Vec::new();
2807 buf.extend_from_slice(b"WRLD");
2809 push_u32(&mut buf, self.version);
2810 push_u32(&mut buf, self.width as u32);
2811 push_u32(&mut buf, self.height as u32);
2812 push_f32(&mut buf, self.cell_size);
2813 push_f32(&mut buf, self.sea_level);
2814
2815 push_u32(&mut buf, self.heightmap.len() as u32);
2817 for &v in &self.heightmap { push_f32(&mut buf, v); }
2818
2819 push_u32(&mut buf, self.biome_map.len() as u32);
2821 buf.extend_from_slice(&self.biome_map);
2822
2823 let name_bytes = self.world_name.as_bytes();
2825 push_u32(&mut buf, name_bytes.len() as u32);
2826 buf.extend_from_slice(name_bytes);
2827
2828 push_u32(&mut buf, self.rivers.len() as u32);
2830 for river in &self.rivers {
2831 push_u32(&mut buf, river.points.len() as u32);
2832 for &[px, py] in &river.points { push_f32(&mut buf, px); push_f32(&mut buf, py); }
2833 for &w in &river.widths { push_f32(&mut buf, w); }
2834 for &d in &river.depths { push_f32(&mut buf, d); }
2835 }
2836
2837 push_u32(&mut buf, self.lakes.len() as u32);
2839 for lake in &self.lakes {
2840 push_f32(&mut buf, lake.water_level);
2841 push_f32(&mut buf, lake.centroid[0]);
2842 push_f32(&mut buf, lake.centroid[1]);
2843 push_f32(&mut buf, lake.volume);
2844 push_f32(&mut buf, lake.surface_area);
2845 }
2846
2847 push_u32(&mut buf, self.roads.len() as u32);
2849 for road in &self.roads {
2850 push_u32(&mut buf, road.id);
2851 buf.push(road.road_type);
2852 push_f32(&mut buf, road.width);
2853 push_f32(&mut buf, road.length);
2854 push_u32(&mut buf, road.points.len() as u32);
2855 for &[px, py] in &road.points { push_f32(&mut buf, px); push_f32(&mut buf, py); }
2856 }
2857
2858 push_u32(&mut buf, self.foliage.len() as u32);
2860 for fi in &self.foliage {
2861 push_u32(&mut buf, fi.asset_id);
2862 buf.push(fi.biome_id);
2863 for &v in &fi.position { push_f32(&mut buf, v); }
2864 for &v in &fi.rotation { push_f32(&mut buf, v); }
2865 for &v in &fi.scale { push_f32(&mut buf, v); }
2866 }
2867
2868 buf
2869 }
2870
2871 pub fn from_bytes(data: &[u8]) -> Option<Self> {
2873 let mut cursor = 0usize;
2874
2875 if data.len() < 4 { return None; }
2876 if &data[0..4] != b"WRLD" { return None; }
2877 cursor += 4;
2878
2879 let version = read_u32(data, &mut cursor)?;
2880 let width = read_u32(data, &mut cursor)? as usize;
2881 let height = read_u32(data, &mut cursor)? as usize;
2882 let cell_size = read_f32(data, &mut cursor)?;
2883 let sea_level = read_f32(data, &mut cursor)?;
2884
2885 let hmap_len = read_u32(data, &mut cursor)? as usize;
2886 let mut heightmap = Vec::with_capacity(hmap_len);
2887 for _ in 0..hmap_len {
2888 heightmap.push(read_f32(data, &mut cursor)?);
2889 }
2890
2891 let biome_len = read_u32(data, &mut cursor)? as usize;
2892 if cursor + biome_len > data.len() { return None; }
2893 let biome_map = data[cursor..cursor + biome_len].to_vec();
2894 cursor += biome_len;
2895
2896 let name_len = read_u32(data, &mut cursor)? as usize;
2897 if cursor + name_len > data.len() { return None; }
2898 let world_name = String::from_utf8(data[cursor..cursor + name_len].to_vec()).ok()?;
2899 cursor += name_len;
2900
2901 Some(SerializedWorld {
2903 version,
2904 width,
2905 height,
2906 cell_size,
2907 heightmap,
2908 biome_map,
2909 rivers: Vec::new(),
2910 lakes: Vec::new(),
2911 roads: Vec::new(),
2912 foliage: Vec::new(),
2913 sea_level,
2914 world_name,
2915 metadata: HashMap::new(),
2916 })
2917 }
2918}
2919
2920fn push_u32(buf: &mut Vec<u8>, v: u32) {
2921 buf.extend_from_slice(&v.to_le_bytes());
2922}
2923fn push_f32(buf: &mut Vec<u8>, v: f32) {
2924 buf.extend_from_slice(&v.to_bits().to_le_bytes());
2925}
2926fn read_u32(data: &[u8], cursor: &mut usize) -> Option<u32> {
2927 if *cursor + 4 > data.len() { return None; }
2928 let v = u32::from_le_bytes(data[*cursor..*cursor+4].try_into().ok()?);
2929 *cursor += 4;
2930 Some(v)
2931}
2932fn read_f32(data: &[u8], cursor: &mut usize) -> Option<f32> {
2933 let bits = read_u32(data, cursor)?;
2934 Some(f32::from_bits(bits))
2935}
2936
2937#[derive(Clone, Debug, PartialEq, Eq)]
2942pub enum EditorTool {
2943 Select,
2944 TerrainRaise,
2945 TerrainLower,
2946 TerrainSmooth,
2947 TerrainFlatten,
2948 TerrainPaint,
2949 TerrainErode,
2950 FoliagePaint,
2951 FoliageErase,
2952 RoadDraw,
2953 WaterPaint,
2954 BiomePaint,
2955 MeasureTool,
2956 ViewOnly,
2957}
2958
2959#[derive(Clone, Debug)]
2960pub struct BrushSettings {
2961 pub radius: f32,
2962 pub strength: f32,
2963 pub falloff: BrushFalloff,
2964 pub scatter: f32,
2965}
2966
2967#[derive(Clone, Debug, PartialEq)]
2968pub enum BrushFalloff {
2969 Linear,
2970 Smooth,
2971 Constant,
2972 Spike,
2973}
2974
2975impl BrushSettings {
2976 pub fn weight_at_radius(&self, dist: f32) -> f32 {
2977 let t = (dist / self.radius).clamp(0.0, 1.0);
2978 match self.falloff {
2979 BrushFalloff::Linear => (1.0 - t) * self.strength,
2980 BrushFalloff::Smooth => { let s = 1.0 - t; s * s * (3.0 - 2.0 * s) * self.strength }
2981 BrushFalloff::Constant => self.strength,
2982 BrushFalloff::Spike => (1.0 - t * t) * self.strength,
2983 }
2984 }
2985}
2986
2987pub fn terrain_brush_raise(
2993 hmap: &mut Heightmap,
2994 cx: f32,
2995 cy: f32,
2996 brush: &BrushSettings,
2997 delta: f32,
2998) -> EditAction {
2999 let r = brush.radius.ceil() as i32;
3000 let cx_i = cx as i32;
3001 let cy_i = cy as i32;
3002
3003 let x0 = (cx_i - r).max(0) as usize;
3004 let y0 = (cy_i - r).max(0) as usize;
3005 let x1 = (cx_i + r).min(hmap.width as i32 - 1) as usize;
3006 let y1 = (cy_i + r).min(hmap.height as i32 - 1) as usize;
3007
3008 let width = x1 - x0 + 1;
3009 let height = y1 - y0 + 1;
3010
3011 let mut old_data = Vec::with_capacity(width * height);
3012 for y in y0..=y1 {
3013 for x in x0..=x1 {
3014 old_data.push(hmap.get(x, y));
3015 }
3016 }
3017
3018 for y in y0..=y1 {
3019 for x in x0..=x1 {
3020 let dx = x as f32 - cx;
3021 let dy = y as f32 - cy;
3022 let dist = (dx*dx + dy*dy).sqrt();
3023 if dist <= brush.radius {
3024 let w = brush.weight_at_radius(dist);
3025 let old = hmap.get(x, y);
3026 hmap.set(x, y, (old + delta * w).clamp(0.0, 1.0));
3027 }
3028 }
3029 }
3030
3031 let mut new_data = Vec::with_capacity(width * height);
3032 for y in y0..=y1 {
3033 for x in x0..=x1 {
3034 new_data.push(hmap.get(x, y));
3035 }
3036 }
3037
3038 EditAction::SetHeightRegion { x: x0, y: y0, width, height, old_data, new_data }
3039}
3040
3041pub fn terrain_brush_smooth(
3043 hmap: &mut Heightmap,
3044 cx: f32,
3045 cy: f32,
3046 brush: &BrushSettings,
3047 iterations: usize,
3048) -> EditAction {
3049 let r = brush.radius.ceil() as i32;
3050 let cx_i = cx as i32;
3051 let cy_i = cy as i32;
3052
3053 let x0 = (cx_i - r).max(0) as usize;
3054 let y0 = (cy_i - r).max(0) as usize;
3055 let x1 = (cx_i + r).min(hmap.width as i32 - 1) as usize;
3056 let y1 = (cy_i + r).min(hmap.height as i32 - 1) as usize;
3057
3058 let width = x1 - x0 + 1;
3059 let height_r = y1 - y0 + 1;
3060
3061 let mut old_data = Vec::with_capacity(width * height_r);
3062 for y in y0..=y1 {
3063 for x in x0..=x1 {
3064 old_data.push(hmap.get(x, y));
3065 }
3066 }
3067
3068 for _iter in 0..iterations {
3069 let copy = hmap.data.clone();
3070 for y in y0..=y1 {
3071 for x in x0..=x1 {
3072 let dx = x as f32 - cx;
3073 let dy = y as f32 - cy;
3074 let dist = (dx*dx + dy*dy).sqrt();
3075 if dist > brush.radius { continue; }
3076 let w = brush.weight_at_radius(dist);
3077
3078 let xi = x as i32;
3079 let yi = y as i32;
3080 let sum = copy[hmap.index(x, y)]
3081 + hmap.get_clamped(xi-1, yi)
3082 + hmap.get_clamped(xi+1, yi)
3083 + hmap.get_clamped(xi, yi-1)
3084 + hmap.get_clamped(xi, yi+1);
3085 let avg = sum / 5.0;
3086 let old = copy[hmap.index(x, y)];
3087 hmap.set(x, y, old + (avg - old) * w);
3088 }
3089 }
3090 }
3091
3092 let mut new_data = Vec::with_capacity(width * height_r);
3093 for y in y0..=y1 {
3094 for x in x0..=x1 {
3095 new_data.push(hmap.get(x, y));
3096 }
3097 }
3098
3099 EditAction::SetHeightRegion { x: x0, y: y0, width, height: height_r, old_data, new_data }
3100}
3101
3102pub fn terrain_brush_flatten(
3104 hmap: &mut Heightmap,
3105 cx: f32,
3106 cy: f32,
3107 brush: &BrushSettings,
3108 target_height: f32,
3109) -> EditAction {
3110 let r = brush.radius.ceil() as i32;
3111 let cx_i = cx as i32;
3112 let cy_i = cy as i32;
3113
3114 let x0 = (cx_i - r).max(0) as usize;
3115 let y0 = (cy_i - r).max(0) as usize;
3116 let x1 = (cx_i + r).min(hmap.width as i32 - 1) as usize;
3117 let y1 = (cy_i + r).min(hmap.height as i32 - 1) as usize;
3118
3119 let width = x1 - x0 + 1;
3120 let height = y1 - y0 + 1;
3121
3122 let mut old_data = Vec::with_capacity(width * height);
3123 for y in y0..=y1 { for x in x0..=x1 { old_data.push(hmap.get(x, y)); } }
3124
3125 for y in y0..=y1 {
3126 for x in x0..=x1 {
3127 let dx = x as f32 - cx;
3128 let dy = y as f32 - cy;
3129 let dist = (dx*dx + dy*dy).sqrt();
3130 if dist > brush.radius { continue; }
3131 let w = brush.weight_at_radius(dist);
3132 let old = hmap.get(x, y);
3133 hmap.set(x, y, old + (target_height - old) * w);
3134 }
3135 }
3136
3137 let mut new_data = Vec::with_capacity(width * height);
3138 for y in y0..=y1 { for x in x0..=x1 { new_data.push(hmap.get(x, y)); } }
3139
3140 EditAction::SetHeightRegion { x: x0, y: y0, width, height, old_data, new_data }
3141}
3142
3143pub fn terrain_stamp(
3145 hmap: &mut Heightmap,
3146 cx: f32,
3147 cy: f32,
3148 stamp: &[f32],
3149 sw: usize,
3150 sh: usize,
3151 scale: f32,
3152) -> EditAction {
3153 let x0 = ((cx - sw as f32 * 0.5) as i32).max(0) as usize;
3154 let y0 = ((cy - sh as f32 * 0.5) as i32).max(0) as usize;
3155 let x1 = (x0 + sw).min(hmap.width);
3156 let y1 = (y0 + sh).min(hmap.height);
3157
3158 let width = x1 - x0;
3159 let height = y1 - y0;
3160
3161 let mut old_data = Vec::with_capacity(width * height);
3162 for y in y0..y1 { for x in x0..x1 { old_data.push(hmap.get(x, y)); } }
3163
3164 for y in y0..y1 {
3165 for x in x0..x1 {
3166 let si = (y - y0) * sw + (x - x0);
3167 if si < stamp.len() {
3168 let old = hmap.get(x, y);
3169 hmap.set(x, y, (old + stamp[si] * scale).clamp(0.0, 1.0));
3170 }
3171 }
3172 }
3173
3174 let mut new_data = Vec::with_capacity(width * height);
3175 for y in y0..y1 { for x in x0..x1 { new_data.push(hmap.get(x, y)); } }
3176
3177 EditAction::SetHeightRegion { x: x0, y: y0, width, height, old_data, new_data }
3178}
3179
3180pub fn generate_temperature_map(
3185 hmap: &Heightmap,
3186 base_temp: f32,
3187 latitude: f32,
3188 noise_scale: f32,
3189 seed: u64,
3190) -> Vec<f32> {
3191 let w = hmap.width;
3192 let h = hmap.height;
3193 let mut temp_map = vec![0.0f32; w * h];
3194
3195 let lapse_rate = 6.5f32;
3198
3199 let lat_range = 60.0f32; let fbm_params = FbmParams { octaves: 4, frequency: noise_scale, lacunarity: 2.0, gain: 0.5, amplitude: 5.0, offset: 0.0, ridge: false };
3204
3205 for y in 0..h {
3206 for x in 0..w {
3207 let nx = x as f32 / w as f32 + (seed as f32 * 0.0001);
3208 let ny = y as f32 / h as f32;
3209 let altitude = hmap.get(x, y);
3210
3211 let lat_factor = (ny - 0.5) * 2.0 * lat_range + latitude;
3213 let lat_temp = base_temp - lat_factor.abs() * 0.5;
3214
3215 let alt_cooling = altitude * lapse_rate * 5.0; let noise_var = fbm_2d(nx * 3.0, ny * 3.0, &fbm_params);
3220
3221 temp_map[y * w + x] = lat_temp - alt_cooling + noise_var;
3222 }
3223 }
3224 temp_map
3225}
3226
3227pub fn generate_humidity_map(
3228 hmap: &Heightmap,
3229 sea_level: f32,
3230 noise_scale: f32,
3231 seed: u64,
3232) -> Vec<f32> {
3233 let w = hmap.width;
3234 let h = hmap.height;
3235 let mut hum_map = vec![0.0f32; w * h];
3236
3237 let fbm_params = FbmParams { octaves: 5, frequency: noise_scale, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
3238
3239 let is_ocean: Vec<bool> = (0..w*h).map(|i| hmap.data[i] <= sea_level).collect();
3242
3243 for y in 0..h {
3245 for x in 0..w {
3246 let nx = x as f32 / w as f32 + (seed as f32 * 0.0002 + 0.5);
3247 let ny = y as f32 / h as f32 + 0.33;
3248 let altitude = hmap.get(x, y);
3249
3250 let coast_humidity = if altitude <= sea_level + 0.05 {
3252 0.85 + fbm_2d(nx * 2.0, ny * 2.0, &fbm_params) * 0.15
3253 } else {
3254 let alt_factor = ((altitude - sea_level) / (1.0 - sea_level)).clamp(0.0, 1.0);
3255 (0.7 - alt_factor * 0.5 + fbm_2d(nx * 4.0, ny * 4.0, &fbm_params) * 0.3).clamp(0.0, 1.0)
3256 };
3257
3258 hum_map[y * w + x] = coast_humidity;
3259 }
3260 }
3261 hum_map
3262}
3263
3264#[derive(Clone, Debug)]
3269pub struct Plane {
3270 pub normal: Vec3,
3271 pub d: f32,
3272}
3273
3274impl Plane {
3275 pub fn new(normal: Vec3, d: f32) -> Self { Plane { normal, d } }
3276 pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
3277 Plane { normal: normal.normalize(), d: -normal.normalize().dot(point) }
3278 }
3279 pub fn distance_to_point(&self, p: Vec3) -> f32 {
3280 self.normal.dot(p) + self.d
3281 }
3282 pub fn normalize(&self) -> Self {
3283 let len = self.normal.length();
3284 Plane { normal: self.normal / len, d: self.d / len }
3285 }
3286}
3287
3288#[derive(Clone, Debug)]
3289pub struct Frustum {
3290 pub planes: [Plane; 6], }
3292
3293impl Frustum {
3294 pub fn from_view_proj(vp: Mat4) -> Self {
3295 let m = vp.to_cols_array();
3296 let planes = [
3298 Plane::new(Vec3::new(m[3]+m[2], m[7]+m[6], m[11]+m[10]), m[15]+m[14]).normalize(), Plane::new(Vec3::new(m[3]-m[2], m[7]-m[6], m[11]-m[10]), m[15]-m[14]).normalize(), Plane::new(Vec3::new(m[3]+m[0], m[7]+m[4], m[11]+m[8]), m[15]+m[12]).normalize(), Plane::new(Vec3::new(m[3]-m[0], m[7]-m[4], m[11]-m[8]), m[15]-m[12]).normalize(), Plane::new(Vec3::new(m[3]+m[1], m[7]+m[5], m[11]+m[9]), m[15]+m[13]).normalize(), Plane::new(Vec3::new(m[3]-m[1], m[7]-m[5], m[11]-m[9]), m[15]-m[13]).normalize(), ];
3305 Frustum { planes }
3306 }
3307
3308 pub fn test_aabb(&self, min: Vec3, max: Vec3) -> bool {
3309 for plane in &self.planes {
3310 let px = if plane.normal.x >= 0.0 { max.x } else { min.x };
3312 let py = if plane.normal.y >= 0.0 { max.y } else { min.y };
3313 let pz = if plane.normal.z >= 0.0 { max.z } else { min.z };
3314 let pv = Vec3::new(px, py, pz);
3315 if plane.distance_to_point(pv) < 0.0 { return false; }
3316 }
3317 true
3318 }
3319
3320 pub fn test_sphere(&self, center: Vec3, radius: f32) -> bool {
3321 for plane in &self.planes {
3322 if plane.distance_to_point(center) < -radius { return false; }
3323 }
3324 true
3325 }
3326}
3327
3328#[derive(Clone, Debug)]
3333pub struct RayHit {
3334 pub point: Vec3,
3335 pub normal: Vec3,
3336 pub t: f32,
3337 pub cell_x: usize,
3338 pub cell_y: usize,
3339 pub altitude: f32,
3340}
3341
3342pub fn ray_heightmap_intersect(
3344 hmap: &Heightmap,
3345 cell_size: f32,
3346 height_scale: f32,
3347 ray_origin: Vec3,
3348 ray_dir: Vec3,
3349) -> Option<RayHit> {
3350 let dir = ray_dir.normalize();
3351 if dir.y.abs() < 1e-6 { return None; }
3352
3353 let w = hmap.width as f32;
3354 let h = hmap.height as f32;
3355
3356 let mut t = 0.0f32;
3357 let step = cell_size * 0.5;
3358 let max_t = (w * w + h * h + height_scale * height_scale).sqrt() * 2.0;
3359
3360 let mut prev_pos = ray_origin;
3361 let mut prev_above = true;
3362
3363 loop {
3364 t += step;
3365 if t > max_t { return None; }
3366
3367 let pos = ray_origin + dir * t;
3368 let gx = pos.x / cell_size;
3369 let gz = pos.z / cell_size;
3370
3371 if gx < 0.0 || gz < 0.0 || gx >= w || gz >= h { continue; }
3372
3373 let ux = gx / w;
3374 let uz = gz / h;
3375 let terrain_h = hmap.sample_bilinear(ux, uz) * height_scale;
3376
3377 let above = pos.y >= terrain_h;
3378 if !above && prev_above {
3379 let mut lo = t - step;
3381 let mut hi = t;
3382 for _ in 0..8 {
3383 let mid = (lo + hi) * 0.5;
3384 let mpos = ray_origin + dir * mid;
3385 let mx = mpos.x / cell_size;
3386 let mz = mpos.z / cell_size;
3387 if mx < 0.0 || mz < 0.0 || mx >= w || mz >= h { hi = mid; continue; }
3388 let mu = mx / w;
3389 let mv = mz / h;
3390 let mh = hmap.sample_bilinear(mu, mv) * height_scale;
3391 if mpos.y >= mh { lo = mid; } else { hi = mid; }
3392 }
3393 let hit_t = (lo + hi) * 0.5;
3394 let hit_pos = ray_origin + dir * hit_t;
3395 let hx = (hit_pos.x / cell_size) as usize;
3396 let hz = (hit_pos.z / cell_size) as usize;
3397 let hx = hx.min(hmap.width - 1);
3398 let hz = hz.min(hmap.height - 1);
3399 let normal = hmap.normal_at(hx, hz, cell_size);
3400 let altitude = hmap.get(hx, hz);
3401 return Some(RayHit { point: hit_pos, normal, t: hit_t, cell_x: hx, cell_y: hz, altitude });
3402 }
3403
3404 prev_above = above;
3405 prev_pos = pos;
3406 }
3407}
3408
3409pub struct WorldEditor {
3414 pub heightmap: Heightmap,
3416 pub cell_size: f32,
3417 pub height_scale: f32,
3418 pub sea_level: f32,
3419 pub world_name: String,
3420 pub metadata: HashMap<String, String>,
3421
3422 pub temperature_map: Vec<f32>,
3424 pub humidity_map: Vec<f32>,
3425
3426 pub biome_system: BiomeSystem,
3428 pub weather: WeatherSystem,
3429 pub road_network: RoadNetwork,
3430 pub atmosphere: AtmosphereParams,
3431
3432 pub rivers: Vec<RiverPath>,
3434 pub lakes: Vec<LakeBody>,
3435 pub shore: Option<OceanShore>,
3436
3437 pub foliage: Vec<FoliageInstance>,
3439 pub foliage_params: Vec<FoliagePlacementParams>,
3440
3441 pub selection: SelectionState,
3443 pub undo_redo: UndoRedoStack,
3444 pub active_tool: EditorTool,
3445 pub brush: BrushSettings,
3446
3447 pub utc_hour: f64,
3449 pub day_of_year: f64,
3450 pub latitude: f64,
3451 pub longitude: f64,
3452 pub solar: SolarState,
3453
3454 pub stats: WorldStats,
3456
3457 pub terrain_fbm_params: FbmParams,
3459 pub warp_strength: f32,
3460 pub erosion_params: ErosionParams,
3461
3462 pub master_seed: u64,
3464
3465 pub heightmap_dirty: bool,
3467 pub climate_dirty: bool,
3468 pub foliage_dirty: bool,
3469 pub water_dirty: bool,
3470}
3471
3472#[derive(Clone, Debug, Default)]
3473pub struct WorldStats {
3474 pub total_cells: usize,
3475 pub ocean_cells: usize,
3476 pub land_cells: usize,
3477 pub mountain_cells: usize,
3478 pub river_count: usize,
3479 pub lake_count: usize,
3480 pub road_segments: usize,
3481 pub road_total_length: f32,
3482 pub foliage_count: usize,
3483 pub min_height: f32,
3484 pub max_height: f32,
3485 pub mean_height: f32,
3486 pub dominant_biome: Option<BiomeId>,
3487}
3488
3489impl WorldEditor {
3490 pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
3491 let heightmap = Heightmap::new(width, height);
3492 let temp_map = vec![15.0f32; width * height];
3493 let hum_map = vec![0.5f32; width * height];
3494
3495 let weather = WeatherSystem::new(12345, 15.0, 45.0);
3496 let atmo = AtmosphereParams::default();
3497
3498 let solar = SolarState::compute(45.0, 0.0, 180.0, 12.0, &atmo);
3499
3500 WorldEditor {
3501 heightmap,
3502 cell_size,
3503 height_scale: 500.0,
3504 sea_level: 0.2,
3505 world_name: String::from("Untitled World"),
3506 metadata: HashMap::new(),
3507
3508 temperature_map: temp_map,
3509 humidity_map: hum_map,
3510
3511 biome_system: BiomeSystem::new(),
3512 weather,
3513 road_network: RoadNetwork::new(),
3514 atmosphere: atmo,
3515
3516 rivers: Vec::new(),
3517 lakes: Vec::new(),
3518 shore: None,
3519
3520 foliage: Vec::new(),
3521 foliage_params: Vec::new(),
3522
3523 selection: SelectionState::new(),
3524 undo_redo: UndoRedoStack::new(256),
3525 active_tool: EditorTool::Select,
3526 brush: BrushSettings {
3527 radius: 20.0,
3528 strength: 0.01,
3529 falloff: BrushFalloff::Smooth,
3530 scatter: 0.0,
3531 },
3532
3533 utc_hour: 12.0,
3534 day_of_year: 180.0,
3535 latitude: 45.0,
3536 longitude: 0.0,
3537 solar,
3538
3539 stats: WorldStats::default(),
3540 terrain_fbm_params: FbmParams::default_terrain(),
3541 warp_strength: 0.3,
3542 erosion_params: ErosionParams::default(),
3543
3544 master_seed: 0xCAFEBABE,
3545 heightmap_dirty: true,
3546 climate_dirty: true,
3547 foliage_dirty: true,
3548 water_dirty: true,
3549 }
3550 }
3551
3552 pub fn generate_terrain(&mut self) {
3555 let seed_offset = Vec2::new(
3556 (self.master_seed & 0xFFFF) as f32 / 65536.0,
3557 ((self.master_seed >> 16) & 0xFFFF) as f32 / 65536.0,
3558 );
3559
3560 if self.warp_strength > 0.0 {
3561 self.heightmap.generate_domain_warp(&self.terrain_fbm_params, self.warp_strength, seed_offset);
3562 } else {
3563 self.heightmap.generate_fbm(&self.terrain_fbm_params, seed_offset);
3564 }
3565
3566 self.heightmap_dirty = true;
3567 self.climate_dirty = true;
3568 self.water_dirty = true;
3569 self.foliage_dirty = true;
3570 }
3571
3572 pub fn apply_erosion(&mut self) {
3573 hydraulic_erosion(&mut self.heightmap, &self.erosion_params);
3574 self.heightmap_dirty = true;
3575 self.water_dirty = true;
3576 self.foliage_dirty = true;
3577 }
3578
3579 pub fn apply_thermal_erosion(&mut self, iterations: usize, talus_deg: f32) {
3580 let talus_rad = talus_deg * DEG2RAD;
3581 thermal_erosion(&mut self.heightmap, iterations, talus_rad);
3582 self.heightmap_dirty = true;
3583 }
3584
3585 pub fn generate_climate(&mut self) {
3588 self.temperature_map = generate_temperature_map(
3589 &self.heightmap,
3590 15.0,
3591 self.latitude as f32,
3592 2.0,
3593 self.master_seed,
3594 );
3595 self.humidity_map = generate_humidity_map(
3596 &self.heightmap,
3597 self.sea_level,
3598 2.0,
3599 self.master_seed ^ 0x55AA,
3600 );
3601 self.climate_dirty = false;
3602 }
3603
3604 pub fn get_biome_at(&self, x: usize, y: usize) -> BiomeBlendSample {
3605 let w = self.heightmap.width;
3606 let idx = y * w + x;
3607 let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
3608 let humidity = if idx < self.humidity_map.len() { self.humidity_map[idx] } else { 0.5 };
3609 let altitude = self.heightmap.get(x, y);
3610 self.biome_system.sample(temp, humidity, altitude)
3611 }
3612
3613 pub fn generate_rivers(&mut self, num_rivers: usize) {
3616 self.rivers.clear();
3617 let mut rng = LcgRng::new(self.master_seed ^ 0xABCDEF);
3618
3619 let w = self.heightmap.width as f32;
3620 let h = self.heightmap.height as f32;
3621
3622 for _ in 0..num_rivers {
3623 let attempts = 20;
3625 let mut start = Vec2::ZERO;
3626 let mut found_start = false;
3627 for _ in 0..attempts {
3628 let sx = rng.next_f32() * w;
3629 let sy = rng.next_f32() * h;
3630 let ux = sx / w;
3631 let uy = sy / h;
3632 let alt = self.heightmap.sample_bilinear(ux, uy);
3633 if alt > 0.55 {
3634 start = Vec2::new(sx, sy);
3635 found_start = true;
3636 break;
3637 }
3638 }
3639 if !found_start { continue; }
3640
3641 let river = simulate_river(&self.heightmap, start, self.sea_level);
3642 if river.points.len() >= 10 {
3643 self.rivers.push(river);
3644 }
3645 }
3646
3647 self.water_dirty = false;
3648 }
3649
3650 pub fn generate_lakes(&mut self, num_lakes: usize, max_water_level: f32) {
3651 self.lakes.clear();
3652 let mut rng = LcgRng::new(self.master_seed ^ 0x123123);
3653 let w = self.heightmap.width;
3654 let h = self.heightmap.height;
3655
3656 for _ in 0..num_lakes {
3657 let sx = (rng.next_f32() * (w - 2) as f32) as usize + 1;
3658 let sy = (rng.next_f32() * (h - 2) as f32) as usize + 1;
3659 let base_h = self.heightmap.get(sx, sy);
3660 if base_h <= self.sea_level || base_h > 0.6 { continue; }
3661 let water_level = base_h + rng.next_f32() * max_water_level;
3662 let lake = fill_lake(&self.heightmap, sx, sy, water_level);
3663 if lake.cells.len() > 4 {
3664 self.lakes.push(lake);
3665 }
3666 }
3667 }
3668
3669 pub fn generate_shore(&mut self) {
3670 self.shore = Some(generate_ocean_shore(&self.heightmap, self.sea_level, 0.02));
3671 }
3672
3673 pub fn place_foliage_layer(&mut self, params: FoliagePlacementParams, seed: u64) {
3676 let new_instances = place_foliage(
3677 &self.heightmap,
3678 None,
3679 ¶ms,
3680 seed,
3681 self.cell_size,
3682 );
3683 self.foliage.extend(new_instances);
3684 }
3685
3686 pub fn clear_foliage(&mut self) {
3687 self.foliage.clear();
3688 }
3689
3690 pub fn cull_foliage(&mut self, frustum: &Frustum) -> Vec<usize> {
3691 let mut visible = Vec::new();
3692 for (i, fi) in self.foliage.iter().enumerate() {
3693 let r = fi.scale.length();
3694 if frustum.test_sphere(fi.position, r) {
3695 visible.push(i);
3696 }
3697 }
3698 visible
3699 }
3700
3701 pub fn build_road(
3704 &mut self,
3705 start: Vec2,
3706 end: Vec2,
3707 road_type: RoadType,
3708 ) -> Option<u32> {
3709 let cost_params = RoadCostParams::default();
3710 let seg_id = self.road_network.build_road(
3711 &self.heightmap,
3712 start,
3713 end,
3714 self.cell_size,
3715 road_type,
3716 &cost_params,
3717 )?;
3718 Some(seg_id)
3719 }
3720
3721 pub fn update_solar(&mut self) {
3724 self.solar = SolarState::compute(
3725 self.latitude,
3726 self.longitude,
3727 self.day_of_year,
3728 self.utc_hour,
3729 &self.atmosphere,
3730 );
3731 }
3732
3733 pub fn advance_time(&mut self, delta_hours: f64) {
3734 self.utc_hour += delta_hours;
3735 if self.utc_hour >= 24.0 {
3736 self.utc_hour -= 24.0;
3737 self.day_of_year += 1.0;
3738 if self.day_of_year > 365.0 {
3739 self.day_of_year = 1.0;
3740 }
3741 }
3742 self.update_solar();
3743 self.weather.advance_season((delta_hours / 8760.0) as f32);
3744 self.weather.step(delta_hours as f32);
3745 }
3746
3747 pub fn raise_terrain(&mut self, cx: f32, cy: f32, delta: f32) {
3750 let action = terrain_brush_raise(&mut self.heightmap, cx, cy, &self.brush, delta);
3751 self.undo_redo.push(action);
3752 self.heightmap_dirty = true;
3753 }
3754
3755 pub fn lower_terrain(&mut self, cx: f32, cy: f32, delta: f32) {
3756 let action = terrain_brush_raise(&mut self.heightmap, cx, cy, &self.brush, -delta);
3757 self.undo_redo.push(action);
3758 self.heightmap_dirty = true;
3759 }
3760
3761 pub fn smooth_terrain(&mut self, cx: f32, cy: f32, iters: usize) {
3762 let action = terrain_brush_smooth(&mut self.heightmap, cx, cy, &self.brush, iters);
3763 self.undo_redo.push(action);
3764 self.heightmap_dirty = true;
3765 }
3766
3767 pub fn flatten_terrain(&mut self, cx: f32, cy: f32, target: f32) {
3768 let action = terrain_brush_flatten(&mut self.heightmap, cx, cy, &self.brush, target);
3769 self.undo_redo.push(action);
3770 self.heightmap_dirty = true;
3771 }
3772
3773 pub fn undo(&mut self) {
3776 if let Some(action) = self.undo_redo.pop_undo() {
3777 let redo_action = self.apply_action_inverse(&action);
3778 self.undo_redo.push_redo(redo_action);
3779 self.heightmap_dirty = true;
3780 }
3781 }
3782
3783 pub fn redo(&mut self) {
3784 if let Some(action) = self.undo_redo.pop_redo() {
3785 let undo_action = self.apply_action_inverse(&action);
3786 self.undo_redo.push(undo_action);
3787 self.heightmap_dirty = true;
3788 }
3789 }
3790
3791 fn apply_action_inverse(&mut self, action: &EditAction) -> EditAction {
3792 match action {
3793 EditAction::SetHeightRegion { x, y, width, height, old_data, new_data } => {
3794 for row in 0..*height {
3795 for col in 0..*width {
3796 let hx = x + col;
3797 let hy = y + row;
3798 if hx < self.heightmap.width && hy < self.heightmap.height {
3799 let i = row * width + col;
3800 if i < old_data.len() {
3801 self.heightmap.set(hx, hy, old_data[i]);
3802 }
3803 }
3804 }
3805 }
3806 EditAction::SetHeightRegion {
3807 x: *x, y: *y, width: *width, height: *height,
3808 old_data: new_data.clone(),
3809 new_data: old_data.clone(),
3810 }
3811 }
3812 EditAction::AddRoadSegment { segment_id, segment } => {
3813 self.road_network.segments.retain(|s| s.id != *segment_id);
3814 EditAction::RemoveRoadSegment { segment_id: *segment_id, segment: segment.clone() }
3815 }
3816 EditAction::RemoveRoadSegment { segment_id, segment } => {
3817 self.road_network.segments.push(segment.clone());
3818 EditAction::AddRoadSegment { segment_id: *segment_id, segment: segment.clone() }
3819 }
3820 EditAction::AddWaterBody { lake, index } => {
3821 if *index < self.lakes.len() { self.lakes.remove(*index); }
3822 EditAction::RemoveWaterBody { lake: lake.clone(), index: *index }
3823 }
3824 EditAction::RemoveWaterBody { lake, index } => {
3825 let i = (*index).min(self.lakes.len());
3826 self.lakes.insert(i, lake.clone());
3827 EditAction::AddWaterBody { lake: lake.clone(), index: *index }
3828 }
3829 EditAction::PlaceFoliageInstances { instances, indices } => {
3830 for &idx in indices.iter().rev() {
3831 if idx < self.foliage.len() { self.foliage.remove(idx); }
3832 }
3833 EditAction::RemoveFoliageInstances {
3834 indices: indices.clone(),
3835 instances: instances.clone(),
3836 }
3837 }
3838 EditAction::RemoveFoliageInstances { instances, indices } => {
3839 for (i, inst) in indices.iter().zip(instances.iter()) {
3840 let insert_at = (*i).min(self.foliage.len());
3841 self.foliage.insert(insert_at, inst.clone());
3842 }
3843 EditAction::PlaceFoliageInstances {
3844 instances: instances.clone(),
3845 indices: indices.clone(),
3846 }
3847 }
3848 EditAction::SetBiomeOverride { .. } => {
3849 action.clone()
3851 }
3852 EditAction::CompoundAction { actions, description } => {
3853 let mut reverse_actions = Vec::with_capacity(actions.len());
3854 for a in actions.iter().rev() {
3855 reverse_actions.push(self.apply_action_inverse(a));
3856 }
3857 EditAction::CompoundAction {
3858 actions: reverse_actions,
3859 description: format!("Undo: {}", description),
3860 }
3861 }
3862 }
3863 }
3864
3865 pub fn ray_cast(&self, ray_origin: Vec3, ray_dir: Vec3) -> Option<RayHit> {
3868 ray_heightmap_intersect(
3869 &self.heightmap,
3870 self.cell_size,
3871 self.height_scale,
3872 ray_origin,
3873 ray_dir,
3874 )
3875 }
3876
3877 pub fn compute_stats(&mut self) {
3880 let w = self.heightmap.width;
3881 let h = self.heightmap.height;
3882 let total = w * h;
3883
3884 let mut ocean = 0usize;
3885 let mut mountain = 0usize;
3886 let mut sum = 0.0f64;
3887 let mut biome_counts = [0usize; 25];
3888
3889 for y in 0..h {
3890 for x in 0..w {
3891 let alt = self.heightmap.get(x, y);
3892 sum += alt as f64;
3893 if alt <= self.sea_level { ocean += 1; }
3894 if alt > 0.7 { mountain += 1; }
3895 let idx = y * w + x;
3896 let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
3897 let hum = if idx < self.humidity_map.len() { self.humidity_map[idx] } else { 0.5 };
3898 let biome = BiomeDescriptor::classify_point(temp, hum, alt);
3899 biome_counts[biome as usize] += 1;
3900 }
3901 }
3902
3903 let dominant_idx = biome_counts.iter().enumerate()
3904 .max_by_key(|(_, &c)| c)
3905 .map(|(i, _)| i)
3906 .unwrap_or(0);
3907
3908 self.heightmap.recompute_minmax();
3909
3910 let road_len: f32 = self.road_network.segments.iter().map(|s| s.length).sum();
3911
3912 self.stats = WorldStats {
3913 total_cells: total,
3914 ocean_cells: ocean,
3915 land_cells: total - ocean,
3916 mountain_cells: mountain,
3917 river_count: self.rivers.len(),
3918 lake_count: self.lakes.len(),
3919 road_segments: self.road_network.segments.len(),
3920 road_total_length: road_len,
3921 foliage_count: self.foliage.len(),
3922 min_height: self.heightmap.min_h,
3923 max_height: self.heightmap.max_h,
3924 mean_height: (sum / total as f64) as f32,
3925 dominant_biome: Some(BiomeId::TropicalRainforest), };
3927 }
3928
3929 pub fn serialize(&self) -> Vec<u8> {
3932 let sw = SerializedWorld::from_editor(self);
3933 sw.to_bytes()
3934 }
3935
3936 pub fn generate_full_world(
3939 &mut self,
3940 num_rivers: usize,
3941 num_lakes: usize,
3942 foliage_density: f32,
3943 ) {
3944 self.generate_terrain();
3946
3947 self.apply_erosion();
3949 self.apply_thermal_erosion(5, 35.0);
3950
3951 self.generate_climate();
3953
3954 self.generate_rivers(num_rivers);
3956 self.generate_lakes(num_lakes, 0.03);
3957 self.generate_shore();
3958
3959 let biome_table = build_biome_table();
3961 for (biome_idx, desc) in biome_table.iter().enumerate() {
3962 let fp = FoliagePlacementParams {
3963 min_radius: 2.0 + (1.0 - desc.tree_density) * 8.0,
3964 max_instances: (desc.tree_density * foliage_density * 50000.0) as usize,
3965 max_slope_rad: 0.6,
3966 min_altitude: desc.alt_min,
3967 max_altitude: desc.alt_max,
3968 density_scale: desc.tree_density * foliage_density,
3969 use_density_map: false,
3970 random_rotation: true,
3971 scale_variance: 0.3,
3972 base_scale: Vec3::new(1.0, 1.0 + desc.tree_density, 1.0),
3973 asset_id: biome_idx as u32,
3974 biome_id: biome_idx as u8,
3975 align_to_normal: false,
3976 };
3977 self.place_foliage_layer(fp, self.master_seed ^ (biome_idx as u64 * 997));
3978 }
3979
3980 self.update_solar();
3982
3983 self.compute_stats();
3985 }
3986
3987 pub fn world_to_heightmap(&self, world: Vec3) -> (usize, usize) {
3990 let x = (world.x / self.cell_size) as usize;
3991 let z = (world.z / self.cell_size) as usize;
3992 (x.min(self.heightmap.width - 1), z.min(self.heightmap.height - 1))
3993 }
3994
3995 pub fn heightmap_to_world(&self, x: usize, z: usize) -> Vec3 {
3996 let height = self.heightmap.get(x, z) * self.height_scale;
3997 Vec3::new(x as f32 * self.cell_size, height, z as f32 * self.cell_size)
3998 }
3999
4000 pub fn world_bounds(&self) -> (Vec3, Vec3) {
4001 let min = Vec3::ZERO;
4002 let max = Vec3::new(
4003 self.heightmap.width as f32 * self.cell_size,
4004 self.height_scale,
4005 self.heightmap.height as f32 * self.cell_size,
4006 );
4007 (min, max)
4008 }
4009
4010 pub fn get_selection_pivot(&self) -> Vec3 {
4013 if let Some(p) = self.selection.pivot { return p; }
4014 let mut sum = Vec3::ZERO;
4016 let mut count = 0;
4017 for item in &self.selection.items {
4018 if let SelectionItem::TerrainCell(x, z) = item {
4019 sum += self.heightmap_to_world(*x, *z);
4020 count += 1;
4021 }
4022 }
4023 if count > 0 { sum / count as f32 } else { Vec3::ZERO }
4024 }
4025
4026 pub fn measure_distance(&self, a_world: Vec3, b_world: Vec3) -> f32 {
4029 (b_world - a_world).length()
4030 }
4031
4032 pub fn measure_area_of_selection(&self) -> f32 {
4033 let count = self.selection.count_terrain_cells();
4034 count as f32 * self.cell_size * self.cell_size
4035 }
4036
4037 pub fn compute_lod_factor(&self, pos: Vec3, camera_pos: Vec3, lod_distances: &[f32]) -> u8 {
4040 let dist = (pos - camera_pos).length();
4041 for (i, &d) in lod_distances.iter().enumerate() {
4042 if dist < d { return i as u8; }
4043 }
4044 lod_distances.len() as u8
4045 }
4046
4047 pub fn update_foliage_lod(&mut self, camera_pos: Vec3) {
4048 let lod_distances = [50.0f32, 150.0, 400.0, 1000.0];
4049 for fi in self.foliage.iter_mut() {
4050 let dist = (fi.position - camera_pos).length();
4051 fi.lod_factor = (dist / lod_distances[lod_distances.len() - 1]).clamp(0.0, 1.0);
4052 }
4053 }
4054
4055 pub fn sample_sky_at_direction(&self, dir: Vec3) -> Vec3 {
4058 let raw = compute_sky_color(dir, self.solar.direction, &self.atmosphere);
4059 let with_sun = raw + sun_disk_color(dir, self.solar.direction, 0.009, self.solar.sun_color * self.solar.sun_intensity);
4060 aces_tonemap(with_sun)
4061 }
4062}
4063
4064fn _0_009_rad_equiv_inner() -> f32 { 0.009 }
4065trait RadEquiv { fn _0_009_rad_equiv(&self) -> f32; }
4066fn zero_point_zero_zero_nine() -> f32 { 0.009 }
4068
4069impl WorldEditor {
4071 pub fn sky_at(&self, view_dir: Vec3) -> Vec3 {
4072 let raw = compute_sky_color(view_dir, self.solar.direction, &self.atmosphere);
4073 let sun = sun_disk_color(view_dir, self.solar.direction, 0.009, self.solar.sun_color * self.solar.sun_intensity);
4074 aces_tonemap(raw + sun)
4075 }
4076}
4077
4078#[inline] pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
4084 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4085 t * t * (3.0 - 2.0 * t)
4086}
4087
4088#[inline] pub fn smootherstep(edge0: f32, edge1: f32, x: f32) -> f32 {
4090 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4091 t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4092}
4093
4094#[inline] pub fn remap(v: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
4096 out_min + (out_max - out_min) * ((v - in_min) / (in_max - in_min)).clamp(0.0, 1.0)
4097}
4098
4099pub fn bilinear_sample(data: &[f32], width: usize, height: usize, u: f32, v: f32) -> f32 {
4101 let px = u * (width - 1) as f32;
4102 let py = v * (height - 1) as f32;
4103 let x0 = px.floor() as usize;
4104 let y0 = py.floor() as usize;
4105 let x1 = (x0 + 1).min(width - 1);
4106 let y1 = (y0 + 1).min(height - 1);
4107 let tx = px - x0 as f32;
4108 let ty = py - y0 as f32;
4109 let a = data[y0 * width + x0];
4110 let b = data[y0 * width + x1];
4111 let c = data[y1 * width + x0];
4112 let d = data[y1 * width + x1];
4113 lerp_f(lerp_f(a, b, tx), lerp_f(c, d, tx), ty)
4114}
4115
4116pub fn gaussian_kernel_2d(sigma: f32, size: usize) -> Vec<f32> {
4118 let half = (size / 2) as i32;
4119 let sigma2 = sigma * sigma;
4120 let mut k = vec![0.0f32; size * size];
4121 let mut sum = 0.0f32;
4122 for y in 0..size as i32 {
4123 for x in 0..size as i32 {
4124 let dx = (x - half) as f32;
4125 let dy = (y - half) as f32;
4126 let v = (-(dx*dx + dy*dy) / (2.0 * sigma2)).exp();
4127 k[(y as usize) * size + (x as usize)] = v;
4128 sum += v;
4129 }
4130 }
4131 for v in k.iter_mut() { *v /= sum; }
4132 k
4133}
4134
4135pub fn gaussian_blur_2d(data: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
4137 let radius = (sigma * 3.0).ceil() as i32;
4138 let size = (radius * 2 + 1) as usize;
4139 let mut kernel = vec![0.0f32; size];
4141 let mut ksum = 0.0f32;
4142 for i in 0..size as i32 {
4143 let d = (i - radius) as f32;
4144 let v = (-(d*d) / (2.0 * sigma * sigma)).exp();
4145 kernel[i as usize] = v;
4146 ksum += v;
4147 }
4148 for v in kernel.iter_mut() { *v /= ksum; }
4149
4150 let mut temp = vec![0.0f32; width * height];
4152 for y in 0..height {
4153 for x in 0..width {
4154 let mut acc = 0.0f32;
4155 for (ki, &kv) in kernel.iter().enumerate() {
4156 let nx = (x as i32 + ki as i32 - radius).clamp(0, width as i32 - 1) as usize;
4157 acc += data[y * width + nx] * kv;
4158 }
4159 temp[y * width + x] = acc;
4160 }
4161 }
4162
4163 let mut out = vec![0.0f32; width * height];
4165 for y in 0..height {
4166 for x in 0..width {
4167 let mut acc = 0.0f32;
4168 for (ki, &kv) in kernel.iter().enumerate() {
4169 let ny = (y as i32 + ki as i32 - radius).clamp(0, height as i32 - 1) as usize;
4170 acc += temp[ny * width + x] * kv;
4171 }
4172 out[y * width + x] = acc;
4173 }
4174 }
4175 out
4176}
4177
4178pub fn diamond_square(size: usize, roughness: f32, seed: u64) -> Vec<f32> {
4180 let mut grid = vec![0.0f32; size * size];
4182 let mut rng = LcgRng::new(seed);
4183
4184 grid[0] = rng.next_f32();
4186 grid[size - 1] = rng.next_f32();
4187 grid[(size-1)*size] = rng.next_f32();
4188 grid[(size-1)*size+size-1]= rng.next_f32();
4189
4190 let mut step = size - 1;
4191 let mut scale = roughness;
4192 let half_size = size as i32;
4193
4194 while step > 1 {
4195 let half = step / 2;
4196
4197 let mut y = 0;
4199 while y < size - 1 {
4200 let mut x = 0;
4201 while x < size - 1 {
4202 let avg = (
4203 grid[y * size + x ]
4204 + grid[y * size + x + step]
4205 + grid[(y+step) * size + x ]
4206 + grid[(y+step) * size + x + step]
4207 ) / 4.0;
4208 grid[(y+half) * size + (x+half)] = avg + (rng.next_f32() * 2.0 - 1.0) * scale;
4209 x += step;
4210 }
4211 y += step;
4212 }
4213
4214 let mut y = 0i32;
4216 while y < size as i32 {
4217 let mut x = (if (y as usize / half) % 2 == 0 { half as i32 } else { 0 });
4218 while x < size as i32 {
4219 let mut sum = 0.0f32;
4220 let mut count = 0;
4221 let offsets: [(i32,i32); 4] = [(-(half as i32), 0), (half as i32, 0), (0, -(half as i32)), (0, half as i32)];
4222 for &(dx, dy) in &offsets {
4223 let nx = x + dx;
4224 let ny = y + dy;
4225 if nx >= 0 && nx < size as i32 && ny >= 0 && ny < size as i32 {
4226 sum += grid[ny as usize * size + nx as usize];
4227 count += 1;
4228 }
4229 }
4230 grid[y as usize * size + x as usize] = sum / count as f32 + (rng.next_f32() * 2.0 - 1.0) * scale;
4231 x += step as i32;
4232 }
4233 y += half as i32;
4234 }
4235
4236 step /= 2;
4237 scale *= roughness.powf(1.0);
4238 }
4239
4240 let min_v = grid.iter().cloned().fold(f32::MAX, f32::min);
4242 let max_v = grid.iter().cloned().fold(f32::MIN, f32::max);
4243 let range = max_v - min_v;
4244 if range > 1e-10 {
4245 for v in grid.iter_mut() { *v = (*v - min_v) / range; }
4246 }
4247
4248 grid
4249}
4250
4251pub fn compute_slope_map(hmap: &Heightmap, cell_size: f32) -> Vec<f32> {
4257 let w = hmap.width;
4258 let h = hmap.height;
4259 let mut slope_map = vec![0.0f32; w * h];
4260 for y in 0..h {
4261 for x in 0..w {
4262 slope_map[y * w + x] = hmap.slope_at(x, y, cell_size);
4263 }
4264 }
4265 slope_map
4266}
4267
4268pub fn compute_curvature_map(hmap: &Heightmap) -> Vec<f32> {
4270 let w = hmap.width;
4271 let h = hmap.height;
4272 let mut curv_map = vec![0.0f32; w * h];
4273 for y in 1..h-1 {
4274 for x in 1..w-1 {
4275 let center = hmap.get(x, y);
4276 let d2hdx2 = hmap.get(x+1, y) - 2.0 * center + hmap.get(x-1, y);
4277 let d2hdy2 = hmap.get(x, y+1) - 2.0 * center + hmap.get(x, y-1);
4278 curv_map[y * w + x] = d2hdx2 + d2hdy2;
4279 }
4280 }
4281 curv_map
4282}
4283
4284pub fn compute_flow_direction(hmap: &Heightmap) -> Vec<u8> {
4286 let w = hmap.width;
4287 let h = hmap.height;
4288 let mut flow = vec![0u8; w * h];
4289 let dirs: [(i32,i32); 8] = [(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1),(1,-1)];
4290 for y in 1..h-1 {
4291 for x in 1..w-1 {
4292 let center = hmap.get(x, y);
4293 let mut best_drop = 0.0f32;
4294 let mut best_dir = 0u8;
4295 for (i, &(dx, dy)) in dirs.iter().enumerate() {
4296 let nh = hmap.get_clamped(x as i32 + dx, y as i32 + dy);
4297 let drop = center - nh;
4298 let dist = if dx != 0 && dy != 0 { (2.0f32).sqrt() } else { 1.0 };
4299 let slope = drop / dist;
4300 if slope > best_drop { best_drop = slope; best_dir = i as u8; }
4301 }
4302 flow[y * w + x] = best_dir;
4303 }
4304 }
4305 flow
4306}
4307
4308pub fn compute_flow_accumulation(flow_dir: &[u8], width: usize, height: usize) -> Vec<u32> {
4310 let mut acc = vec![1u32; width * height]; let dirs: [(i32,i32); 8] = [(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1),(1,-1)];
4312
4313 for _pass in 0..height {
4315 for y in 1..height-1 {
4316 for x in 1..width-1 {
4317 let dir = flow_dir[y * width + x] as usize;
4318 let (dx, dy) = dirs[dir];
4319 let nx = (x as i32 + dx) as usize;
4320 let ny = (y as i32 + dy) as usize;
4321 if nx < width && ny < height {
4322 acc[ny * width + nx] += acc[y * width + x];
4323 }
4324 }
4325 }
4326 }
4327 acc
4328}
4329
4330pub fn compute_terrain_ao(hmap: &Heightmap, num_rays: usize, max_dist: f32, cell_size: f32) -> Vec<f32> {
4337 let w = hmap.width;
4338 let h = hmap.height;
4339 let mut ao = vec![1.0f32; w * h];
4340
4341 let angle_step = TWO_PI / num_rays as f32;
4342
4343 for y in 0..h {
4344 for x in 0..w {
4345 let base_h = hmap.get(x, y);
4346 let mut occ = 0.0f32;
4347
4348 for ray in 0..num_rays {
4349 let angle = ray as f32 * angle_step;
4350 let ray_dx = angle.cos();
4351 let ray_dz = angle.sin();
4352 let mut max_horizon = 0.0f32; let steps = (max_dist / cell_size).ceil() as usize;
4355 for step in 1..=steps {
4356 let t = step as f32 * cell_size;
4357 let nx = x as f32 + ray_dx * t / cell_size;
4358 let nz = y as f32 + ray_dz * t / cell_size;
4359 if nx < 0.0 || nz < 0.0 || nx >= w as f32 || nz >= h as f32 { break; }
4360
4361 let ux = (nx / w as f32).clamp(0.0, 1.0);
4362 let uz = (nz / h as f32).clamp(0.0, 1.0);
4363 let nh = hmap.sample_bilinear(ux, uz);
4364
4365 let elevation_angle = (nh - base_h) / t * cell_size; if elevation_angle > max_horizon {
4367 max_horizon = elevation_angle;
4368 }
4369 }
4370
4371 let horizon_angle = max_horizon.atan();
4373 occ += (horizon_angle / HALF_PI).clamp(0.0, 1.0);
4374 }
4375
4376 ao[y * w + x] = 1.0 - (occ / num_rays as f32).clamp(0.0, 1.0);
4377 }
4378 }
4379
4380 ao
4381}
4382
4383#[derive(Clone, Debug)]
4388pub struct CloudLayer {
4389 pub altitude_km: f32,
4390 pub thickness_km: f32,
4391 pub coverage: f32, pub density: f32,
4393 pub wind_vel: Vec2,
4394 pub noise_offset: Vec2,
4395}
4396
4397impl CloudLayer {
4398 pub fn new(altitude_km: f32, thickness_km: f32, coverage: f32) -> Self {
4399 CloudLayer {
4400 altitude_km,
4401 thickness_km,
4402 coverage,
4403 density: coverage * 0.5,
4404 wind_vel: Vec2::new(5.0, 2.0),
4405 noise_offset: Vec2::ZERO,
4406 }
4407 }
4408
4409 pub fn opacity_at(&self, u: f32, v: f32, time: f32) -> f32 {
4411 let offset = self.wind_vel * time * 0.0001;
4412 let su = u + offset.x + self.noise_offset.x;
4413 let sv = v + offset.y + self.noise_offset.y;
4414
4415 let cloud_params = FbmParams { octaves: 5, frequency: 2.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
4416 let n = fbm_2d(su * 3.0, sv * 3.0, &cloud_params) * 0.5 + 0.5;
4417 let cloud_val = smoothstep(1.0 - self.coverage, 1.0, n);
4418 cloud_val * self.density
4419 }
4420
4421 pub fn update(&mut self, delta_time: f32) {
4423 self.noise_offset += self.wind_vel * delta_time * 0.00001;
4424 }
4425}
4426
4427pub struct SkySystem {
4428 pub cloud_layers: Vec<CloudLayer>,
4429 pub params: AtmosphereParams,
4430 pub time: f32,
4431}
4432
4433impl SkySystem {
4434 pub fn new() -> Self {
4435 SkySystem {
4436 cloud_layers: vec![
4437 CloudLayer::new(2.0, 0.5, 0.4),
4438 CloudLayer::new(5.0, 1.0, 0.3),
4439 CloudLayer::new(8.0, 2.0, 0.2),
4440 ],
4441 params: AtmosphereParams::default(),
4442 time: 0.0,
4443 }
4444 }
4445
4446 pub fn update(&mut self, delta_time: f32) {
4447 self.time += delta_time;
4448 for layer in self.cloud_layers.iter_mut() {
4449 layer.update(delta_time);
4450 }
4451 }
4452
4453 pub fn cloud_coverage_at(&self, u: f32, v: f32) -> f32 {
4455 self.cloud_layers.iter()
4456 .map(|l| l.opacity_at(u, v, self.time))
4457 .fold(0.0f32, f32::max)
4458 }
4459
4460 pub fn render_sky(&self, view_dir: Vec3, sun_dir: Vec3) -> Vec3 {
4461 let sky = compute_sky_color(view_dir, sun_dir, &self.params);
4462 let sun = sun_disk_color(view_dir, sun_dir, 0.009, Vec3::new(10.0, 9.0, 8.0));
4463 aces_tonemap(sky + sun)
4464 }
4465}
4466
4467#[derive(Clone, Debug)]
4472pub struct QuadtreeNode {
4473 pub x: usize,
4474 pub y: usize,
4475 pub size: usize,
4476 pub lod: u8,
4477 pub children: Option<[Box<QuadtreeNode>; 4]>,
4478 pub min_h: f32,
4479 pub max_h: f32,
4480 pub center: Vec3,
4481 pub is_leaf: bool,
4482}
4483
4484impl QuadtreeNode {
4485 pub fn new(x: usize, y: usize, size: usize, cell_size: f32) -> Self {
4486 let half = size as f32 * 0.5;
4487 let center = Vec3::new(
4488 (x as f32 + half) * cell_size,
4489 0.0,
4490 (y as f32 + half) * cell_size,
4491 );
4492 QuadtreeNode { x, y, size, lod: 0, children: None, min_h: 0.0, max_h: 1.0, center, is_leaf: true }
4493 }
4494
4495 pub fn build(hmap: &Heightmap, x: usize, y: usize, size: usize, min_size: usize, cell_size: f32, depth: u8) -> Box<Self> {
4496 let mut node = QuadtreeNode::new(x, y, size, cell_size);
4497 node.lod = depth;
4498
4499 let mut min_h = f32::MAX;
4501 let mut max_h = f32::MIN;
4502 let x1 = (x + size).min(hmap.width);
4503 let y1 = (y + size).min(hmap.height);
4504 for cy in y..y1 {
4505 for cx in x..x1 {
4506 let h = hmap.get(cx, cy);
4507 if h < min_h { min_h = h; }
4508 if h > max_h { max_h = h; }
4509 }
4510 }
4511 node.min_h = min_h;
4512 node.max_h = max_h;
4513 node.center.y = (min_h + max_h) * 0.5 * 500.0;
4514
4515 if size <= min_size {
4516 node.is_leaf = true;
4517 return Box::new(node);
4518 }
4519
4520 let half = size / 2;
4521 node.is_leaf = false;
4522 node.children = Some([
4523 QuadtreeNode::build(hmap, x, y, half, min_size, cell_size, depth + 1),
4524 QuadtreeNode::build(hmap, x + half, y, half, min_size, cell_size, depth + 1),
4525 QuadtreeNode::build(hmap, x, y + half, half, min_size, cell_size, depth + 1),
4526 QuadtreeNode::build(hmap, x + half, y + half, half, min_size, cell_size, depth + 1),
4527 ]);
4528
4529 Box::new(node)
4530 }
4531
4532 pub fn collect_visible<'a>(&'a self, frustum: &Frustum, cam_pos: Vec3, max_lod: u8, out: &mut Vec<&'a QuadtreeNode>, cell_size: f32, height_scale: f32) {
4534 let aabb_min = Vec3::new(
4535 self.x as f32 * cell_size,
4536 self.min_h * height_scale,
4537 self.y as f32 * cell_size,
4538 );
4539 let aabb_max = Vec3::new(
4540 (self.x + self.size) as f32 * cell_size,
4541 self.max_h * height_scale,
4542 (self.y + self.size) as f32 * cell_size,
4543 );
4544
4545 if !frustum.test_aabb(aabb_min, aabb_max) { return; }
4546
4547 if self.is_leaf || self.lod >= max_lod {
4548 out.push(self);
4549 return;
4550 }
4551
4552 let dist = (self.center - cam_pos).length();
4553 let lod_size = self.size as f32 * cell_size;
4554 if lod_size / dist < 0.5 {
4556 out.push(self);
4557 return;
4558 }
4559
4560 if let Some(ref ch) = self.children {
4561 for c in ch.iter() {
4562 c.collect_visible(frustum, cam_pos, max_lod, out, cell_size, height_scale);
4563 }
4564 } else {
4565 out.push(self);
4566 }
4567 }
4568}
4569
4570pub fn generate_vegetation_density_map(
4575 hmap: &Heightmap,
4576 temp_map: &[f32],
4577 hum_map: &[f32],
4578 biome_sys: &BiomeSystem,
4579) -> Vec<f32> {
4580 let w = hmap.width;
4581 let h = hmap.height;
4582 let mut density = vec![0.0f32; w * h];
4583 for y in 0..h {
4584 for x in 0..w {
4585 let idx = y * w + x;
4586 let altitude = hmap.get(x, y);
4587 let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
4588 let humidity = if idx < hum_map.len() { hum_map[idx] } else { 0.5 };
4589 let sample = biome_sys.sample(temp, humidity, altitude);
4590 density[idx] = sample.blended_tree_density * sample.blended_grass_density;
4591 }
4592 }
4593 density
4594}
4595
4596pub fn compute_terrain_shadow_map(
4603 hmap: &Heightmap,
4604 sun_dir: Vec3,
4605 cell_size: f32,
4606 height_scale: f32,
4607) -> Vec<f32> {
4608 let w = hmap.width;
4609 let h = hmap.height;
4610 let mut shadow = vec![1.0f32; w * h];
4611
4612 if sun_dir.y < 0.01 {
4614 return vec![0.0f32; w * h];
4615 }
4616
4617 let sun_horiz = Vec2::new(sun_dir.x, sun_dir.z);
4619 if sun_horiz.length() < 1e-6 { return shadow; } let sun_2d = sun_horiz.normalize();
4622 let slope_inv = sun_dir.y / sun_horiz.length();
4623 let step_dist = cell_size;
4624 let max_steps = ((w + h) / 2) as usize;
4625
4626 for y in 0..h {
4627 for x in 0..w {
4628 let base_h = hmap.get(x, y) * height_scale;
4629 let mut cur_x = x as f32;
4630 let mut cur_z = y as f32;
4631 let mut shadowed = false;
4632
4633 for step in 1..max_steps {
4634 cur_x += sun_2d.x * step_dist / cell_size;
4635 cur_z += sun_2d.y * step_dist / cell_size; if cur_x < 0.0 || cur_z < 0.0 || cur_x >= w as f32 || cur_z >= h as f32 { break; }
4637
4638 let ux = (cur_x / w as f32).clamp(0.0, 1.0);
4639 let uz = (cur_z / h as f32).clamp(0.0, 1.0);
4640 let terrain_h = hmap.sample_bilinear(ux, uz) * height_scale;
4641 let expected_h = base_h + step as f32 * step_dist * slope_inv;
4642
4643 if terrain_h > expected_h {
4644 shadowed = true;
4645 break;
4646 }
4647 }
4648
4649 shadow[y * w + x] = if shadowed { 0.0 } else { 1.0 };
4650 }
4651 }
4652
4653 shadow
4654}
4655
4656#[derive(Clone, Debug)]
4661pub struct ColorRamp {
4662 pub stops: Vec<(f32, Vec3)>, }
4664
4665impl ColorRamp {
4666 pub fn terrain_default() -> Self {
4667 ColorRamp {
4668 stops: vec![
4669 (0.00, Vec3::new(0.05, 0.15, 0.60)), (0.18, Vec3::new(0.10, 0.40, 0.80)), (0.22, Vec3::new(0.90, 0.85, 0.65)), (0.30, Vec3::new(0.30, 0.55, 0.15)), (0.50, Vec3::new(0.20, 0.45, 0.10)), (0.65, Vec3::new(0.45, 0.40, 0.30)), (0.80, Vec3::new(0.55, 0.50, 0.45)), (0.92, Vec3::new(0.80, 0.85, 0.90)), (1.00, Vec3::new(0.95, 0.97, 1.00)), ],
4679 }
4680 }
4681
4682 pub fn sample(&self, t: f32) -> Vec3 {
4683 let t = t.clamp(0.0, 1.0);
4684 if self.stops.is_empty() { return Vec3::ZERO; }
4685 if self.stops.len() == 1 { return self.stops[0].1; }
4686
4687 for i in 0..self.stops.len() - 1 {
4688 let (ta, ca) = self.stops[i];
4689 let (tb, cb) = self.stops[i + 1];
4690 if t >= ta && t <= tb {
4691 let local_t = (t - ta) / (tb - ta);
4692 let st = smoothstep(0.0, 1.0, local_t);
4693 return ca + (cb - ca) * st;
4694 }
4695 }
4696
4697 self.stops.last().unwrap().1
4698 }
4699}
4700
4701#[derive(Clone, Debug)]
4706pub struct EditorCamera {
4707 pub position: Vec3,
4708 pub target: Vec3,
4709 pub up: Vec3,
4710 pub fov_deg: f32,
4711 pub aspect: f32,
4712 pub near: f32,
4713 pub far: f32,
4714 pub orbit_yaw: f32,
4715 pub orbit_pitch: f32,
4716 pub orbit_dist: f32,
4717}
4718
4719impl EditorCamera {
4720 pub fn new(aspect: f32) -> Self {
4721 EditorCamera {
4722 position: Vec3::new(512.0, 200.0, 512.0),
4723 target: Vec3::new(512.0, 0.0, 512.0),
4724 up: Vec3::Y,
4725 fov_deg: 60.0,
4726 aspect,
4727 near: 1.0,
4728 far: 50000.0,
4729 orbit_yaw: -30.0,
4730 orbit_pitch: 45.0,
4731 orbit_dist: 600.0,
4732 }
4733 }
4734
4735 pub fn view_matrix(&self) -> Mat4 {
4736 Mat4::look_at_rh(self.position, self.target, self.up)
4737 }
4738
4739 pub fn proj_matrix(&self) -> Mat4 {
4740 Mat4::perspective_rh(self.fov_deg * DEG2RAD, self.aspect, self.near, self.far)
4741 }
4742
4743 pub fn view_proj(&self) -> Mat4 {
4744 self.proj_matrix() * self.view_matrix()
4745 }
4746
4747 pub fn frustum(&self) -> Frustum {
4748 Frustum::from_view_proj(self.view_proj())
4749 }
4750
4751 pub fn update_orbit(&mut self) {
4753 let yaw_rad = self.orbit_yaw * DEG2RAD;
4754 let pitch_rad = self.orbit_pitch * DEG2RAD;
4755
4756 let x = self.orbit_dist * pitch_rad.cos() * yaw_rad.sin();
4757 let y = self.orbit_dist * pitch_rad.sin();
4758 let z = self.orbit_dist * pitch_rad.cos() * yaw_rad.cos();
4759
4760 self.position = self.target + Vec3::new(x, y, z);
4761 }
4762
4763 pub fn orbit(&mut self, delta_yaw: f32, delta_pitch: f32) {
4764 self.orbit_yaw += delta_yaw;
4765 self.orbit_pitch = (self.orbit_pitch + delta_pitch).clamp(5.0, 85.0);
4766 self.update_orbit();
4767 }
4768
4769 pub fn zoom(&mut self, delta: f32) {
4770 self.orbit_dist = (self.orbit_dist + delta).clamp(10.0, 10000.0);
4771 self.update_orbit();
4772 }
4773
4774 pub fn pan(&mut self, delta: Vec3) {
4775 self.target += delta;
4776 self.position += delta;
4777 }
4778
4779 pub fn screen_to_ray(&self, ndc_x: f32, ndc_y: f32) -> (Vec3, Vec3) {
4781 let inv_vp = self.view_proj().inverse();
4782 let near_ndc = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
4783 let far_ndc = Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
4784
4785 let near_world = inv_vp * near_ndc;
4786 let far_world = inv_vp * far_ndc;
4787
4788 let nw = Vec3::new(near_world.x / near_world.w, near_world.y / near_world.w, near_world.z / near_world.w);
4789 let fw = Vec3::new(far_world.x / far_world.w, far_world.y / far_world.w, far_world.z / far_world.w);
4790
4791 let dir = (fw - nw).normalize();
4792 (nw, dir)
4793 }
4794}
4795
4796#[derive(Clone, Debug)]
4801pub struct TerrainLayer {
4802 pub id: usize,
4803 pub name: String,
4804 pub weight_map: Vec<f32>, pub tiling: f32,
4806 pub normal_strength: f32,
4807}
4808
4809impl TerrainLayer {
4810 pub fn new(id: usize, name: &str, width: usize, height: usize) -> Self {
4811 TerrainLayer {
4812 id,
4813 name: name.to_string(),
4814 weight_map: vec![0.0; width * height],
4815 tiling: 10.0,
4816 normal_strength: 1.0,
4817 }
4818 }
4819
4820 pub fn paint(&mut self, cx: f32, cy: f32, brush: &BrushSettings, width: usize, height: usize) {
4821 let r = brush.radius.ceil() as i32;
4822 let cx_i = cx as i32;
4823 let cy_i = cy as i32;
4824 let x0 = (cx_i - r).max(0) as usize;
4825 let y0 = (cy_i - r).max(0) as usize;
4826 let x1 = (cx_i + r).min(width as i32 - 1) as usize;
4827 let y1 = (cy_i + r).min(height as i32 - 1) as usize;
4828
4829 for y in y0..=y1 {
4830 for x in x0..=x1 {
4831 let dx = x as f32 - cx;
4832 let dy = y as f32 - cy;
4833 let dist = (dx*dx + dy*dy).sqrt();
4834 if dist > brush.radius { continue; }
4835 let w = brush.weight_at_radius(dist);
4836 let idx = y * width + x;
4837 self.weight_map[idx] = (self.weight_map[idx] + w).clamp(0.0, 1.0);
4838 }
4839 }
4840 }
4841}
4842
4843pub fn normalize_paint_weights(layers: &mut [TerrainLayer], width: usize, height: usize) {
4845 for i in 0..width * height {
4846 let total: f32 = layers.iter().map(|l| l.weight_map[i]).sum();
4847 if total > 1e-6 {
4848 for l in layers.iter_mut() {
4849 l.weight_map[i] /= total;
4850 }
4851 }
4852 }
4853}
4854
4855#[derive(Clone, Debug)]
4860pub struct Particle {
4861 pub position: Vec3,
4862 pub velocity: Vec3,
4863 pub life: f32,
4864 pub max_life: f32,
4865 pub size: f32,
4866 pub color: Vec4,
4867}
4868
4869impl Particle {
4870 pub fn lifetime_t(&self) -> f32 { 1.0 - self.life / self.max_life }
4871}
4872
4873pub struct ParticleSystem {
4874 pub particles: Vec<Particle>,
4875 pub max_count: usize,
4876 rng: LcgRng,
4877}
4878
4879impl ParticleSystem {
4880 pub fn new(max_count: usize, seed: u64) -> Self {
4881 ParticleSystem { particles: Vec::with_capacity(max_count), max_count, rng: LcgRng::new(seed) }
4882 }
4883
4884 pub fn emit_rain(&mut self, camera_pos: Vec3, wind: Vec2, density: f32) {
4885 let count = (density * self.max_count as f32) as usize;
4886 let existing = self.particles.len();
4887 let to_emit = (count.saturating_sub(existing)).min(500);
4888
4889 for _ in 0..to_emit {
4890 let rx = camera_pos.x + (self.rng.next_f32() - 0.5) * 200.0;
4891 let rz = camera_pos.z + (self.rng.next_f32() - 0.5) * 200.0;
4892 let ry = camera_pos.y + 80.0 + self.rng.next_f32() * 40.0;
4893
4894 self.particles.push(Particle {
4895 position: Vec3::new(rx, ry, rz),
4896 velocity: Vec3::new(wind.x * 0.3, -10.0 - self.rng.next_f32() * 5.0, wind.y * 0.3),
4897 life: 0.5 + self.rng.next_f32() * 2.0,
4898 max_life: 2.5,
4899 size: 0.02 + self.rng.next_f32() * 0.01,
4900 color: Vec4::new(0.6, 0.7, 0.9, 0.6),
4901 });
4902 }
4903 }
4904
4905 pub fn emit_snow(&mut self, camera_pos: Vec3, wind: Vec2, density: f32) {
4906 let count = (density * self.max_count as f32) as usize;
4907 let existing = self.particles.len();
4908 let to_emit = (count.saturating_sub(existing)).min(300);
4909
4910 for _ in 0..to_emit {
4911 let rx = camera_pos.x + (self.rng.next_f32() - 0.5) * 300.0;
4912 let rz = camera_pos.z + (self.rng.next_f32() - 0.5) * 300.0;
4913 let ry = camera_pos.y + 60.0 + self.rng.next_f32() * 30.0;
4914
4915 self.particles.push(Particle {
4916 position: Vec3::new(rx, ry, rz),
4917 velocity: Vec3::new(
4918 wind.x * 0.5 + (self.rng.next_f32() - 0.5) * 0.5,
4919 -1.5 - self.rng.next_f32(),
4920 wind.y * 0.5 + (self.rng.next_f32() - 0.5) * 0.5,
4921 ),
4922 life: 3.0 + self.rng.next_f32() * 4.0,
4923 max_life: 7.0,
4924 size: 0.05 + self.rng.next_f32() * 0.08,
4925 color: Vec4::new(0.95, 0.97, 1.0, 0.8),
4926 });
4927 }
4928 }
4929
4930 pub fn update(&mut self, dt: f32, gravity: f32) {
4931 self.particles.retain_mut(|p| {
4932 p.velocity.y -= gravity * dt;
4933 p.position += p.velocity * dt;
4934 p.life -= dt;
4935 p.life > 0.0
4936 });
4937 }
4938
4939 pub fn count(&self) -> usize { self.particles.len() }
4940}
4941
4942pub fn snap_to_grid(pos: Vec3, grid_size: f32) -> Vec3 {
4947 Vec3::new(
4948 (pos.x / grid_size).round() * grid_size,
4949 (pos.y / grid_size).round() * grid_size,
4950 (pos.z / grid_size).round() * grid_size,
4951 )
4952}
4953
4954pub fn snap_to_terrain(pos: Vec3, hmap: &Heightmap, cell_size: f32, height_scale: f32) -> Vec3 {
4955 let gx = pos.x / cell_size;
4956 let gz = pos.z / cell_size;
4957 let ux = (gx / hmap.width as f32).clamp(0.0, 1.0);
4958 let uz = (gz / hmap.height as f32).clamp(0.0, 1.0);
4959 let h = hmap.sample_bilinear(ux, uz) * height_scale;
4960 Vec3::new(pos.x, h, pos.z)
4961}
4962
4963pub fn world_to_uv(pos: Vec3, world_width: f32, world_depth: f32) -> Vec2 {
4964 Vec2::new(
4965 (pos.x / world_width).clamp(0.0, 1.0),
4966 (pos.z / world_depth).clamp(0.0, 1.0),
4967 )
4968}
4969
4970#[derive(Clone, Debug)]
4975pub enum EditorObjectKind {
4976 SpawnPoint,
4977 Trigger { radius: f32 },
4978 LightProbe { radius: f32 },
4979 NavigationMarker,
4980 CustomMarker { label: String },
4981}
4982
4983#[derive(Clone, Debug)]
4984pub struct EditorObject {
4985 pub id: u32,
4986 pub name: String,
4987 pub transform: Mat4,
4988 pub kind: EditorObjectKind,
4989 pub visible: bool,
4990 pub locked: bool,
4991 pub selected: bool,
4992}
4993
4994impl EditorObject {
4995 pub fn new(id: u32, name: &str, pos: Vec3, kind: EditorObjectKind) -> Self {
4996 EditorObject {
4997 id,
4998 name: name.to_string(),
4999 transform: Mat4::from_translation(pos),
5000 kind,
5001 visible: true,
5002 locked: false,
5003 selected: false,
5004 }
5005 }
5006
5007 pub fn position(&self) -> Vec3 {
5008 Vec3::new(self.transform.w_axis.x, self.transform.w_axis.y, self.transform.w_axis.z)
5009 }
5010}
5011
5012pub struct Minimap {
5017 pub width: usize,
5018 pub height: usize,
5019 pub pixels: Vec<Vec4>, pub ramp: ColorRamp,
5021 pub dirty: bool,
5022}
5023
5024impl Minimap {
5025 pub fn new(width: usize, height: usize) -> Self {
5026 Minimap {
5027 width,
5028 height,
5029 pixels: vec![Vec4::ZERO; width * height],
5030 ramp: ColorRamp::terrain_default(),
5031 dirty: true,
5032 }
5033 }
5034
5035 pub fn update(&mut self, hmap: &Heightmap, ao_map: Option<&[f32]>, sea_level: f32) {
5037 let tw = hmap.width;
5038 let th = hmap.height;
5039
5040 for y in 0..self.height {
5041 for x in 0..self.width {
5042 let u = x as f32 / self.width as f32;
5043 let v = y as f32 / self.height as f32;
5044 let h = hmap.sample_bilinear(u, v);
5045 let mut color = self.ramp.sample(h);
5046
5047 if let Some(ao) = ao_map {
5049 let ax = (u * (tw - 1) as f32) as usize;
5050 let ay = (v * (th - 1) as f32) as usize;
5051 let ao_val = ao[ay * tw + ax];
5052 color *= ao_val * 0.7 + 0.3;
5053 }
5054
5055 self.pixels[y * self.width + x] = Vec4::new(color.x, color.y, color.z, 1.0);
5056 }
5057 }
5058 self.dirty = false;
5059 }
5060
5061 pub fn draw_marker(&mut self, world_x: f32, world_z: f32, world_w: f32, world_h: f32, color: Vec4) {
5063 let u = (world_x / world_w).clamp(0.0, 1.0);
5064 let v = (world_z / world_h).clamp(0.0, 1.0);
5065 let px = (u * (self.width - 1) as f32) as usize;
5066 let py = (v * (self.height - 1) as f32) as usize;
5067
5068 let radius = 3usize;
5069 let x0 = px.saturating_sub(radius);
5070 let y0 = py.saturating_sub(radius);
5071 let x1 = (px + radius).min(self.width - 1);
5072 let y1 = (py + radius).min(self.height - 1);
5073 for cy in y0..=y1 {
5074 for cx in x0..=x1 {
5075 let dx = cx as i32 - px as i32;
5076 let dy = cy as i32 - py as i32;
5077 if dx*dx + dy*dy <= (radius*radius) as i32 {
5078 self.pixels[cy * self.width + cx] = color;
5079 }
5080 }
5081 }
5082 }
5083}
5084
5085#[derive(Clone, Debug)]
5090pub struct EditorStateSnapshot {
5091 pub heightmap_data: Vec<f32>,
5092 pub foliage_count: usize,
5093 pub lake_count: usize,
5094 pub river_count: usize,
5095 pub road_count: usize,
5096 pub utc_hour: f64,
5097 pub day_of_year: f64,
5098 pub weather_state: WeatherState,
5099 pub sea_level: f32,
5100 pub world_name: String,
5101}
5102
5103impl WorldEditor {
5104 pub fn snapshot(&self) -> EditorStateSnapshot {
5105 EditorStateSnapshot {
5106 heightmap_data: self.heightmap.data.clone(),
5107 foliage_count: self.foliage.len(),
5108 lake_count: self.lakes.len(),
5109 river_count: self.rivers.len(),
5110 road_count: self.road_network.segments.len(),
5111 utc_hour: self.utc_hour,
5112 day_of_year: self.day_of_year,
5113 weather_state: self.weather.current.state,
5114 sea_level: self.sea_level,
5115 world_name: self.world_name.clone(),
5116 }
5117 }
5118
5119 pub fn restore_heightmap(&mut self, snapshot: &EditorStateSnapshot) {
5120 if snapshot.heightmap_data.len() == self.heightmap.data.len() {
5121 self.heightmap.data = snapshot.heightmap_data.clone();
5122 self.heightmap.recompute_minmax();
5123 self.heightmap_dirty = true;
5124 }
5125 }
5126}
5127
5128pub fn benchmark_noise(width: usize, height: usize, params: &FbmParams) -> f64 {
5133 let mut sum = 0.0f64;
5134 for y in 0..height {
5135 for x in 0..width {
5136 let nx = x as f32 / width as f32;
5137 let ny = y as f32 / height as f32;
5138 sum += fbm_2d(nx, ny, params) as f64;
5139 }
5140 }
5141 sum / (width * height) as f64
5142}
5143
5144pub fn benchmark_erosion(size: usize) -> Heightmap {
5145 let mut hmap = Heightmap::new(size, size);
5146 let params = FbmParams::default_terrain();
5147 hmap.generate_fbm(¶ms, Vec2::ZERO);
5148 let ep = ErosionParams { num_particles: 10_000, ..Default::default() };
5149 hydraulic_erosion(&mut hmap, &ep);
5150 hmap
5151}
5152
5153pub fn benchmark_pathfinding(hmap: &Heightmap) -> Option<Vec<GridNode>> {
5154 let w = hmap.width as i32;
5155 let h = hmap.height as i32;
5156 let start = GridNode::new(1, 1);
5157 let goal = GridNode::new(w - 2, h - 2);
5158 astar_path(hmap, start, goal, &RoadCostParams::default())
5159}
5160
5161impl Default for WorldEditor {
5166 fn default() -> Self {
5167 WorldEditor::new(512, 512, 1.0)
5168 }
5169}
5170
5171impl std::fmt::Display for WeatherState {
5172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5173 write!(f, "{}", self.name())
5174 }
5175}
5176
5177impl std::fmt::Display for BiomeId {
5178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5179 write!(f, "{:?}", self)
5180 }
5181}
5182
5183impl std::fmt::Display for WorldStats {
5184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5185 write!(f,
5186 "World: {}x{} cells | Land: {} | Ocean: {} | Rivers: {} | Lakes: {} | Roads: {} segs ({:.0}m) | Foliage: {}",
5187 (self.total_cells as f32).sqrt() as usize,
5188 (self.total_cells as f32).sqrt() as usize,
5189 self.land_cells,
5190 self.ocean_cells,
5191 self.river_count,
5192 self.lake_count,
5193 self.road_segments,
5194 self.road_total_length,
5195 self.foliage_count,
5196 )
5197 }
5198}
5199
5200impl Clone for EditAction {
5201 fn clone(&self) -> Self {
5202 match self {
5203 EditAction::SetHeightRegion { x, y, width, height, old_data, new_data } =>
5204 EditAction::SetHeightRegion { x: *x, y: *y, width: *width, height: *height, old_data: old_data.clone(), new_data: new_data.clone() },
5205 EditAction::PlaceFoliageInstances { instances, indices } =>
5206 EditAction::PlaceFoliageInstances { instances: instances.clone(), indices: indices.clone() },
5207 EditAction::RemoveFoliageInstances { indices, instances } =>
5208 EditAction::RemoveFoliageInstances { indices: indices.clone(), instances: instances.clone() },
5209 EditAction::AddRoadSegment { segment_id, segment } =>
5210 EditAction::AddRoadSegment { segment_id: *segment_id, segment: segment.clone() },
5211 EditAction::RemoveRoadSegment { segment_id, segment } =>
5212 EditAction::RemoveRoadSegment { segment_id: *segment_id, segment: segment.clone() },
5213 EditAction::SetBiomeOverride { x, y, old_biome, new_biome } =>
5214 EditAction::SetBiomeOverride { x: *x, y: *y, old_biome: *old_biome, new_biome: *new_biome },
5215 EditAction::AddWaterBody { lake, index } =>
5216 EditAction::AddWaterBody { lake: lake.clone(), index: *index },
5217 EditAction::RemoveWaterBody { lake, index } =>
5218 EditAction::RemoveWaterBody { lake: lake.clone(), index: *index },
5219 EditAction::CompoundAction { actions, description } =>
5220 EditAction::CompoundAction { actions: actions.clone(), description: description.clone() },
5221 }
5222 }
5223}
5224
5225#[derive(Clone, Debug)]
5230pub struct RockInstance {
5231 pub position: Vec3,
5232 pub rotation: Quat,
5233 pub scale: Vec3,
5234 pub rock_type: u8,
5235}
5236
5237pub fn place_rocks(
5238 hmap: &Heightmap,
5239 cell_size: f32,
5240 min_slope: f32, max_alt: f32,
5242 density: f32,
5243 seed: u64,
5244) -> Vec<RockInstance> {
5245 let w = hmap.width as f32;
5246 let h = hmap.height as f32;
5247 let min_dist = 3.0 + (1.0 - density) * 7.0;
5248 let candidates = poisson_disk_2d(w, h, min_dist, 30, seed);
5249 let mut rng = LcgRng::new(seed ^ 0xB00B);
5250 let mut result = Vec::new();
5251
5252 for pos in &candidates {
5253 let ux = (pos.x / w).clamp(0.0, 1.0);
5254 let uy = (pos.y / h).clamp(0.0, 1.0);
5255 let alt = hmap.sample_bilinear(ux, uy);
5256 if alt > max_alt { continue; }
5257
5258 let xi = pos.x as usize;
5259 let yi = pos.y as usize;
5260 let xi_c = xi.min(hmap.width - 1);
5261 let yi_c = yi.min(hmap.height - 1);
5262 let slope = hmap.slope_at(xi_c, yi_c, cell_size);
5263 if slope < min_slope { continue; }
5264
5265 let angle = rng.next_f32() * TWO_PI;
5266 let tilt = slope * 0.5;
5267 let rot = Quat::from_rotation_y(angle) * Quat::from_rotation_x(tilt);
5268
5269 let sv = 0.5 + rng.next_f32() * 2.0;
5270 let sxz = 0.7 + rng.next_f32() * 0.6;
5271 let scale = Vec3::new(sv * sxz, sv, sv * sxz);
5272
5273 let world_y = alt * 500.0;
5274 result.push(RockInstance {
5275 position: Vec3::new(pos.x * cell_size, world_y, pos.y * cell_size),
5276 rotation: rot,
5277 scale,
5278 rock_type: (rng.next_f32() * 8.0) as u8,
5279 });
5280 }
5281
5282 result
5283}
5284
5285pub fn compute_snow_accumulation(
5290 hmap: &Heightmap,
5291 temp_map: &[f32],
5292 snow_line: f32, temp_thresh: f32, ) -> Vec<f32> {
5295 let w = hmap.width;
5296 let h = hmap.height;
5297 let mut snow_map = vec![0.0f32; w * h];
5298
5299 for y in 0..h {
5300 for x in 0..w {
5301 let idx = y * w + x;
5302 let alt = hmap.get(x, y);
5303 let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
5304 if alt >= snow_line && temp <= temp_thresh {
5305 let alt_factor = ((alt - snow_line) / (1.0 - snow_line)).clamp(0.0, 1.0);
5307 let temp_factor = ((temp_thresh - temp) / 30.0).clamp(0.0, 1.0);
5308 snow_map[idx] = (alt_factor * 0.6 + temp_factor * 0.4).clamp(0.0, 1.0);
5309 }
5310 }
5311 }
5312 snow_map
5313}
5314
5315pub const PERM2: [u8; 256] = [
5321 198, 11, 59, 119, 138, 22, 40, 216, 69, 175, 89, 201, 90, 142, 76, 250,
5322 220, 37, 104, 82, 127, 248, 13, 99, 179, 42, 222, 194, 230, 106, 26, 155,
5323 36, 83, 18, 72, 67, 17, 162, 167, 147, 137, 50, 133, 23, 213, 80, 125,
5324 200, 192, 29, 180, 10, 218, 146, 183, 234, 60, 215, 38, 244, 239, 169, 91,
5325 34, 190, 185, 171, 27, 203, 240, 254, 158, 52, 249, 153, 214, 54, 47, 207,
5326 140, 55, 102, 182, 111, 170, 232, 101, 96, 173, 166, 136, 43, 20, 88, 115,
5327 129, 156, 126, 233, 221, 74, 62, 48, 86, 35, 109, 224, 165, 131, 187, 246,
5328 71, 63, 141, 108, 24, 148, 45, 79, 121, 210, 144, 196, 93, 228, 28, 9,
5329 177, 118, 110, 120, 243, 41, 251, 107, 49, 117, 160, 85, 247, 65, 6, 64,
5330 189, 58, 132, 235, 75, 7, 163, 205, 188, 3, 139, 197, 208, 150, 116, 168,
5331 15, 95, 16, 151, 217, 77, 66, 152, 204, 57, 199, 12, 161, 184, 81, 31,
5332 229, 211, 53, 39, 78, 206, 236, 4, 46, 25, 227, 241, 174, 159, 14, 253,
5333 154, 191, 73, 238, 135, 209, 181, 33, 226, 123, 68, 32, 130, 193, 21, 84,
5334 237, 123, 145, 172, 44, 5, 176, 143, 100, 219, 114, 56, 252, 149, 92, 245,
5335 103, 157, 2, 8, 19, 97, 122, 202, 134, 255, 112, 30, 70, 186, 61, 98,
5336 105, 94, 113, 87, 231, 178, 164, 124, 51, 1, 212, 76, 128, 242, 223, 195,
5337];
5338
5339pub const BIOME_CLASSIFICATION_TABLE: &str = "\
5341T >24 H >0.80 -> Tropical Rainforest\n\
5342T >24 H >0.50 -> Tropical Savanna\n\
5343T >24 H >0.25 -> Xeric Shrubland\n\
5344T >24 H * -> Hot Desert\n\
5345T >10 H >0.70 -> Temperate Rainforest\n\
5346T >10 H >0.50 -> Temperate Deciduous\n\
5347T >10 H >0.28 -> Mediterranean Shrub\n\
5348T >10 H * -> Xeric Shrubland\n\
5349T >0 H >0.65 -> Boreal Forest\n\
5350T >0 H >0.40 -> Temperate Grassland\n\
5351T >0 H * -> Cold Desert\n\
5352T>-10 H >0.50 -> Taiga Spruce\n\
5353T>-10 H * -> Tundra\n\
5354T * H >0.30 -> Tundra\n\
5355T * H * -> Arctic Desert\n\
5356ALT >0.88 -> Polar Ice Cap\n\
5357ALT >0.75 -> Alpine Tundra\n\
5358ALT >0.62 -> Alpine Meadow\n";
5359
5360impl WorldEditor {
5365 pub fn generate_from_seed(&mut self, seed: u64) {
5367 self.master_seed = seed;
5368 let mut rng = LcgRng::new(seed);
5369 self.terrain_fbm_params.octaves = 7 + (rng.next_f32() * 3.0) as usize;
5370 self.terrain_fbm_params.lacunarity = 1.8 + rng.next_f32() * 0.5;
5371 self.terrain_fbm_params.gain = 0.45 + rng.next_f32() * 0.15;
5372 self.warp_strength = 0.2 + rng.next_f32() * 0.4;
5373 self.sea_level = 0.15 + rng.next_f32() * 0.15;
5374 self.generate_terrain();
5375 self.apply_erosion();
5376 self.apply_thermal_erosion(3, 30.0 + rng.next_f32() * 15.0);
5377 self.generate_climate();
5378 self.generate_rivers(5 + (rng.next_f32() * 10.0) as usize);
5379 self.generate_lakes(3 + (rng.next_f32() * 7.0) as usize, 0.02);
5380 self.generate_shore();
5381 self.compute_stats();
5382 }
5383
5384 pub fn height_at_world(&self, x: f32, z: f32) -> f32 {
5386 let ux = (x / (self.heightmap.width as f32 * self.cell_size)).clamp(0.0, 1.0);
5387 let uz = (z / (self.heightmap.height as f32 * self.cell_size)).clamp(0.0, 1.0);
5388 self.heightmap.sample_bilinear(ux, uz) * self.height_scale
5389 }
5390
5391 pub fn normal_at_world(&self, x: f32, z: f32) -> Vec3 {
5393 let gx = (x / self.cell_size) as usize;
5394 let gz = (z / self.cell_size) as usize;
5395 let gx_c = gx.min(self.heightmap.width - 1);
5396 let gz_c = gz.min(self.heightmap.height - 1);
5397 self.heightmap.normal_at(gx_c, gz_c, self.cell_size)
5398 }
5399
5400 pub fn is_underwater(&self, x: f32, z: f32) -> bool {
5402 let alt = self.height_at_world(x, z) / self.height_scale;
5403 alt <= self.sea_level
5404 }
5405
5406 pub fn biome_at_world(&self, x: f32, z: f32) -> BiomeId {
5408 let gx = ((x / self.cell_size) as usize).min(self.heightmap.width - 1);
5409 let gz = ((z / self.cell_size) as usize).min(self.heightmap.height - 1);
5410 let idx = gz * self.heightmap.width + gx;
5411 let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
5412 let hum = if idx < self.humidity_map.len() { self.humidity_map[idx] } else { 0.5 };
5413 let alt = self.heightmap.get(gx, gz);
5414 BiomeDescriptor::classify_point(temp, hum, alt)
5415 }
5416
5417 pub fn resize_world(&mut self, new_width: usize, new_height: usize) {
5419 let mut new_hmap = Heightmap::new(new_width, new_height);
5420 for y in 0..new_height {
5421 for x in 0..new_width {
5422 let u = x as f32 / (new_width - 1) as f32;
5423 let v = y as f32 / (new_height - 1) as f32;
5424 let h = self.heightmap.sample_bilinear(u, v);
5425 new_hmap.set(x, y, h);
5426 }
5427 }
5428 new_hmap.recompute_minmax();
5429 self.heightmap = new_hmap;
5430 self.temperature_map = vec![15.0; new_width * new_height];
5431 self.humidity_map = vec![0.5; new_width * new_height];
5432 self.climate_dirty = true;
5433 self.heightmap_dirty = true;
5434 self.foliage.clear();
5435 self.foliage_dirty = true;
5436 }
5437
5438 pub fn import_heightmap(&mut self, data: &[f32], width: usize, height: usize) {
5440 if data.len() != width * height { return; }
5441 self.heightmap = Heightmap {
5442 width,
5443 height,
5444 data: data.to_vec(),
5445 min_h: 0.0,
5446 max_h: 1.0,
5447 };
5448 self.heightmap.recompute_minmax();
5449 self.heightmap.normalize_to_01();
5450 self.heightmap_dirty = true;
5451 self.climate_dirty = true;
5452 }
5453
5454 pub fn export_heightmap_u16(&self) -> Vec<u8> {
5456 let mut out = Vec::with_capacity(self.heightmap.data.len() * 2);
5457 for &h in &self.heightmap.data {
5458 let v = (h.clamp(0.0, 1.0) * 65535.0) as u16;
5459 out.push((v >> 8) as u8);
5460 out.push((v & 0xFF) as u8);
5461 }
5462 out
5463 }
5464
5465 pub fn export_heightmap_u8(&self) -> Vec<u8> {
5467 self.heightmap.data.iter()
5468 .map(|&h| (h.clamp(0.0, 1.0) * 255.0) as u8)
5469 .collect()
5470 }
5471
5472 pub fn horizon_angle_at(&self, x: usize, y: usize, direction: f32, max_dist: f32) -> f32 {
5474 let base_h = self.heightmap.get(x, y) * self.height_scale;
5475 let dx = direction.cos();
5476 let dz = direction.sin();
5477 let steps = (max_dist / self.cell_size) as usize;
5478 let mut max_elev = 0.0f32;
5479
5480 for step in 1..=steps {
5481 let t = step as f32 * self.cell_size;
5482 let nx = x as f32 + dx * t / self.cell_size;
5483 let nz = y as f32 + dz * t / self.cell_size;
5484 let w = self.heightmap.width as f32;
5485 let h = self.heightmap.height as f32;
5486 if nx < 0.0 || nz < 0.0 || nx >= w || nz >= h { break; }
5487 let ux = nx / w;
5488 let uz = nz / h;
5489 let nh = self.heightmap.sample_bilinear(ux, uz) * self.height_scale;
5490 let elev = ((nh - base_h) / t).atan();
5491 if elev > max_elev { max_elev = elev; }
5492 }
5493 max_elev
5494 }
5495}
5496
5497pub fn visual_radius_from_altitude(altitude_km: f32) -> f32 {
5503 let r = EARTH_RADIUS as f32;
5505 let h = altitude_km;
5506 (2.0 * r * h + h * h).sqrt()
5507}
5508
5509pub fn normalised_to_metres(h: f32, height_scale_m: f32) -> f32 {
5511 h * height_scale_m
5512}
5513
5514pub fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
5516 let r = EARTH_RADIUS;
5517 let dlat = (lat2 - lat1).to_radians();
5518 let dlon = (lon2 - lon1).to_radians();
5519 let a = (dlat / 2.0).sin().powi(2)
5520 + lat1.to_radians().cos() * lat2.to_radians().cos() * (dlon / 2.0).sin().powi(2);
5521 let c = 2.0 * a.sqrt().asin();
5522 r * c
5523}
5524
5525#[inline] pub fn c_to_f(c: f32) -> f32 { c * 1.8 + 32.0 }
5527
5528#[inline] pub fn f_to_c(f: f32) -> f32 { (f - 32.0) / 1.8 }
5530
5531pub fn dew_point(temp_c: f32, relative_humidity: f32) -> f32 {
5533 let a = 17.27f32;
5534 let b = 237.7f32;
5535 let alpha = (a * temp_c / (b + temp_c)) + (relative_humidity.max(1e-5)).ln();
5536 b * alpha / (a - alpha)
5537}
5538
5539pub fn wind_chill(temp_c: f32, wind_speed_ms: f32) -> f32 {
5541 if wind_speed_ms < 1.4 || temp_c > 10.0 { return temp_c; }
5542 let v = wind_speed_ms * 3.6; 13.12 + 0.6215 * temp_c - 11.37 * v.powf(0.16) + 0.3965 * temp_c * v.powf(0.16)
5544}
5545
5546pub fn heat_index(temp_c: f32, humidity: f32) -> f32 {
5548 let t = c_to_f(temp_c);
5549 let r = humidity * 100.0; let hi = -42.379
5551 + 2.04901523 * t
5552 + 10.14333127 * r
5553 - 0.22475541 * t * r
5554 - 0.00683783 * t * t
5555 - 0.05481717 * r * r
5556 + 0.00122874 * t * t * r
5557 + 0.00085282 * t * r * r
5558 - 0.00000199 * t * t * r * r;
5559 f_to_c(hi)
5560}
5561
5562pub fn beaufort_scale(wind_speed_ms: f32) -> u8 {
5564 match wind_speed_ms as u32 {
5565 0 => 0,
5566 1..=2 => 1,
5567 3..=5 => 2,
5568 6..=9 => 3,
5569 10..=14 => 4,
5570 15..=21 => 5,
5571 22..=29 => 6,
5572 30..=38 => 7,
5573 39..=49 => 8,
5574 50..=61 => 9,
5575 62..=74 => 10,
5576 75..=88 => 11,
5577 _ => 12,
5578 }
5579}
5580
5581pub fn value_noise_2d(x: f32, y: f32) -> f32 {
5587 let xi = x.floor() as i32;
5588 let yi = y.floor() as i32;
5589 let xf = x - xi as f32;
5590 let yf = y - yi as f32;
5591 let u = fade(xf);
5592 let v = fade(yf);
5593 let aa = PERM[((PERM[(xi & 255) as usize] as i32 + (yi & 255)) & 255) as usize] as f32 / 255.0;
5594 let ba = PERM[((PERM[((xi+1) & 255) as usize] as i32 + (yi & 255)) & 255) as usize] as f32 / 255.0;
5595 let ab = PERM[((PERM[(xi & 255) as usize] as i32 + ((yi+1) & 255)) & 255) as usize] as f32 / 255.0;
5596 let bb = PERM[((PERM[((xi+1) & 255) as usize] as i32 + ((yi+1) & 255)) & 255) as usize] as f32 / 255.0;
5597 lerp_f(lerp_f(aa, ba, u), lerp_f(ab, bb, u), v)
5598}
5599
5600pub fn voronoi_noise_2d(x: f32, y: f32, jitter: f32) -> (f32, u32) {
5602 let cx = x.floor() as i32;
5603 let cy = y.floor() as i32;
5604 let mut min_dist = f32::MAX;
5605 let mut min_id = 0u32;
5606 for dy in -2..=2i32 {
5607 for dx in -2..=2i32 {
5608 let nx = cx + dx;
5609 let ny = cy + dy;
5610 let h = worley_hash(nx, ny);
5611 let fx = nx as f32 + jitter * ((h & 0xFFFF) as f32 / 65535.0 - 0.5) * 2.0 + 0.5;
5612 let fy = ny as f32 + jitter * (((h >> 16) & 0xFFFF) as f32 / 65535.0 - 0.5) * 2.0 + 0.5;
5613 let dist = ((fx - x) * (fx - x) + (fy - y) * (fy - y)).sqrt();
5614 if dist < min_dist { min_dist = dist; min_id = h; }
5615 }
5616 }
5617 (min_dist, min_id)
5618}
5619
5620pub fn ridged_multifractal_2d(x: f32, y: f32, octaves: usize, freq: f32, lacunarity: f32, gain: f32, offset: f32) -> f32 {
5622 let mut f = freq;
5623 let mut amp = 1.0f32;
5624 let mut value = 0.0f32;
5625 let mut weight = 1.0f32;
5626 for _ in 0..octaves {
5627 let n = (offset - perlin_noise_2d(x * f, y * f).abs()).abs();
5628 let signal = n * n * weight;
5629 weight = (signal * 2.0).clamp(0.0, 1.0);
5630 value += signal * amp;
5631 f *= lacunarity;
5632 amp *= gain;
5633 }
5634 value
5635}
5636
5637#[derive(Clone, Debug)]
5642pub struct TerrainMesh {
5643 pub vertices: Vec<Vec3>,
5644 pub normals: Vec<Vec3>,
5645 pub uvs: Vec<Vec2>,
5646 pub indices: Vec<u32>,
5647 pub lod_level: u8,
5648}
5649
5650impl TerrainMesh {
5651 pub fn new() -> Self {
5652 TerrainMesh { vertices: Vec::new(), normals: Vec::new(), uvs: Vec::new(), indices: Vec::new(), lod_level: 0 }
5653 }
5654}
5655
5656pub fn generate_terrain_mesh(
5658 hmap: &Heightmap,
5659 chunk_x: usize,
5660 chunk_z: usize,
5661 chunk_size: usize,
5662 cell_size: f32,
5663 height_scale: f32,
5664 lod_step: usize,
5665) -> TerrainMesh {
5666 let step = lod_step.max(1);
5667 let x_end = (chunk_x + chunk_size).min(hmap.width - 1);
5668 let z_end = (chunk_z + chunk_size).min(hmap.height - 1);
5669 let mut mesh = TerrainMesh::new();
5670 let mut vert_idx_map: HashMap<(usize, usize), u32> = HashMap::new();
5671
5672 let mut xz = chunk_z;
5673 while xz <= z_end {
5674 let mut xx = chunk_x;
5675 while xx <= x_end {
5676 let h = hmap.get(xx, xz) * height_scale;
5677 let pos = Vec3::new(xx as f32 * cell_size, h, xz as f32 * cell_size);
5678 let norm = hmap.normal_at(xx, xz, cell_size);
5679 let uv = Vec2::new(
5680 (xx - chunk_x) as f32 / chunk_size as f32,
5681 (xz - chunk_z) as f32 / chunk_size as f32,
5682 );
5683 let idx = mesh.vertices.len() as u32;
5684 vert_idx_map.insert((xx, xz), idx);
5685 mesh.vertices.push(pos);
5686 mesh.normals.push(norm);
5687 mesh.uvs.push(uv);
5688 xx += step;
5689 }
5690 xz += step;
5691 }
5692
5693 let mut xz = chunk_z;
5694 while xz + step <= z_end {
5695 let mut xx = chunk_x;
5696 while xx + step <= x_end {
5697 let nx = (xx + step).min(x_end);
5698 let nz = (xz + step).min(z_end);
5699 if let (Some(&i00), Some(&i10), Some(&i01), Some(&i11)) = (
5700 vert_idx_map.get(&(xx, xz)),
5701 vert_idx_map.get(&(nx, xz)),
5702 vert_idx_map.get(&(xx, nz)),
5703 vert_idx_map.get(&(nx, nz)),
5704 ) {
5705 mesh.indices.extend_from_slice(&[i00, i10, i01, i10, i11, i01]);
5706 }
5707 xx += step;
5708 }
5709 xz += step;
5710 }
5711 mesh
5712}
5713
5714pub fn compute_tangents(mesh: &mut TerrainMesh) -> Vec<Vec3> {
5716 let mut tangents = vec![Vec3::ZERO; mesh.vertices.len()];
5717 let tri_count = mesh.indices.len() / 3;
5718 for t in 0..tri_count {
5719 let i0 = mesh.indices[t * 3] as usize;
5720 let i1 = mesh.indices[t * 3 + 1] as usize;
5721 let i2 = mesh.indices[t * 3 + 2] as usize;
5722 let v0 = mesh.vertices[i0];
5723 let v1 = mesh.vertices[i1];
5724 let v2 = mesh.vertices[i2];
5725 let uv0 = mesh.uvs[i0];
5726 let uv1 = mesh.uvs[i1];
5727 let uv2 = mesh.uvs[i2];
5728 let e1 = v1 - v0;
5729 let e2 = v2 - v0;
5730 let du1 = uv1.x - uv0.x;
5731 let dv1 = uv1.y - uv0.y;
5732 let du2 = uv2.x - uv0.x;
5733 let dv2 = uv2.y - uv0.y;
5734 let det = du1 * dv2 - du2 * dv1;
5735 if det.abs() < 1e-10 { continue; }
5736 let tang = (e1 * dv2 - e2 * dv1) / det;
5737 tangents[i0] += tang;
5738 tangents[i1] += tang;
5739 tangents[i2] += tang;
5740 }
5741 tangents.iter().enumerate().map(|(i, t)| {
5742 let n = mesh.normals[i];
5743 (*t - n * n.dot(*t)).normalize_or_zero()
5744 }).collect()
5745}
5746
5747#[derive(Clone, Debug)]
5752pub struct WindField {
5753 pub width: usize,
5754 pub height: usize,
5755 pub vectors: Vec<Vec2>,
5756 pub turbulence: Vec<f32>,
5757}
5758
5759impl WindField {
5760 pub fn new(width: usize, height: usize) -> Self {
5761 WindField { width, height, vectors: vec![Vec2::ZERO; width*height], turbulence: vec![0.0; width*height] }
5762 }
5763
5764 pub fn generate_from_terrain(hmap: &Heightmap, base_wind: Vec2, turbulence_strength: f32, seed: u64) -> Self {
5765 let w = hmap.width;
5766 let h = hmap.height;
5767 let mut field = WindField::new(w, h);
5768 let fbm_p = FbmParams { octaves: 4, frequency: 2.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
5769 for y in 0..h {
5770 for x in 0..w {
5771 let idx = y * w + x;
5772 let slope = hmap.slope_at(x, y, 1.0);
5773 let grad = hmap.gradient_at(x, y);
5774 let upslope = base_wind.dot(grad);
5775 let deflect = -grad * upslope * slope * 2.0;
5776 let speed_m = 1.0 + (if upslope < 0.0 { 1.0 } else { 0.0 }) * slope * 0.5;
5777 let nx = x as f32 / w as f32 + seed as f32 * 1e-5;
5778 let ny = y as f32 / h as f32;
5779 let noise_x = fbm_2d(nx, ny, &fbm_p) * turbulence_strength;
5780 let noise_y = fbm_2d(nx + 100.0, ny + 100.0, &fbm_p) * turbulence_strength;
5781 field.vectors[idx] = (base_wind + deflect) * speed_m + Vec2::new(noise_x, noise_y);
5782 field.turbulence[idx] = noise_x.abs() + noise_y.abs();
5783 }
5784 }
5785 field
5786 }
5787
5788 pub fn sample(&self, x: f32, y: f32) -> Vec2 {
5789 let gx = x.clamp(0.0, (self.width - 1) as f32);
5790 let gy = y.clamp(0.0, (self.height - 1) as f32);
5791 let x0 = gx.floor() as usize;
5792 let y0 = gy.floor() as usize;
5793 let x1 = (x0 + 1).min(self.width - 1);
5794 let y1 = (y0 + 1).min(self.height - 1);
5795 let tx = gx - x0 as f32;
5796 let ty = gy - y0 as f32;
5797 let a = self.vectors[y0 * self.width + x0];
5798 let b = self.vectors[y0 * self.width + x1];
5799 let c = self.vectors[y1 * self.width + x0];
5800 let d = self.vectors[y1 * self.width + x1];
5801 a.lerp(b, tx).lerp(c.lerp(d, tx), ty)
5802 }
5803}
5804
5805pub fn generate_island_mask(width: usize, height: usize, falloff_exp: f32) -> Vec<f32> {
5811 let mut mask = vec![0.0f32; width * height];
5812 let cx = width as f32 * 0.5;
5813 let cy = height as f32 * 0.5;
5814 let max_r = cx.min(cy) * 0.95;
5815 for y in 0..height {
5816 for x in 0..width {
5817 let dx = x as f32 - cx;
5818 let dy = y as f32 - cy;
5819 let t = ((dx*dx + dy*dy).sqrt() / max_r).clamp(0.0, 1.0);
5820 mask[y * width + x] = (1.0 - t.powf(falloff_exp)).clamp(0.0, 1.0);
5821 }
5822 }
5823 mask
5824}
5825
5826pub fn apply_mask(hmap: &mut Heightmap, mask: &[f32]) {
5828 let len = hmap.data.len().min(mask.len());
5829 for i in 0..len { hmap.data[i] *= mask[i]; }
5830 hmap.recompute_minmax();
5831}
5832
5833#[derive(Clone, Debug)]
5834pub enum MaskBlendMode { Add, Multiply, Screen, Max, Min, Subtract }
5835
5836pub fn blend_masks(a: &[f32], b: &[f32], mode: MaskBlendMode) -> Vec<f32> {
5837 let len = a.len().min(b.len());
5838 (0..len).map(|i| match mode {
5839 MaskBlendMode::Add => (a[i] + b[i]).clamp(0.0, 1.0),
5840 MaskBlendMode::Multiply => a[i] * b[i],
5841 MaskBlendMode::Screen => 1.0 - (1.0 - a[i]) * (1.0 - b[i]),
5842 MaskBlendMode::Max => a[i].max(b[i]),
5843 MaskBlendMode::Min => a[i].min(b[i]),
5844 MaskBlendMode::Subtract => (a[i] - b[i]).clamp(0.0, 1.0),
5845 }).collect()
5846}
5847
5848pub fn cubic_bezier(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
5854 let mt = 1.0 - t;
5855 p0 * (mt*mt*mt) + p1 * (3.0*mt*mt*t) + p2 * (3.0*mt*t*t) + p3 * (t*t*t)
5856}
5857
5858pub fn cubic_bezier_tangent(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
5860 let mt = 1.0 - t;
5861 (p1 - p0) * (3.0*mt*mt) + (p2 - p1) * (6.0*mt*t) + (p3 - p2) * (3.0*t*t)
5862}
5863
5864pub fn auto_smooth_polyline(pts: &[Vec2], tension: f32, samples: usize) -> Vec<Vec2> {
5866 if pts.len() < 2 { return pts.to_vec(); }
5867 let mut result = Vec::new();
5868 for i in 0..pts.len() - 1 {
5869 let p0 = if i == 0 { pts[0] + (pts[0] - pts[1]) } else { pts[i-1] };
5870 let p1 = pts[i];
5871 let p2 = pts[i+1];
5872 let p3 = if i+2 >= pts.len() { pts[pts.len()-1] + (pts[pts.len()-1] - pts[pts.len()-2]) } else { pts[i+2] };
5873 let cp1 = p1 + (p2 - p0) * (tension / 6.0);
5874 let cp2 = p2 - (p3 - p1) * (tension / 6.0);
5875 for s in 0..samples {
5876 result.push(cubic_bezier(p1, cp1, cp2, p2, s as f32 / samples as f32));
5877 }
5878 }
5879 result.push(*pts.last().unwrap());
5880 result
5881}
5882
5883pub fn bezier_arc_length(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, steps: usize) -> f32 {
5885 let mut len = 0.0f32;
5886 let mut prev = p0;
5887 for i in 1..=steps {
5888 let t = i as f32 / steps as f32;
5889 let curr = cubic_bezier(p0, p1, p2, p3, t);
5890 len += (curr - prev).length();
5891 prev = curr;
5892 }
5893 len
5894}
5895
5896pub fn tile_heightmap(source: &Heightmap, tile_x: usize, tile_y: usize) -> Heightmap {
5902 let new_w = source.width * tile_x;
5903 let new_h = source.height * tile_y;
5904 let mut out = Heightmap::new(new_w, new_h);
5905 for ty in 0..tile_y {
5906 for tx in 0..tile_x {
5907 for y in 0..source.height {
5908 for x in 0..source.width {
5909 out.set(tx * source.width + x, ty * source.height + y, source.get(x, y));
5910 }
5911 }
5912 }
5913 }
5914 out.recompute_minmax();
5915 out
5916}
5917
5918pub fn crop_heightmap(source: &Heightmap, ox: usize, oy: usize, w: usize, h: usize) -> Heightmap {
5920 let x_end = (ox + w).min(source.width);
5921 let y_end = (oy + h).min(source.height);
5922 let out_w = x_end - ox;
5923 let out_h = y_end - oy;
5924 let mut out = Heightmap::new(out_w, out_h);
5925 for ry in 0..out_h { for rx in 0..out_w { out.set(rx, ry, source.get(ox + rx, oy + ry)); } }
5926 out.recompute_minmax();
5927 out
5928}
5929
5930pub fn stitch_heightmaps_horizontal(left: &Heightmap, right: &Heightmap, seam_width: usize) -> Heightmap {
5932 assert_eq!(left.height, right.height);
5933 let total_w = left.width + right.width;
5934 let h = left.height;
5935 let mut out = Heightmap::new(total_w, h);
5936 for y in 0..h {
5937 for x in 0..left.width { out.set(x, y, left.get(x, y)); }
5938 for x in 0..right.width { out.set(left.width + x, y, right.get(x, y)); }
5939 }
5940 let sw = seam_width.min(left.width).min(right.width);
5941 for y in 0..h {
5942 for s in 0..sw {
5943 let t = smoothstep(0.0, 1.0, s as f32 / sw as f32);
5944 let lx = left.width - sw + s;
5945 let rx = left.width + s;
5946 let blended = lerp_f(out.get(lx, y), out.get(rx, y), t);
5947 out.set(lx, y, blended);
5948 out.set(rx, y, blended);
5949 }
5950 }
5951 out.recompute_minmax();
5952 out
5953}
5954
5955#[derive(Clone, Debug)]
5960pub struct VertexColorMap {
5961 pub width: usize,
5962 pub height: usize,
5963 pub data: Vec<Vec4>,
5964}
5965
5966impl VertexColorMap {
5967 pub fn new(width: usize, height: usize, fill: Vec4) -> Self {
5968 VertexColorMap { width, height, data: vec![fill; width * height] }
5969 }
5970
5971 pub fn paint(&mut self, cx: f32, cy: f32, brush: &BrushSettings, color: Vec4) {
5972 let r = brush.radius.ceil() as i32;
5973 let x0 = ((cx as i32 - r).max(0)) as usize;
5974 let y0 = ((cy as i32 - r).max(0)) as usize;
5975 let x1 = ((cx as i32 + r).min(self.width as i32 - 1)) as usize;
5976 let y1 = ((cy as i32 + r).min(self.height as i32 - 1)) as usize;
5977 for y in y0..=y1 {
5978 for x in x0..=x1 {
5979 let dist = ((x as f32 - cx).powi(2) + (y as f32 - cy).powi(2)).sqrt();
5980 if dist > brush.radius { continue; }
5981 let w = brush.weight_at_radius(dist);
5982 let idx = y * self.width + x;
5983 let old = self.data[idx];
5984 let a = color.w * w;
5985 self.data[idx] = Vec4::new(
5986 lerp_f(old.x, color.x, a),
5987 lerp_f(old.y, color.y, a),
5988 lerp_f(old.z, color.z, a),
5989 lerp_f(old.w, 1.0, a),
5990 );
5991 }
5992 }
5993 }
5994
5995 pub fn sample_bilinear(&self, u: f32, v: f32) -> Vec4 {
5996 let px = u * (self.width - 1) as f32;
5997 let py = v * (self.height - 1) as f32;
5998 let x0 = px.floor() as usize;
5999 let y0 = py.floor() as usize;
6000 let x1 = (x0 + 1).min(self.width - 1);
6001 let y1 = (y0 + 1).min(self.height - 1);
6002 let tx = px - x0 as f32;
6003 let ty = py - y0 as f32;
6004 fn l4(a: Vec4, b: Vec4, t: f32) -> Vec4 { a + (b - a) * t }
6005 let a = self.data[y0 * self.width + x0];
6006 let b = self.data[y0 * self.width + x1];
6007 let c = self.data[y1 * self.width + x0];
6008 let d = self.data[y1 * self.width + x1];
6009 l4(l4(a, b, tx), l4(c, d, tx), ty)
6010 }
6011}
6012
6013#[derive(Clone, Debug)]
6018pub struct Decal {
6019 pub id: u32,
6020 pub position: Vec3,
6021 pub rotation_y: f32,
6022 pub size: Vec2,
6023 pub texture_id: u32,
6024 pub alpha: f32,
6025 pub tint: Vec4,
6026}
6027
6028impl Decal {
6029 pub fn transform(&self) -> Mat4 {
6030 Mat4::from_translation(self.position)
6031 * Mat4::from_rotation_y(self.rotation_y)
6032 * Mat4::from_scale(Vec3::new(self.size.x, 1.0, self.size.y))
6033 }
6034 pub fn world_aabb(&self) -> (Vec3, Vec3) {
6035 let hs = Vec3::new(self.size.x * 0.5, 1.0, self.size.y * 0.5);
6036 (self.position - hs, self.position + hs)
6037 }
6038}
6039
6040pub struct DecalLayer {
6041 pub decals: Vec<Decal>,
6042 pub next_id: u32,
6043}
6044
6045impl DecalLayer {
6046 pub fn new() -> Self { DecalLayer { decals: Vec::new(), next_id: 0 } }
6047 pub fn add(&mut self, position: Vec3, rotation_y: f32, size: Vec2, texture_id: u32) -> u32 {
6048 let id = self.next_id; self.next_id += 1;
6049 self.decals.push(Decal { id, position, rotation_y, size, texture_id, alpha: 1.0, tint: Vec4::ONE });
6050 id
6051 }
6052 pub fn remove(&mut self, id: u32) { self.decals.retain(|d| d.id != id); }
6053 pub fn query_sphere(&self, center: Vec3, radius: f32) -> Vec<&Decal> {
6054 self.decals.iter().filter(|d| (d.position - center).length() <= radius + d.size.x.max(d.size.y)).collect()
6055 }
6056}
6057
6058pub fn find_peaks(hmap: &Heightmap, min_height: f32, search_radius: usize) -> Vec<(usize, usize, f32)> {
6063 let w = hmap.width;
6064 let h = hmap.height;
6065 let r = search_radius as i32;
6066 let mut peaks = Vec::new();
6067 for y in r as usize..h - r as usize {
6068 for x in r as usize..w - r as usize {
6069 let ch = hmap.get(x, y);
6070 if ch < min_height { continue; }
6071 let mut is_max = true;
6072 'chk: for dy in -r..=r { for dx in -r..=r {
6073 if dx == 0 && dy == 0 { continue; }
6074 if hmap.get_clamped(x as i32 + dx, y as i32 + dy) > ch { is_max = false; break 'chk; }
6075 }}
6076 if is_max { peaks.push((x, y, ch)); }
6077 }
6078 }
6079 peaks
6080}
6081
6082pub fn find_cliffs(hmap: &Heightmap, cliff_threshold: f32) -> Vec<(usize, usize)> {
6083 let w = hmap.width;
6084 let h = hmap.height;
6085 let mut cliffs = Vec::new();
6086 for y in 1..h-1 { for x in 1..w-1 {
6087 let center = hmap.get(x, y);
6088 let max_diff = [hmap.get(x+1,y), hmap.get(x-1,y), hmap.get(x,y+1), hmap.get(x,y-1)]
6089 .iter().map(|&n| (center - n).abs()).fold(0.0f32, f32::max);
6090 if max_diff >= cliff_threshold { cliffs.push((x, y)); }
6091 }}
6092 cliffs
6093}
6094
6095#[derive(Clone, Debug)]
6100pub enum LayerOperation { Add, Multiply, Subtract, Max, Min, Blend(f32) }
6101
6102#[derive(Clone, Debug)]
6103pub struct TerrainLayerStack {
6104 pub base: Vec<f32>,
6105 pub layers: Vec<(Vec<f32>, LayerOperation, f32)>,
6106 pub width: usize,
6107 pub height: usize,
6108}
6109
6110impl TerrainLayerStack {
6111 pub fn new(width: usize, height: usize) -> Self {
6112 TerrainLayerStack { base: vec![0.0; width*height], layers: Vec::new(), width, height }
6113 }
6114 pub fn push_layer(&mut self, data: Vec<f32>, op: LayerOperation, strength: f32) {
6115 self.layers.push((data, op, strength));
6116 }
6117 pub fn flatten(&self) -> Vec<f32> {
6118 let mut result = self.base.clone();
6119 let n = result.len();
6120 for (layer_data, op, strength) in &self.layers {
6121 for i in 0..n.min(layer_data.len()) {
6122 result[i] = match op {
6123 LayerOperation::Add => (result[i] + layer_data[i] * strength).clamp(0.0, 1.0),
6124 LayerOperation::Multiply => result[i] * (1.0 + (layer_data[i] - 0.5) * strength * 2.0),
6125 LayerOperation::Subtract => (result[i] - layer_data[i] * strength).clamp(0.0, 1.0),
6126 LayerOperation::Max => result[i].max(layer_data[i]),
6127 LayerOperation::Min => result[i].min(layer_data[i]),
6128 LayerOperation::Blend(t) => lerp_f(result[i], layer_data[i], *t * strength),
6129 };
6130 }
6131 }
6132 result
6133 }
6134}
6135
6136
6137
6138impl WorldEditor {
6143 pub fn apply_island_mask(&mut self, falloff_exp: f32) {
6144 let mask = generate_island_mask(self.heightmap.width, self.heightmap.height, falloff_exp);
6145 apply_mask(&mut self.heightmap, &mask);
6146 self.heightmap_dirty = true;
6147 }
6148
6149 pub fn compute_snow_map(&self, snow_line: f32, temp_thresh: f32) -> Vec<f32> {
6150 let w = self.heightmap.width;
6151 let h = self.heightmap.height;
6152 let mut snow = vec![0.0f32; w * h];
6153 for y in 0..h { for x in 0..w {
6154 let idx = y * w + x;
6155 let alt = self.heightmap.get(x, y);
6156 let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
6157 if alt >= snow_line && temp <= temp_thresh {
6158 let af = ((alt - snow_line) / (1.0 - snow_line)).clamp(0.0, 1.0);
6159 let tf = ((temp_thresh - temp) / 30.0).clamp(0.0, 1.0);
6160 snow[idx] = (af * 0.6 + tf * 0.4).clamp(0.0, 1.0);
6161 }
6162 }}
6163 snow
6164 }
6165
6166 pub fn build_chunk_mesh(&self, chunk_x: usize, chunk_z: usize, chunk_size: usize, lod_step: usize) -> TerrainMesh {
6167 generate_terrain_mesh(&self.heightmap, chunk_x, chunk_z, chunk_size, self.cell_size, self.height_scale, lod_step)
6168 }
6169
6170 pub fn compute_ao(&self, num_rays: usize, max_dist: f32) -> Vec<f32> {
6171 compute_terrain_ao(&self.heightmap, num_rays, max_dist, self.cell_size)
6172 }
6173
6174 pub fn compute_shadow_map(&self) -> Vec<f32> {
6175 compute_terrain_shadow_map(&self.heightmap, self.solar.direction, self.cell_size, self.height_scale)
6176 }
6177
6178 pub fn render_minimap(&self, out_w: usize, out_h: usize) -> Vec<u8> {
6179 let ramp = ColorRamp::terrain_default();
6180 let w = self.heightmap.width;
6181 let h = self.heightmap.height;
6182 let mut bytes = Vec::with_capacity(out_w * out_h * 4);
6183 for my in 0..out_h {
6184 for mx in 0..out_w {
6185 let u = mx as f32 / out_w as f32;
6186 let v = my as f32 / out_h as f32;
6187 let ht = self.heightmap.sample_bilinear(u, v);
6188 let col = ramp.sample(ht);
6189 bytes.push((col.x * 255.0) as u8);
6190 bytes.push((col.y * 255.0) as u8);
6191 bytes.push((col.z * 255.0) as u8);
6192 bytes.push(255u8);
6193 }
6194 }
6195 bytes
6196 }
6197
6198 pub fn find_peaks(&self, min_height: f32, search_radius: usize) -> Vec<(usize, usize, f32)> {
6199 find_peaks(&self.heightmap, min_height, search_radius)
6200 }
6201
6202 pub fn generate_wind_field(&self, turbulence: f32) -> WindField {
6203 WindField::generate_from_terrain(&self.heightmap, self.weather.wind_vector(), turbulence, self.master_seed ^ 0xABCD0001)
6204 }
6205
6206 pub fn stamp_hill(&mut self, world_x: f32, world_z: f32, radius: f32, height: f32, sharpness: f32) {
6207 let cx = world_x / self.cell_size;
6208 let cz = world_z / self.cell_size;
6209 let r = (radius / self.cell_size) as usize;
6210 let w = self.heightmap.width;
6211 let h = self.heightmap.height;
6212 let x0 = ((cx as usize).saturating_sub(r)).min(w.saturating_sub(1));
6213 let y0 = ((cz as usize).saturating_sub(r)).min(h.saturating_sub(1));
6214 let x1 = ((cx as usize + r + 1)).min(w);
6215 let y1 = ((cz as usize + r + 1)).min(h);
6216 let rw = x1 - x0;
6217 let rh = y1 - y0;
6218 let mut stamp = vec![0.0f32; rw * rh];
6219 for ry in 0..rh { for rx in 0..rw {
6220 let dx = (x0 + rx) as f32 - cx;
6221 let dz = (y0 + ry) as f32 - cz;
6222 let dist = (dx*dx + dz*dz).sqrt() * self.cell_size;
6223 stamp[ry * rw + rx] = (1.0 - (dist / radius).clamp(0.0, 1.0).powf(sharpness)).max(0.0) * height / self.height_scale;
6224 }}
6225 let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, rw, rh, 1.0);
6226 self.undo_redo.push(action);
6227 self.heightmap_dirty = true;
6228 }
6229
6230
6231 pub fn world_report(&self) -> String {
6232 let s = &self.stats;
6233 format!(
6234 "World '{}' {}x{} @ {:.1}m | H: {:.3}..{:.3} | Sea: {:.3}\n\
6235 Sun: alt={:.1}° az={:.1}° ({}) | DoY: {} Time: {:.1}h\n\
6236 Weather: {} Temp: {:.1}°C Wind: {:.1}m/s@{:.0}° Cloud: {:.0}%\n\
6237 Content: {} foliage | {} rivers | {} lakes | {} roads ({:.0}m)",
6238 self.world_name,
6239 self.heightmap.width, self.heightmap.height,
6240 self.cell_size,
6241 s.min_height, s.max_height, self.sea_level,
6242 self.solar.altitude_deg, self.solar.azimuth_deg,
6243 if self.solar.is_day { "day" } else { "night" },
6244 self.day_of_year as usize, self.utc_hour,
6245 self.weather.current.state.name(),
6246 self.weather.current.temperature_c,
6247 self.weather.current.wind_speed_ms,
6248 self.weather.current.wind_dir_deg,
6249 self.weather.current.cloud_cover * 100.0,
6250 s.foliage_count, s.river_count, s.lake_count,
6251 s.road_segments, s.road_total_length,
6252 )
6253 }
6254
6255 pub fn validate(&self) -> Vec<String> {
6256 let mut w = Vec::new();
6257 if self.heightmap.width < 16 { w.push("Heightmap width very small".into()); }
6258 if self.heightmap.height < 16 { w.push("Heightmap height very small".into()); }
6259 if self.sea_level > 0.9 { w.push("Sea level very high".into()); }
6260 if self.foliage.len() > 500_000 { w.push("Very large foliage count".into()); }
6261 for (i, lake) in self.lakes.iter().enumerate() {
6262 if lake.water_level < self.sea_level { w.push(format!("Lake {} below sea level", i)); }
6263 }
6264 w
6265 }
6266
6267 pub fn is_navigable(&self, x: usize, y: usize, max_slope_deg: f32) -> bool {
6268 let alt = self.heightmap.get(x, y);
6269 if alt <= self.sea_level { return false; }
6270 self.heightmap.slope_at(x, y, self.cell_size) * RAD2DEG <= max_slope_deg
6271 }
6272
6273 pub fn export_nav_passability(&self, max_slope_deg: f32) -> Vec<bool> {
6274 let w = self.heightmap.width;
6275 let h = self.heightmap.height;
6276 (0..h).flat_map(|y| (0..w).map(move |x| self.is_navigable(x, y, max_slope_deg))).collect()
6277 }
6278
6279 pub fn day_length_hours(&self) -> f64 {
6280 match sunrise_sunset(self.latitude, self.longitude, self.day_of_year) {
6281 Some((rise, set)) => set - rise,
6282 None => if self.solar.altitude_deg > 0.0 { 24.0 } else { 0.0 },
6283 }
6284 }
6285
6286 pub fn rebuild_all(&mut self) {
6287 self.generate_climate();
6288 self.compute_stats();
6289 self.update_solar();
6290 self.heightmap_dirty = false;
6291 self.climate_dirty = false;
6292 }
6293
6294 pub fn sample_sky_color(&self, view_dir: Vec3) -> Vec3 {
6295 let clear = self.sky_at(view_dir);
6296 let cloud = Vec3::new(0.8, 0.85, 0.9);
6297 clear.lerp(cloud, self.weather.current.cloud_cover)
6298 }
6299
6300
6301 pub fn compute_viewshed(&self, obs_x: usize, obs_z: usize, obs_height: f32, max_dist: f32) -> Vec<bool> {
6302 let w = self.heightmap.width;
6303 let h = self.heightmap.height;
6304 let obs_h = self.heightmap.get(obs_x, obs_z) * self.height_scale + obs_height;
6305 let mut visible = vec![false; w * h];
6306 for tz in 0..h { for tx in 0..w {
6307 let dc = (((tx as i32 - obs_x as i32).pow(2) + (tz as i32 - obs_z as i32).pow(2)) as f32).sqrt();
6308 if dc * self.cell_size > max_dist { continue; }
6309 let steps = dc.ceil() as usize;
6310 if steps == 0 { visible[tz*w+tx] = true; continue; }
6311 let tgt_h = self.heightmap.get(tx.min(w-1), tz.min(h-1)) * self.height_scale;
6312 let mut los = true;
6313 for s in 1..steps {
6314 let t = s as f32 / steps as f32;
6315 let lx = obs_x as f32 + (tx as f32 - obs_x as f32) * t;
6316 let lz = obs_z as f32 + (tz as f32 - obs_z as f32) * t;
6317 let ux = (lx / w as f32).clamp(0.0, 1.0);
6318 let uz = (lz / h as f32).clamp(0.0, 1.0);
6319 let th = self.heightmap.sample_bilinear(ux, uz) * self.height_scale;
6320 let los_h = obs_h + (tgt_h - obs_h) * t;
6321 if th > los_h { los = false; break; }
6322 }
6323 visible[tz*w+tx] = los;
6324 }}
6325 visible
6326 }
6327}
6328
6329
6330pub fn halton(index: usize, base: usize) -> f32 {
6335 let mut f = 1.0f32;
6336 let mut r = 0.0f32;
6337 let mut i = index;
6338 while i > 0 {
6339 f /= base as f32;
6340 r += f * (i % base) as f32;
6341 i /= base;
6342 }
6343 r
6344}
6345
6346pub fn halton_2d(count: usize) -> Vec<Vec2> {
6347 (0..count).map(|i| Vec2::new(halton(i+1, 2), halton(i+1, 3))).collect()
6348}
6349
6350pub fn fibonacci_sphere_points(n: usize) -> Vec<Vec3> {
6351 let gr = (1.0 + 5.0_f32.sqrt()) * 0.5;
6352 (0..n).map(|i| {
6353 let theta = (1.0 - 2.0 * i as f32 / (n as f32 - 1.0)).acos();
6354 let phi = TWO_PI * i as f32 / gr;
6355 Vec3::new(theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos())
6356 }).collect()
6357}
6358
6359
6360pub const BEAUFORT_NAMES: [&str; 13] = [
6361 "Calm","Light air","Light breeze","Gentle breeze","Moderate breeze",
6362 "Fresh breeze","Strong breeze","Near gale","Gale","Strong gale",
6363 "Storm","Violent storm","Hurricane",
6364];
6365
6366pub fn classify_cloud(coverage: f32, altitude_km: f32) -> &'static str {
6367 if coverage < 0.1 { return "Clear"; }
6368 if altitude_km < 2.0 { if coverage > 0.7 { "Stratus" } else { "Stratocumulus" } }
6369 else if altitude_km < 6.0 { if coverage > 0.6 { "Altostratus" } else { "Altocumulus" } }
6370 else { if coverage > 0.5 { "Cirrostratus" } else { "Cirrus" } }
6371}
6372
6373pub fn pressure_tendency(history: &VecDeque<WeatherSnapshot>) -> &'static str {
6374 if history.len() < 3 { return "Steady"; }
6375 let v: Vec<f32> = history.iter().rev().take(3).map(|s| s.pressure_hpa).collect();
6376 let trend = v[0] - v[2];
6377 if trend > 1.5 { "Rising rapidly" } else if trend > 0.5 { "Rising" }
6378 else if trend < -1.5 { "Falling rapidly" } else if trend < -0.5 { "Falling" }
6379 else { "Steady" }
6380}
6381
6382pub fn compute_biome_weight_map(
6387 hmap: &Heightmap,
6388 temp_map: &[f32],
6389 hum_map: &[f32],
6390 biome_sys: &BiomeSystem,
6391) -> Vec<[f32; 25]> {
6392 let w = hmap.width;
6393 let h = hmap.height;
6394 let mut wmap = vec![[0.0f32; 25]; w * h];
6395 for y in 0..h { for x in 0..w {
6396 let idx = y * w + x;
6397 let alt = hmap.get(x, y);
6398 let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
6399 let hum = if idx < hum_map.len() { hum_map[idx] } else { 0.5 };
6400 wmap[idx] = biome_sys.sample(temp, hum, alt).weights;
6401 }}
6402 wmap
6403}
6404
6405pub fn build_biome_id_map(weight_map: &[[f32; 25]]) -> Vec<u8> {
6406 weight_map.iter().map(|ws|
6407 ws.iter().enumerate()
6408 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
6409 .map(|(i, _)| i as u8).unwrap_or(0)
6410 ).collect()
6411}
6412
6413
6414#[derive(Clone, Debug)]
6419pub struct ShallowWaterSim {
6420 pub width: usize,
6421 pub height: usize,
6422 pub height_h: Vec<f32>,
6423 pub vel_x: Vec<f32>,
6424 pub vel_z: Vec<f32>,
6425 pub depth: Vec<f32>,
6426 pub cell_size: f32,
6427 pub gravity: f32,
6428 pub friction: f32,
6429}
6430
6431impl ShallowWaterSim {
6432 pub fn new(width: usize, height: usize, cell_size: f32, gravity: f32) -> Self {
6433 let n = width * height;
6434 ShallowWaterSim { width, height, height_h: vec![0.0;n], vel_x: vec![0.0;n], vel_z: vec![0.0;n], depth: vec![0.0;n], cell_size, gravity, friction: 0.99 }
6435 }
6436
6437 pub fn init_from_heightmap(&mut self, terrain: &Heightmap, sea_level: f32, height_scale: f32) {
6438 for y in 0..self.height { for x in 0..self.width {
6439 let th = terrain.get(x.min(terrain.width-1), y.min(terrain.height-1)) * height_scale;
6440 let wh = sea_level * height_scale;
6441 let idx = y * self.width + x;
6442 self.height_h[idx] = wh;
6443 self.depth[idx] = (wh - th).max(0.0);
6444 }}
6445 }
6446
6447 pub fn step(&mut self, terrain: &Heightmap, height_scale: f32, dt: f32) {
6448 let w = self.width; let h = self.height;
6449 let g = self.gravity; let cs = self.cell_size;
6450 let hh = self.height_h.clone();
6451 for y in 1..h-1 { for x in 1..w-1 {
6452 let idx = y*w+x;
6453 let depth = self.depth[idx];
6454 if depth < 0.001 { continue; }
6455 let dhdx = (hh[y*w+x+1] - hh[y*w+x-1]) / (2.0*cs);
6456 let dhdz = (hh[(y+1)*w+x] - hh[(y-1)*w+x]) / (2.0*cs);
6457 self.vel_x[idx] = (self.vel_x[idx] - g*dhdx*dt) * self.friction;
6458 self.vel_z[idx] = (self.vel_z[idx] - g*dhdz*dt) * self.friction;
6459 }}
6460 let vx = self.vel_x.clone();
6461 let vz = self.vel_z.clone();
6462 for y in 1..h-1 { for x in 1..w-1 {
6463 let idx = y*w+x;
6464 let depth = self.depth[idx];
6465 if depth < 0.001 { continue; }
6466 let fx = vx[idx] * depth * dt / cs;
6467 let fz = vz[idx] * depth * dt / cs;
6468 let nx = (x as i32 + fx.signum() as i32).clamp(0, w as i32 - 1) as usize;
6469 let nz = (y as i32 + fz.signum() as i32).clamp(0, h as i32 - 1) as usize;
6470 let tx_a = fx.abs().min(depth * 0.5);
6471 let tz_a = fz.abs().min(depth * 0.5);
6472 self.height_h[idx] -= tx_a + tz_a;
6473 self.height_h[nz*w+x] += tz_a;
6474 self.height_h[y*w+nx] += tx_a;
6475 let th = terrain.get(x.min(terrain.width-1), y.min(terrain.height-1)) * height_scale;
6476 self.depth[idx] = (self.height_h[idx] - th).max(0.0);
6477 }}
6478 }
6479}
6480
6481pub fn gen_rock_texture(width: usize, height: usize, seed: u64) -> Vec<f32> {
6486 let params = FbmParams { octaves: 6, frequency: 4.0, lacunarity: 2.1, gain: 0.55, amplitude: 1.0, offset: 1.0, ridge: true };
6487 let off = (seed as f32 * 1e-5, seed as f32 * 1e-5 + 50.0);
6488 let mut out = vec![0.0f32; width * height];
6489 for y in 0..height { for x in 0..width {
6490 out[y * width + x] = (fbm_2d(x as f32 / width as f32 + off.0, y as f32 / height as f32 + off.1, ¶ms) * 0.5 + 0.5).clamp(0.0, 1.0);
6491 }}
6492 out
6493}
6494
6495pub fn gen_soil_texture(width: usize, height: usize, seed: u64) -> Vec<f32> {
6496 let base_p = FbmParams { octaves: 4, frequency: 8.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
6497 let crack_p = FbmParams { octaves: 3, frequency: 12.0, lacunarity: 2.5, gain: 0.4, amplitude: 0.5, offset: 0.0, ridge: false };
6498 let off = (seed as f32 * 1e-5 + 100.0, seed as f32 * 1e-5 + 200.0);
6499 let mut out = vec![0.0f32; width * height];
6500 for y in 0..height { for x in 0..width {
6501 let nx = x as f32 / width as f32 + off.0;
6502 let ny = y as f32 / height as f32 + off.1;
6503 let b = fbm_2d(nx, ny, &base_p) * 0.5 + 0.5;
6504 let cr = fbm_2d(nx * 0.5, ny * 0.5, &crack_p).abs();
6505 out[y * width + x] = (b * 0.7 + cr * 0.3).clamp(0.0, 1.0);
6506 }}
6507 out
6508}
6509
6510
6511#[derive(Clone, Debug)]
6516pub struct TerrainPaintLayer {
6517 pub id: usize,
6518 pub name: String,
6519 pub weight_map: Vec<f32>,
6520 pub tiling: f32,
6521 pub normal_strength: f32,
6522}
6523
6524impl TerrainPaintLayer {
6525 pub fn new(id: usize, name: &str, width: usize, height: usize) -> Self {
6526 TerrainPaintLayer { id, name: name.into(), weight_map: vec![0.0; width*height], tiling: 10.0, normal_strength: 1.0 }
6527 }
6528
6529 pub fn paint(&mut self, cx: f32, cy: f32, brush: &BrushSettings, width: usize, height: usize) {
6530 let r = brush.radius.ceil() as i32;
6531 let x0 = ((cx as i32 - r).max(0)) as usize;
6532 let y0 = ((cy as i32 - r).max(0)) as usize;
6533 let x1 = ((cx as i32 + r).min(width as i32 - 1)) as usize;
6534 let y1 = ((cy as i32 + r).min(height as i32 - 1)) as usize;
6535 for y in y0..=y1 { for x in x0..=x1 {
6536 let dist = ((x as f32 - cx).powi(2) + (y as f32 - cy).powi(2)).sqrt();
6537 if dist > brush.radius { continue; }
6538 let idx = y * width + x;
6539 self.weight_map[idx] = (self.weight_map[idx] + brush.weight_at_radius(dist)).clamp(0.0, 1.0);
6540 }}
6541 }
6542}
6543
6544#[derive(Clone, Debug)]
6549pub struct AtmosphericHaze {
6550 pub density: f32,
6551 pub haze_color: Vec3,
6552 pub fog_start: f32,
6553 pub fog_end: f32,
6554 pub height_falloff: f32,
6555}
6556
6557impl AtmosphericHaze {
6558 pub fn new(density: f32, color: Vec3) -> Self {
6559 AtmosphericHaze { density, haze_color: color, fog_start: 100.0, fog_end: 5000.0, height_falloff: 0.002 }
6560 }
6561 pub fn fog_factor(&self, distance: f32, height: f32) -> f32 {
6562 (smoothstep(self.fog_start, self.fog_end, distance) * (-(height * self.height_falloff)).exp() * self.density).clamp(0.0, 1.0)
6563 }
6564 pub fn apply(&self, color: Vec3, distance: f32, height: f32) -> Vec3 {
6565 color.lerp(self.haze_color, self.fog_factor(distance, height))
6566 }
6567}
6568
6569
6570pub fn find_erosion_sources(hmap: &Heightmap, count: usize, min_altitude: f32, min_slope: f32, seed: u64) -> Vec<Vec2> {
6575 let w = hmap.width;
6576 let h = hmap.height;
6577 let mut candidates: Vec<(f32, Vec2)> = Vec::new();
6578 for y in 0..h { for x in 0..w {
6579 let alt = hmap.get(x, y);
6580 let slope = hmap.slope_at(x, y, 1.0);
6581 if alt >= min_altitude && slope >= min_slope {
6582 candidates.push((alt * slope, Vec2::new(x as f32, y as f32)));
6583 }
6584 }}
6585 candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
6586 let min_spacing = ((w * h) as f32 / count as f32).sqrt() * 0.3;
6587 let mut result = Vec::new();
6588 for (_, pos) in candidates.iter() {
6589 if result.len() >= count { break; }
6590 if !result.iter().any(|p: &Vec2| (*p - *pos).length() < min_spacing) {
6591 result.push(*pos);
6592 }
6593 }
6594 result
6595}
6596
6597pub fn warp_heightmap(hmap: &Heightmap, disp_x: &[f32], disp_z: &[f32], strength: f32) -> Heightmap {
6602 let w = hmap.width;
6603 let h = hmap.height;
6604 let mut out = Heightmap::new(w, h);
6605 for y in 0..h { for x in 0..w {
6606 let idx = y * w + x;
6607 let dx = if idx < disp_x.len() { disp_x[idx] * strength } else { 0.0 };
6608 let dz = if idx < disp_z.len() { disp_z[idx] * strength } else { 0.0 };
6609 let src_x = (x as f32 + dx * w as f32).clamp(0.0, (w-1) as f32);
6610 let src_z = (y as f32 + dz * h as f32).clamp(0.0, (h-1) as f32);
6611 out.set(x, y, hmap.sample_bilinear(src_x / (w-1) as f32, src_z / (h-1) as f32));
6612 }}
6613 out.recompute_minmax();
6614 out
6615}
6616
6617
6618
6619#[derive(Clone, Debug)]
6620pub struct FlareElement { pub offset: f32, pub size: f32, pub color: Vec4, pub texture_id: u32 }
6621
6622#[derive(Clone, Debug)]
6623pub struct LensFlare {
6624 pub elements: Vec<FlareElement>,
6625 pub intensity: f32,
6626 pub streak_count: u8,
6627 pub streak_size: f32,
6628}
6629
6630impl LensFlare {
6631 pub fn sun_flare() -> Self {
6632 LensFlare {
6633 elements: vec![
6634 FlareElement { offset: 0.0, size: 0.15, color: Vec4::new(1.0, 0.9, 0.7, 0.8), texture_id: 0 },
6635 FlareElement { offset: 0.2, size: 0.05, color: Vec4::new(0.8, 0.8, 1.0, 0.4), texture_id: 1 },
6636 FlareElement { offset: 0.5, size: 0.08, color: Vec4::new(1.0, 0.7, 0.3, 0.3), texture_id: 2 },
6637 FlareElement { offset: 0.8, size: 0.04, color: Vec4::new(0.7, 1.0, 0.7, 0.2), texture_id: 1 },
6638 FlareElement { offset: 1.2, size: 0.10, color: Vec4::new(0.6, 0.8, 1.0, 0.2), texture_id: 0 },
6639 ],
6640 intensity: 1.0, streak_count: 6, streak_size: 0.4,
6641 }
6642 }
6643 pub fn screen_positions<'a>(&'a self, sun_screen: Vec2, screen_center: Vec2) -> Vec<(Vec2, &'a FlareElement)> {
6644 let axis = screen_center - sun_screen;
6645 self.elements.iter().map(|e| (sun_screen + axis * e.offset, e)).collect()
6646 }
6647}
6648
6649
6650#[derive(Clone, Debug)]
6651pub struct NavCell {
6652 pub x: usize,
6653 pub y: usize,
6654 pub passable: bool,
6655 pub cost: f32,
6656 pub region_id: u32,
6657}
6658
6659#[derive(Clone, Debug)]
6660pub struct NavGrid {
6661 pub width: usize,
6662 pub height: usize,
6663 pub cells: Vec<NavCell>,
6664}
6665
6666impl NavGrid {
6667 pub fn from_heightmap(hmap: &Heightmap, cell_size: f32, sea_level: f32, max_slope_deg: f32) -> Self {
6668 let w = hmap.width;
6669 let h = hmap.height;
6670 let cells: Vec<NavCell> = (0..h).flat_map(|y| (0..w).map(move |x| {
6671 let alt = hmap.get(x, y);
6672 let slope = hmap.slope_at(x.min(w-1), y.min(h-1), cell_size) * RAD2DEG;
6673 let pass = alt > sea_level && slope <= max_slope_deg;
6674 let cost = 1.0 + slope / max_slope_deg;
6675 NavCell { x, y, passable: pass, cost, region_id: 0 }
6676 })).collect();
6677 NavGrid { width: w, height: h, cells }
6678 }
6679
6680 pub fn get(&self, x: usize, y: usize) -> &NavCell {
6681 &self.cells[y * self.width + x]
6682 }
6683
6684 pub fn label_regions(&mut self) {
6685 let w = self.width;
6686 let h = self.height;
6687 let mut region = 0u32;
6688 let mut visited = vec![false; w * h];
6689
6690 for sy in 0..h {
6691 for sx in 0..w {
6692 if visited[sy * w + sx] || !self.cells[sy * w + sx].passable { continue; }
6693 region += 1;
6694 let mut queue = VecDeque::new();
6695 queue.push_back((sx, sy));
6696 visited[sy * w + sx] = true;
6697 while let Some((cx, cy)) = queue.pop_front() {
6698 self.cells[cy * w + cx].region_id = region;
6699 let neighbors: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
6700 for &(dx, dy) in &neighbors {
6701 let nx = cx as i32 + dx;
6702 let ny = cy as i32 + dy;
6703 if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 { continue; }
6704 let ni = ny as usize * w + nx as usize;
6705 if !visited[ni] && self.cells[ni].passable {
6706 visited[ni] = true;
6707 queue.push_back((nx as usize, ny as usize));
6708 }
6709 }
6710 }
6711 }
6712 }
6713 }
6714
6715 pub fn region_count(&self) -> u32 {
6716 self.cells.iter().map(|c| c.region_id).max().unwrap_or(0)
6717 }
6718
6719 pub fn largest_region_size(&self) -> usize {
6720 let mut counts: HashMap<u32, usize> = HashMap::new();
6721 for c in &self.cells { if c.passable { *counts.entry(c.region_id).or_insert(0) += 1; } }
6722 counts.values().copied().max().unwrap_or(0)
6723 }
6724}
6725
6726#[derive(Clone, Debug)]
6731pub struct HeightmapStats {
6732 pub min: f32,
6733 pub max: f32,
6734 pub mean: f32,
6735 pub median: f32,
6736 pub stddev: f32,
6737 pub skewness: f32,
6738 pub percentile_25: f32,
6739 pub percentile_75: f32,
6740 pub histogram: Vec<u32>, }
6742
6743pub fn compute_heightmap_stats(hmap: &Heightmap) -> HeightmapStats {
6744 let n = hmap.data.len();
6745 if n == 0 {
6746 return HeightmapStats { min:0.0, max:0.0, mean:0.0, median:0.0, stddev:0.0,
6747 skewness:0.0, percentile_25:0.0, percentile_75:0.0, histogram: vec![0;256] };
6748 }
6749
6750 let mut sorted = hmap.data.clone();
6751 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6752
6753 let min = *sorted.first().unwrap();
6754 let max = *sorted.last().unwrap();
6755 let sum: f64 = sorted.iter().map(|&v| v as f64).sum();
6756 let mean = (sum / n as f64) as f32;
6757 let median = sorted[n / 2];
6758 let p25 = sorted[n / 4];
6759 let p75 = sorted[3 * n / 4];
6760
6761 let variance: f64 = sorted.iter().map(|&v| { let d = v as f64 - mean as f64; d*d }).sum::<f64>() / n as f64;
6762 let stddev = variance.sqrt() as f32;
6763
6764 let skewness: f64 = if stddev > 1e-10 {
6765 sorted.iter().map(|&v| { let d = (v as f64 - mean as f64) / stddev as f64; d*d*d }).sum::<f64>() / n as f64
6766 } else { 0.0 };
6767
6768 let range = max - min;
6769 let mut histogram = vec![0u32; 256];
6770 for &v in &hmap.data {
6771 if range > 1e-10 {
6772 let b = ((v - min) / range * 255.0).clamp(0.0, 255.0) as usize;
6773 histogram[b] += 1;
6774 }
6775 }
6776
6777 HeightmapStats { min, max, mean, median, stddev, skewness: skewness as f32, percentile_25: p25, percentile_75: p75, histogram }
6778}
6779
6780impl ErosionParams {
6785 pub fn preset_light() -> Self {
6786 ErosionParams { num_particles: 20_000, inertia: 0.03, capacity: 3.0,
6787 deposition: 0.4, erosion_speed: 0.2, evaporation: 0.025, min_slope: 0.005,
6788 gravity: 3.0, max_steps: 48, erosion_radius: 2.0, seed: 0xDEAD }
6789 }
6790 pub fn preset_heavy() -> Self {
6791 ErosionParams { num_particles: 150_000, inertia: 0.06, capacity: 6.0,
6792 deposition: 0.2, erosion_speed: 0.5, evaporation: 0.015, min_slope: 0.01,
6793 gravity: 5.0, max_steps: 80, erosion_radius: 4.0, seed: 0xBEEF }
6794 }
6795 pub fn preset_rivers() -> Self {
6796 ErosionParams { num_particles: 80_000, inertia: 0.08, capacity: 8.0,
6797 deposition: 0.1, erosion_speed: 0.8, evaporation: 0.01, min_slope: 0.02,
6798 gravity: 6.0, max_steps: 120, erosion_radius: 5.0, seed: 0xFACE }
6799 }
6800}
6801
6802impl FbmParams {
6807 pub fn mountains() -> Self {
6808 FbmParams { octaves: 8, frequency: 1.0, lacunarity: 2.1, gain: 0.52, amplitude: 1.0, offset: 1.0, ridge: true }
6809 }
6810 pub fn plains() -> Self {
6811 FbmParams { octaves: 4, frequency: 0.5, lacunarity: 2.0, gain: 0.6, amplitude: 0.4, offset: 0.0, ridge: false }
6812 }
6813 pub fn hills() -> Self {
6814 FbmParams { octaves: 6, frequency: 1.5, lacunarity: 2.0, gain: 0.55, amplitude: 0.7, offset: 0.0, ridge: false }
6815 }
6816 pub fn canyon() -> Self {
6817 FbmParams { octaves: 5, frequency: 1.2, lacunarity: 2.3, gain: 0.45, amplitude: 1.0, offset: 0.8, ridge: true }
6818 }
6819 pub fn island() -> Self {
6820 FbmParams { octaves: 7, frequency: 1.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false }
6821 }
6822}
6823
6824#[inline]
6830pub fn cubic_hermite(y0: f32, y1: f32, y2: f32, y3: f32, t: f32) -> f32 {
6831 let a = -0.5*y0 + 1.5*y1 - 1.5*y2 + 0.5*y3;
6832 let b = y0 - 2.5*y1 + 2.0*y2 - 0.5*y3;
6833 let c = -0.5*y0 + 0.5*y2;
6834 let d = y1;
6835 ((a*t + b)*t + c)*t + d
6836}
6837
6838#[inline]
6840pub fn quintic_interp(t: f32) -> f32 {
6841 t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
6842}
6843
6844pub fn slerp(a: Quat, b: Quat, t: f32) -> Quat {
6846 let dot = a.dot(b).clamp(-1.0, 1.0);
6847 let b_adj = if dot < 0.0 { Quat::from_array([-b.x,-b.y,-b.z,-b.w]) } else { b };
6848 let dot_adj = dot.abs();
6849 if dot_adj > 0.9995 {
6850 return Quat::from_array([
6851 a.x + (b_adj.x - a.x) * t,
6852 a.y + (b_adj.y - a.y) * t,
6853 a.z + (b_adj.z - a.z) * t,
6854 a.w + (b_adj.w - a.w) * t,
6855 ]).normalize();
6856 }
6857 let theta_0 = dot_adj.acos();
6858 let theta = theta_0 * t;
6859 let sin_t0 = theta_0.sin();
6860 let sin_t = theta.sin();
6861 let s1 = (theta_0 - theta).sin() / sin_t0;
6862 let s2 = sin_t / sin_t0;
6863 Quat::from_array([
6864 a.x * s1 + b_adj.x * s2,
6865 a.y * s1 + b_adj.y * s2,
6866 a.z * s1 + b_adj.z * s2,
6867 a.w * s1 + b_adj.w * s2,
6868 ])
6869}
6870
6871pub fn inverse_bilinear(p: Vec2, a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> Option<Vec2> {
6873 let e = b - a;
6875 let f = c - a;
6876 let g = a - b - c + d;
6877 let h = p - a;
6878
6879 let k2 = g.perp_dot(e);
6881 let k1 = e.perp_dot(h) + g.perp_dot(f); let k0 = f.perp_dot(h);
6883
6884 let (v, u);
6885 if k2.abs() < 1e-6 {
6886 if k1.abs() < 1e-6 { return None; }
6887 v = -k0 / k1;
6888 let denom = e.x + g.x * v;
6889 u = if denom.abs() > 1e-6 { (h.x - f.x * v) / denom } else { (h.y - f.y * v) / (e.y + g.y * v) };
6890 } else {
6891 let disc = k1 * k1 - 4.0 * k0 * k2;
6892 if disc < 0.0 { return None; }
6893 v = (-k1 - disc.sqrt()) / (2.0 * k2);
6894 let denom = e.x + g.x * v;
6895 u = if denom.abs() > 1e-6 { (h.x - f.x * v) / denom } else { (h.y - f.y * v) / (e.y + g.y * v) };
6896 }
6897
6898 Some(Vec2::new(u, v))
6899}
6900
6901#[derive(Clone, Debug)]
6906pub struct WorldSeedPreset {
6907 pub name: &'static str,
6908 pub seed: u64,
6909 pub style: WorldStyle,
6910 pub size: usize,
6911}
6912
6913#[derive(Clone, Debug, PartialEq, Eq)]
6914pub enum WorldStyle {
6915 Continental,
6916 Island,
6917 Archipelago,
6918 Mountains,
6919 Desert,
6920 Tundra,
6921 Jungle,
6922 Mixed,
6923}
6924
6925pub const WORLD_SEED_PRESETS: [WorldSeedPreset; 12] = [
6926 WorldSeedPreset { name: "Verdant Valley", seed: 0x1A2B3C4D, style: WorldStyle::Continental, size: 512 },
6927 WorldSeedPreset { name: "Dragon's Peak", seed: 0xDEAD1234, style: WorldStyle::Mountains, size: 1024 },
6928 WorldSeedPreset { name: "Lost Atoll", seed: 0x42424242, style: WorldStyle::Island, size: 512 },
6929 WorldSeedPreset { name: "Frozen North", seed: 0xCE000001, style: WorldStyle::Tundra, size: 1024 },
6930 WorldSeedPreset { name: "Amber Waste", seed: 0xDEAD5A1D, style: WorldStyle::Desert, size: 512 },
6931 WorldSeedPreset { name: "Emerald Canopy", seed: 0x74726545, style: WorldStyle::Jungle, size: 1024 },
6932 WorldSeedPreset { name: "Shattered Isles", seed: 0xB0CA5501, style: WorldStyle::Archipelago, size: 2048 },
6933 WorldSeedPreset { name: "Old Frontier", seed: 0xF121E510, style: WorldStyle::Mixed, size: 1024 },
6934 WorldSeedPreset { name: "Crystal Spires", seed: 0xCCC00DDD, style: WorldStyle::Mountains, size: 512 },
6935 WorldSeedPreset { name: "River Delta", seed: 0xD37741AA, style: WorldStyle::Continental, size: 1024 },
6936 WorldSeedPreset { name: "Thunder Plains", seed: 0xBADC0DE1, style: WorldStyle::Mixed, size: 2048 },
6937 WorldSeedPreset { name: "Ancient Caldera", seed: 0xCA1D3EA0, style: WorldStyle::Island, size: 512 },
6938];
6939
6940pub fn fill_sinks(hmap: &mut Heightmap, epsilon: f32) {
6946 let w = hmap.width;
6947 let h = hmap.height;
6948 let big = 1e9f32;
6949 let mut wl = vec![big; w * h];
6950
6951 for x in 0..w {
6953 wl[0 * w + x] = hmap.get(x, 0);
6954 wl[(h-1) * w + x] = hmap.get(x, h-1);
6955 }
6956 for y in 0..h {
6957 wl[y * w + 0] = hmap.get(0, y);
6958 wl[y * w + w-1] = hmap.get(w-1, y);
6959 }
6960
6961 let dirs: [(i32, i32); 8] = [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)];
6962
6963 let mut changed = true;
6965 let mut iter = 0;
6966 while changed && iter < 1000 {
6967 changed = false;
6968 iter += 1;
6969 for y in 1..h-1 {
6970 for x in 1..w-1 {
6971 let idx = y * w + x;
6972 let hval = hmap.data[idx];
6973 let mut new_wl = wl[idx];
6974 for &(dx, dy) in &dirs {
6975 let nx = (x as i32 + dx) as usize;
6976 let ny = (y as i32 + dy) as usize;
6977 let nidx = ny * w + nx;
6978 let candidate = wl[nidx] + epsilon;
6979 if hval >= candidate {
6980 new_wl = new_wl.min(hval);
6981 } else {
6982 new_wl = new_wl.min(candidate);
6983 }
6984 }
6985 if new_wl < wl[idx] {
6986 wl[idx] = new_wl;
6987 changed = true;
6988 }
6989 }
6990 }
6991 }
6992
6993 for (i, v) in wl.iter().enumerate() {
6995 if *v < big * 0.5 {
6996 hmap.data[i] = hmap.data[i].max(*v);
6997 }
6998 }
6999 hmap.recompute_minmax();
7000}
7001
7002pub fn cell_neighbors_8(x: usize, y: usize, width: usize, height: usize) -> Vec<(usize, usize)> {
7008 let mut result = Vec::with_capacity(8);
7009 let xi = x as i32;
7010 let yi = y as i32;
7011 for dy in -1..=1i32 {
7012 for dx in -1..=1i32 {
7013 if dx == 0 && dy == 0 { continue; }
7014 let nx = xi + dx;
7015 let ny = yi + dy;
7016 if nx >= 0 && ny >= 0 && nx < width as i32 && ny < height as i32 {
7017 result.push((nx as usize, ny as usize));
7018 }
7019 }
7020 }
7021 result
7022}
7023
7024pub fn cell_neighbors_4(x: usize, y: usize, width: usize, height: usize) -> Vec<(usize, usize)> {
7025 let mut result = Vec::with_capacity(4);
7026 let xi = x as i32;
7027 let yi = y as i32;
7028 for &(dx, dy) in &[(1i32,0i32),(-1,0),(0,1),(0,-1)] {
7029 let nx = xi + dx;
7030 let ny = yi + dy;
7031 if nx >= 0 && ny >= 0 && nx < width as i32 && ny < height as i32 {
7032 result.push((nx as usize, ny as usize));
7033 }
7034 }
7035 result
7036}
7037
7038pub fn flood_fill_bool(mask: &mut Vec<bool>, width: usize, height: usize, sx: usize, sy: usize, fill_value: bool) {
7040 let init = mask[sy * width + sx];
7041 if init == fill_value { return; }
7042 let mut queue = VecDeque::new();
7043 queue.push_back((sx, sy));
7044 mask[sy * width + sx] = fill_value;
7045 while let Some((cx, cy)) = queue.pop_front() {
7046 for (nx, ny) in cell_neighbors_4(cx, cy, width, height) {
7047 if mask[ny * width + nx] != fill_value {
7048 mask[ny * width + nx] = fill_value;
7049 queue.push_back((nx, ny));
7050 }
7051 }
7052 }
7053}
7054
7055#[derive(Clone, Debug)]
7060pub struct WorldGenProfile {
7061 pub name: String,
7062 pub width: usize,
7063 pub height: usize,
7064 pub cell_size_m: f32,
7065 pub height_scale_m: f32,
7066 pub sea_level_frac: f32,
7067 pub fbm_params: FbmParams,
7068 pub warp_strength: f32,
7069 pub erosion_params: ErosionParams,
7070 pub thermal_iters: usize,
7071 pub thermal_talus_deg: f32,
7072 pub num_rivers: usize,
7073 pub num_lakes: usize,
7074 pub foliage_density: f32,
7075 pub apply_island_mask: bool,
7076 pub island_falloff: f32,
7077 pub fill_sinks: bool,
7078 pub latitude: f64,
7079 pub longitude: f64,
7080 pub start_doy: f64,
7081 pub start_utc: f64,
7082}
7083
7084impl WorldGenProfile {
7085 pub fn default_continental() -> Self {
7086 WorldGenProfile {
7087 name: "Continental".into(), width: 1024, height: 1024, cell_size_m: 1.0,
7088 height_scale_m: 500.0, sea_level_frac: 0.22, fbm_params: FbmParams::default_terrain(),
7089 warp_strength: 0.35, erosion_params: ErosionParams::default(), thermal_iters: 5,
7090 thermal_talus_deg: 32.0, num_rivers: 8, num_lakes: 5, foliage_density: 0.8,
7091 apply_island_mask: false, island_falloff: 2.0, fill_sinks: true,
7092 latitude: 45.0, longitude: 0.0, start_doy: 180.0, start_utc: 12.0,
7093 }
7094 }
7095
7096 pub fn default_island() -> Self {
7097 WorldGenProfile {
7098 name: "Island".into(), width: 512, height: 512, cell_size_m: 1.0,
7099 height_scale_m: 300.0, sea_level_frac: 0.28, fbm_params: FbmParams::island(),
7100 warp_strength: 0.4, erosion_params: ErosionParams::preset_light(), thermal_iters: 3,
7101 thermal_talus_deg: 28.0, num_rivers: 4, num_lakes: 2, foliage_density: 1.0,
7102 apply_island_mask: true, island_falloff: 2.5, fill_sinks: false,
7103 latitude: 10.0, longitude: -30.0, start_doy: 80.0, start_utc: 10.0,
7104 }
7105 }
7106
7107 pub fn default_mountains() -> Self {
7108 WorldGenProfile {
7109 name: "Mountains".into(), width: 1024, height: 1024, cell_size_m: 1.0,
7110 height_scale_m: 1000.0, sea_level_frac: 0.12, fbm_params: FbmParams::mountains(),
7111 warp_strength: 0.2, erosion_params: ErosionParams::preset_heavy(), thermal_iters: 10,
7112 thermal_talus_deg: 40.0, num_rivers: 12, num_lakes: 6, foliage_density: 0.4,
7113 apply_island_mask: false, island_falloff: 2.0, fill_sinks: true,
7114 latitude: 55.0, longitude: 10.0, start_doy: 240.0, start_utc: 8.0,
7115 }
7116 }
7117}
7118
7119impl WorldEditor {
7120 pub fn apply_profile(&mut self, profile: &WorldGenProfile, seed: u64) {
7122 if self.heightmap.width != profile.width || self.heightmap.height != profile.height {
7124 self.resize_world(profile.width, profile.height);
7125 }
7126
7127 self.cell_size = profile.cell_size_m;
7128 self.height_scale = profile.height_scale_m;
7129 self.sea_level = profile.sea_level_frac;
7130 self.latitude = profile.latitude;
7131 self.longitude = profile.longitude;
7132 self.day_of_year = profile.start_doy;
7133 self.utc_hour = profile.start_utc;
7134 self.master_seed = seed;
7135 self.world_name = profile.name.clone();
7136 self.terrain_fbm_params = profile.fbm_params.clone();
7137 self.warp_strength = profile.warp_strength;
7138 self.erosion_params = profile.erosion_params.clone();
7139
7140 self.generate_terrain();
7142
7143 if profile.apply_island_mask {
7145 self.apply_island_mask(profile.island_falloff);
7146 }
7147
7148 if profile.fill_sinks {
7150 fill_sinks(&mut self.heightmap, 0.0001);
7151 }
7152
7153 self.apply_erosion();
7155 self.apply_thermal_erosion(profile.thermal_iters, profile.thermal_talus_deg);
7156
7157 self.generate_climate();
7159
7160 self.generate_rivers(profile.num_rivers);
7162 self.generate_lakes(profile.num_lakes, 0.025);
7163 self.generate_shore();
7164
7165 let density = profile.foliage_density;
7167 self.place_foliage_layer(FoliagePlacementParams {
7168 min_radius: 2.0, max_instances: (density * 100_000.0) as usize,
7169 max_slope_rad: 0.65, min_altitude: 0.05, max_altitude: 0.75,
7170 density_scale: density, use_density_map: false, random_rotation: true,
7171 scale_variance: 0.3, base_scale: Vec3::ONE, asset_id: 0, biome_id: 0,
7172 align_to_normal: false,
7173 }, seed ^ 0xF01_1A6E);
7174
7175 self.update_solar();
7177 self.compute_stats();
7178 self.heightmap_dirty = false;
7179 self.climate_dirty = false;
7180 }
7181}
7182
7183pub fn volcano_stamp(size: usize, rim_radius: f32, rim_height: f32, caldera_depth: f32) -> Vec<f32> {
7189 let center = size as f32 * 0.5;
7190 let mut stamp = vec![0.0f32; size * size];
7191 for y in 0..size {
7192 for x in 0..size {
7193 let dx = x as f32 - center;
7194 let dy = y as f32 - center;
7195 let dist = (dx*dx + dy*dy).sqrt() / (size as f32 * 0.5);
7196 let t = dist / rim_radius;
7197 let h = if t < 0.6 {
7198 rim_height - caldera_depth + caldera_depth * smoothstep(0.0, 0.6, t)
7200 } else if t <= 1.0 {
7201 let rt = (t - 0.6) / 0.4;
7203 rim_height * (1.0 - smoothstep(0.0, 1.0, rt))
7204 } else {
7205 rim_height * (1.0 - smoothstep(1.0, 2.0, t)).max(0.0)
7207 };
7208 stamp[y * size + x] = h.max(0.0);
7209 }
7210 }
7211 stamp
7212}
7213
7214pub fn mesa_stamp(size: usize, top_radius: f32, cliff_steepness: f32, height: f32) -> Vec<f32> {
7216 let center = size as f32 * 0.5;
7217 let mut stamp = vec![0.0f32; size * size];
7218 for y in 0..size {
7219 for x in 0..size {
7220 let dx = x as f32 - center;
7221 let dy = y as f32 - center;
7222 let dist = (dx*dx + dy*dy).sqrt() / (size as f32 * 0.5);
7223 let h = if dist <= top_radius {
7224 height
7225 } else {
7226 let edge_dist = (dist - top_radius) / (1.0 - top_radius);
7227 height * (1.0 - edge_dist.powf(cliff_steepness)).max(0.0)
7228 };
7229 stamp[y * size + x] = h.max(0.0);
7230 }
7231 }
7232 stamp
7233}
7234
7235pub fn crater_stamp(size: usize, crater_radius: f32, rim_height: f32, depth: f32) -> Vec<f32> {
7237 let center = size as f32 * 0.5;
7238 let mut stamp = vec![0.0f32; size * size];
7239 for y in 0..size {
7240 for x in 0..size {
7241 let dx = x as f32 - center;
7242 let dy = y as f32 - center;
7243 let dist = (dx*dx + dy*dy).sqrt() / (size as f32 * 0.5);
7244 let t = dist / crater_radius;
7245 let h = if t < 0.8 {
7246 -depth * (1.0 - smoothstep(0.6, 0.8, t))
7248 } else if t <= 1.0 {
7249 let rt = (t - 0.8) / 0.2;
7251 rim_height * (1.0 - (rt * 2.0 - 1.0).powi(2))
7252 } else {
7253 rim_height * (1.0 - smoothstep(1.0, 1.5, t)).max(0.0)
7255 };
7256 stamp[y * size + x] = h;
7257 }
7258 }
7259 stamp
7260}
7261
7262#[derive(Clone, Debug)]
7267pub struct VoronoiCell {
7268 pub site: Vec2,
7269 pub id: u32,
7270 pub biome: BiomeId,
7271 pub area: f32,
7272 pub members: Vec<(usize, usize)>,
7273}
7274
7275pub struct VoronoiMap {
7276 pub width: usize,
7277 pub height: usize,
7278 pub cell_id: Vec<u32>,
7279 pub cells: Vec<VoronoiCell>,
7280}
7281
7282impl VoronoiMap {
7283 pub fn generate(width: usize, height: usize, num_sites: usize, seed: u64) -> Self {
7284 let mut rng = LcgRng::new(seed);
7285 let sites: Vec<Vec2> = (0..num_sites).map(|_| {
7286 Vec2::new(rng.next_f32() * width as f32, rng.next_f32() * height as f32)
7287 }).collect();
7288
7289 let mut cell_id = vec![0u32; width * height];
7290 let mut cells: Vec<VoronoiCell> = (0..num_sites).map(|i| VoronoiCell {
7291 site: sites[i],
7292 id: i as u32,
7293 biome: BiomeId::TemperateGrassland,
7294 area: 0.0,
7295 members: Vec::new(),
7296 }).collect();
7297
7298 for y in 0..height {
7299 for x in 0..width {
7300 let p = Vec2::new(x as f32, y as f32);
7301 let best_idx = sites.iter().enumerate()
7302 .min_by(|(_, a), (_, b)| {
7303 let da = (**a - p).length_squared();
7304 let db = (**b - p).length_squared();
7305 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
7306 })
7307 .map(|(i, _)| i)
7308 .unwrap_or(0);
7309 cell_id[y * width + x] = best_idx as u32;
7310 cells[best_idx].members.push((x, y));
7311 cells[best_idx].area += 1.0;
7312 }
7313 }
7314
7315 VoronoiMap { width, height, cell_id, cells }
7316 }
7317
7318 pub fn assign_biomes(&mut self, hmap: &Heightmap, temp_map: &[f32], hum_map: &[f32]) {
7319 let w = hmap.width;
7320 for cell in self.cells.iter_mut() {
7321 let sx = cell.site.x as usize;
7322 let sy = cell.site.y as usize;
7323 let sx = sx.min(hmap.width - 1);
7324 let sy = sy.min(hmap.height - 1);
7325 let idx = sy * w + sx;
7326 let alt = hmap.get(sx, sy);
7327 let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
7328 let hum = if idx < hum_map.len() { hum_map[idx] } else { 0.5 };
7329 cell.biome = BiomeDescriptor::classify_point(temp, hum, alt);
7330 }
7331 }
7332
7333 pub fn biome_at(&self, x: usize, y: usize) -> BiomeId {
7334 let cid = self.cell_id[y * self.width + x] as usize;
7335 if cid < self.cells.len() { self.cells[cid].biome } else { BiomeId::TemperateGrassland }
7336 }
7337}
7338
7339pub fn compute_temperature_inversion(
7345 hmap: &Heightmap,
7346 temp_map: &[f32],
7347 acc_map: &[u32], threshold: u32, ) -> Vec<f32> {
7350 let w = hmap.width;
7351 let h = hmap.height;
7352 let mut inv_map = vec![0.0f32; w * h];
7353
7354 for y in 1..h-1 {
7355 for x in 1..w-1 {
7356 let idx = y * w + x;
7357 if acc_map[idx] < threshold { continue; }
7358 let alt = hmap.get(x, y);
7360 let mut sum_h = 0.0f32;
7362 let mut cnt = 0;
7363 for &(dx, dy) in &[(2i32,0i32),(-2,0),(0,2),(0,-2)] {
7364 let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as usize;
7365 let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as usize;
7366 sum_h += hmap.get(nx, ny);
7367 cnt += 1;
7368 }
7369 let avg_h = sum_h / cnt as f32;
7370 let inv = (avg_h - alt).max(0.0) * 20.0; inv_map[idx] = inv;
7372 }
7373 }
7374 inv_map
7375}
7376
7377pub fn compute_sediment_flux(
7383 hmap: &Heightmap,
7384 acc_map: &[u32],
7385 k: f32, m: f32, n: f32, cell_size: f32,
7389) -> Vec<f32> {
7390 let w = hmap.width;
7391 let h = hmap.height;
7392 let mut flux = vec![0.0f32; w * h];
7393 for y in 1..h-1 {
7394 for x in 1..w-1 {
7395 let idx = y * w + x;
7396 let slope = hmap.slope_at(x, y, cell_size);
7397 let area = acc_map[idx] as f32 * cell_size * cell_size;
7398 flux[idx] = k * area.powf(m) * slope.powf(n);
7399 }
7400 }
7401 flux
7402}
7403
7404#[inline]
7410pub fn hash_f32(x: f32) -> f32 {
7411 let mut h = (x.to_bits() ^ 0x9e3779b9u32).wrapping_mul(0x6c62272e);
7412 h ^= h >> 16;
7413 h = h.wrapping_mul(0x45d9f3b);
7414 h ^= h >> 16;
7415 (h as f32) / u32::MAX as f32
7416}
7417
7418#[inline]
7419pub fn hash_vec2(v: Vec2) -> f32 {
7420 hash_f32(v.x * 127.1 + v.y * 311.7)
7421}
7422
7423#[inline]
7424pub fn hash_vec3(v: Vec3) -> f32 {
7425 hash_f32(v.x * 127.1 + v.y * 311.7 + v.z * 74.7)
7426}
7427
7428#[inline]
7430pub fn wang_hash(mut n: u32) -> u32 {
7431 n = (n ^ 61) ^ (n >> 16);
7432 n = n.wrapping_mul(9);
7433 n ^= n >> 4;
7434 n = n.wrapping_mul(0x27d4eb2d);
7435 n ^= n >> 15;
7436 n
7437}
7438
7439#[inline]
7441pub fn float_from_hash(hash: u32) -> f32 {
7442 (hash as f32) / 4_294_967_296.0
7443}
7444
7445#[inline]
7447pub fn clamp_vec3(v: Vec3, lo: Vec3, hi: Vec3) -> Vec3 {
7448 Vec3::new(v.x.clamp(lo.x, hi.x), v.y.clamp(lo.y, hi.y), v.z.clamp(lo.z, hi.z))
7449}
7450
7451#[inline]
7453pub fn reflect(v: Vec3, n: Vec3) -> Vec3 {
7454 v - n * (2.0 * v.dot(n))
7455}
7456
7457pub fn refract(v: Vec3, n: Vec3, eta: f32) -> Option<Vec3> {
7459 let cos_i = -v.dot(n);
7460 let sin2_t = eta * eta * (1.0 - cos_i * cos_i);
7461 if sin2_t > 1.0 { return None; }
7462 let cos_t = (1.0 - sin2_t).sqrt();
7463 Some(v * eta + n * (eta * cos_i - cos_t))
7464}
7465
7466#[inline]
7468pub fn fresnel_schlick(cos_theta: f32, r0: f32) -> f32 {
7469 r0 + (1.0 - r0) * (1.0 - cos_theta).powi(5)
7470}
7471
7472pub fn frenet_frame(tangent: Vec3, up_hint: Vec3) -> (Vec3, Vec3, Vec3) {
7474 let t = tangent.normalize();
7475 let b = t.cross(up_hint).normalize();
7476 let n = b.cross(t);
7477 (t, n, b) }
7479
7480#[derive(Clone, Debug)]
7485pub struct TerrainMaterial {
7486 pub albedo_color: Vec4,
7487 pub roughness: f32,
7488 pub metallic: f32,
7489 pub normal_strength: f32,
7490 pub displacement: f32,
7491 pub tiling_scale: Vec2,
7492 pub texture_ids: [u32; 4], }
7494
7495impl TerrainMaterial {
7496 pub fn default_grass() -> Self {
7497 TerrainMaterial { albedo_color: Vec4::new(0.20, 0.55, 0.12, 1.0), roughness: 0.85, metallic: 0.0,
7498 normal_strength: 0.8, displacement: 0.05, tiling_scale: Vec2::new(8.0, 8.0), texture_ids: [0,1,2,3] }
7499 }
7500 pub fn default_rock() -> Self {
7501 TerrainMaterial { albedo_color: Vec4::new(0.50, 0.45, 0.40, 1.0), roughness: 0.90, metallic: 0.0,
7502 normal_strength: 1.2, displacement: 0.15, tiling_scale: Vec2::new(4.0, 4.0), texture_ids: [4,5,6,7] }
7503 }
7504 pub fn default_snow() -> Self {
7505 TerrainMaterial { albedo_color: Vec4::new(0.95, 0.97, 1.0, 1.0), roughness: 0.30, metallic: 0.0,
7506 normal_strength: 0.3, displacement: 0.02, tiling_scale: Vec2::new(6.0, 6.0), texture_ids: [8,9,10,11] }
7507 }
7508 pub fn default_sand() -> Self {
7509 TerrainMaterial { albedo_color: Vec4::new(0.87, 0.79, 0.55, 1.0), roughness: 0.95, metallic: 0.0,
7510 normal_strength: 0.5, displacement: 0.08, tiling_scale: Vec2::new(10.0, 10.0), texture_ids: [12,13,14,15] }
7511 }
7512 pub fn default_water() -> Self {
7513 TerrainMaterial { albedo_color: Vec4::new(0.10, 0.35, 0.65, 0.85), roughness: 0.05, metallic: 0.0,
7514 normal_strength: 1.5, displacement: 0.0, tiling_scale: Vec2::new(20.0, 20.0), texture_ids: [16,17,18,19] }
7515 }
7516
7517 pub fn blend(&self, other: &TerrainMaterial, t: f32) -> TerrainMaterial {
7519 let lf = |a: f32, b: f32| a + (b - a) * t;
7520 let lv4 = |a: Vec4, b: Vec4| a + (b - a) * t;
7521 let lv2 = |a: Vec2, b: Vec2| a + (b - a) * t;
7522 TerrainMaterial {
7523 albedo_color: lv4(self.albedo_color, other.albedo_color),
7524 roughness: lf(self.roughness, other.roughness),
7525 metallic: lf(self.metallic, other.metallic),
7526 normal_strength: lf(self.normal_strength, other.normal_strength),
7527 displacement: lf(self.displacement, other.displacement),
7528 tiling_scale: lv2(self.tiling_scale, other.tiling_scale),
7529 texture_ids: if t < 0.5 { self.texture_ids } else { other.texture_ids },
7530 }
7531 }
7532}
7533
7534impl WorldEditor {
7539 pub fn place_volcano(&mut self, world_x: f32, world_z: f32, radius: f32, rim_height: f32, caldera_depth: f32) {
7541 let stamp_size = (radius * 2.0 / self.cell_size) as usize + 4;
7542 let stamp = volcano_stamp(stamp_size, 0.6, rim_height / self.height_scale, caldera_depth / self.height_scale);
7543 let cx = world_x / self.cell_size;
7544 let cz = world_z / self.cell_size;
7545 let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, stamp_size, stamp_size, 1.0);
7546 self.undo_redo.push(action);
7547 self.heightmap_dirty = true;
7548 }
7549
7550 pub fn place_mesa(&mut self, world_x: f32, world_z: f32, radius: f32, height: f32, steepness: f32) {
7552 let stamp_size = (radius * 2.5 / self.cell_size) as usize + 4;
7553 let stamp = mesa_stamp(stamp_size, 0.5, steepness, height / self.height_scale);
7554 let cx = world_x / self.cell_size;
7555 let cz = world_z / self.cell_size;
7556 let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, stamp_size, stamp_size, 1.0);
7557 self.undo_redo.push(action);
7558 self.heightmap_dirty = true;
7559 }
7560
7561 pub fn place_crater(&mut self, world_x: f32, world_z: f32, radius: f32, rim_height: f32, depth: f32) {
7563 let stamp_size = (radius * 3.0 / self.cell_size) as usize + 4;
7564 let stamp = crater_stamp(stamp_size, 0.55, rim_height / self.height_scale, depth / self.height_scale);
7565 let cx = world_x / self.cell_size;
7566 let cz = world_z / self.cell_size;
7567 let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, stamp_size, stamp_size, 1.0);
7568 self.undo_redo.push(action);
7569 self.heightmap_dirty = true;
7570 }
7571
7572 pub fn fill_terrain_sinks(&mut self) {
7574 fill_sinks(&mut self.heightmap, 0.0001);
7575 self.heightmap_dirty = true;
7576 }
7577
7578 pub fn stats_for_region(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> WorldStats {
7580 let w = self.heightmap.width;
7581 let h = self.heightmap.height;
7582 let x1 = x1.min(w);
7583 let y1 = y1.min(h);
7584 let total = (x1 - x0) * (y1 - y0);
7585 let mut ocean = 0usize;
7586 let mut mountain = 0usize;
7587 let mut sum = 0.0f64;
7588 let mut min_h = f32::MAX;
7589 let mut max_h = f32::MIN;
7590 for y in y0..y1 { for x in x0..x1 {
7591 let ht = self.heightmap.get(x, y);
7592 sum += ht as f64;
7593 if ht < min_h { min_h = ht; }
7594 if ht > max_h { max_h = ht; }
7595 if ht <= self.sea_level { ocean += 1; }
7596 if ht > 0.7 { mountain += 1; }
7597 }}
7598 WorldStats {
7599 total_cells: total, ocean_cells: ocean, land_cells: total - ocean, mountain_cells: mountain,
7600 river_count: 0, lake_count: 0, road_segments: 0, road_total_length: 0.0,
7601 foliage_count: 0, min_height: min_h, max_height: max_h,
7602 mean_height: (sum / total as f64) as f32, dominant_biome: None,
7603 }
7604 }
7605
7606 pub fn gaussian_smooth(&mut self, sigma: f32) {
7608 let w = self.heightmap.width;
7609 let h = self.heightmap.height;
7610 let old = self.heightmap.data.clone();
7611 let smoothed = gaussian_blur_2d(&old, w, h, sigma);
7612 let x0 = 0; let y0 = 0;
7613 let action = EditAction::SetHeightRegion {
7614 x: 0, y: 0, width: w, height: h,
7615 old_data: old,
7616 new_data: smoothed.clone(),
7617 };
7618 self.heightmap.data = smoothed;
7619 self.heightmap.recompute_minmax();
7620 self.undo_redo.push(action);
7621 self.heightmap_dirty = true;
7622 }
7623
7624 pub fn build_nav_grid(&self, max_slope_deg: f32) -> NavGrid {
7626 let mut grid = NavGrid::from_heightmap(&self.heightmap, self.cell_size, self.sea_level, max_slope_deg);
7627 grid.label_regions();
7628 grid
7629 }
7630
7631 pub fn biome_percentages(&self) -> [(BiomeId, f32); 25] {
7633 let w = self.heightmap.width;
7634 let h = self.heightmap.height;
7635 let total = (w * h) as f32;
7636 let mut counts = [0usize; 25];
7637 for y in 0..h { for x in 0..w {
7638 let idx = y * w + x;
7639 let alt = self.heightmap.get(x, y);
7640 let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
7641 let hum = if idx < self.humidity_map.len() { self.humidity_map[idx] } else { 0.5 };
7642 let biome = BiomeDescriptor::classify_point(temp, hum, alt);
7643 counts[biome as usize] += 1;
7644 }}
7645 [
7646 (BiomeId::TropicalRainforest, counts[0] as f32 / total),
7647 (BiomeId::TropicalSavanna, counts[1] as f32 / total),
7648 (BiomeId::HotDesert, counts[2] as f32 / total),
7649 (BiomeId::ColdDesert, counts[3] as f32 / total),
7650 (BiomeId::XericShrubland, counts[4] as f32 / total),
7651 (BiomeId::MediterraneanShrub, counts[5] as f32 / total),
7652 (BiomeId::TemperateGrassland, counts[6] as f32 / total),
7653 (BiomeId::TemperateRainforest, counts[7] as f32 / total),
7654 (BiomeId::TemperateDeciduous, counts[8] as f32 / total),
7655 (BiomeId::BorealForest, counts[9] as f32 / total),
7656 (BiomeId::TaigaSpruce, counts[10] as f32 / total),
7657 (BiomeId::Tundra, counts[11] as f32 / total),
7658 (BiomeId::ArcticDesert, counts[12] as f32 / total),
7659 (BiomeId::AlpineMeadow, counts[13] as f32 / total),
7660 (BiomeId::AlpineTundra, counts[14] as f32 / total),
7661 (BiomeId::PolarIceCap, counts[15] as f32 / total),
7662 (BiomeId::Mangrove, counts[16] as f32 / total),
7663 (BiomeId::Wetland, counts[17] as f32 / total),
7664 (BiomeId::FloodPlain, counts[18] as f32 / total),
7665 (BiomeId::VolcanicLandscape, counts[19] as f32 / total),
7666 (BiomeId::SaltFlat, counts[20] as f32 / total),
7667 (BiomeId::GlacialValley, counts[21] as f32 / total),
7668 (BiomeId::CoastalDunes, counts[22] as f32 / total),
7669 (BiomeId::DeepOceanFloor, counts[23] as f32 / total),
7670 (BiomeId::CoralReef, counts[24] as f32 / total),
7671 ]
7672 }
7673}
7674
7675pub struct TransmittanceLut {
7681 pub width: usize,
7682 pub height: usize,
7683 pub data: Vec<[f32; 3]>, }
7685
7686impl TransmittanceLut {
7687 pub fn bake(params: &AtmosphereParams, width: usize, height: usize) -> Self {
7688 let mut data = vec![[0.0f32; 3]; width * height];
7689
7690 for v_idx in 0..height {
7691 for u_idx in 0..width {
7692 let u = u_idx as f64 / (width - 1) as f64;
7694 let v = v_idx as f64 / (height - 1) as f64 * 2.0 - 1.0;
7695 let altitude_km = u * (params.atmo_radius - params.planet_radius);
7696 let cos_zenith = v;
7697
7698 let h = altitude_km;
7699 let hr = (-(h / params.rayleigh_scale_height)).exp();
7700 let hm = (-(h / params.mie_scale_height)).exp();
7701
7702 let path_len = if cos_zenith.abs() < 1e-6 {
7704 params.atmo_radius - params.planet_radius
7705 } else {
7706 ((params.atmo_radius * params.atmo_radius
7707 - (params.planet_radius + h) * (params.planet_radius + h) * (1.0 - cos_zenith * cos_zenith)).sqrt()
7708 - (params.planet_radius + h) * cos_zenith).max(0.0)
7709 };
7710
7711 let tau_r = [
7712 params.rayleigh_coeff[0] * hr * path_len,
7713 params.rayleigh_coeff[1] * hr * path_len,
7714 params.rayleigh_coeff[2] * hr * path_len,
7715 ];
7716 let tau_m_val = 1.1 * params.mie_coeff * hm * path_len;
7717
7718 data[v_idx * width + u_idx] = [
7719 (-(tau_r[0] + tau_m_val)).exp() as f32,
7720 (-(tau_r[1] + tau_m_val)).exp() as f32,
7721 (-(tau_r[2] + tau_m_val)).exp() as f32,
7722 ];
7723 }
7724 }
7725
7726 TransmittanceLut { width, height, data }
7727 }
7728
7729 pub fn sample(&self, altitude_norm: f32, cos_zenith: f32) -> Vec3 {
7730 let u = altitude_norm.clamp(0.0, 1.0) * (self.width - 1) as f32;
7731 let v = ((cos_zenith + 1.0) * 0.5).clamp(0.0, 1.0) * (self.height - 1) as f32;
7732 let x0 = u.floor() as usize;
7733 let y0 = v.floor() as usize;
7734 let x1 = (x0 + 1).min(self.width - 1);
7735 let y1 = (y0 + 1).min(self.height - 1);
7736 let tx = u - x0 as f32;
7737 let ty = v - y0 as f32;
7738 let s = |xi: usize, yi: usize| { let d = self.data[yi * self.width + xi]; Vec3::new(d[0], d[1], d[2]) };
7739 let a = s(x0, y0).lerp(s(x1, y0), tx);
7740 let b = s(x0, y1).lerp(s(x1, y1), tx);
7741 a.lerp(b, ty)
7742 }
7743}
7744
7745pub const WORLD_EDITOR_VERSION: &str = "0.1.0";
7750pub const WORLD_EDITOR_BUILD: u32 = 10001;
7751
7752
7753pub fn editor_version() -> String {
7755 format!("WorldEditor v{} (build {})", WORLD_EDITOR_VERSION, WORLD_EDITOR_BUILD)
7756}
7757
7758#[inline]
7760pub fn grid_distance(x0: usize, y0: usize, x1: usize, y1: usize, cell_size: f32) -> f32 {
7761 let dx = (x1 as i32 - x0 as i32) as f32;
7762 let dy = (y1 as i32 - y0 as i32) as f32;
7763 (dx*dx + dy*dy).sqrt() * cell_size
7764}
7765
7766#[inline]
7768pub fn aabb_overlap(min_a: Vec3, max_a: Vec3, min_b: Vec3, max_b: Vec3) -> bool {
7769 min_a.x <= max_b.x && max_a.x >= min_b.x &&
7770 min_a.y <= max_b.y && max_a.y >= min_b.y &&
7771 min_a.z <= max_b.z && max_a.z >= min_b.z
7772}
7773
7774#[inline]
7776pub fn triangle_area_2d(a: Vec2, b: Vec2, c: Vec2) -> f32 {
7777 ((b - a).perp_dot(c - a)).abs() * 0.5
7778}
7779
7780pub fn barycentric(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> Vec3 {
7782 let v0 = c - a;
7783 let v1 = b - a;
7784 let v2 = p - a;
7785 let dot00 = v0.dot(v0);
7786 let dot01 = v0.dot(v1);
7787 let dot02 = v0.dot(v2);
7788 let dot11 = v1.dot(v1);
7789 let dot12 = v1.dot(v2);
7790 let inv_denom = 1.0 / (dot00 * dot11 - dot01 * dot01);
7791 let u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
7792 let v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
7793 Vec3::new(1.0 - u - v, v, u)
7794}
7795
7796#[inline]
7798pub fn point_in_triangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> bool {
7799 let bary = barycentric(p, a, b, c);
7800 bary.x >= 0.0 && bary.y >= 0.0 && bary.z >= 0.0
7801}
7802
7803#[inline]
7805pub fn clamp_to_aabb(p: Vec3, min: Vec3, max: Vec3) -> Vec3 {
7806 Vec3::new(p.x.clamp(min.x, max.x), p.y.clamp(min.y, max.y), p.z.clamp(min.z, max.z))
7807}
7808
7809#[inline]
7811pub fn signed_distance_to_plane(point: Vec3, plane_normal: Vec3, plane_d: f32) -> f32 {
7812 plane_normal.dot(point) + plane_d
7813}
7814
7815#[derive(Clone, Debug)]
7820pub struct SplinePath {
7821 pub id: u32,
7822 pub control_pts: Vec<Vec3>,
7823 pub name: String,
7824 pub closed: bool,
7825 pub tangents: Vec<Vec3>,
7826}
7827
7828impl SplinePath {
7829 pub fn new(id: u32, name: &str) -> Self {
7830 SplinePath { id, control_pts: Vec::new(), name: name.into(), closed: false, tangents: Vec::new() }
7831 }
7832
7833 pub fn add_point(&mut self, p: Vec3) {
7834 self.control_pts.push(p);
7835 self.recompute_tangents();
7836 }
7837
7838 pub fn remove_point(&mut self, idx: usize) {
7839 if idx < self.control_pts.len() {
7840 self.control_pts.remove(idx);
7841 self.recompute_tangents();
7842 }
7843 }
7844
7845 pub fn move_point(&mut self, idx: usize, new_pos: Vec3) {
7846 if idx < self.control_pts.len() {
7847 self.control_pts[idx] = new_pos;
7848 self.recompute_tangents();
7849 }
7850 }
7851
7852 pub fn recompute_tangents(&mut self) {
7853 let n = self.control_pts.len();
7854 self.tangents = vec![Vec3::ZERO; n];
7855 if n < 2 { return; }
7856 for i in 0..n {
7857 let prev = if i == 0 { self.control_pts[0] } else { self.control_pts[i - 1] };
7858 let next = if i == n-1 { self.control_pts[n-1] } else { self.control_pts[i + 1] };
7859 self.tangents[i] = (next - prev).normalize_or_zero();
7860 }
7861 }
7862
7863 pub fn evaluate(&self, t: f32) -> Vec3 {
7864 let n = self.control_pts.len();
7865 if n == 0 { return Vec3::ZERO; }
7866 if n == 1 { return self.control_pts[0]; }
7867 let total_t = if self.closed { n as f32 } else { (n - 1) as f32 };
7868 let t_clamped = t.clamp(0.0, 1.0) * total_t;
7869 let seg = t_clamped.floor() as usize;
7870 let local_t = t_clamped - seg as f32;
7871 let i0 = seg.min(n - 1);
7872 let i1 = (seg + 1).min(n - 1);
7873 let p0 = self.control_pts[i0];
7874 let p1 = self.control_pts[i1];
7875 let tan0 = self.tangents[i0] * (p1 - p0).length() * 0.3;
7876 let tan1 = self.tangents[i1] * (p1 - p0).length() * 0.3;
7877 let h00 = 2.0 * local_t.powi(3) - 3.0 * local_t.powi(2) + 1.0;
7879 let h10 = local_t.powi(3) - 2.0 * local_t.powi(2) + local_t;
7880 let h01 = -2.0 * local_t.powi(3) + 3.0 * local_t.powi(2);
7881 let h11 = local_t.powi(3) - local_t.powi(2);
7882 p0 * h00 + tan0 * h10 + p1 * h01 + tan1 * h11
7883 }
7884
7885 pub fn arc_length(&self, steps_per_seg: usize) -> f32 {
7886 let n = self.control_pts.len();
7887 if n < 2 { return 0.0; }
7888 let total_steps = (n - 1) * steps_per_seg;
7889 let mut len = 0.0f32;
7890 let mut prev = self.evaluate(0.0);
7891 for i in 1..=total_steps {
7892 let t = i as f32 / total_steps as f32;
7893 let curr = self.evaluate(t);
7894 len += (curr - prev).length();
7895 prev = curr;
7896 }
7897 len
7898 }
7899
7900 pub fn sample_uniform(&self, count: usize) -> Vec<Vec3> {
7902 if count == 0 { return Vec::new(); }
7903 if count == 1 { return vec![self.evaluate(0.5)]; }
7904 (0..count).map(|i| self.evaluate(i as f32 / (count - 1) as f32)).collect()
7905 }
7906}
7907
7908#[derive(Clone, Debug)]
7913pub enum HeightmapOp {
7914 Noise { params: FbmParams, offset: Vec2 },
7915 Erosion { params: ErosionParams },
7916 Thermal { iterations: usize, talus_deg: f32 },
7917 Blur { sigma: f32 },
7918 Normalize,
7919 Clamp { min: f32, max: f32 },
7920 Multiply { factor: f32 },
7921 Add { value: f32 },
7922 FillSinks,
7923 IslandMask { falloff: f32 },
7924}
7925
7926pub struct HeightmapOpQueue {
7927 pub ops: VecDeque<HeightmapOp>,
7928 pub dirty: bool,
7929}
7930
7931impl HeightmapOpQueue {
7932 pub fn new() -> Self { HeightmapOpQueue { ops: VecDeque::new(), dirty: false } }
7933
7934 pub fn push(&mut self, op: HeightmapOp) { self.ops.push_back(op); self.dirty = true; }
7935
7936 pub fn execute_all(&mut self, hmap: &mut Heightmap) {
7937 while let Some(op) = self.ops.pop_front() {
7938 match op {
7939 HeightmapOp::Noise { params, offset } => {
7940 hmap.generate_fbm(¶ms, offset);
7941 }
7942 HeightmapOp::Erosion { params } => {
7943 hydraulic_erosion(hmap, ¶ms);
7944 }
7945 HeightmapOp::Thermal { iterations, talus_deg } => {
7946 thermal_erosion(hmap, iterations, talus_deg * DEG2RAD);
7947 }
7948 HeightmapOp::Blur { sigma } => {
7949 let w = hmap.width; let h = hmap.height;
7950 let blurred = gaussian_blur_2d(&hmap.data.clone(), w, h, sigma);
7951 hmap.data = blurred;
7952 hmap.recompute_minmax();
7953 }
7954 HeightmapOp::Normalize => {
7955 hmap.normalize_to_01();
7956 }
7957 HeightmapOp::Clamp { min, max } => {
7958 for v in hmap.data.iter_mut() { *v = v.clamp(min, max); }
7959 hmap.recompute_minmax();
7960 }
7961 HeightmapOp::Multiply { factor } => {
7962 for v in hmap.data.iter_mut() { *v = (*v * factor).clamp(0.0, 1.0); }
7963 hmap.recompute_minmax();
7964 }
7965 HeightmapOp::Add { value } => {
7966 for v in hmap.data.iter_mut() { *v = (*v + value).clamp(0.0, 1.0); }
7967 hmap.recompute_minmax();
7968 }
7969 HeightmapOp::FillSinks => {
7970 fill_sinks(hmap, 0.0001);
7971 }
7972 HeightmapOp::IslandMask { falloff } => {
7973 let mask = generate_island_mask(hmap.width, hmap.height, falloff);
7974 apply_mask(hmap, &mask);
7975 }
7976 }
7977 }
7978 self.dirty = false;
7979 }
7980}
7981
7982#[derive(Clone, Debug)]
7987pub struct SoundSource {
7988 pub id: u32,
7989 pub position: Vec3,
7990 pub max_dist: f32,
7991 pub base_volume: f32,
7992 pub sound_id: u32,
7993 pub looping: bool,
7994 pub terrain_occ: bool,
7995}
7996
7997impl SoundSource {
7998 pub fn volume_at(&self, listener: Vec3, hmap: &Heightmap, cell_size: f32, height_scale: f32) -> f32 {
7999 let dist = (self.position - listener).length();
8000 if dist >= self.max_dist { return 0.0; }
8001 let atten = (1.0 - dist / self.max_dist).powi(2);
8002 if !self.terrain_occ { return (self.base_volume * atten).clamp(0.0, 1.0); }
8003
8004 let dir = (listener - self.position).normalize();
8005 let steps = (dist / cell_size) as usize;
8006 let blocked = (1..steps).any(|s| {
8007 let p = self.position + dir * s as f32 * cell_size;
8008 let ux = (p.x / (hmap.width as f32 * cell_size)).clamp(0.0, 1.0);
8009 let uz = (p.z / (hmap.height as f32 * cell_size)).clamp(0.0, 1.0);
8010 hmap.sample_bilinear(ux, uz) * height_scale > p.y + 2.0
8011 });
8012 (self.base_volume * atten * if blocked { 0.15 } else { 1.0 }).clamp(0.0, 1.0)
8013 }
8014}
8015
8016pub fn generate_caustics_pattern(width: usize, height: usize, time: f32, wave_count: usize) -> Vec<f32> {
8022 let mut out = vec![0.0f32; width * height];
8023 let mut rng = LcgRng::new(0xCA05710C);
8024
8025 let waves: Vec<(f32, f32, f32, f32)> = (0..wave_count).map(|_| {
8026 let angle = rng.next_f32() * TWO_PI;
8027 let freq = 3.0 + rng.next_f32() * 8.0;
8028 let phase = rng.next_f32() * TWO_PI;
8029 let amp = 0.5 + rng.next_f32() * 0.5;
8030 (angle, freq, phase, amp)
8031 }).collect();
8032
8033 for y in 0..height {
8034 for x in 0..width {
8035 let ux = x as f32 / width as f32;
8036 let uy = y as f32 / height as f32;
8037 let mut v = 0.0f32;
8038 for &(angle, freq, phase, amp) in &waves {
8039 let proj = ux * angle.cos() + uy * angle.sin();
8040 v += amp * (proj * freq * TWO_PI + phase + time * 2.0).sin();
8041 }
8042 v = v / wave_count as f32 * 0.5 + 0.5;
8043 out[y * width + x] = v.powi(2); }
8045 }
8046 out
8047}
8048
8049#[derive(Clone, Debug)]
8054pub struct VegetationRule {
8055 pub asset_id: u32,
8056 pub name: &'static str,
8057 pub min_alt: f32,
8058 pub max_alt: f32,
8059 pub min_slope: f32,
8060 pub max_slope: f32,
8061 pub min_temp: f32,
8062 pub max_temp: f32,
8063 pub min_hum: f32,
8064 pub max_hum: f32,
8065 pub density: f32,
8066 pub min_radius: f32,
8067}
8068
8069pub fn build_default_vegetation_rules() -> Vec<VegetationRule> {
8070 vec![
8071 VegetationRule { asset_id:0, name:"Oak Tree", min_alt:0.05, max_alt:0.60, min_slope:0.0, max_slope:0.5, min_temp:5.0, max_temp:25.0, min_hum:0.40, max_hum:0.80, density:0.5, min_radius:4.0 },
8072 VegetationRule { asset_id:1, name:"Pine Tree", min_alt:0.20, max_alt:0.75, min_slope:0.0, max_slope:0.6, min_temp:-5.0, max_temp:15.0, min_hum:0.35, max_hum:0.75, density:0.6, min_radius:3.5 },
8073 VegetationRule { asset_id:2, name:"Palm Tree", min_alt:0.00, max_alt:0.20, min_slope:0.0, max_slope:0.3, min_temp:20.0, max_temp:40.0, min_hum:0.30, max_hum:0.80, density:0.4, min_radius:5.0 },
8074 VegetationRule { asset_id:3, name:"Spruce", min_alt:0.30, max_alt:0.70, min_slope:0.0, max_slope:0.5, min_temp:-15.0, max_temp:8.0, min_hum:0.40, max_hum:0.80, density:0.7, min_radius:3.0 },
8075 VegetationRule { asset_id:4, name:"Cactus", min_alt:0.00, max_alt:0.40, min_slope:0.0, max_slope:0.4, min_temp:15.0, max_temp:50.0, min_hum:0.00, max_hum:0.20, density:0.2, min_radius:2.0 },
8076 VegetationRule { asset_id:5, name:"Birch", min_alt:0.05, max_alt:0.55, min_slope:0.0, max_slope:0.5, min_temp:-5.0, max_temp:20.0, min_hum:0.45, max_hum:0.75, density:0.5, min_radius:3.5 },
8077 VegetationRule { asset_id:6, name:"Bamboo", min_alt:0.02, max_alt:0.35, min_slope:0.0, max_slope:0.4, min_temp:15.0, max_temp:35.0, min_hum:0.60, max_hum:1.00, density:0.8, min_radius:1.5 },
8078 VegetationRule { asset_id:7, name:"Fern Shrub", min_alt:0.00, max_alt:0.50, min_slope:0.0, max_slope:0.6, min_temp:5.0, max_temp:30.0, min_hum:0.50, max_hum:1.00, density:0.7, min_radius:1.0 },
8079 VegetationRule { asset_id:8, name:"Bush", min_alt:0.00, max_alt:0.60, min_slope:0.0, max_slope:0.5, min_temp:0.0, max_temp:35.0, min_hum:0.25, max_hum:0.75, density:0.6, min_radius:1.5 },
8080 VegetationRule { asset_id:9, name:"Tundra Grass", min_alt:0.00, max_alt:0.65, min_slope:0.0, max_slope:0.4, min_temp:-25.0, max_temp:5.0, min_hum:0.20, max_hum:0.60, density:0.5, min_radius:0.5 },
8081 VegetationRule { asset_id:10, name:"Tall Grass", min_alt:0.00, max_alt:0.45, min_slope:0.0, max_slope:0.4, min_temp:5.0, max_temp:30.0, min_hum:0.30, max_hum:0.70, density:0.9, min_radius:0.3 },
8082 VegetationRule { asset_id:11, name:"Reed", min_alt:0.00, max_alt:0.10, min_slope:0.0, max_slope:0.1, min_temp:5.0, max_temp:35.0, min_hum:0.75, max_hum:1.00, density:0.8, min_radius:0.5 },
8083 VegetationRule { asset_id:12, name:"Mangrove Root", min_alt:0.00, max_alt:0.08, min_slope:0.0, max_slope:0.1, min_temp:20.0, max_temp:36.0, min_hum:0.75, max_hum:1.00, density:0.6, min_radius:3.0 },
8084 ]
8085}
8086
8087pub fn apply_vegetation_rules(
8088 hmap: &Heightmap,
8089 temp_map: &[f32],
8090 hum_map: &[f32],
8091 rules: &[VegetationRule],
8092 cell_size: f32,
8093 seed: u64,
8094) -> Vec<FoliageInstance> {
8095 let mut result = Vec::new();
8096 let w = hmap.width as f32;
8097 let h = hmap.height as f32;
8098
8099 for (ri, rule) in rules.iter().enumerate() {
8100 let candidates = poisson_disk_2d(w, h, rule.min_radius, 30, seed ^ (ri as u64 * 31337));
8101 let mut rng = LcgRng::new(seed ^ ri as u64 * 997);
8102
8103 for pos in &candidates {
8104 let ux = (pos.x / w).clamp(0.0, 1.0);
8105 let uy = (pos.y / h).clamp(0.0, 1.0);
8106 let alt = hmap.sample_bilinear(ux, uy);
8107 if alt < rule.min_alt || alt > rule.max_alt { continue; }
8108
8109 let xi = (pos.x as usize).min(hmap.width - 1);
8110 let yi = (pos.y as usize).min(hmap.height - 1);
8111 let slope = hmap.slope_at(xi, yi, cell_size);
8112 if slope < rule.min_slope || slope > rule.max_slope { continue; }
8113
8114 let idx = yi * hmap.width + xi;
8115 let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
8116 let hum = if idx < hum_map.len() { hum_map[idx] } else { 0.5 };
8117 if temp < rule.min_temp || temp > rule.max_temp { continue; }
8118 if hum < rule.min_hum || hum > rule.max_hum { continue; }
8119
8120 if rng.next_f32() > rule.density { continue; }
8121
8122 let angle = rng.next_f32() * TWO_PI;
8123 let sv = 0.75 + rng.next_f32() * 0.5;
8124 result.push(FoliageInstance {
8125 position: Vec3::new(pos.x * cell_size, alt * 500.0, pos.y * cell_size),
8126 rotation: Quat::from_rotation_y(angle),
8127 scale: Vec3::new(sv, sv * (0.8 + rng.next_f32() * 0.4), sv),
8128 asset_id: rule.asset_id,
8129 biome_id: 0,
8130 lod_factor: 1.0,
8131 });
8132 }
8133 }
8134 result
8135}
8136
8137#[derive(Clone, Debug)]
8142pub struct LodBand {
8143 pub max_distance: f32,
8144 pub mesh_step: usize, pub texture_lod: u8,
8146 pub foliage: bool,
8147 pub shadows: bool,
8148}
8149
8150pub const LOD_BANDS: [LodBand; 5] = [
8151 LodBand { max_distance: 50.0, mesh_step: 1, texture_lod: 0, foliage: true, shadows: true },
8152 LodBand { max_distance: 150.0, mesh_step: 1, texture_lod: 0, foliage: true, shadows: true },
8153 LodBand { max_distance: 400.0, mesh_step: 2, texture_lod: 1, foliage: true, shadows: false },
8154 LodBand { max_distance: 1000.0, mesh_step: 4, texture_lod: 2, foliage: false, shadows: false },
8155 LodBand { max_distance: 3000.0, mesh_step: 8, texture_lod: 3, foliage: false, shadows: false },
8156];
8157
8158pub fn select_lod_band(distance: f32) -> &'static LodBand {
8159 for band in &LOD_BANDS {
8160 if distance < band.max_distance { return band; }
8161 }
8162 &LOD_BANDS[LOD_BANDS.len() - 1]
8163}
8164
8165pub fn water_specular(view_dir: Vec3, sun_dir: Vec3, water_normal: Vec3, roughness: f32) -> f32 {
8171 let half_vec = (view_dir + sun_dir).normalize();
8172 let n_dot_h = water_normal.dot(half_vec).max(0.0);
8173 let alpha = roughness * roughness;
8174 let alpha2 = alpha * alpha;
8175 let denom = n_dot_h * n_dot_h * (alpha2 - 1.0) + 1.0;
8176 let ggx_ndf = alpha2 / (PI * denom * denom);
8177 let n_dot_l = water_normal.dot(sun_dir).max(0.0);
8178 let n_dot_v = water_normal.dot(view_dir).max(0.0);
8179 let r0 = 0.02; let fresnel = fresnel_schlick(n_dot_v, r0);
8181 ggx_ndf * fresnel * n_dot_l
8182}
8183
8184pub fn gerstner_wave(pos: Vec2, amplitude: f32, wavelength: f32, direction: Vec2, speed: f32, steepness: f32, time: f32) -> Vec3 {
8186 let k = TWO_PI / wavelength;
8187 let c = speed;
8188 let d = direction.normalize();
8189 let f = k * d.dot(pos) - c * time;
8190 let q = steepness / (k * amplitude);
8191 Vec3::new(
8192 q * amplitude * d.x * f.cos(),
8193 amplitude * f.sin(),
8194 q * amplitude * d.y * f.cos(),
8195 )
8196}
8197
8198pub fn gerstner_wave_sum(pos: Vec2, time: f32) -> Vec3 {
8200 let waves: [(f32, f32, Vec2, f32, f32); 4] = [
8201 (0.15, 8.0, Vec2::new(1.0, 0.3).normalize(), 1.5, 0.3),
8202 (0.08, 5.0, Vec2::new(0.5, 1.0).normalize(), 2.0, 0.25),
8203 (0.05, 3.0, Vec2::new(-0.3, 1.0).normalize(), 2.5, 0.2),
8204 (0.03, 2.0, Vec2::new(0.8, -0.5).normalize(), 3.0, 0.15),
8205 ];
8206 let mut disp = Vec3::ZERO;
8207 for &(amp, wl, dir, speed, steep) in &waves {
8208 disp += gerstner_wave(pos, amp, wl, dir, speed, steep, time);
8209 }
8210 disp
8211}
8212
8213impl WorldEditor {
8218 pub fn place_vegetation_by_rules(&mut self, seed: u64) {
8220 let rules = build_default_vegetation_rules();
8221 let instances = apply_vegetation_rules(
8222 &self.heightmap,
8223 &self.temperature_map,
8224 &self.humidity_map,
8225 &rules,
8226 self.cell_size,
8227 seed,
8228 );
8229 self.foliage.extend(instances);
8230 self.foliage_dirty = false;
8231 }
8232
8233 pub fn build_voronoi_biome_map(&self, num_sites: usize) -> VoronoiMap {
8235 let mut vmap = VoronoiMap::generate(self.heightmap.width, self.heightmap.height, num_sites, self.master_seed ^ 0x707010);
8236 vmap.assign_biomes(&self.heightmap, &self.temperature_map, &self.humidity_map);
8237 vmap
8238 }
8239
8240 pub fn terrain_material_at(&self, x: usize, y: usize) -> TerrainMaterial {
8242 let alt = self.heightmap.get(x, y);
8243 let slope = self.heightmap.slope_at(x, y, self.cell_size) * RAD2DEG;
8244 let snow = self.compute_snow_map(0.75, 2.0);
8245 let w = self.heightmap.width;
8246 let snow_v = if y * w + x < snow.len() { snow[y * w + x] } else { 0.0 };
8247
8248 let rock_blend = smoothstep(25.0, 45.0, slope);
8249 let snow_blend = snow_v;
8250 let water_blend = if alt <= self.sea_level { 1.0 } else { 0.0 };
8251
8252 let grass = TerrainMaterial::default_grass();
8253 let rock = TerrainMaterial::default_rock();
8254 let snow = TerrainMaterial::default_snow();
8255 let water = TerrainMaterial::default_water();
8256
8257 if water_blend > 0.5 { return water; }
8258 let base = grass.blend(&rock, rock_blend);
8259 base.blend(&snow, snow_blend)
8260 }
8261
8262 pub fn set_metadata(&mut self, key: &str, value: &str) {
8264 self.metadata.insert(key.to_string(), value.to_string());
8265 }
8266
8267 pub fn get_metadata(&self, key: &str) -> Option<&String> {
8268 self.metadata.get(key)
8269 }
8270
8271 pub fn simulate_rain_event(&mut self, intensity: f32, duration_hours: f32) {
8273 for v in self.humidity_map.iter_mut() {
8275 *v = (*v + intensity * 0.3).clamp(0.0, 1.0);
8276 }
8277 if intensity > 0.5 {
8279 let mut ep = self.erosion_params.clone();
8280 ep.num_particles = (ep.num_particles as f32 * intensity * 0.5) as usize;
8281 hydraulic_erosion(&mut self.heightmap, &ep);
8282 self.heightmap_dirty = true;
8283 }
8284 self.weather.current.precipitation_mm += intensity * 10.0 * duration_hours;
8286 self.weather.current.humidity = (self.weather.current.humidity + intensity * 0.2).clamp(0.0, 1.0);
8287 }
8288
8289 pub fn reseat_foliage_to_terrain(&mut self) {
8291 let new_ys: Vec<f32> = self.foliage.iter()
8292 .map(|fi| self.height_at_world(fi.position.x, fi.position.z))
8293 .collect();
8294 for (fi, new_y) in self.foliage.iter_mut().zip(new_ys) {
8295 fi.position.y = new_y;
8296 }
8297 }
8298
8299 pub fn cull_underwater_foliage(&mut self) {
8301 let sea_h = self.sea_level * self.height_scale;
8302 self.foliage.retain(|fi| fi.position.y >= sea_h - 0.5);
8303 }
8304
8305 pub fn terrain_triangle_count(&self) -> usize {
8307 let w = self.heightmap.width;
8308 let h = self.heightmap.height;
8309 (w - 1) * (h - 1) * 2
8310 }
8311
8312 pub fn terrain_memory_bytes(&self) -> usize {
8314 let hmap_bytes = self.heightmap.data.len() * 4;
8315 let temp_bytes = self.temperature_map.len() * 4;
8316 let hum_bytes = self.humidity_map.len() * 4;
8317 let foliage_bytes = self.foliage.len() * std::mem::size_of::<FoliageInstance>();
8318 hmap_bytes + temp_bytes + hum_bytes + foliage_bytes
8319 }
8320
8321 pub fn recalculate_rivers(&mut self, num_rivers: usize) {
8323 self.rivers.clear();
8324 self.generate_rivers(num_rivers);
8325 }
8326
8327 pub fn full_save(&self) -> Vec<u8> {
8329 self.serialize()
8330 }
8331}
8332
8333pub fn perlin_noise_2d_deriv(x: f32, y: f32) -> (f32, f32, f32) {
8339 let xi = x.floor() as i32;
8340 let yi = y.floor() as i32;
8341 let xf = x - xi as f32;
8342 let yf = y - yi as f32;
8343 let u = fade(xf);
8344 let v = fade(yf);
8345 let du = 30.0 * xf * xf * (xf * xf - 2.0 * xf + 1.0);
8347 let dv = 30.0 * yf * yf * (yf * yf - 2.0 * yf + 1.0);
8348
8349 let xi_u = (xi & 255) as usize;
8350 let yi_u = (yi & 255) as usize;
8351 let a = PERM[xi_u + PERM[yi_u ] as usize];
8352 let b = PERM[xi_u + 1 + PERM[yi_u ] as usize];
8353 let c = PERM[xi_u + PERM[yi_u + 1] as usize];
8354 let d = PERM[xi_u + 1 + PERM[yi_u + 1] as usize];
8355
8356 fn g2(h: u8, x: f32, y: f32) -> f32 {
8357 let hh = (h & 7) as usize;
8358 let gx: f32 = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 0.0, 0.0][hh];
8359 let gy: f32 = [0.0, 0.0, 1.0, 1.0,-1.0, -1.0, 1.0,-1.0][hh];
8360 gx * x + gy * y
8361 }
8362
8363 let a00 = g2(a, xf, yf);
8364 let b00 = g2(b, xf - 1.0, yf);
8365 let a10 = g2(c, xf, yf - 1.0);
8366 let b10 = g2(d, xf - 1.0, yf - 1.0);
8367
8368 let val = lerp_f(lerp_f(a00, b00, u), lerp_f(a10, b10, u), v);
8369 let dx = du * lerp_f(b00 - a00, b10 - a10, v)
8370 + u * lerp_f(0.0, 0.0, dv); let dy = dv * (lerp_f(a10, b10, u) - lerp_f(a00, b00, u));
8372
8373 (val, dx, dy)
8374}
8375
8376impl EditorCamera {
8381 pub fn preset_top_down(center: Vec3) -> Self {
8382 EditorCamera {
8383 position: center + Vec3::new(0.0, 1000.0, 0.0),
8384 target: center,
8385 up: Vec3::new(0.0, 0.0, -1.0),
8386 fov_deg: 45.0,
8387 aspect: 16.0 / 9.0,
8388 near: 1.0,
8389 far: 20000.0,
8390 orbit_yaw: 0.0,
8391 orbit_pitch: 90.0,
8392 orbit_dist: 1000.0,
8393 }
8394 }
8395
8396 pub fn preset_horizon(center: Vec3) -> Self {
8397 let mut cam = EditorCamera {
8398 position: center + Vec3::new(0.0, 200.0, 800.0),
8399 target: center,
8400 up: Vec3::Y,
8401 fov_deg: 70.0,
8402 aspect: 16.0 / 9.0,
8403 near: 0.5,
8404 far: 50000.0,
8405 orbit_yaw: 0.0,
8406 orbit_pitch: 15.0,
8407 orbit_dist: 800.0,
8408 };
8409 cam.update_orbit();
8410 cam
8411 }
8412
8413 pub fn clamp_to_terrain(&mut self, hmap: &Heightmap, cell_size: f32, height_scale: f32, min_height_above: f32) {
8414 let ux = (self.position.x / (hmap.width as f32 * cell_size)).clamp(0.0, 1.0);
8415 let uz = (self.position.z / (hmap.height as f32 * cell_size)).clamp(0.0, 1.0);
8416 let terrain_h = hmap.sample_bilinear(ux, uz) * height_scale;
8417 if self.position.y < terrain_h + min_height_above {
8418 let diff = terrain_h + min_height_above - self.position.y;
8419 self.position.y += diff;
8420 self.target.y += diff;
8421 }
8422 }
8423}
8424
8425
8426#[derive(Clone, Debug)]
8431pub struct WorldRenderSettings {
8432 pub enable_shadows: bool,
8433 pub shadow_distance: f32,
8434 pub shadow_cascades: u8,
8435 pub enable_ao: bool,
8436 pub ao_radius: f32,
8437 pub ao_samples: u32,
8438 pub enable_fog: bool,
8439 pub fog_start: f32,
8440 pub fog_end: f32,
8441 pub fog_color: Vec3,
8442 pub enable_bloom: bool,
8443 pub bloom_threshold: f32,
8444 pub bloom_intensity: f32,
8445 pub exposure: f32,
8446 pub gamma: f32,
8447 pub tonemap_mode: TonemapMode,
8448 pub enable_ssao: bool,
8449 pub enable_motion_blur: bool,
8450 pub motion_blur_amount: f32,
8451 pub enable_vignette: bool,
8452 pub vignette_strength: f32,
8453 pub enable_chromatic: bool,
8454 pub chromatic_amount: f32,
8455 pub water_tessellation: u8,
8456 pub terrain_max_lod: u8,
8457 pub foliage_distance: f32,
8458 pub foliage_density_scale: f32,
8459 pub sky_samples: u32,
8460 pub render_wireframe: bool,
8461 pub render_colliders: bool,
8462 pub render_navmesh: bool,
8463}
8464
8465#[derive(Clone, Debug, PartialEq, Eq)]
8466pub enum TonemapMode {
8467 Linear,
8468 Reinhard,
8469 ACES,
8470 Filmic,
8471 Uncharted2,
8472}
8473
8474impl Default for WorldRenderSettings {
8475 fn default() -> Self {
8476 WorldRenderSettings {
8477 enable_shadows: true, shadow_distance: 500.0, shadow_cascades: 4,
8478 enable_ao: true, ao_radius: 2.0, ao_samples: 16,
8479 enable_fog: true, fog_start: 200.0, fog_end: 4000.0, fog_color: Vec3::new(0.7, 0.8, 0.9),
8480 enable_bloom: true, bloom_threshold: 1.2, bloom_intensity: 0.4,
8481 exposure: 1.0, gamma: 2.2, tonemap_mode: TonemapMode::ACES,
8482 enable_ssao: true, enable_motion_blur: false, motion_blur_amount: 0.5,
8483 enable_vignette: true, vignette_strength: 0.3,
8484 enable_chromatic: false, chromatic_amount: 0.003,
8485 water_tessellation: 4, terrain_max_lod: 4,
8486 foliage_distance: 500.0, foliage_density_scale: 1.0,
8487 sky_samples: 16, render_wireframe: false, render_colliders: false, render_navmesh: false,
8488 }
8489 }
8490}
8491
8492impl std::fmt::Display for BiomeDescriptor {
8498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8499 write!(f, "Biome[{}] '{}' T:{:.0}..{:.0}°C H:{:.0}..{:.0}%",
8500 self.id as usize, self.name,
8501 self.temp_min, self.temp_max,
8502 self.humidity_min * 100.0, self.humidity_max * 100.0)
8503 }
8504}
8505
8506impl std::fmt::Display for RoadType {
8507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8508 write!(f, "{}", match self {
8509 RoadType::Dirt => "Dirt",
8510 RoadType::Gravel => "Gravel",
8511 RoadType::Paved => "Paved",
8512 RoadType::Highway => "Highway",
8513 RoadType::Trail => "Trail",
8514 })
8515 }
8516}
8517
8518impl std::fmt::Display for EditorTool {
8519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8520 write!(f, "{:?}", self)
8521 }
8522}
8523
8524pub fn height_to_color(h: f32) -> Vec3 {
8526 ColorRamp::terrain_default().sample(h)
8527}
8528
8529pub fn checkerboard_pattern(width: usize, height: usize, cell_size: usize) -> Vec<f32> {
8531 (0..height).flat_map(|y| (0..width).map(move |x| {
8532 let cx = x / cell_size;
8533 let cy = y / cell_size;
8534 if (cx + cy) % 2 == 0 { 1.0 } else { 0.0 }
8535 })).collect()
8536}
8537
8538pub fn bounding_sphere(points: &[Vec3]) -> (Vec3, f32) {
8540 if points.is_empty() { return (Vec3::ZERO, 0.0); }
8541 let center = points.iter().fold(Vec3::ZERO, |acc, &p| acc + p) / points.len() as f32;
8542 let radius = points.iter().map(|&p| (p - center).length()).fold(0.0f32, f32::max);
8543 (center, radius)
8544}
8545
8546pub fn bounding_aabb(points: &[Vec3]) -> (Vec3, Vec3) {
8548 if points.is_empty() { return (Vec3::ZERO, Vec3::ZERO); }
8549 let mut mn = Vec3::splat(f32::MAX);
8550 let mut mx = Vec3::splat(f32::MIN);
8551 for &p in points {
8552 mn.x = mn.x.min(p.x); mn.y = mn.y.min(p.y); mn.z = mn.z.min(p.z);
8553 mx.x = mx.x.max(p.x); mx.y = mx.y.max(p.y); mx.z = mx.z.max(p.z);
8554 }
8555 (mn, mx)
8556}
8557
8558pub fn random_on_sphere(rng: &mut LcgRng) -> Vec3 {
8560 loop {
8561 let v = Vec3::new(
8562 rng.next_f32() * 2.0 - 1.0,
8563 rng.next_f32() * 2.0 - 1.0,
8564 rng.next_f32() * 2.0 - 1.0,
8565 );
8566 let len = v.length();
8567 if len > 0.0001 && len <= 1.0 { return v / len; }
8568 }
8569}
8570
8571pub fn random_on_disk(rng: &mut LcgRng) -> Vec2 {
8573 loop {
8574 let v = Vec2::new(rng.next_f32() * 2.0 - 1.0, rng.next_f32() * 2.0 - 1.0);
8575 if v.length_squared() <= 1.0 { return v; }
8576 }
8577}
8578
8579