Skip to main content

proof_engine/surfaces/
heightfield.rs

1//! # Height Field Surfaces
2//!
3//! Height-map driven surfaces with configurable noise sources, real-time deformation,
4//! collision detection, LOD, and chunk-based infinite scrolling.
5//!
6//! The [`HeightFieldSurface`] type generates a 2D grid of heights from a [`NoiseSource`]
7//! and provides normal computation, ray intersection, and mesh export. The [`ChunkManager`]
8//! implements infinite scrolling by generating chunks around a camera position and recycling
9//! distant ones.
10
11use glam::{Vec2, Vec3};
12use std::f32::consts::{PI, TAU};
13
14// ─────────────────────────────────────────────────────────────────────────────
15// Noise sources
16// ─────────────────────────────────────────────────────────────────────────────
17
18/// Noise generation method for height fields.
19#[derive(Debug, Clone)]
20pub enum NoiseSource {
21    /// Simple Perlin noise at a single frequency.
22    Perlin {
23        frequency: f32,
24        amplitude: f32,
25        seed: u32,
26    },
27    /// Fractional Brownian Motion — multiple octaves of Perlin noise.
28    Fbm {
29        frequency: f32,
30        amplitude: f32,
31        octaves: u32,
32        lacunarity: f32,
33        persistence: f32,
34        seed: u32,
35    },
36    /// Ridged multifractal noise — sharp ridges.
37    Ridged {
38        frequency: f32,
39        amplitude: f32,
40        octaves: u32,
41        lacunarity: f32,
42        gain: f32,
43        offset: f32,
44        seed: u32,
45    },
46    /// Domain-warped noise — feeds noise through itself for organic distortion.
47    DomainWarped {
48        base: Box<NoiseSource>,
49        warp_frequency: f32,
50        warp_amplitude: f32,
51        warp_seed: u32,
52    },
53    /// Flat surface (height = constant).
54    Flat {
55        height: f32,
56    },
57    /// Sinusoidal terrain (for testing).
58    Sinusoidal {
59        frequency_x: f32,
60        frequency_z: f32,
61        amplitude: f32,
62    },
63    /// Composite: sum of multiple noise sources.
64    Composite {
65        sources: Vec<NoiseSource>,
66    },
67}
68
69impl Default for NoiseSource {
70    fn default() -> Self {
71        NoiseSource::Fbm {
72            frequency: 0.02,
73            amplitude: 30.0,
74            octaves: 6,
75            lacunarity: 2.0,
76            persistence: 0.5,
77            seed: 42,
78        }
79    }
80}
81
82// ─────────────────────────────────────────────────────────────────────────────
83// Built-in noise evaluation (std-only, no external crate)
84// ─────────────────────────────────────────────────────────────────────────────
85
86/// Permutation table for noise evaluation.
87const PERM: [u8; 256] = [
88    151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
89    140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
90    247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
91     57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
92     74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
93     60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
94     65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
95    200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
96     52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
97    207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
98    119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
99    129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
100    218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
101     81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
102    184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
103    222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
104];
105
106#[inline(always)]
107fn perm(i: i32) -> usize {
108    PERM[((i % 256 + 256) % 256) as usize] as usize
109}
110
111#[inline(always)]
112fn perm_seeded(i: i32, seed: u32) -> usize {
113    PERM[(((i.wrapping_add(seed as i32)) % 256 + 256) % 256) as usize] as usize
114}
115
116#[inline(always)]
117fn fade(t: f32) -> f32 {
118    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
119}
120
121#[inline(always)]
122fn lerp(a: f32, b: f32, t: f32) -> f32 {
123    a + t * (b - a)
124}
125
126#[inline(always)]
127fn grad2(hash: usize, x: f32, y: f32) -> f32 {
128    match hash & 3 {
129        0 => x + y,
130        1 => -x + y,
131        2 => x - y,
132        _ => -x - y,
133    }
134}
135
136/// 2D Perlin noise with seed. Output in approximately [-1, 1].
137fn perlin2_seeded(x: f32, y: f32, seed: u32) -> f32 {
138    let xi = x.floor() as i32;
139    let yi = y.floor() as i32;
140    let xf = x - x.floor();
141    let yf = y - y.floor();
142    let u = fade(xf);
143    let v = fade(yf);
144
145    let aa = perm_seeded(perm_seeded(xi, seed) as i32 + yi, seed);
146    let ba = perm_seeded(perm_seeded(xi + 1, seed) as i32 + yi, seed);
147    let ab = perm_seeded(perm_seeded(xi, seed) as i32 + yi + 1, seed);
148    let bb = perm_seeded(perm_seeded(xi + 1, seed) as i32 + yi + 1, seed);
149
150    lerp(
151        lerp(grad2(aa, xf, yf), grad2(ba, xf - 1.0, yf), u),
152        lerp(grad2(ab, xf, yf - 1.0), grad2(bb, xf - 1.0, yf - 1.0), u),
153        v,
154    )
155}
156
157impl NoiseSource {
158    /// Evaluate the noise source at world-space position (x, z).
159    pub fn sample(&self, x: f32, z: f32) -> f32 {
160        match self {
161            NoiseSource::Perlin { frequency, amplitude, seed } => {
162                perlin2_seeded(x * frequency, z * frequency, *seed) * amplitude
163            }
164            NoiseSource::Fbm { frequency, amplitude, octaves, lacunarity, persistence, seed } => {
165                let mut value = 0.0_f32;
166                let mut freq = *frequency;
167                let mut amp = *amplitude;
168                for oct in 0..*octaves {
169                    let s = seed.wrapping_add(oct * 31);
170                    value += perlin2_seeded(x * freq, z * freq, s) * amp;
171                    freq *= lacunarity;
172                    amp *= persistence;
173                }
174                value
175            }
176            NoiseSource::Ridged { frequency, amplitude, octaves, lacunarity, gain, offset, seed } => {
177                let mut value = 0.0_f32;
178                let mut freq = *frequency;
179                let mut amp = *amplitude;
180                let mut weight = 1.0_f32;
181                for oct in 0..*octaves {
182                    let s = seed.wrapping_add(oct * 31);
183                    let signal = perlin2_seeded(x * freq, z * freq, s).abs();
184                    let signal = offset - signal;
185                    let signal = signal * signal * weight;
186                    weight = (signal * gain).clamp(0.0, 1.0);
187                    value += signal * amp;
188                    freq *= lacunarity;
189                    amp *= 0.5;
190                }
191                value
192            }
193            NoiseSource::DomainWarped { base, warp_frequency, warp_amplitude, warp_seed } => {
194                let wx = perlin2_seeded(x * warp_frequency, z * warp_frequency, *warp_seed)
195                    * warp_amplitude;
196                let wz = perlin2_seeded(
197                    (x + 100.0) * warp_frequency,
198                    (z + 100.0) * warp_frequency,
199                    warp_seed.wrapping_add(7),
200                ) * warp_amplitude;
201                base.sample(x + wx, z + wz)
202            }
203            NoiseSource::Flat { height } => *height,
204            NoiseSource::Sinusoidal { frequency_x, frequency_z, amplitude } => {
205                (x * frequency_x * TAU).sin() * (z * frequency_z * TAU).sin() * amplitude
206            }
207            NoiseSource::Composite { sources } => {
208                sources.iter().map(|s| s.sample(x, z)).sum()
209            }
210        }
211    }
212
213    /// Compute the gradient of the noise at (x, z) using central differences.
214    pub fn gradient(&self, x: f32, z: f32) -> Vec2 {
215        let eps = 0.1;
216        let dx = (self.sample(x + eps, z) - self.sample(x - eps, z)) / (2.0 * eps);
217        let dz = (self.sample(x, z + eps) - self.sample(x, z - eps)) / (2.0 * eps);
218        Vec2::new(dx, dz)
219    }
220}
221
222// ─────────────────────────────────────────────────────────────────────────────
223// HeightFieldSurface
224// ─────────────────────────────────────────────────────────────────────────────
225
226/// A height field surface driven by a noise source.
227///
228/// The surface occupies a rectangular region in XZ-space. Each grid cell stores
229/// a height value (Y coordinate). Heights are lazily generated from the noise source.
230#[derive(Clone)]
231pub struct HeightFieldSurface {
232    /// The noise source generating heights.
233    pub noise: NoiseSource,
234    /// World-space origin (bottom-left corner of the heightfield in XZ).
235    pub origin: Vec2,
236    /// World-space size of the heightfield in XZ.
237    pub size: Vec2,
238    /// Number of samples along X.
239    pub resolution_x: usize,
240    /// Number of samples along Z.
241    pub resolution_z: usize,
242    /// Computed height values. Row-major: heights[z * resolution_x + x].
243    pub heights: Vec<f32>,
244    /// Time parameter for animated deformations.
245    pub time: f32,
246    /// Animation modes applied to the surface.
247    pub animations: Vec<HeightFieldAnimation>,
248}
249
250/// Real-time animation modes for height fields.
251#[derive(Debug, Clone)]
252pub enum HeightFieldAnimation {
253    /// Terrain breathes up and down.
254    Breathe {
255        amplitude: f32,
256        frequency: f32,
257    },
258    /// Ripple from a point.
259    Ripple {
260        center: Vec2,
261        speed: f32,
262        amplitude: f32,
263        wavelength: f32,
264        decay: f32,
265    },
266    /// Terrain warps like a cloth in wind.
267    Warp {
268        direction: Vec2,
269        speed: f32,
270        amplitude: f32,
271        frequency: f32,
272    },
273    /// Terrain collapses toward a point.
274    Collapse {
275        center: Vec2,
276        speed: f32,
277        radius: f32,
278        depth: f32,
279    },
280    /// Sinusoidal wave propagation.
281    Wave {
282        direction: Vec2,
283        speed: f32,
284        amplitude: f32,
285        wavelength: f32,
286    },
287}
288
289impl HeightFieldSurface {
290    /// Create a new height field surface.
291    pub fn new(
292        noise: NoiseSource,
293        origin: Vec2,
294        size: Vec2,
295        resolution_x: usize,
296        resolution_z: usize,
297    ) -> Self {
298        let rx = resolution_x.max(2);
299        let rz = resolution_z.max(2);
300        let mut surface = Self {
301            noise,
302            origin,
303            size,
304            resolution_x: rx,
305            resolution_z: rz,
306            heights: vec![0.0; rx * rz],
307            time: 0.0,
308            animations: Vec::new(),
309        };
310        surface.regenerate();
311        surface
312    }
313
314    /// Regenerate all height values from the noise source.
315    pub fn regenerate(&mut self) {
316        for iz in 0..self.resolution_z {
317            for ix in 0..self.resolution_x {
318                let wx = self.origin.x + (ix as f32 / (self.resolution_x - 1) as f32) * self.size.x;
319                let wz = self.origin.y + (iz as f32 / (self.resolution_z - 1) as f32) * self.size.y;
320                self.heights[iz * self.resolution_x + ix] = self.noise.sample(wx, wz);
321            }
322        }
323    }
324
325    /// Get the height at grid indices (ix, iz).
326    #[inline]
327    pub fn height_at_index(&self, ix: usize, iz: usize) -> f32 {
328        let ix = ix.min(self.resolution_x - 1);
329        let iz = iz.min(self.resolution_z - 1);
330        self.heights[iz * self.resolution_x + ix]
331    }
332
333    /// Set the height at grid indices (ix, iz).
334    #[inline]
335    pub fn set_height(&mut self, ix: usize, iz: usize, h: f32) {
336        if ix < self.resolution_x && iz < self.resolution_z {
337            self.heights[iz * self.resolution_x + ix] = h;
338        }
339    }
340
341    /// Sample height at world-space (x, z) using bilinear interpolation.
342    pub fn sample_height(&self, x: f32, z: f32) -> f32 {
343        let fx = ((x - self.origin.x) / self.size.x) * (self.resolution_x - 1) as f32;
344        let fz = ((z - self.origin.y) / self.size.y) * (self.resolution_z - 1) as f32;
345
346        let ix = fx.floor() as i32;
347        let iz = fz.floor() as i32;
348
349        if ix < 0 || iz < 0 || ix >= (self.resolution_x - 1) as i32 || iz >= (self.resolution_z - 1) as i32 {
350            // Out of bounds: sample directly from noise
351            return self.noise.sample(x, z);
352        }
353
354        let ix = ix as usize;
355        let iz = iz as usize;
356        let sx = fx - ix as f32;
357        let sz = fz - iz as f32;
358
359        let h00 = self.height_at_index(ix, iz);
360        let h10 = self.height_at_index(ix + 1, iz);
361        let h01 = self.height_at_index(ix, iz + 1);
362        let h11 = self.height_at_index(ix + 1, iz + 1);
363
364        let top = h00 * (1.0 - sx) + h10 * sx;
365        let bottom = h01 * (1.0 - sx) + h11 * sx;
366        top * (1.0 - sz) + bottom * sz
367    }
368
369    /// Compute the surface normal at world-space (x, z) using central differences.
370    pub fn normal_at(&self, x: f32, z: f32) -> Vec3 {
371        let cell_x = self.size.x / (self.resolution_x - 1) as f32;
372        let cell_z = self.size.y / (self.resolution_z - 1) as f32;
373        let eps_x = cell_x * 0.5;
374        let eps_z = cell_z * 0.5;
375
376        let hx_pos = self.sample_height(x + eps_x, z);
377        let hx_neg = self.sample_height(x - eps_x, z);
378        let hz_pos = self.sample_height(x, z + eps_z);
379        let hz_neg = self.sample_height(x, z - eps_z);
380
381        let dx = (hx_pos - hx_neg) / (2.0 * eps_x);
382        let dz = (hz_pos - hz_neg) / (2.0 * eps_z);
383
384        Vec3::new(-dx, 1.0, -dz).normalize()
385    }
386
387    /// Compute the normal at grid index using central differences.
388    pub fn normal_at_index(&self, ix: usize, iz: usize) -> Vec3 {
389        let h_left = if ix > 0 { self.height_at_index(ix - 1, iz) } else { self.height_at_index(ix, iz) };
390        let h_right = if ix + 1 < self.resolution_x { self.height_at_index(ix + 1, iz) } else { self.height_at_index(ix, iz) };
391        let h_down = if iz > 0 { self.height_at_index(ix, iz - 1) } else { self.height_at_index(ix, iz) };
392        let h_up = if iz + 1 < self.resolution_z { self.height_at_index(ix, iz + 1) } else { self.height_at_index(ix, iz) };
393
394        let cell_x = self.size.x / (self.resolution_x - 1) as f32;
395        let cell_z = self.size.y / (self.resolution_z - 1) as f32;
396
397        let dx = (h_right - h_left) / (2.0 * cell_x);
398        let dz = (h_up - h_down) / (2.0 * cell_z);
399
400        Vec3::new(-dx, 1.0, -dz).normalize()
401    }
402
403    /// Update time and apply real-time animations.
404    pub fn tick(&mut self, dt: f32) {
405        self.time += dt;
406        if self.animations.is_empty() {
407            return;
408        }
409
410        // Re-generate base heights
411        self.regenerate();
412
413        // Apply each animation on top
414        let t = self.time;
415        let animations = self.animations.clone();
416
417        for iz in 0..self.resolution_z {
418            for ix in 0..self.resolution_x {
419                let wx = self.origin.x + (ix as f32 / (self.resolution_x - 1) as f32) * self.size.x;
420                let wz = self.origin.y + (iz as f32 / (self.resolution_z - 1) as f32) * self.size.y;
421                let idx = iz * self.resolution_x + ix;
422                let pos = Vec2::new(wx, wz);
423
424                for anim in &animations {
425                    match anim {
426                        HeightFieldAnimation::Breathe { amplitude, frequency } => {
427                            self.heights[idx] += (t * frequency * TAU).sin() * amplitude;
428                        }
429                        HeightFieldAnimation::Ripple { center, speed, amplitude, wavelength, decay } => {
430                            let dist = pos.distance(*center);
431                            let wave = ((dist / wavelength - t * speed) * TAU).sin();
432                            let falloff = (-dist * decay).exp();
433                            self.heights[idx] += wave * amplitude * falloff;
434                        }
435                        HeightFieldAnimation::Warp { direction, speed, amplitude, frequency } => {
436                            let phase = pos.dot(*direction) * frequency - t * speed;
437                            self.heights[idx] += (phase * TAU).sin() * amplitude;
438                        }
439                        HeightFieldAnimation::Collapse { center, speed, radius, depth } => {
440                            let dist = pos.distance(*center);
441                            let progress = (t * speed).min(1.0);
442                            let factor = (1.0 - (dist / radius).min(1.0)).max(0.0);
443                            let factor = factor * factor; // smooth falloff
444                            self.heights[idx] -= factor * depth * progress;
445                        }
446                        HeightFieldAnimation::Wave { direction, speed, amplitude, wavelength } => {
447                            let phase = pos.dot(*direction) / wavelength - t * speed;
448                            self.heights[idx] += (phase * TAU).sin() * amplitude;
449                        }
450                    }
451                }
452            }
453        }
454    }
455
456    /// Apply a deformation brush: raise or lower terrain in a radius around (cx, cz).
457    pub fn deform_brush(&mut self, cx: f32, cz: f32, radius: f32, strength: f32) {
458        for iz in 0..self.resolution_z {
459            for ix in 0..self.resolution_x {
460                let wx = self.origin.x + (ix as f32 / (self.resolution_x - 1) as f32) * self.size.x;
461                let wz = self.origin.y + (iz as f32 / (self.resolution_z - 1) as f32) * self.size.y;
462                let dist = ((wx - cx).powi(2) + (wz - cz).powi(2)).sqrt();
463                if dist < radius {
464                    let falloff = 1.0 - dist / radius;
465                    let falloff = falloff * falloff * (3.0 - 2.0 * falloff); // smoothstep
466                    self.heights[iz * self.resolution_x + ix] += strength * falloff;
467                }
468            }
469        }
470    }
471
472    /// Flatten terrain in a radius around (cx, cz) toward a target height.
473    pub fn flatten_brush(&mut self, cx: f32, cz: f32, radius: f32, target: f32, strength: f32) {
474        for iz in 0..self.resolution_z {
475            for ix in 0..self.resolution_x {
476                let wx = self.origin.x + (ix as f32 / (self.resolution_x - 1) as f32) * self.size.x;
477                let wz = self.origin.y + (iz as f32 / (self.resolution_z - 1) as f32) * self.size.y;
478                let dist = ((wx - cx).powi(2) + (wz - cz).powi(2)).sqrt();
479                if dist < radius {
480                    let falloff = 1.0 - dist / radius;
481                    let falloff = falloff * falloff * (3.0 - 2.0 * falloff);
482                    let idx = iz * self.resolution_x + ix;
483                    self.heights[idx] += (target - self.heights[idx]) * strength * falloff;
484                }
485            }
486        }
487    }
488
489    /// Get the world-space position of a grid vertex.
490    pub fn world_position(&self, ix: usize, iz: usize) -> Vec3 {
491        let wx = self.origin.x + (ix as f32 / (self.resolution_x - 1) as f32) * self.size.x;
492        let wz = self.origin.y + (iz as f32 / (self.resolution_z - 1) as f32) * self.size.y;
493        let h = self.height_at_index(ix, iz);
494        Vec3::new(wx, h, wz)
495    }
496
497    /// Compute the bounding box: (min, max).
498    pub fn bounding_box(&self) -> (Vec3, Vec3) {
499        let mut min_h = f32::MAX;
500        let mut max_h = f32::MIN;
501        for &h in &self.heights {
502            min_h = min_h.min(h);
503            max_h = max_h.max(h);
504        }
505        (
506            Vec3::new(self.origin.x, min_h, self.origin.y),
507            Vec3::new(self.origin.x + self.size.x, max_h, self.origin.y + self.size.y),
508        )
509    }
510
511    /// Export to a list of positions and normals (interleaved grid).
512    pub fn to_vertex_data(&self) -> (Vec<Vec3>, Vec<Vec3>) {
513        let mut positions = Vec::with_capacity(self.resolution_x * self.resolution_z);
514        let mut normals = Vec::with_capacity(self.resolution_x * self.resolution_z);
515
516        for iz in 0..self.resolution_z {
517            for ix in 0..self.resolution_x {
518                positions.push(self.world_position(ix, iz));
519                normals.push(self.normal_at_index(ix, iz));
520            }
521        }
522
523        (positions, normals)
524    }
525
526    /// Generate triangle indices for the height field grid.
527    pub fn generate_indices(&self) -> Vec<[u32; 3]> {
528        let mut indices = Vec::with_capacity((self.resolution_x - 1) * (self.resolution_z - 1) * 2);
529        for iz in 0..self.resolution_z - 1 {
530            for ix in 0..self.resolution_x - 1 {
531                let tl = (iz * self.resolution_x + ix) as u32;
532                let tr = tl + 1;
533                let bl = tl + self.resolution_x as u32;
534                let br = bl + 1;
535                indices.push([tl, bl, tr]);
536                indices.push([tr, bl, br]);
537            }
538        }
539        indices
540    }
541
542    /// Sample height directly from noise (without grid interpolation).
543    pub fn sample_noise(&self, x: f32, z: f32) -> f32 {
544        self.noise.sample(x, z)
545    }
546}
547
548// ─────────────────────────────────────────────────────────────────────────────
549// Height field collision detection
550// ─────────────────────────────────────────────────────────────────────────────
551
552/// Collision detector for ray-heightfield intersection.
553pub struct HeightFieldCollider;
554
555/// Result of a height field ray intersection.
556#[derive(Debug, Clone, Copy)]
557pub struct HeightFieldHit {
558    pub position: Vec3,
559    pub normal: Vec3,
560    pub distance: f32,
561}
562
563impl HeightFieldCollider {
564    /// Cast a ray against a height field surface using ray marching + binary search refinement.
565    ///
566    /// The ray starts at `origin` and moves in `direction` (should be normalized).
567    /// `max_distance` limits the search. `step_size` controls the initial march resolution.
568    pub fn ray_cast(
569        surface: &HeightFieldSurface,
570        origin: Vec3,
571        direction: Vec3,
572        max_distance: f32,
573        step_size: f32,
574    ) -> Option<HeightFieldHit> {
575        let dir = direction.normalize();
576        let mut t = 0.0_f32;
577        let mut prev_above = true;
578        let mut prev_pos = origin;
579
580        // Phase 1: Ray march to find bracket
581        while t < max_distance {
582            let pos = origin + dir * t;
583            let terrain_h = surface.sample_height(pos.x, pos.z);
584            let above = pos.y > terrain_h;
585
586            if !above && prev_above && t > 0.0 {
587                // Crossed the surface between prev_pos and pos
588                // Phase 2: Binary search refinement
589                let mut lo = t - step_size;
590                let mut hi = t;
591
592                for _ in 0..16 {
593                    let mid = (lo + hi) * 0.5;
594                    let mid_pos = origin + dir * mid;
595                    let mid_h = surface.sample_height(mid_pos.x, mid_pos.z);
596                    if mid_pos.y > mid_h {
597                        lo = mid;
598                    } else {
599                        hi = mid;
600                    }
601                }
602
603                let final_t = (lo + hi) * 0.5;
604                let hit_pos = origin + dir * final_t;
605                let normal = surface.normal_at(hit_pos.x, hit_pos.z);
606
607                return Some(HeightFieldHit {
608                    position: hit_pos,
609                    normal,
610                    distance: final_t,
611                });
612            }
613
614            prev_above = above;
615            prev_pos = pos;
616            t += step_size;
617        }
618
619        None
620    }
621
622    /// Test if a point is above or below the terrain.
623    pub fn is_above(surface: &HeightFieldSurface, point: Vec3) -> bool {
624        point.y > surface.sample_height(point.x, point.z)
625    }
626
627    /// Get the vertical distance from a point to the terrain surface.
628    pub fn distance_to_surface(surface: &HeightFieldSurface, point: Vec3) -> f32 {
629        point.y - surface.sample_height(point.x, point.z)
630    }
631
632    /// Project a point onto the terrain (snap Y to terrain height).
633    pub fn project_onto(surface: &HeightFieldSurface, point: Vec3) -> Vec3 {
634        Vec3::new(point.x, surface.sample_height(point.x, point.z), point.z)
635    }
636
637    /// Sphere-terrain intersection test.
638    /// Returns true if a sphere at `center` with given `radius` intersects the terrain.
639    pub fn sphere_intersects(
640        surface: &HeightFieldSurface,
641        center: Vec3,
642        radius: f32,
643    ) -> bool {
644        // Sample terrain at the sphere center and nearby points
645        let h = surface.sample_height(center.x, center.z);
646        if center.y - radius < h {
647            return true;
648        }
649
650        // Check 4 surrounding sample points
651        for &(dx, dz) in &[(radius, 0.0), (-radius, 0.0), (0.0, radius), (0.0, -radius)] {
652            let px = center.x + dx;
653            let pz = center.z + dz;
654            let ph = surface.sample_height(px, pz);
655            let dist_sq = (center.x - px).powi(2) + (center.y - ph).powi(2) + (center.z - pz).powi(2);
656            if dist_sq < radius * radius {
657                return true;
658            }
659        }
660
661        false
662    }
663}
664
665// ─────────────────────────────────────────────────────────────────────────────
666// LOD system
667// ─────────────────────────────────────────────────────────────────────────────
668
669/// Level of detail for height field rendering.
670#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
671pub enum LodLevel {
672    /// Full resolution.
673    Full,
674    /// Half resolution.
675    Half,
676    /// Quarter resolution.
677    Quarter,
678    /// Eighth resolution.
679    Eighth,
680}
681
682impl LodLevel {
683    /// Get the stride (skip) factor for this LOD level.
684    pub fn stride(self) -> usize {
685        match self {
686            LodLevel::Full => 1,
687            LodLevel::Half => 2,
688            LodLevel::Quarter => 4,
689            LodLevel::Eighth => 8,
690        }
691    }
692
693    /// Select a LOD level based on distance from camera.
694    pub fn from_distance(distance: f32, thresholds: &LodThresholds) -> Self {
695        if distance < thresholds.full_distance {
696            LodLevel::Full
697        } else if distance < thresholds.half_distance {
698            LodLevel::Half
699        } else if distance < thresholds.quarter_distance {
700            LodLevel::Quarter
701        } else {
702            LodLevel::Eighth
703        }
704    }
705
706    /// All LOD levels from finest to coarsest.
707    pub fn all() -> &'static [LodLevel] {
708        &[LodLevel::Full, LodLevel::Half, LodLevel::Quarter, LodLevel::Eighth]
709    }
710}
711
712/// Distance thresholds for LOD level selection.
713#[derive(Debug, Clone, Copy)]
714pub struct LodThresholds {
715    pub full_distance: f32,
716    pub half_distance: f32,
717    pub quarter_distance: f32,
718}
719
720impl Default for LodThresholds {
721    fn default() -> Self {
722        Self {
723            full_distance: 100.0,
724            half_distance: 200.0,
725            quarter_distance: 400.0,
726        }
727    }
728}
729
730/// Generate indices for a height field at a given LOD level.
731pub fn generate_lod_indices(resolution_x: usize, resolution_z: usize, lod: LodLevel) -> Vec<[u32; 3]> {
732    let stride = lod.stride();
733    let mut indices = Vec::new();
734
735    let mut iz = 0;
736    while iz + stride < resolution_z {
737        let mut ix = 0;
738        while ix + stride < resolution_x {
739            let tl = (iz * resolution_x + ix) as u32;
740            let tr = (iz * resolution_x + ix + stride) as u32;
741            let bl = ((iz + stride) * resolution_x + ix) as u32;
742            let br = ((iz + stride) * resolution_x + ix + stride) as u32;
743            indices.push([tl, bl, tr]);
744            indices.push([tr, bl, br]);
745            ix += stride;
746        }
747        iz += stride;
748    }
749
750    indices
751}
752
753// ─────────────────────────────────────────────────────────────────────────────
754// Chunk-based infinite scrolling
755// ─────────────────────────────────────────────────────────────────────────────
756
757/// A coordinate in chunk space.
758#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
759pub struct ChunkCoord {
760    pub x: i32,
761    pub z: i32,
762}
763
764impl ChunkCoord {
765    pub fn new(x: i32, z: i32) -> Self { Self { x, z } }
766
767    /// Get the world-space origin of this chunk.
768    pub fn world_origin(self, chunk_size: f32) -> Vec2 {
769        Vec2::new(self.x as f32 * chunk_size, self.z as f32 * chunk_size)
770    }
771
772    /// Compute the distance from this chunk's center to a point.
773    pub fn distance_to(self, point: Vec2, chunk_size: f32) -> f32 {
774        let center = Vec2::new(
775            (self.x as f32 + 0.5) * chunk_size,
776            (self.z as f32 + 0.5) * chunk_size,
777        );
778        center.distance(point)
779    }
780
781    /// Get the chunk coordinate that contains a world-space point.
782    pub fn from_world(x: f32, z: f32, chunk_size: f32) -> Self {
783        Self {
784            x: (x / chunk_size).floor() as i32,
785            z: (z / chunk_size).floor() as i32,
786        }
787    }
788}
789
790/// A single terrain chunk.
791pub struct HeightFieldChunk {
792    pub coord: ChunkCoord,
793    pub surface: HeightFieldSurface,
794    pub lod: LodLevel,
795    /// Cached indices at the current LOD level.
796    pub cached_indices: Vec<[u32; 3]>,
797}
798
799impl HeightFieldChunk {
800    /// Create a new chunk at the given coordinate.
801    pub fn new(
802        coord: ChunkCoord,
803        noise: &NoiseSource,
804        chunk_size: f32,
805        resolution: usize,
806    ) -> Self {
807        let origin = coord.world_origin(chunk_size);
808        let surface = HeightFieldSurface::new(
809            noise.clone(),
810            origin,
811            Vec2::splat(chunk_size),
812            resolution,
813            resolution,
814        );
815        let cached_indices = surface.generate_indices();
816        Self {
817            coord,
818            surface,
819            lod: LodLevel::Full,
820            cached_indices,
821        }
822    }
823
824    /// Update the LOD level and regenerate indices if changed.
825    pub fn update_lod(&mut self, new_lod: LodLevel) {
826        if self.lod != new_lod {
827            self.lod = new_lod;
828            self.cached_indices = generate_lod_indices(
829                self.surface.resolution_x,
830                self.surface.resolution_z,
831                new_lod,
832            );
833        }
834    }
835
836    /// Tick animation on this chunk.
837    pub fn tick(&mut self, dt: f32) {
838        self.surface.tick(dt);
839    }
840}
841
842/// Manages a dynamic set of terrain chunks around a camera position.
843pub struct ChunkManager {
844    /// The noise source shared by all chunks.
845    pub noise: NoiseSource,
846    /// Size of each chunk in world units.
847    pub chunk_size: f32,
848    /// Resolution of each chunk (vertices per side).
849    pub chunk_resolution: usize,
850    /// How many chunks to keep around the camera in each direction.
851    pub view_radius: i32,
852    /// LOD distance thresholds.
853    pub lod_thresholds: LodThresholds,
854    /// Currently loaded chunks.
855    pub chunks: std::collections::HashMap<ChunkCoord, HeightFieldChunk>,
856    /// Last known camera position (for determining which chunks to load/unload).
857    pub last_camera_pos: Vec2,
858    /// Animations applied to all chunks.
859    pub animations: Vec<HeightFieldAnimation>,
860}
861
862impl ChunkManager {
863    /// Create a new chunk manager.
864    pub fn new(
865        noise: NoiseSource,
866        chunk_size: f32,
867        chunk_resolution: usize,
868        view_radius: i32,
869    ) -> Self {
870        Self {
871            noise,
872            chunk_size,
873            chunk_resolution,
874            view_radius,
875            lod_thresholds: LodThresholds::default(),
876            chunks: std::collections::HashMap::new(),
877            last_camera_pos: Vec2::ZERO,
878            animations: Vec::new(),
879        }
880    }
881
882    /// Update the chunk manager with a new camera position.
883    /// Loads new chunks that are now in range, unloads chunks that are too far.
884    pub fn update(&mut self, camera_x: f32, camera_z: f32) {
885        let cam_pos = Vec2::new(camera_x, camera_z);
886        self.last_camera_pos = cam_pos;
887
888        let center_coord = ChunkCoord::from_world(camera_x, camera_z, self.chunk_size);
889
890        // Determine which chunks should be loaded
891        let mut needed: std::collections::HashSet<ChunkCoord> = std::collections::HashSet::new();
892        for dz in -self.view_radius..=self.view_radius {
893            for dx in -self.view_radius..=self.view_radius {
894                needed.insert(ChunkCoord::new(center_coord.x + dx, center_coord.z + dz));
895            }
896        }
897
898        // Remove chunks that are no longer needed
899        let to_remove: Vec<ChunkCoord> = self.chunks.keys()
900            .filter(|c| !needed.contains(c))
901            .copied()
902            .collect();
903        for coord in to_remove {
904            self.chunks.remove(&coord);
905        }
906
907        // Add chunks that are newly needed
908        for coord in &needed {
909            if !self.chunks.contains_key(coord) {
910                let mut chunk = HeightFieldChunk::new(
911                    *coord,
912                    &self.noise,
913                    self.chunk_size,
914                    self.chunk_resolution,
915                );
916                chunk.surface.animations = self.animations.clone();
917                self.chunks.insert(*coord, chunk);
918            }
919        }
920
921        // Update LOD levels based on distance
922        for (coord, chunk) in &mut self.chunks {
923            let dist = coord.distance_to(cam_pos, self.chunk_size);
924            let lod = LodLevel::from_distance(dist, &self.lod_thresholds);
925            chunk.update_lod(lod);
926        }
927    }
928
929    /// Tick all chunks (for animation).
930    pub fn tick(&mut self, dt: f32) {
931        for chunk in self.chunks.values_mut() {
932            chunk.tick(dt);
933        }
934    }
935
936    /// Sample height at an arbitrary world-space position.
937    pub fn sample_height(&self, x: f32, z: f32) -> f32 {
938        let coord = ChunkCoord::from_world(x, z, self.chunk_size);
939        if let Some(chunk) = self.chunks.get(&coord) {
940            chunk.surface.sample_height(x, z)
941        } else {
942            // Chunk not loaded; sample directly from noise
943            self.noise.sample(x, z)
944        }
945    }
946
947    /// Sample normal at an arbitrary world-space position.
948    pub fn sample_normal(&self, x: f32, z: f32) -> Vec3 {
949        let coord = ChunkCoord::from_world(x, z, self.chunk_size);
950        if let Some(chunk) = self.chunks.get(&coord) {
951            chunk.surface.normal_at(x, z)
952        } else {
953            Vec3::Y
954        }
955    }
956
957    /// Get the number of currently loaded chunks.
958    pub fn loaded_chunk_count(&self) -> usize {
959        self.chunks.len()
960    }
961
962    /// Get an iterator over all loaded chunks.
963    pub fn iter_chunks(&self) -> impl Iterator<Item = &HeightFieldChunk> {
964        self.chunks.values()
965    }
966
967    /// Get a mutable iterator over all loaded chunks.
968    pub fn iter_chunks_mut(&mut self) -> impl Iterator<Item = &mut HeightFieldChunk> {
969        self.chunks.values_mut()
970    }
971
972    /// Cast a ray against all loaded chunks.
973    pub fn ray_cast(&self, origin: Vec3, direction: Vec3, max_distance: f32) -> Option<HeightFieldHit> {
974        let mut closest: Option<HeightFieldHit> = None;
975
976        for chunk in self.chunks.values() {
977            if let Some(hit) = HeightFieldCollider::ray_cast(
978                &chunk.surface, origin, direction, max_distance, 0.5,
979            ) {
980                if closest.as_ref().map_or(true, |c| hit.distance < c.distance) {
981                    closest = Some(hit);
982                }
983            }
984        }
985
986        closest
987    }
988
989    /// Apply a deformation brush at world position.
990    pub fn deform(&mut self, x: f32, z: f32, radius: f32, strength: f32) {
991        // Determine which chunks might be affected
992        let chunk_radius = (radius / self.chunk_size).ceil() as i32 + 1;
993        let center = ChunkCoord::from_world(x, z, self.chunk_size);
994
995        for dz in -chunk_radius..=chunk_radius {
996            for dx in -chunk_radius..=chunk_radius {
997                let coord = ChunkCoord::new(center.x + dx, center.z + dz);
998                if let Some(chunk) = self.chunks.get_mut(&coord) {
999                    chunk.surface.deform_brush(x, z, radius, strength);
1000                }
1001            }
1002        }
1003    }
1004}
1005
1006// ─────────────────────────────────────────────────────────────────────────────
1007// Erosion simulation
1008// ─────────────────────────────────────────────────────────────────────────────
1009
1010/// Simple hydraulic erosion parameters.
1011#[derive(Debug, Clone, Copy)]
1012pub struct ErosionParams {
1013    pub iterations: usize,
1014    pub inertia: f32,
1015    pub sediment_capacity: f32,
1016    pub min_sediment_capacity: f32,
1017    pub deposit_speed: f32,
1018    pub erode_speed: f32,
1019    pub evaporate_speed: f32,
1020    pub gravity: f32,
1021    pub max_droplet_lifetime: usize,
1022    pub initial_water: f32,
1023    pub initial_speed: f32,
1024}
1025
1026impl Default for ErosionParams {
1027    fn default() -> Self {
1028        Self {
1029            iterations: 5000,
1030            inertia: 0.05,
1031            sediment_capacity: 4.0,
1032            min_sediment_capacity: 0.01,
1033            deposit_speed: 0.3,
1034            erode_speed: 0.3,
1035            evaporate_speed: 0.01,
1036            gravity: 4.0,
1037            max_droplet_lifetime: 30,
1038            initial_water: 1.0,
1039            initial_speed: 1.0,
1040        }
1041    }
1042}
1043
1044/// Simple pseudo-random number generator (xorshift32) for erosion.
1045struct SimpleRng {
1046    state: u32,
1047}
1048
1049impl SimpleRng {
1050    fn new(seed: u32) -> Self {
1051        Self { state: seed.max(1) }
1052    }
1053
1054    fn next_u32(&mut self) -> u32 {
1055        self.state ^= self.state << 13;
1056        self.state ^= self.state >> 17;
1057        self.state ^= self.state << 5;
1058        self.state
1059    }
1060
1061    fn next_f32(&mut self) -> f32 {
1062        (self.next_u32() as f32) / (u32::MAX as f32)
1063    }
1064}
1065
1066/// Run hydraulic erosion simulation on a height field.
1067pub fn erode(surface: &mut HeightFieldSurface, params: &ErosionParams, seed: u32) {
1068    let mut rng = SimpleRng::new(seed);
1069    let rx = surface.resolution_x;
1070    let rz = surface.resolution_z;
1071
1072    for _ in 0..params.iterations {
1073        // Random starting position
1074        let mut pos_x = rng.next_f32() * (rx - 2) as f32 + 0.5;
1075        let mut pos_z = rng.next_f32() * (rz - 2) as f32 + 0.5;
1076        let mut dir_x = 0.0_f32;
1077        let mut dir_z = 0.0_f32;
1078        let mut speed = params.initial_speed;
1079        let mut water = params.initial_water;
1080        let mut sediment = 0.0_f32;
1081
1082        for _ in 0..params.max_droplet_lifetime {
1083            let ix = pos_x as usize;
1084            let iz = pos_z as usize;
1085
1086            if ix < 1 || ix >= rx - 1 || iz < 1 || iz >= rz - 1 {
1087                break;
1088            }
1089
1090            // Compute gradient using central differences on the grid
1091            let h_l = surface.heights[iz * rx + ix - 1];
1092            let h_r = surface.heights[iz * rx + ix + 1];
1093            let h_d = surface.heights[(iz - 1) * rx + ix];
1094            let h_u = surface.heights[(iz + 1) * rx + ix];
1095
1096            let grad_x = (h_r - h_l) * 0.5;
1097            let grad_z = (h_u - h_d) * 0.5;
1098
1099            // Update direction with inertia
1100            dir_x = dir_x * params.inertia - grad_x * (1.0 - params.inertia);
1101            dir_z = dir_z * params.inertia - grad_z * (1.0 - params.inertia);
1102
1103            let len = (dir_x * dir_x + dir_z * dir_z).sqrt();
1104            if len < 1e-6 {
1105                // Random direction if stuck
1106                let angle = rng.next_f32() * TAU;
1107                dir_x = angle.cos();
1108                dir_z = angle.sin();
1109            } else {
1110                dir_x /= len;
1111                dir_z /= len;
1112            }
1113
1114            // Move droplet
1115            let new_x = pos_x + dir_x;
1116            let new_z = pos_z + dir_z;
1117
1118            let new_ix = new_x as usize;
1119            let new_iz = new_z as usize;
1120            if new_ix < 1 || new_ix >= rx - 1 || new_iz < 1 || new_iz >= rz - 1 {
1121                break;
1122            }
1123
1124            let old_h = surface.heights[iz * rx + ix];
1125            let new_h = surface.heights[new_iz * rx + new_ix];
1126            let delta_h = new_h - old_h;
1127
1128            // Compute sediment capacity
1129            let capacity = (-delta_h * speed * water * params.sediment_capacity)
1130                .max(params.min_sediment_capacity);
1131
1132            if sediment > capacity || delta_h > 0.0 {
1133                // Deposit
1134                let amount = if delta_h > 0.0 {
1135                    delta_h.min(sediment)
1136                } else {
1137                    (sediment - capacity) * params.deposit_speed
1138                };
1139                sediment -= amount;
1140                surface.heights[iz * rx + ix] += amount;
1141            } else {
1142                // Erode
1143                let amount = ((capacity - sediment) * params.erode_speed).min(-delta_h);
1144                sediment += amount;
1145                surface.heights[iz * rx + ix] -= amount;
1146            }
1147
1148            // Update physics
1149            speed = (speed * speed + delta_h * params.gravity).abs().sqrt();
1150            water *= 1.0 - params.evaporate_speed;
1151
1152            pos_x = new_x;
1153            pos_z = new_z;
1154
1155            if water < 0.001 {
1156                break;
1157            }
1158        }
1159    }
1160}
1161
1162// ─────────────────────────────────────────────────────────────────────────────
1163// Tests
1164// ─────────────────────────────────────────────────────────────────────────────
1165
1166#[cfg(test)]
1167mod tests {
1168    use super::*;
1169
1170    #[test]
1171    fn flat_noise_source() {
1172        let ns = NoiseSource::Flat { height: 5.0 };
1173        assert!((ns.sample(0.0, 0.0) - 5.0).abs() < 1e-5);
1174        assert!((ns.sample(100.0, 200.0) - 5.0).abs() < 1e-5);
1175    }
1176
1177    #[test]
1178    fn sinusoidal_terrain() {
1179        let ns = NoiseSource::Sinusoidal {
1180            frequency_x: 1.0,
1181            frequency_z: 1.0,
1182            amplitude: 10.0,
1183        };
1184        let h = ns.sample(0.25, 0.25);
1185        assert!(h.abs() <= 10.0);
1186    }
1187
1188    #[test]
1189    fn heightfield_create() {
1190        let hf = HeightFieldSurface::new(
1191            NoiseSource::Flat { height: 3.0 },
1192            Vec2::ZERO,
1193            Vec2::splat(100.0),
1194            16,
1195            16,
1196        );
1197        assert!((hf.sample_height(50.0, 50.0) - 3.0).abs() < 1e-3);
1198    }
1199
1200    #[test]
1201    fn heightfield_normal_flat() {
1202        let hf = HeightFieldSurface::new(
1203            NoiseSource::Flat { height: 0.0 },
1204            Vec2::ZERO,
1205            Vec2::splat(100.0),
1206            16,
1207            16,
1208        );
1209        let n = hf.normal_at(50.0, 50.0);
1210        assert!((n.y - 1.0).abs() < 0.01);
1211    }
1212
1213    #[test]
1214    fn ray_cast_flat() {
1215        let hf = HeightFieldSurface::new(
1216            NoiseSource::Flat { height: 0.0 },
1217            Vec2::new(-50.0, -50.0),
1218            Vec2::splat(100.0),
1219            32,
1220            32,
1221        );
1222        let hit = HeightFieldCollider::ray_cast(
1223            &hf,
1224            Vec3::new(0.0, 10.0, 0.0),
1225            Vec3::new(0.0, -1.0, 0.0),
1226            100.0,
1227            0.5,
1228        );
1229        assert!(hit.is_some());
1230        let h = hit.unwrap();
1231        assert!((h.position.y).abs() < 1.0);
1232    }
1233
1234    #[test]
1235    fn chunk_manager_loading() {
1236        let mut mgr = ChunkManager::new(
1237            NoiseSource::Flat { height: 0.0 },
1238            64.0,
1239            16,
1240            1,
1241        );
1242        mgr.update(0.0, 0.0);
1243        // 3x3 grid of chunks (radius 1)
1244        assert_eq!(mgr.loaded_chunk_count(), 9);
1245    }
1246
1247    #[test]
1248    fn chunk_manager_move_camera() {
1249        let mut mgr = ChunkManager::new(
1250            NoiseSource::Flat { height: 0.0 },
1251            64.0,
1252            8,
1253            1,
1254        );
1255        mgr.update(0.0, 0.0);
1256        let initial = mgr.loaded_chunk_count();
1257        mgr.update(1000.0, 0.0);
1258        assert_eq!(mgr.loaded_chunk_count(), initial);
1259    }
1260
1261    #[test]
1262    fn deform_brush() {
1263        let mut hf = HeightFieldSurface::new(
1264            NoiseSource::Flat { height: 0.0 },
1265            Vec2::ZERO,
1266            Vec2::splat(100.0),
1267            32,
1268            32,
1269        );
1270        hf.deform_brush(50.0, 50.0, 10.0, 5.0);
1271        let h = hf.sample_height(50.0, 50.0);
1272        assert!(h > 4.0);
1273    }
1274
1275    #[test]
1276    fn lod_levels() {
1277        let thresholds = LodThresholds::default();
1278        assert_eq!(LodLevel::from_distance(50.0, &thresholds), LodLevel::Full);
1279        assert_eq!(LodLevel::from_distance(150.0, &thresholds), LodLevel::Half);
1280        assert_eq!(LodLevel::from_distance(300.0, &thresholds), LodLevel::Quarter);
1281        assert_eq!(LodLevel::from_distance(500.0, &thresholds), LodLevel::Eighth);
1282    }
1283
1284    #[test]
1285    fn erosion_runs() {
1286        let mut hf = HeightFieldSurface::new(
1287            NoiseSource::Sinusoidal {
1288                frequency_x: 0.1,
1289                frequency_z: 0.1,
1290                amplitude: 10.0,
1291            },
1292            Vec2::ZERO,
1293            Vec2::splat(100.0),
1294            32,
1295            32,
1296        );
1297        let params = ErosionParams { iterations: 100, ..Default::default() };
1298        erode(&mut hf, &params, 12345);
1299        // Just verify it doesn't panic and modifies heights
1300    }
1301
1302    #[test]
1303    fn composite_noise() {
1304        let ns = NoiseSource::Composite {
1305            sources: vec![
1306                NoiseSource::Flat { height: 5.0 },
1307                NoiseSource::Flat { height: 3.0 },
1308            ],
1309        };
1310        assert!((ns.sample(0.0, 0.0) - 8.0).abs() < 1e-5);
1311    }
1312}