Skip to main content

proof_engine/editor/
terrain.rs

1// terrain.rs — Procedural terrain system for proof-engine
2// Height map generation, erosion simulation, biome blending,
3// road/river carving, LOD mesh generation, and texture splatting.
4
5use std::collections::HashMap;
6
7// ─── Height map ───────────────────────────────────────────────────────────────
8
9#[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        // Laplacian
95        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// ─── Noise generator ─────────────────────────────────────────────────────────
180
181#[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        // Domain warp
242        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        // Simplified Swiss turbulence
325        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// ─── Erosion ─────────────────────────────────────────────────────────────────
337
338#[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) {  // cap for speed in tests
374        // Simple droplet simulation
375        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            // Calculate gradient
393            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// ─── Biome system ─────────────────────────────────────────────────────────────
438
439#[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// ─── Biome map ────────────────────────────────────────────────────────────────
523
524#[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// ─── Splat map ────────────────────────────────────────────────────────────────
578
579#[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>>,   // weights[layer][pixel]
595}
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        // Add standard terrain layers
644        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        // Fill weights based on biome / height
652        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// ─── LOD system ──────────────────────────────────────────────────────────────
687
688#[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// ─── Terrain chunk ────────────────────────────────────────────────────────────
714
715#[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        // Build LOD levels
752        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// ─── Terrain world ────────────────────────────────────────────────────────────
776
777#[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        // Load needed chunks
848        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        // Unload far chunks
859        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        // Load new chunks
869        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// ─── Tests ───────────────────────────────────────────────────────────────────
897
898#[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        // After painting, weights should sum to ≤ 1
955        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}