Skip to main content

proof_engine/fractal/
terrain.rs

1//! Fractal terrain generation — diamond-square, midpoint displacement, fBm.
2
3/// Terrain generation parameters.
4#[derive(Debug, Clone)]
5pub struct TerrainParams {
6    pub size: u32,       // Must be 2^n + 1
7    pub roughness: f32,  // 0.0 = smooth, 1.0 = very rough
8    pub amplitude: f32,  // Initial displacement range
9    pub seed: u64,
10}
11impl Default for TerrainParams {
12    fn default() -> Self { Self { size: 129, roughness: 0.5, amplitude: 1.0, seed: 42 } }
13}
14
15/// Generated fractal terrain heightmap.
16#[derive(Debug, Clone)]
17pub struct FractalTerrain {
18    pub heightmap: Vec<f32>,
19    pub size: u32,
20    pub min_height: f32,
21    pub max_height: f32,
22}
23
24impl FractalTerrain {
25    /// Generate terrain using the diamond-square algorithm.
26    pub fn diamond_square(params: &TerrainParams) -> Self {
27        let size = params.size as usize;
28        let mut map = vec![0.0f32; size * size];
29        let mut rng = params.seed;
30
31        // Seed corners
32        map[0] = rand(&mut rng) * params.amplitude;
33        map[size - 1] = rand(&mut rng) * params.amplitude;
34        map[(size - 1) * size] = rand(&mut rng) * params.amplitude;
35        map[(size - 1) * size + size - 1] = rand(&mut rng) * params.amplitude;
36
37        let mut step = size - 1;
38        let mut scale = params.amplitude;
39
40        while step > 1 {
41            let half = step / 2;
42
43            // Diamond step
44            for y in (0..size - 1).step_by(step) {
45                for x in (0..size - 1).step_by(step) {
46                    let avg = (map[y * size + x] + map[y * size + x + step]
47                        + map[(y + step) * size + x] + map[(y + step) * size + x + step]) * 0.25;
48                    map[(y + half) * size + x + half] = avg + rand(&mut rng) * scale;
49                }
50            }
51
52            // Square step
53            for y in (0..size).step_by(half) {
54                let x_start = if (y / half) % 2 == 0 { half } else { 0 };
55                for x in (x_start..size).step_by(step) {
56                    let mut sum = 0.0f32;
57                    let mut count = 0;
58                    if y >= half { sum += map[(y - half) * size + x]; count += 1; }
59                    if y + half < size { sum += map[(y + half) * size + x]; count += 1; }
60                    if x >= half { sum += map[y * size + x - half]; count += 1; }
61                    if x + half < size { sum += map[y * size + x + half]; count += 1; }
62                    map[y * size + x] = sum / count as f32 + rand(&mut rng) * scale;
63                }
64            }
65
66            step = half;
67            scale *= 2.0_f32.powf(-params.roughness);
68        }
69
70        let min_h = map.iter().copied().fold(f32::MAX, f32::min);
71        let max_h = map.iter().copied().fold(f32::MIN, f32::max);
72
73        Self { heightmap: map, size: params.size, min_height: min_h, max_height: max_h }
74    }
75
76    pub fn get(&self, x: u32, y: u32) -> f32 {
77        self.heightmap[(y * self.size + x) as usize]
78    }
79
80    /// Normalize heights to [0, 1].
81    pub fn normalize(&mut self) {
82        let range = (self.max_height - self.min_height).max(1e-6);
83        for v in &mut self.heightmap { *v = (*v - self.min_height) / range; }
84        self.min_height = 0.0;
85        self.max_height = 1.0;
86    }
87
88    /// Sample height with bilinear interpolation at fractional coordinates.
89    pub fn sample(&self, u: f32, v: f32) -> f32 {
90        let fx = u * (self.size - 1) as f32;
91        let fy = v * (self.size - 1) as f32;
92        let x0 = (fx.floor() as u32).min(self.size - 2);
93        let y0 = (fy.floor() as u32).min(self.size - 2);
94        let fx = fx.fract();
95        let fy = fy.fract();
96        let h00 = self.get(x0, y0);
97        let h10 = self.get(x0 + 1, y0);
98        let h01 = self.get(x0, y0 + 1);
99        let h11 = self.get(x0 + 1, y0 + 1);
100        let h0 = h00 + (h10 - h00) * fx;
101        let h1 = h01 + (h11 - h01) * fx;
102        h0 + (h1 - h0) * fy
103    }
104}
105
106fn rand(rng: &mut u64) -> f32 {
107    *rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
108    (*rng >> 33) as f32 / (u32::MAX >> 1) as f32 * 2.0 - 1.0
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    #[test]
115    fn diamond_square_generates() {
116        let t = FractalTerrain::diamond_square(&TerrainParams { size: 33, ..Default::default() });
117        assert_eq!(t.heightmap.len(), 33 * 33);
118        assert!(t.max_height > t.min_height);
119    }
120    #[test]
121    fn terrain_sample_in_range() {
122        let mut t = FractalTerrain::diamond_square(&TerrainParams::default());
123        t.normalize();
124        let h = t.sample(0.5, 0.5);
125        assert!(h >= 0.0 && h <= 1.0, "h={h}");
126    }
127}