Skip to main content

symbios_ground/
heightmap.rs

1use std::sync::OnceLock;
2
3use serde::{Deserialize, Serialize};
4
5/// A 2D heightmap stored as a flat row-major `Vec<f32>` buffer.
6///
7/// Covers world space `[0, width * scale) × [0, height * scale)`.
8/// `width` and `height` are grid-cell counts; `scale` is world units per cell.
9///
10/// The invariant `data.len() == width * height` is enforced by all constructors
11/// and mutation methods; fields are private to prevent external corruption.
12///
13/// A grid-cell normal cache is computed lazily on first read and reused across
14/// repeated queries (e.g. by [`SplatMapper`](crate::SplatMapper)). It is
15/// invalidated whenever the heights change via [`HeightMap::set`],
16/// [`HeightMap::get_mut`], [`HeightMap::data_mut`], or [`HeightMap::normalize`].
17#[derive(Debug, Serialize, Deserialize)]
18pub struct HeightMap {
19    data: Vec<f32>,
20    width: usize,
21    height: usize,
22    scale: f32,
23    /// Pooled lakes detected by hydraulic erosion. Empty until erosion writes
24    /// to it; persisted across serialisation so renderers can visualise water
25    /// without re-running the simulation.
26    #[serde(default)]
27    lakes: Vec<Lake>,
28    /// Lazy per-grid-cell central-difference normals. Skipped from serde so the
29    /// cache stays consistent with the height data after deserialisation.
30    #[serde(skip)]
31    normals: OnceLock<Vec<[f32; 3]>>,
32}
33
34/// A pooled body of standing water produced by [`HydraulicErosion`](crate::HydraulicErosion).
35///
36/// Built up at points where droplet velocity drops below the configured
37/// threshold while still above `water_level`, marking realistic basins and
38/// depressions in the heightmap.
39#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
40pub struct Lake {
41    /// Row-major index into the heightmap (`z * width + x`).
42    pub index: usize,
43    /// Accumulated water depth at this cell.
44    pub depth: f32,
45    /// World-space area attributed to this lake cell — `scale * scale`.
46    pub area: f32,
47}
48
49impl Clone for HeightMap {
50    fn clone(&self) -> Self {
51        Self {
52            data: self.data.clone(),
53            width: self.width,
54            height: self.height,
55            scale: self.scale,
56            lakes: self.lakes.clone(),
57            normals: OnceLock::new(),
58        }
59    }
60}
61
62impl HeightMap {
63    /// Create a heightmap of `width × height` cells, all initialised to `0.0`.
64    ///
65    /// `scale` is the world-unit size of each cell (must be `> 0`).
66    ///
67    /// # Panics
68    ///
69    /// Panics if `width == 0`, `height == 0`, or `scale <= 0.0`.
70    pub fn new(width: usize, height: usize, scale: f32) -> Self {
71        assert!(width > 0 && height > 0, "dimensions must be positive");
72        assert!(scale > 0.0, "scale must be positive");
73        Self {
74            data: vec![0.0; width * height],
75            width,
76            height,
77            scale,
78            lakes: Vec::new(),
79            normals: OnceLock::new(),
80        }
81    }
82
83    /// Grid-cell width.
84    #[inline]
85    pub fn width(&self) -> usize {
86        self.width
87    }
88
89    /// Grid-cell depth.
90    #[inline]
91    pub fn height(&self) -> usize {
92        self.height
93    }
94
95    /// World units per grid cell.
96    #[inline]
97    pub fn scale(&self) -> f32 {
98        self.scale
99    }
100
101    /// Read-only view of the flat row-major height buffer.
102    #[inline]
103    pub fn data(&self) -> &[f32] {
104        &self.data
105    }
106
107    /// Mutable view of the flat row-major height buffer.
108    ///
109    /// The caller must not change the slice length; only element values may be
110    /// modified.  Dimensions (`width`, `height`) are unaffected. Invalidates
111    /// the normal cache.
112    #[inline]
113    pub fn data_mut(&mut self) -> &mut [f32] {
114        self.invalidate_caches();
115        &mut self.data
116    }
117
118    /// Return the height at grid cell `(x, z)`.
119    ///
120    /// # Panics
121    ///
122    /// Panics if `x >= width` or `z >= height`.
123    #[inline]
124    pub fn get(&self, x: usize, z: usize) -> f32 {
125        self.data[z * self.width + x]
126    }
127
128    /// Return a mutable reference to the height at grid cell `(x, z)`.
129    /// Invalidates the normal cache.
130    ///
131    /// # Panics
132    ///
133    /// Panics if `x >= width` or `z >= height`.
134    #[inline]
135    pub fn get_mut(&mut self, x: usize, z: usize) -> &mut f32 {
136        self.invalidate_caches();
137        &mut self.data[z * self.width + x]
138    }
139
140    /// Set the height at grid cell `(x, z)` to `val`. Invalidates the normal
141    /// cache.
142    ///
143    /// # Panics
144    ///
145    /// Panics if `x >= width` or `z >= height`.
146    #[inline]
147    pub fn set(&mut self, x: usize, z: usize, val: f32) {
148        self.invalidate_caches();
149        self.data[z * self.width + x] = val;
150    }
151
152    /// Return the height at grid cell `(x, z)`, clamping coordinates to the
153    /// valid range instead of panicking on out-of-bounds indices.
154    #[inline]
155    pub fn get_clamped(&self, x: i32, z: i32) -> f32 {
156        let cx = x.clamp(0, self.width as i32 - 1) as usize;
157        let cz = z.clamp(0, self.height as i32 - 1) as usize;
158        self.get(cx, cz)
159    }
160
161    /// Sample height at world position using bilinear interpolation.
162    /// Clamps to heightmap boundaries.
163    pub fn get_height_at(&self, world_x: f32, world_z: f32) -> f32 {
164        let gx = world_x / self.scale;
165        let gz = world_z / self.scale;
166
167        let x0 = gx.floor() as i32;
168        let z0 = gz.floor() as i32;
169        let fx = gx - x0 as f32;
170        let fz = gz - z0 as f32;
171
172        let h00 = self.get_clamped(x0, z0);
173        let h10 = self.get_clamped(x0 + 1, z0);
174        let h01 = self.get_clamped(x0, z0 + 1);
175        let h11 = self.get_clamped(x0 + 1, z0 + 1);
176
177        let h0 = h00 + (h10 - h00) * fx;
178        let h1 = h01 + (h11 - h01) * fx;
179        h0 + (h1 - h0) * fz
180    }
181
182    /// Compute surface normal at world position using central differences.
183    /// Returns a normalized `[x, y, z]` vector where `y` is up.
184    ///
185    /// `scale` is always > 0 (enforced by the constructor), so the `2*scale`
186    /// divisor and the `len` (≥ 1.0 since ny = 1) are both safe.
187    pub fn get_normal_at(&self, world_x: f32, world_z: f32) -> [f32; 3] {
188        let step = self.scale;
189        let hl = self.get_height_at(world_x - step, world_z);
190        let hr = self.get_height_at(world_x + step, world_z);
191        let hd = self.get_height_at(world_x, world_z - step);
192        let hu = self.get_height_at(world_x, world_z + step);
193
194        let dhdx = (hr - hl) / (2.0 * step);
195        let dhdz = (hu - hd) / (2.0 * step);
196
197        let nx = -dhdx;
198        let ny = 1.0_f32;
199        let nz = -dhdz;
200        let len = (nx * nx + ny * ny + nz * nz).sqrt();
201        // If scale is a denormal (< ~1e-38), dhdx/dhdz can overflow to ±INF,
202        // making len = INF and the division NaN. Return a flat up-normal instead.
203        if !len.is_finite() {
204            return [0.0, 1.0, 0.0];
205        }
206        [nx / len, ny / len, nz / len]
207    }
208
209    /// Cached central-difference normal at grid cell `(x, z)`.
210    ///
211    /// Equivalent to `get_normal_at(x as f32 * scale, z as f32 * scale)` for
212    /// in-bounds cells, but reads from a lazily populated cache so callers that
213    /// scan the whole grid (e.g. [`SplatMapper`](crate::SplatMapper)) avoid
214    /// recomputing four central differences per pixel.
215    ///
216    /// # Panics
217    ///
218    /// Panics if `x >= width` or `z >= height`.
219    pub fn normal_at_grid(&self, x: usize, z: usize) -> [f32; 3] {
220        self.normals_grid()[z * self.width + x]
221    }
222
223    /// Lazily populated per-grid-cell normal table; row-major, length
224    /// `width * height`.
225    pub fn normals_grid(&self) -> &[[f32; 3]] {
226        self.normals.get_or_init(|| self.compute_normals())
227    }
228
229    fn compute_normals(&self) -> Vec<[f32; 3]> {
230        let w = self.width;
231        let h = self.height;
232        let mut out = Vec::with_capacity(w * h);
233        let step = self.scale;
234        let inv_2step = 1.0_f32 / (2.0 * step);
235        for z in 0..h {
236            for x in 0..w {
237                let hl = self.get_clamped(x as i32 - 1, z as i32);
238                let hr = self.get_clamped(x as i32 + 1, z as i32);
239                let hd = self.get_clamped(x as i32, z as i32 - 1);
240                let hu = self.get_clamped(x as i32, z as i32 + 1);
241
242                let dhdx = (hr - hl) * inv_2step;
243                let dhdz = (hu - hd) * inv_2step;
244
245                let nx = -dhdx;
246                let ny = 1.0_f32;
247                let nz = -dhdz;
248                let len = (nx * nx + ny * ny + nz * nz).sqrt();
249                if !len.is_finite() {
250                    out.push([0.0, 1.0, 0.0]);
251                } else {
252                    out.push([nx / len, ny / len, nz / len]);
253                }
254            }
255        }
256        out
257    }
258
259    /// Lakes detected by the most recent hydraulic erosion pass. Empty until
260    /// [`HydraulicErosion::erode`](crate::HydraulicErosion::erode) populates
261    /// it.
262    pub fn lakes(&self) -> &[Lake] {
263        &self.lakes
264    }
265
266    /// Internal: replace the lake list (used by erosion).
267    pub(crate) fn set_lakes(&mut self, lakes: Vec<Lake>) {
268        self.lakes = lakes;
269    }
270
271    /// Internal: clear all derived caches that depend on `data`.
272    fn invalidate_caches(&mut self) {
273        self.normals.take();
274    }
275
276    /// Normalize all height values to `[0.0, 1.0]`.
277    pub fn normalize(&mut self) {
278        let min = self.data.iter().cloned().fold(f32::INFINITY, f32::min);
279        let max = self.data.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
280        let range = max - min;
281        if range > f32::EPSILON {
282            for v in &mut self.data {
283                *v = (*v - min) / range;
284            }
285        }
286        self.invalidate_caches();
287    }
288
289    /// World-space width of the heightmap.
290    pub fn world_width(&self) -> f32 {
291        self.width as f32 * self.scale
292    }
293
294    /// World-space depth of the heightmap.
295    pub fn world_depth(&self) -> f32 {
296        self.height as f32 * self.scale
297    }
298}