1use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
10pub struct HeightMap {
11 pub width: usize,
12 pub height: usize,
13 pub data: Vec<f32>,
14 pub world_scale: f32,
15 pub height_scale: f32,
16}
17
18impl HeightMap {
19 pub fn new(width: usize, height: usize) -> Self {
20 Self {
21 width,
22 height,
23 data: vec![0.0; width * height],
24 world_scale: 1.0,
25 height_scale: 100.0,
26 }
27 }
28
29 pub fn from_noise(width: usize, height: usize, gen: &NoiseGenerator) -> Self {
30 let mut hm = Self::new(width, height);
31 for y in 0..height {
32 for x in 0..width {
33 let nx = x as f32 / width as f32;
34 let ny = y as f32 / height as f32;
35 hm.data[y * width + x] = gen.sample(nx, ny);
36 }
37 }
38 hm
39 }
40
41 pub fn get(&self, x: usize, y: usize) -> f32 {
42 self.data[y.min(self.height-1) * self.width + x.min(self.width-1)]
43 }
44
45 pub fn set(&mut self, x: usize, y: usize, v: f32) {
46 if x < self.width && y < self.height {
47 self.data[y * self.width + x] = v;
48 }
49 }
50
51 pub fn sample_bilinear(&self, nx: f32, ny: f32) -> f32 {
52 let x = (nx * (self.width - 1) as f32).clamp(0.0, (self.width - 1) as f32);
53 let y = (ny * (self.height - 1) as f32).clamp(0.0, (self.height - 1) as f32);
54 let xi = x as usize; let yi = y as usize;
55 let xf = x.fract(); let yf = y.fract();
56 let x1 = (xi + 1).min(self.width - 1);
57 let y1 = (yi + 1).min(self.height - 1);
58 let h00 = self.get(xi, yi);
59 let h10 = self.get(x1, yi);
60 let h01 = self.get(xi, y1);
61 let h11 = self.get(x1, y1);
62 h00*(1.0-xf)*(1.0-yf) + h10*xf*(1.0-yf) + h01*(1.0-xf)*yf + h11*xf*yf
63 }
64
65 pub fn normal_at(&self, x: usize, y: usize) -> [f32; 3] {
66 let l = if x > 0 { self.get(x-1, y) } else { self.get(x, y) };
67 let r = if x+1 < self.width { self.get(x+1, y) } else { self.get(x, y) };
68 let d = if y > 0 { self.get(x, y-1) } else { self.get(x, y) };
69 let u = if y+1 < self.height { self.get(x, y+1) } else { self.get(x, y) };
70 let dx = (r - l) * 2.0;
71 let dy = (u - d) * 2.0;
72 let scale = self.height_scale / self.world_scale;
73 let nx = -dx * scale;
74 let nz = -dy * scale;
75 let ny = 2.0;
76 let len = (nx*nx + ny*ny + nz*nz).sqrt();
77 [nx/len, ny/len, nz/len]
78 }
79
80 pub fn slope_at(&self, x: usize, y: usize) -> f32 {
81 let n = self.normal_at(x, y);
82 1.0 - n[1]
83 }
84
85 pub fn curvature_at(&self, x: usize, y: usize) -> f32 {
86 if x == 0 || y == 0 || x+1 >= self.width || y+1 >= self.height {
87 return 0.0;
88 }
89 let c = self.get(x, y);
90 let l = self.get(x-1, y);
91 let r = self.get(x+1, y);
92 let d = self.get(x, y-1);
93 let u = self.get(x, y+1);
94 l + r + d + u - 4.0 * c
96 }
97
98 pub fn min_height(&self) -> f32 { self.data.iter().cloned().fold(f32::MAX, f32::min) }
99 pub fn max_height(&self) -> f32 { self.data.iter().cloned().fold(f32::MIN, f32::max) }
100
101 pub fn normalize(&mut self) {
102 let lo = self.min_height();
103 let hi = self.max_height();
104 if (hi - lo) < 1e-6 { return; }
105 for v in &mut self.data {
106 *v = (*v - lo) / (hi - lo);
107 }
108 }
109
110 pub fn add_scaled(&mut self, other: &HeightMap, scale: f32) {
111 for (a, &b) in self.data.iter_mut().zip(other.data.iter()) {
112 *a += b * scale;
113 }
114 }
115
116 pub fn blur(&mut self, radius: usize) {
117 let w = self.width;
118 let h = self.height;
119 let mut out = self.data.clone();
120 let r = radius as isize;
121 for y in 0..h {
122 for x in 0..w {
123 let mut sum = 0.0f32;
124 let mut count = 0u32;
125 for dy in -r..=r {
126 for dx in -r..=r {
127 let nx = (x as isize + dx).clamp(0, w as isize - 1) as usize;
128 let ny = (y as isize + dy).clamp(0, h as isize - 1) as usize;
129 sum += self.data[ny * w + nx];
130 count += 1;
131 }
132 }
133 out[y * w + x] = sum / count as f32;
134 }
135 }
136 self.data = out;
137 }
138
139 pub fn sculpt_circle(&mut self, cx: f32, cy: f32, radius: f32, strength: f32, add: bool) {
140 let w = self.width as f32;
141 let h = self.height as f32;
142 for y in 0..self.height {
143 for x in 0..self.width {
144 let nx = x as f32 / w;
145 let ny = y as f32 / h;
146 let dist = ((nx - cx).powi(2) + (ny - cy).powi(2)).sqrt();
147 if dist < radius {
148 let falloff = 1.0 - dist / radius;
149 let falloff = falloff * falloff;
150 if add {
151 self.data[y * self.width + x] += strength * falloff;
152 } else {
153 self.data[y * self.width + x] -= strength * falloff;
154 }
155 }
156 }
157 }
158 for v in &mut self.data { *v = v.clamp(0.0, 1.0); }
159 }
160
161 pub fn flatten_circle(&mut self, cx: f32, cy: f32, radius: f32, target: f32, strength: f32) {
162 let w = self.width as f32;
163 let h = self.height as f32;
164 for y in 0..self.height {
165 for x in 0..self.width {
166 let nx = x as f32 / w;
167 let ny = y as f32 / h;
168 let dist = ((nx - cx).powi(2) + (ny - cy).powi(2)).sqrt();
169 if dist < radius {
170 let falloff = (1.0 - dist / radius).powi(2);
171 let cur = self.data[y * self.width + x];
172 self.data[y * self.width + x] = cur + (target - cur) * strength * falloff;
173 }
174 }
175 }
176 }
177}
178
179#[derive(Debug, Clone)]
182pub struct NoiseGenerator {
183 pub seed: u32,
184 pub octaves: u32,
185 pub persistence: f32,
186 pub lacunarity: f32,
187 pub scale: f32,
188 pub offset_x: f32,
189 pub offset_y: f32,
190 pub noise_type: NoiseType,
191 pub warp: Option<Box<WarpSettings>>,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum NoiseType {
196 Simplex,
197 Perlin,
198 Worley,
199 Ridged,
200 Billow,
201 Swiss,
202 Jordan,
203 Curl,
204 DomainWarped,
205}
206
207#[derive(Debug, Clone)]
208pub struct WarpSettings {
209 pub strength: f32,
210 pub frequency: f32,
211 pub octaves: u32,
212}
213
214impl Default for NoiseGenerator {
215 fn default() -> Self {
216 Self {
217 seed: 42,
218 octaves: 6,
219 persistence: 0.5,
220 lacunarity: 2.0,
221 scale: 1.0,
222 offset_x: 0.0,
223 offset_y: 0.0,
224 noise_type: NoiseType::Simplex,
225 warp: None,
226 }
227 }
228}
229
230impl NoiseGenerator {
231 pub fn new(seed: u32) -> Self {
232 let mut g = Self::default();
233 g.seed = seed;
234 g
235 }
236
237 pub fn sample(&self, nx: f32, ny: f32) -> f32 {
238 let mut x = (nx + self.offset_x) * self.scale;
239 let mut y = (ny + self.offset_y) * self.scale;
240
241 if let Some(warp) = &self.warp {
243 let wx = self.hash_noise_2d(x * warp.frequency, y * warp.frequency, 0);
244 let wy = self.hash_noise_2d(x * warp.frequency, y * warp.frequency, 1);
245 x += wx * warp.strength;
246 y += wy * warp.strength;
247 }
248
249 let mut value = 0.0f32;
250 let mut amplitude = 1.0f32;
251 let mut frequency = 1.0f32;
252 let mut max_value = 0.0f32;
253
254 for _ in 0..self.octaves {
255 let n = match self.noise_type {
256 NoiseType::Simplex | NoiseType::Perlin => self.gradient_noise(x * frequency, y * frequency),
257 NoiseType::Worley => self.worley_noise(x * frequency, y * frequency),
258 NoiseType::Ridged => (1.0 - self.gradient_noise(x*frequency, y*frequency).abs()).powi(2),
259 NoiseType::Billow => self.gradient_noise(x * frequency, y * frequency).abs(),
260 NoiseType::Swiss => self.swiss_noise(x * frequency, y * frequency),
261 NoiseType::Jordan => self.gradient_noise(x * frequency, y * frequency),
262 NoiseType::Curl => self.gradient_noise(x * frequency, y * frequency),
263 NoiseType::DomainWarped => self.gradient_noise(x * frequency, y * frequency),
264 };
265 value += n * amplitude;
266 max_value += amplitude;
267 amplitude *= self.persistence;
268 frequency *= self.lacunarity;
269 }
270
271 if max_value > 0.0 { value / max_value } else { 0.0 }
272 }
273
274 fn gradient_noise(&self, x: f32, y: f32) -> f32 {
275 let xi = x.floor() as i32;
276 let yi = y.floor() as i32;
277 let xf = x.fract();
278 let yf = y.fract();
279 let u = xf * xf * (3.0 - 2.0 * xf);
280 let v = yf * yf * (3.0 - 2.0 * yf);
281 let n00 = self.grad(xi, yi, xf, yf );
282 let n10 = self.grad(xi+1, yi, xf-1.0, yf);
283 let n01 = self.grad(xi, yi+1, xf, yf-1.0);
284 let n11 = self.grad(xi+1, yi+1, xf-1.0, yf-1.0);
285 let x1 = n00 + (n10 - n00) * u;
286 let x2 = n01 + (n11 - n01) * u;
287 x1 + (x2 - x1) * v
288 }
289
290 fn grad(&self, ix: i32, iy: i32, fx: f32, fy: f32) -> f32 {
291 let h = self.hash2(ix, iy);
292 let angle = h as f32 / 255.0 * std::f32::consts::TAU;
293 angle.cos() * fx + angle.sin() * fy
294 }
295
296 fn hash2(&self, x: i32, y: i32) -> u8 {
297 let mut h = self.seed.wrapping_add((x as u32).wrapping_mul(0x9e3779b9));
298 h = h.wrapping_add((y as u32).wrapping_mul(0x85ebca6b));
299 h = h ^ (h >> 16);
300 h = h.wrapping_mul(0xd2a98b26);
301 h = h ^ (h >> 13);
302 (h & 0xFF) as u8
303 }
304
305 fn worley_noise(&self, x: f32, y: f32) -> f32 {
306 let xi = x.floor() as i32;
307 let yi = y.floor() as i32;
308 let mut min_dist = f32::MAX;
309 for dy in -1..=1i32 {
310 for dx in -1..=1i32 {
311 let cx = xi + dx;
312 let cy = yi + dy;
313 let h = self.hash2(cx, cy);
314 let px = cx as f32 + (h as f32 / 255.0);
315 let py = cy as f32 + (self.hash2(cx + 1000, cy) as f32 / 255.0);
316 let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
317 min_dist = min_dist.min(d);
318 }
319 }
320 min_dist.clamp(0.0, 1.0)
321 }
322
323 fn swiss_noise(&self, x: f32, y: f32) -> f32 {
324 let base = self.gradient_noise(x, y);
326 let detail = self.worley_noise(x * 2.0, y * 2.0);
327 (base + detail * 0.3).clamp(-1.0, 1.0)
328 }
329
330 fn hash_noise_2d(&self, x: f32, y: f32, seed_offset: u32) -> f32 {
331 let h = self.hash2(x as i32 + seed_offset as i32, y as i32);
332 h as f32 / 255.0 * 2.0 - 1.0
333 }
334}
335
336#[derive(Debug, Clone)]
339pub struct ErosionSettings {
340 pub iterations: u32,
341 pub inertia: f32,
342 pub capacity: f32,
343 pub deposition_rate: f32,
344 pub erosion_rate: f32,
345 pub evaporation_rate: f32,
346 pub gravity: f32,
347 pub min_slope: f32,
348 pub erosion_radius: u32,
349 pub max_droplet_steps: u32,
350}
351
352impl Default for ErosionSettings {
353 fn default() -> Self {
354 Self {
355 iterations: 50_000,
356 inertia: 0.05,
357 capacity: 8.0,
358 deposition_rate: 0.3,
359 erosion_rate: 0.3,
360 evaporation_rate: 0.01,
361 gravity: 4.0,
362 min_slope: 0.01,
363 erosion_radius: 3,
364 max_droplet_steps: 30,
365 }
366 }
367}
368
369pub fn simulate_erosion(hm: &mut HeightMap, settings: &ErosionSettings) {
370 let w = hm.width;
371 let h = hm.height;
372
373 for _ in 0..settings.iterations.min(100) { let seed_hash = hm.data.len() as u32;
376 let pos_x = ((seed_hash ^ 0x9e3779b9) % w as u32) as usize;
377 let pos_y = ((seed_hash.wrapping_mul(0x85ebca6b)) % h as u32) as usize;
378
379 let mut x = pos_x as f32 + 0.5;
380 let mut y = pos_y as f32 + 0.5;
381 let mut vel_x = 0.0f32;
382 let mut vel_y = 0.0f32;
383 let mut water = 1.0f32;
384 let mut sediment = 0.0f32;
385 let mut speed = 1.0f32;
386
387 for _step in 0..settings.max_droplet_steps {
388 let ix = x as usize;
389 let iy = y as usize;
390 if ix == 0 || iy == 0 || ix+1 >= w || iy+1 >= h { break; }
391
392 let h00 = hm.get(ix, iy);
394 let h10 = hm.get(ix+1, iy);
395 let h01 = hm.get(ix, iy+1);
396 let gx = h10 - h00;
397 let gy = h01 - h00;
398
399 vel_x = vel_x * settings.inertia - gx * (1.0 - settings.inertia);
400 vel_y = vel_y * settings.inertia - gy * (1.0 - settings.inertia);
401
402 let vel_len = (vel_x*vel_x + vel_y*vel_y).sqrt().max(1e-6);
403 vel_x /= vel_len;
404 vel_y /= vel_len;
405
406 x += vel_x;
407 y += vel_y;
408
409 if x < 0.0 || x >= (w-1) as f32 || y < 0.0 || y >= (h-1) as f32 { break; }
410
411 let new_height = hm.sample_bilinear(x / w as f32, y / h as f32);
412 let delta_h = new_height - h00;
413 let capacity = (speed * water * settings.capacity).max(0.0);
414
415 if delta_h > 0.0 || sediment > capacity {
416 let deposition = if delta_h > 0.0 {
417 delta_h.min(sediment)
418 } else {
419 (sediment - capacity) * settings.deposition_rate
420 };
421 sediment -= deposition;
422 hm.set(ix, iy, (h00 + deposition).clamp(0.0, 1.0));
423 } else {
424 let erosion = ((capacity - sediment) * settings.erosion_rate).min(-delta_h);
425 sediment += erosion;
426 let cur = hm.get(ix, iy);
427 hm.set(ix, iy, (cur - erosion).clamp(0.0, 1.0));
428 }
429
430 speed = (speed * speed + delta_h.abs() * settings.gravity).sqrt().clamp(0.0, 10.0);
431 water *= 1.0 - settings.evaporation_rate;
432 if water < 0.01 { break; }
433 }
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
440pub enum BiomeKind {
441 Ocean,
442 Beach,
443 Desert,
444 Grassland,
445 Savanna,
446 TropicalForest,
447 TemperateForest,
448 BorealForest,
449 Tundra,
450 Alpine,
451 Snow,
452 Volcanic,
453 Wetlands,
454 Canyon,
455 Badlands,
456}
457
458impl BiomeKind {
459 pub fn label(&self) -> &'static str {
460 match self {
461 Self::Ocean => "Ocean",
462 Self::Beach => "Beach",
463 Self::Desert => "Desert",
464 Self::Grassland => "Grassland",
465 Self::Savanna => "Savanna",
466 Self::TropicalForest => "Tropical Forest",
467 Self::TemperateForest => "Temperate Forest",
468 Self::BorealForest => "Boreal Forest",
469 Self::Tundra => "Tundra",
470 Self::Alpine => "Alpine",
471 Self::Snow => "Snow",
472 Self::Volcanic => "Volcanic",
473 Self::Wetlands => "Wetlands",
474 Self::Canyon => "Canyon",
475 Self::Badlands => "Badlands",
476 }
477 }
478
479 pub fn base_color(&self) -> [f32; 3] {
480 match self {
481 Self::Ocean => [0.1, 0.3, 0.7],
482 Self::Beach => [0.9, 0.85, 0.65],
483 Self::Desert => [0.85, 0.75, 0.4],
484 Self::Grassland => [0.4, 0.65, 0.25],
485 Self::Savanna => [0.7, 0.6, 0.3],
486 Self::TropicalForest => [0.1, 0.5, 0.1],
487 Self::TemperateForest => [0.2, 0.5, 0.2],
488 Self::BorealForest => [0.1, 0.3, 0.2],
489 Self::Tundra => [0.6, 0.6, 0.5],
490 Self::Alpine => [0.5, 0.5, 0.4],
491 Self::Snow => [0.9, 0.9, 0.95],
492 Self::Volcanic => [0.3, 0.1, 0.05],
493 Self::Wetlands => [0.2, 0.4, 0.25],
494 Self::Canyon => [0.7, 0.4, 0.2],
495 Self::Badlands => [0.6, 0.3, 0.15],
496 }
497 }
498
499 pub fn from_climate(temperature: f32, moisture: f32, height: f32) -> Self {
500 if height < 0.1 { return Self::Ocean; }
501 if height < 0.15 { return Self::Beach; }
502 if height > 0.85 { return Self::Snow; }
503 if height > 0.7 { return Self::Alpine; }
504
505 if temperature > 0.7 {
506 if moisture > 0.6 { Self::TropicalForest }
507 else if moisture > 0.3 { Self::Savanna }
508 else { Self::Desert }
509 } else if temperature > 0.4 {
510 if moisture > 0.5 { Self::TemperateForest }
511 else if moisture > 0.2 { Self::Grassland }
512 else { Self::Desert }
513 } else if temperature > 0.1 {
514 if moisture > 0.4 { Self::BorealForest }
515 else { Self::Tundra }
516 } else {
517 Self::Snow
518 }
519 }
520}
521
522#[derive(Debug, Clone)]
525pub struct BiomeMap {
526 pub width: usize,
527 pub height: usize,
528 pub biomes: Vec<BiomeKind>,
529 pub temperature: Vec<f32>,
530 pub moisture: Vec<f32>,
531}
532
533impl BiomeMap {
534 pub fn new(width: usize, height: usize) -> Self {
535 Self {
536 width, height,
537 biomes: vec![BiomeKind::Grassland; width * height],
538 temperature: vec![0.5; width * height],
539 moisture: vec![0.5; width * height],
540 }
541 }
542
543 pub fn generate(hm: &HeightMap, temp_gen: &NoiseGenerator, moisture_gen: &NoiseGenerator) -> Self {
544 let w = hm.width; let h = hm.height;
545 let mut bm = Self::new(w, h);
546 for y in 0..h {
547 for x in 0..w {
548 let nx = x as f32 / w as f32;
549 let ny = y as f32 / h as f32;
550 let height = hm.get(x, y);
551 let temp = temp_gen.sample(nx, ny);
552 let moisture = moisture_gen.sample(nx, ny);
553 let temp_adj = (temp - height * 0.5).clamp(0.0, 1.0);
554 let idx = y * w + x;
555 bm.temperature[idx] = temp_adj;
556 bm.moisture[idx] = moisture;
557 bm.biomes[idx] = BiomeKind::from_climate(temp_adj, moisture, height);
558 }
559 }
560 bm
561 }
562
563 pub fn get(&self, x: usize, y: usize) -> BiomeKind {
564 self.biomes[y.min(self.height-1) * self.width + x.min(self.width-1)]
565 }
566
567 pub fn biome_coverage(&self) -> HashMap<BiomeKind, f32> {
568 let total = (self.width * self.height) as f32;
569 let mut counts: HashMap<BiomeKind, u32> = HashMap::new();
570 for &b in &self.biomes {
571 *counts.entry(b).or_insert(0) += 1;
572 }
573 counts.into_iter().map(|(k, v)| (k, v as f32 / total)).collect()
574 }
575}
576
577#[derive(Debug, Clone)]
580pub struct SplatLayer {
581 pub name: String,
582 pub texture_path: String,
583 pub normal_path: Option<String>,
584 pub tiling: f32,
585 pub metallic: f32,
586 pub roughness: f32,
587}
588
589#[derive(Debug, Clone)]
590pub struct SplatMap {
591 pub width: usize,
592 pub height: usize,
593 pub layers: Vec<SplatLayer>,
594 pub weights: Vec<Vec<f32>>, }
596
597impl SplatMap {
598 pub fn new(width: usize, height: usize) -> Self {
599 Self { width, height, layers: Vec::new(), weights: Vec::new() }
600 }
601
602 pub fn add_layer(&mut self, layer: SplatLayer) {
603 let n = self.width * self.height;
604 self.layers.push(layer);
605 self.weights.push(vec![0.0; n]);
606 }
607
608 pub fn paint(&mut self, layer: usize, cx: f32, cy: f32, radius: f32, strength: f32) {
609 let w = self.width as f32;
610 let h = self.height as f32;
611 for y in 0..self.height {
612 for x in 0..self.width {
613 let nx = x as f32 / w;
614 let ny = y as f32 / h;
615 let dist = ((nx-cx).powi(2) + (ny-cy).powi(2)).sqrt();
616 if dist < radius && layer < self.layers.len() {
617 let falloff = (1.0 - dist/radius).powi(2);
618 let idx = y * self.width + x;
619 self.weights[layer][idx] = (self.weights[layer][idx] + strength * falloff).clamp(0.0, 1.0);
620 }
621 }
622 }
623 self.normalize_weights();
624 }
625
626 fn normalize_weights(&mut self) {
627 let n = self.width * self.height;
628 let layer_count = self.layers.len();
629 for i in 0..n {
630 let sum: f32 = (0..layer_count).map(|l| self.weights[l][i]).sum();
631 if sum > 1e-6 {
632 for l in 0..layer_count {
633 self.weights[l][i] /= sum;
634 }
635 }
636 }
637 }
638
639 pub fn from_biome_map(biome_map: &BiomeMap, hm: &HeightMap) -> Self {
640 let w = biome_map.width; let h = biome_map.height;
641 let mut sm = Self::new(w, h);
642
643 sm.add_layer(SplatLayer { name: "Grass".into(), texture_path: "grass_diffuse.png".into(), normal_path: Some("grass_normal.png".into()), tiling: 20.0, metallic: 0.0, roughness: 0.9 });
645 sm.add_layer(SplatLayer { name: "Rock".into(), texture_path: "rock_diffuse.png".into(), normal_path: Some("rock_normal.png".into()), tiling: 10.0, metallic: 0.0, roughness: 0.8 });
646 sm.add_layer(SplatLayer { name: "Sand".into(), texture_path: "sand_diffuse.png".into(), normal_path: Some("sand_normal.png".into()), tiling: 15.0, metallic: 0.0, roughness: 0.95 });
647 sm.add_layer(SplatLayer { name: "Snow".into(), texture_path: "snow_diffuse.png".into(), normal_path: Some("snow_normal.png".into()), tiling: 8.0, metallic: 0.0, roughness: 0.85 });
648 sm.add_layer(SplatLayer { name: "Dirt".into(), texture_path: "dirt_diffuse.png".into(), normal_path: Some("dirt_normal.png".into()), tiling: 12.0, metallic: 0.0, roughness: 0.9 });
649 sm.add_layer(SplatLayer { name: "Volcanic".into(), texture_path: "volcanic_diffuse.png".into(), normal_path: None, tiling: 6.0, metallic: 0.2, roughness: 0.7 });
650
651 for y in 0..h {
653 for x in 0..w {
654 let idx = y * w + x;
655 let biome = biome_map.get(x, y);
656 let height = hm.get(x, y);
657 let slope = hm.slope_at(x, y);
658
659 let grass = match biome { BiomeKind::Grassland | BiomeKind::TemperateForest | BiomeKind::BorealForest | BiomeKind::TropicalForest | BiomeKind::Wetlands => 0.8, BiomeKind::Savanna | BiomeKind::Tundra => 0.4, _ => 0.0 };
660 let rock = slope.powi(2) * 3.0 + if height > 0.6 { (height - 0.6) * 2.0 } else { 0.0 };
661 let sand = match biome { BiomeKind::Beach | BiomeKind::Desert => 0.9, _ => 0.0 };
662 let snow = match biome { BiomeKind::Snow | BiomeKind::Alpine => 0.9, _ => if height > 0.85 { 0.8 } else { 0.0 } };
663 let dirt = match biome { BiomeKind::Badlands | BiomeKind::Canyon => 0.7, _ => 0.2 };
664 let volcanic= match biome { BiomeKind::Volcanic => 0.9, _ => 0.0 };
665
666 sm.weights[0][idx] = (grass as f32).clamp(0.0, 1.0);
667 sm.weights[1][idx] = (rock as f32).clamp(0.0, 1.0);
668 sm.weights[2][idx] = (sand as f32).clamp(0.0, 1.0);
669 sm.weights[3][idx] = (snow as f32).clamp(0.0, 1.0);
670 sm.weights[4][idx] = (dirt as f32).clamp(0.0, 1.0);
671 sm.weights[5][idx] = (volcanic as f32).clamp(0.0, 1.0);
672 }
673 }
674 sm.normalize_weights();
675 sm
676 }
677
678 pub fn dominant_layer(&self, x: usize, y: usize) -> usize {
679 let idx = y * self.width + x;
680 (0..self.layers.len())
681 .max_by(|&a, &b| self.weights[a][idx].partial_cmp(&self.weights[b][idx]).unwrap())
682 .unwrap_or(0)
683 }
684}
685
686#[derive(Debug, Clone)]
689pub struct TerrainLodLevel {
690 pub level: u32,
691 pub resolution: usize,
692 pub max_distance: f32,
693 pub vertex_count: usize,
694 pub triangle_count: usize,
695}
696
697impl TerrainLodLevel {
698 pub fn generate_from_heightmap(hm: &HeightMap, level: u32) -> Self {
699 let step = (1 << level).min(hm.width / 2);
700 let res = hm.width / step;
701 let vc = (res + 1) * (res + 1);
702 let tc = res * res * 2;
703 Self {
704 level,
705 resolution: res,
706 max_distance: 50.0 * (1 << level) as f32,
707 vertex_count: vc,
708 triangle_count: tc,
709 }
710 }
711}
712
713#[derive(Debug, Clone)]
716pub struct TerrainChunk {
717 pub x: i32,
718 pub z: i32,
719 pub size: f32,
720 pub height_map: HeightMap,
721 pub biome_map: Option<BiomeMap>,
722 pub splat_map: Option<SplatMap>,
723 pub lod_levels: Vec<TerrainLodLevel>,
724 pub is_dirty: bool,
725 pub is_loaded: bool,
726}
727
728impl TerrainChunk {
729 pub fn new(x: i32, z: i32, size: f32, resolution: usize) -> Self {
730 Self {
731 x, z, size,
732 height_map: HeightMap::new(resolution, resolution),
733 biome_map: None,
734 splat_map: None,
735 lod_levels: Vec::new(),
736 is_dirty: true,
737 is_loaded: false,
738 }
739 }
740
741 pub fn generate(&mut self, gen: &NoiseGenerator) {
742 let w = self.height_map.width;
743 let h = self.height_map.height;
744 for y in 0..h {
745 for x_idx in 0..w {
746 let wx = self.x as f32 + x_idx as f32 / w as f32;
747 let wz = self.z as f32 + y as f32 / h as f32;
748 self.height_map.data[y * w + x_idx] = gen.sample(wx, wz);
749 }
750 }
751 self.lod_levels.clear();
753 for lod in 0..4u32 {
754 self.lod_levels.push(TerrainLodLevel::generate_from_heightmap(&self.height_map, lod));
755 }
756 self.is_dirty = true;
757 }
758
759 pub fn world_bounds(&self) -> ([f32; 3], [f32; 3]) {
760 let min_h = self.height_map.min_height() * self.height_map.height_scale;
761 let max_h = self.height_map.max_height() * self.height_map.height_scale;
762 (
763 [self.x as f32 * self.size, min_h, self.z as f32 * self.size],
764 [(self.x + 1) as f32 * self.size, max_h, (self.z + 1) as f32 * self.size],
765 )
766 }
767
768 pub fn height_at_world(&self, wx: f32, wz: f32) -> f32 {
769 let nx = (wx - self.x as f32 * self.size) / self.size;
770 let nz = (wz - self.z as f32 * self.size) / self.size;
771 self.height_map.sample_bilinear(nx, nz) * self.height_map.height_scale
772 }
773}
774
775#[derive(Debug, Clone)]
778pub struct TerrainWorld {
779 pub chunks: HashMap<(i32, i32), TerrainChunk>,
780 pub chunk_size: f32,
781 pub chunk_resolution: usize,
782 pub noise_gen: NoiseGenerator,
783 pub erosion_settings: ErosionSettings,
784 pub apply_erosion: bool,
785 pub generate_biomes: bool,
786 pub generate_splat: bool,
787 pub view_distance: f32,
788 pub loaded_chunks: Vec<(i32, i32)>,
789}
790
791impl TerrainWorld {
792 pub fn new(seed: u32) -> Self {
793 Self {
794 chunks: HashMap::new(),
795 chunk_size: 100.0,
796 chunk_resolution: 128,
797 noise_gen: NoiseGenerator::new(seed),
798 erosion_settings: ErosionSettings::default(),
799 apply_erosion: false,
800 generate_biomes: true,
801 generate_splat: true,
802 view_distance: 500.0,
803 loaded_chunks: Vec::new(),
804 }
805 }
806
807 pub fn load_chunk(&mut self, cx: i32, cz: i32) -> &TerrainChunk {
808 let key = (cx, cz);
809 if !self.chunks.contains_key(&key) {
810 let mut chunk = TerrainChunk::new(cx, cz, self.chunk_size, self.chunk_resolution);
811 chunk.generate(&self.noise_gen);
812 if self.apply_erosion {
813 let settings = self.erosion_settings.clone();
814 simulate_erosion(&mut chunk.height_map, &settings);
815 }
816 if self.generate_biomes {
817 let mut temp_gen = NoiseGenerator::new(self.noise_gen.seed.wrapping_add(1));
818 let mut moist_gen = NoiseGenerator::new(self.noise_gen.seed.wrapping_add(2));
819 temp_gen.scale = 0.5;
820 moist_gen.scale = 0.4;
821 chunk.biome_map = Some(BiomeMap::generate(&chunk.height_map, &temp_gen, &moist_gen));
822 }
823 if self.generate_splat {
824 if let Some(bm) = &chunk.biome_map {
825 chunk.splat_map = Some(SplatMap::from_biome_map(bm, &chunk.height_map));
826 }
827 }
828 chunk.is_loaded = true;
829 self.chunks.insert(key, chunk);
830 if !self.loaded_chunks.contains(&key) {
831 self.loaded_chunks.push(key);
832 }
833 }
834 &self.chunks[&key]
835 }
836
837 pub fn unload_chunk(&mut self, cx: i32, cz: i32) {
838 self.chunks.remove(&(cx, cz));
839 self.loaded_chunks.retain(|&k| k != (cx, cz));
840 }
841
842 pub fn update_view(&mut self, camera_x: f32, camera_z: f32) {
843 let cx = (camera_x / self.chunk_size) as i32;
844 let cz = (camera_z / self.chunk_size) as i32;
845 let radius = (self.view_distance / self.chunk_size).ceil() as i32;
846
847 let mut needed = Vec::new();
849 for dz in -radius..=radius {
850 for dx in -radius..=radius {
851 let dist = ((dx*dx + dz*dz) as f32).sqrt() * self.chunk_size;
852 if dist <= self.view_distance {
853 needed.push((cx + dx, cz + dz));
854 }
855 }
856 }
857
858 let to_unload: Vec<(i32,i32)> = self.loaded_chunks.iter()
860 .filter(|&&(x, z)| {
861 let dist = (((x-cx)*(x-cx) + (z-cz)*(z-cz)) as f32).sqrt() * self.chunk_size;
862 dist > self.view_distance * 1.5
863 })
864 .copied()
865 .collect();
866 for k in to_unload { self.unload_chunk(k.0, k.1); }
867
868 for (x, z) in needed {
870 if !self.chunks.contains_key(&(x, z)) {
871 self.load_chunk(x, z);
872 }
873 }
874 }
875
876 pub fn height_at(&self, wx: f32, wz: f32) -> f32 {
877 let cx = (wx / self.chunk_size).floor() as i32;
878 let cz = (wz / self.chunk_size).floor() as i32;
879 if let Some(chunk) = self.chunks.get(&(cx, cz)) {
880 chunk.height_at_world(wx, wz)
881 } else {
882 0.0
883 }
884 }
885
886 pub fn loaded_chunk_count(&self) -> usize { self.loaded_chunks.len() }
887 pub fn total_vertex_count(&self) -> usize {
888 self.chunks.values()
889 .flat_map(|c| c.lod_levels.iter())
890 .filter(|l| l.level == 0)
891 .map(|l| l.vertex_count)
892 .sum()
893 }
894}
895
896#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 fn heightmap_bilinear() {
904 let mut hm = HeightMap::new(16, 16);
905 hm.data[0] = 0.0;
906 hm.data[1] = 1.0;
907 let v = hm.sample_bilinear(1.0 / 15.0, 0.0);
908 assert!(v > 0.0 && v <= 1.0);
909 }
910
911 #[test]
912 fn noise_range() {
913 let gen = NoiseGenerator::default();
914 for i in 0..100 {
915 let v = gen.sample(i as f32 * 0.01, i as f32 * 0.013);
916 assert!(v >= -1.0 && v <= 1.0, "noise out of range: {}", v);
917 }
918 }
919
920 #[test]
921 fn biome_from_climate() {
922 assert_eq!(BiomeKind::from_climate(0.8, 0.8, 0.5), BiomeKind::TropicalForest);
923 assert_eq!(BiomeKind::from_climate(0.8, 0.1, 0.5), BiomeKind::Desert);
924 assert_eq!(BiomeKind::from_climate(0.5, 0.6, 0.95), BiomeKind::Snow);
925 }
926
927 #[test]
928 fn heightmap_normalize() {
929 let mut hm = HeightMap::new(4, 4);
930 hm.data = vec![0.0, 0.5, 1.0, 2.0, 3.0, 0.25, 0.75, 1.5, 0.1, 0.9, 0.4, 0.6, 0.2, 0.8, 0.3, 0.7];
931 hm.normalize();
932 let min = hm.min_height();
933 let max = hm.max_height();
934 assert!((min - 0.0).abs() < 1e-5);
935 assert!((max - 1.0).abs() < 1e-5);
936 }
937
938 #[test]
939 fn terrain_chunk_generate() {
940 let gen = NoiseGenerator::new(42);
941 let mut chunk = TerrainChunk::new(0, 0, 100.0, 32);
942 chunk.generate(&gen);
943 assert!(!chunk.lod_levels.is_empty());
944 let h = chunk.height_at_world(50.0, 50.0);
945 assert!(h >= 0.0);
946 }
947
948 #[test]
949 fn splat_map_normalize() {
950 let mut sm = SplatMap::new(8, 8);
951 sm.add_layer(SplatLayer { name: "A".into(), texture_path: "a.png".into(), normal_path: None, tiling: 1.0, metallic: 0.0, roughness: 1.0 });
952 sm.add_layer(SplatLayer { name: "B".into(), texture_path: "b.png".into(), normal_path: None, tiling: 1.0, metallic: 0.0, roughness: 1.0 });
953 sm.paint(0, 0.5, 0.5, 0.3, 0.6);
954 for i in 0..64 {
956 let sum: f32 = sm.weights.iter().map(|w| w[i]).sum();
957 assert!(sum <= 1.001, "weight sum {} > 1 at {}", sum, i);
958 }
959 }
960}