Skip to main content

oxiphysics_geometry/heightfield/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxiphysics_core::math::{Real, Vec3};
6
7use super::functions::{heightfield_tessellate, ray_aabb_xz, ray_triangle, tri_area};
8
9/// A grid-based height field for terrain representation.
10///
11/// Heights are stored in row-major order. The field spans from
12/// (0,0) to (scale_x * (cols-1), scale_z * (rows-1)) in the XZ plane.
13#[derive(Debug, Clone)]
14pub struct HeightField {
15    /// Height values in row-major order.
16    pub heights: Vec<Real>,
17    /// Number of rows (Z direction).
18    pub rows: usize,
19    /// Number of columns (X direction).
20    pub cols: usize,
21    /// Spacing in the X direction.
22    pub scale_x: Real,
23    /// Spacing in the Z direction.
24    pub scale_z: Real,
25}
26impl HeightField {
27    /// Create a new height field.
28    pub fn new(heights: Vec<Real>, rows: usize, cols: usize, scale_x: Real, scale_z: Real) -> Self {
29        assert_eq!(heights.len(), rows * cols);
30        Self {
31            heights,
32            rows,
33            cols,
34            scale_x,
35            scale_z,
36        }
37    }
38    /// Get the height at grid position (row, col).
39    pub fn height_at(&self, row: usize, col: usize) -> Real {
40        self.heights[row * self.cols + col]
41    }
42    /// Create a height field from a function.
43    ///
44    /// The function `f(col, row)` returns the height at each grid point.
45    pub fn from_fn(
46        cols: usize,
47        rows: usize,
48        cell_size: Real,
49        f: impl Fn(usize, usize) -> Real,
50    ) -> Self {
51        let mut heights = Vec::with_capacity(rows * cols);
52        for row in 0..rows {
53            for col in 0..cols {
54                heights.push(f(col, row));
55            }
56        }
57        Self {
58            heights,
59            rows,
60            cols,
61            scale_x: cell_size,
62            scale_z: cell_size,
63        }
64    }
65    /// Bilinear interpolation of height at normalized coordinates (u, v).
66    ///
67    /// `u` and `v` are in `[0, 1]`, mapping to the full extent of the grid.
68    pub fn height_at_uv(&self, u: Real, v: Real) -> Real {
69        let u = u.clamp(0.0, 1.0);
70        let v = v.clamp(0.0, 1.0);
71        let fx = u * (self.cols - 1) as Real;
72        let fz = v * (self.rows - 1) as Real;
73        let col0 = (fx as usize).min(self.cols - 2);
74        let row0 = (fz as usize).min(self.rows - 2);
75        let col1 = col0 + 1;
76        let row1 = row0 + 1;
77        let tx = fx - col0 as Real;
78        let tz = fz - row0 as Real;
79        let h00 = self.height_at(row0, col0);
80        let h10 = self.height_at(row0, col1);
81        let h01 = self.height_at(row1, col0);
82        let h11 = self.height_at(row1, col1);
83        let h0 = h00 * (1.0 - tx) + h10 * tx;
84        let h1 = h01 * (1.0 - tx) + h11 * tx;
85        h0 * (1.0 - tz) + h1 * tz
86    }
87    /// Compute the surface normal at grid point (col, row) via finite differences.
88    pub fn normal_at_grid(&self, col: usize, row: usize) -> [Real; 3] {
89        let dh_dx = if col == 0 {
90            (self.height_at(row, col + 1) - self.height_at(row, col)) / self.scale_x
91        } else if col == self.cols - 1 {
92            (self.height_at(row, col) - self.height_at(row, col - 1)) / self.scale_x
93        } else {
94            (self.height_at(row, col + 1) - self.height_at(row, col - 1)) / (2.0 * self.scale_x)
95        };
96        let dh_dz = if row == 0 {
97            (self.height_at(row + 1, col) - self.height_at(row, col)) / self.scale_z
98        } else if row == self.rows - 1 {
99            (self.height_at(row, col) - self.height_at(row - 1, col)) / self.scale_z
100        } else {
101            (self.height_at(row + 1, col) - self.height_at(row - 1, col)) / (2.0 * self.scale_z)
102        };
103        let nx = -dh_dx;
104        let ny = 1.0;
105        let nz = -dh_dz;
106        let len = (nx * nx + ny * ny + nz * nz).sqrt();
107        [nx / len, ny / len, nz / len]
108    }
109    /// Tessellate the height field into a triangle mesh.
110    ///
111    /// Returns (vertices, triangles) where each vertex is `[x, y, z]`
112    /// and each triangle is 3 vertex indices.
113    pub fn to_triangle_mesh(&self) -> (Vec<[Real; 3]>, Vec<[usize; 3]>) {
114        let mut vertices = Vec::with_capacity(self.rows * self.cols);
115        for row in 0..self.rows {
116            for col in 0..self.cols {
117                vertices.push([
118                    col as Real * self.scale_x,
119                    self.height_at(row, col),
120                    row as Real * self.scale_z,
121                ]);
122            }
123        }
124        let mut triangles = Vec::with_capacity((self.rows - 1) * (self.cols - 1) * 2);
125        for row in 0..(self.rows - 1) {
126            for col in 0..(self.cols - 1) {
127                let i00 = row * self.cols + col;
128                let i10 = row * self.cols + col + 1;
129                let i01 = (row + 1) * self.cols + col;
130                let i11 = (row + 1) * self.cols + col + 1;
131                triangles.push([i00, i10, i11]);
132                triangles.push([i00, i11, i01]);
133            }
134        }
135        (vertices, triangles)
136    }
137    /// Return the minimum height in the field.
138    pub fn min_height(&self) -> Real {
139        self.height_bounds().0
140    }
141    /// Return the maximum height in the field.
142    pub fn max_height(&self) -> Real {
143        self.height_bounds().1
144    }
145    /// Compute the total surface area by summing triangle areas from tessellation.
146    pub fn surface_area(&self) -> Real {
147        if self.rows < 2 || self.cols < 2 {
148            return 0.0;
149        }
150        let mut area = 0.0;
151        for row in 0..(self.rows - 1) {
152            for col in 0..(self.cols - 1) {
153                let v00 = [
154                    col as Real * self.scale_x,
155                    self.height_at(row, col),
156                    row as Real * self.scale_z,
157                ];
158                let v10 = [
159                    (col + 1) as Real * self.scale_x,
160                    self.height_at(row, col + 1),
161                    row as Real * self.scale_z,
162                ];
163                let v01 = [
164                    col as Real * self.scale_x,
165                    self.height_at(row + 1, col),
166                    (row + 1) as Real * self.scale_z,
167                ];
168                let v11 = [
169                    (col + 1) as Real * self.scale_x,
170                    self.height_at(row + 1, col + 1),
171                    (row + 1) as Real * self.scale_z,
172                ];
173                area += tri_area(&v00, &v10, &v11);
174                area += tri_area(&v00, &v11, &v01);
175            }
176        }
177        area
178    }
179    /// Apply Laplacian smoothing to the height field.
180    ///
181    /// Each interior height is replaced by the average of its 4-connected neighbors.
182    /// Boundary heights are unchanged.
183    pub fn smooth(&mut self, iterations: usize) {
184        for _ in 0..iterations {
185            let mut new_heights = self.heights.clone();
186            for row in 1..(self.rows - 1) {
187                for col in 1..(self.cols - 1) {
188                    let up = self.height_at(row - 1, col);
189                    let down = self.height_at(row + 1, col);
190                    let left = self.height_at(row, col - 1);
191                    let right = self.height_at(row, col + 1);
192                    new_heights[row * self.cols + col] = (up + down + left + right) / 4.0;
193                }
194            }
195            self.heights = new_heights;
196        }
197    }
198    /// Ray cast using grid DDA traversal and per-cell triangle intersection.
199    ///
200    /// Returns `Some((toi, [nx, ny, nz]))` for the closest hit within `max_toi`,
201    /// or `None` if no intersection.
202    pub fn ray_cast_grid(
203        &self,
204        origin: [Real; 3],
205        dir: [Real; 3],
206        max_toi: Real,
207    ) -> Option<(Real, [Real; 3])> {
208        if self.rows < 2 || self.cols < 2 {
209            return None;
210        }
211        let grid_w = (self.cols - 1) as Real * self.scale_x;
212        let grid_h = (self.rows - 1) as Real * self.scale_z;
213        let (t_enter, t_exit) = ray_aabb_xz(
214            origin[0], origin[2], dir[0], dir[2], grid_w, grid_h, max_toi,
215        )?;
216        let eps = 1e-6;
217        let mut t = t_enter.max(0.0);
218        let step = self.scale_x.min(self.scale_z) * 0.5;
219        let mut best: Option<(Real, [Real; 3])> = None;
220        while t <= t_exit.min(max_toi) {
221            let px = origin[0] + dir[0] * t;
222            let pz = origin[2] + dir[2] * t;
223            let col = ((px / self.scale_x).floor() as isize)
224                .max(0)
225                .min((self.cols - 2) as isize) as usize;
226            let row = ((pz / self.scale_z).floor() as isize)
227                .max(0)
228                .min((self.rows - 2) as isize) as usize;
229            let v00 = Vec3::new(
230                col as Real * self.scale_x,
231                self.height_at(row, col),
232                row as Real * self.scale_z,
233            );
234            let v10 = Vec3::new(
235                (col + 1) as Real * self.scale_x,
236                self.height_at(row, col + 1),
237                row as Real * self.scale_z,
238            );
239            let v01 = Vec3::new(
240                col as Real * self.scale_x,
241                self.height_at(row + 1, col),
242                (row + 1) as Real * self.scale_z,
243            );
244            let v11 = Vec3::new(
245                (col + 1) as Real * self.scale_x,
246                self.height_at(row + 1, col + 1),
247                (row + 1) as Real * self.scale_z,
248            );
249            let o = Vec3::new(origin[0], origin[1], origin[2]);
250            let d = Vec3::new(dir[0], dir[1], dir[2]);
251            for (a, b, c) in [(&v00, &v10, &v11), (&v00, &v11, &v01)] {
252                if let Some(hit) = ray_triangle(&o, &d, max_toi, a, b, c)
253                    && best.as_ref().is_none_or(|(bt, _)| hit.toi < *bt)
254                {
255                    let dot = hit.normal.dot(&d);
256                    let n = if dot > 0.0 {
257                        [-hit.normal.x, -hit.normal.y, -hit.normal.z]
258                    } else {
259                        [hit.normal.x, hit.normal.y, hit.normal.z]
260                    };
261                    best = Some((hit.toi, n));
262                }
263            }
264            if best.is_some() {
265                return best;
266            }
267            t += step + eps;
268        }
269        best
270    }
271    /// Get min and max heights.
272    pub(super) fn height_bounds(&self) -> (Real, Real) {
273        let mut min = Real::INFINITY;
274        let mut max = Real::NEG_INFINITY;
275        for &h in &self.heights {
276            if h < min {
277                min = h;
278            }
279            if h > max {
280                max = h;
281            }
282        }
283        (min, max)
284    }
285}
286impl HeightField {
287    /// Generate a downsampled (LOD) version of the height field.
288    ///
289    /// `factor` must be ≥ 1.  A factor of 2 halves the resolution by averaging
290    /// 2×2 blocks of cells.  If the grid is too small the original is returned.
291    pub fn lod_downsample(&self, factor: usize) -> HeightField {
292        if factor <= 1 {
293            return self.clone();
294        }
295        let new_cols = self.cols.div_ceil(factor);
296        let new_rows = self.rows.div_ceil(factor);
297        if new_cols < 2 || new_rows < 2 {
298            return self.clone();
299        }
300        let mut heights = Vec::with_capacity(new_rows * new_cols);
301        for r in 0..new_rows {
302            for c in 0..new_cols {
303                let r0 = r * factor;
304                let c0 = c * factor;
305                let r1 = (r0 + factor).min(self.rows);
306                let c1 = (c0 + factor).min(self.cols);
307                let mut sum = 0.0;
308                let mut count = 0usize;
309                for rr in r0..r1 {
310                    for cc in c0..c1 {
311                        sum += self.height_at(rr, cc);
312                        count += 1;
313                    }
314                }
315                heights.push(if count > 0 { sum / count as Real } else { 0.0 });
316            }
317        }
318        HeightField::new(
319            heights,
320            new_rows,
321            new_cols,
322            self.scale_x * factor as Real,
323            self.scale_z * factor as Real,
324        )
325    }
326    /// Generate multiple LOD levels.
327    ///
328    /// Returns a Vec where index 0 is the original, index 1 is 2× downsampled,
329    /// index 2 is 4× downsampled, etc., until the grid is too small.
330    pub fn lod_pyramid(&self, levels: usize) -> Vec<HeightField> {
331        let mut result = vec![self.clone()];
332        let mut factor = 2;
333        for _ in 1..levels {
334            let prev = result.last().expect("collection should not be empty");
335            if prev.rows < 4 || prev.cols < 4 {
336                break;
337            }
338            result.push(prev.lod_downsample(2));
339            factor *= 2;
340        }
341        let _ = factor;
342        result
343    }
344    /// Compute the normal at an arbitrary world-space XZ position via bilinear
345    /// interpolation of the four surrounding grid normals.
346    pub fn normal_at_world(&self, x: Real, z: Real) -> [Real; 3] {
347        let fx = (x / self.scale_x).clamp(0.0, (self.cols - 1) as Real);
348        let fz = (z / self.scale_z).clamp(0.0, (self.rows - 1) as Real);
349        let c0 = (fx as usize).min(self.cols - 1);
350        let r0 = (fz as usize).min(self.rows - 1);
351        let c1 = (c0 + 1).min(self.cols - 1);
352        let r1 = (r0 + 1).min(self.rows - 1);
353        let tx = fx - c0 as Real;
354        let tz = fz - r0 as Real;
355        let n00 = self.normal_at_grid(c0, r0);
356        let n10 = self.normal_at_grid(c1, r0);
357        let n01 = self.normal_at_grid(c0, r1);
358        let n11 = self.normal_at_grid(c1, r1);
359        let mut n = [0.0f64; 3];
360        for i in 0..3 {
361            let a = n00[i] * (1.0 - tx) + n10[i] * tx;
362            let b = n01[i] * (1.0 - tx) + n11[i] * tx;
363            n[i] = a * (1.0 - tz) + b * tz;
364        }
365        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
366        if len > 1e-10 {
367            [n[0] / len, n[1] / len, n[2] / len]
368        } else {
369            [0.0, 1.0, 0.0]
370        }
371    }
372    /// Serialize the height field to a flat `Vec`f64` prefixed by metadata.
373    ///
374    /// Format: `\[rows, cols, scale_x, scale_z, h0, h1, ...\]`
375    pub fn serialize(&self) -> Vec<Real> {
376        let mut data = Vec::with_capacity(4 + self.heights.len());
377        data.push(self.rows as Real);
378        data.push(self.cols as Real);
379        data.push(self.scale_x);
380        data.push(self.scale_z);
381        data.extend_from_slice(&self.heights);
382        data
383    }
384    /// Deserialize a height field from a flat `Vec`f64` (inverse of `serialize`).
385    pub fn deserialize(data: &[Real]) -> Option<HeightField> {
386        if data.len() < 4 {
387            return None;
388        }
389        let rows = data[0] as usize;
390        let cols = data[1] as usize;
391        let scale_x = data[2];
392        let scale_z = data[3];
393        if data.len() != 4 + rows * cols {
394            return None;
395        }
396        Some(HeightField::new(
397            data[4..].to_vec(),
398            rows,
399            cols,
400            scale_x,
401            scale_z,
402        ))
403    }
404    /// Compute per-vertex normals for all grid vertices.
405    ///
406    /// Returns a flat `Vec<[Real; 3]>` in row-major order.
407    pub fn compute_all_normals(&self) -> Vec<[Real; 3]> {
408        let mut normals = Vec::with_capacity(self.rows * self.cols);
409        for row in 0..self.rows {
410            for col in 0..self.cols {
411                normals.push(self.normal_at_grid(col, row));
412            }
413        }
414        normals
415    }
416    /// Height at arbitrary world-space XZ position (bilinear interpolation).
417    ///
418    /// Clamps to grid extents.
419    pub fn height_at_world(&self, x: Real, z: Real) -> Real {
420        let u = (x / (self.scale_x * (self.cols - 1) as Real)).clamp(0.0, 1.0);
421        let v = (z / (self.scale_z * (self.rows - 1) as Real)).clamp(0.0, 1.0);
422        self.height_at_uv(u, v)
423    }
424    /// Ray cast with DDA and a maximum number of steps (bounded version).
425    ///
426    /// Avoids unbounded loops on very large grids.
427    pub fn ray_cast_bounded(
428        &self,
429        origin: [Real; 3],
430        dir: [Real; 3],
431        max_toi: Real,
432        max_steps: usize,
433    ) -> Option<(Real, [Real; 3])> {
434        let step = self.scale_x.min(self.scale_z) * 0.25;
435        let mut t = 0.0;
436        let mut best: Option<(Real, [Real; 3])> = None;
437        for _ in 0..max_steps {
438            if t > max_toi {
439                break;
440            }
441            let px = origin[0] + dir[0] * t;
442            let pz = origin[2] + dir[2] * t;
443            if px < 0.0
444                || px > (self.cols - 1) as Real * self.scale_x
445                || pz < 0.0
446                || pz > (self.rows - 1) as Real * self.scale_z
447            {
448                t += step;
449                continue;
450            }
451            let py = origin[1] + dir[1] * t;
452            let terrain_y = self.height_at_world(px, pz);
453            if py <= terrain_y {
454                let refined_t = if t > step { t - step * 0.5 } else { 0.0 };
455                let col = ((px / self.scale_x) as usize).min(self.cols.saturating_sub(2));
456                let row = ((pz / self.scale_z) as usize).min(self.rows.saturating_sub(2));
457                let normal = self.normal_at_grid(col, row);
458                best = Some((refined_t.max(0.0), normal));
459                break;
460            }
461            t += step;
462        }
463        best
464    }
465}
466impl HeightField {
467    /// Axis-aligned bounding box of this height field in world space.
468    ///
469    /// Returns `(min, max)` where each is `[x, y, z]`.
470    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
471        let (min_h, max_h) = self.height_bounds();
472        let max_x = (self.cols - 1) as f64 * self.scale_x;
473        let max_z = (self.rows - 1) as f64 * self.scale_z;
474        ([0.0, min_h, 0.0], [max_x, max_h, max_z])
475    }
476    /// Bilinear height interpolation at world-space position `(x, z)`.
477    ///
478    /// Unlike `height_at(row, col)` (grid indices) and `height_at_world`,
479    /// this method accepts `(x, z)` world coordinates and returns the
480    /// interpolated height, clamping to the grid extents.
481    pub fn height_at_xz(&self, x: f64, z: f64) -> f64 {
482        self.height_at_world(x, z)
483    }
484    /// Per-vertex normals for the entire grid in row-major order.
485    ///
486    /// Each normal is computed via finite differences over the 4-connected
487    /// neighborhood and returned as a unit vector `[nx, ny, nz]`.
488    pub fn normals(&self) -> Vec<[f64; 3]> {
489        self.compute_all_normals()
490    }
491    /// Tessellate the height field into a triangle mesh.
492    ///
493    /// Returns `(vertices, indices)` where:
494    /// - `vertices` is a list of world-space `[x, y, z]` positions,
495    ///   one per grid point, in row-major order.
496    /// - `indices` is a list of `[i0, i1, i2]` triangles (two per quad cell).
497    pub fn tessellate(&self) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
498        let vertices: Vec<[f64; 3]> = (0..self.rows)
499            .flat_map(|row| {
500                (0..self.cols).map(move |col| {
501                    [
502                        col as f64 * self.scale_x,
503                        self.height_at(row, col),
504                        row as f64 * self.scale_z,
505                    ]
506                })
507            })
508            .collect();
509        let indices = heightfield_tessellate(self);
510        (vertices, indices)
511    }
512    /// DDA ray intersection returning `(t, normal)` for the first hit.
513    ///
514    /// Uses grid-cell DDA traversal in the XZ plane, testing the two
515    /// triangles of each cell with Möller–Trumbore intersection.
516    /// Returns `None` if the ray misses the terrain or is going away from it.
517    ///
518    /// The search is limited to a reasonable distance (diagonal of the AABB).
519    pub fn ray_intersect(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<(f64, [f64; 3])> {
520        let (_, max_h) = self.height_bounds();
521        if dir[1] > 0.0 && origin[1] >= max_h {
522            return None;
523        }
524        let grid_w = (self.cols - 1) as f64 * self.scale_x;
525        let grid_h = (self.rows - 1) as f64 * self.scale_z;
526        let diag = (grid_w * grid_w + grid_h * grid_h + (max_h - 0.0).abs().powi(2)).sqrt();
527        let max_toi = (origin[1] - max_h).abs() / dir[1].abs().max(1e-12) + diag * 2.0;
528        let max_toi = max_toi.min(1e9);
529        self.ray_cast_grid(origin, dir, max_toi)
530    }
531}
532impl HeightField {
533    /// Compute the slope magnitude at grid point `(col, row)`.
534    ///
535    /// Slope is defined as `sqrt((dh/dx)² + (dh/dz)²)` using the same
536    /// finite-difference stencil as `normal_at_grid`.
537    pub fn slope_at(&self, col: usize, row: usize) -> f64 {
538        let dh_dx = if col == 0 {
539            (self.height_at(row, col + 1) - self.height_at(row, col)) / self.scale_x
540        } else if col == self.cols - 1 {
541            (self.height_at(row, col) - self.height_at(row, col - 1)) / self.scale_x
542        } else {
543            (self.height_at(row, col + 1) - self.height_at(row, col - 1)) / (2.0 * self.scale_x)
544        };
545        let dh_dz = if row == 0 {
546            (self.height_at(row + 1, col) - self.height_at(row, col)) / self.scale_z
547        } else if row == self.rows - 1 {
548            (self.height_at(row, col) - self.height_at(row - 1, col)) / self.scale_z
549        } else {
550            (self.height_at(row + 1, col) - self.height_at(row - 1, col)) / (2.0 * self.scale_z)
551        };
552        (dh_dx * dh_dx + dh_dz * dh_dz).sqrt()
553    }
554    /// Compute the mean curvature at grid point `(col, row)`.
555    ///
556    /// Uses a second-order central difference approximation:
557    /// `κ ≈ (d²h/dx² + d²h/dz²) / 2`.
558    pub fn curvature_at(&self, col: usize, row: usize) -> f64 {
559        let h = self.height_at(row, col);
560        let d2h_dx2 = if col == 0 || col == self.cols - 1 {
561            0.0
562        } else {
563            let hl = self.height_at(row, col - 1);
564            let hr = self.height_at(row, col + 1);
565            (hl - 2.0 * h + hr) / (self.scale_x * self.scale_x)
566        };
567        let d2h_dz2 = if row == 0 || row == self.rows - 1 {
568            0.0
569        } else {
570            let hd = self.height_at(row - 1, col);
571            let hu = self.height_at(row + 1, col);
572            (hd - 2.0 * h + hu) / (self.scale_z * self.scale_z)
573        };
574        (d2h_dx2 + d2h_dz2) * 0.5
575    }
576    /// Return a flat `Vec`f64` of slope magnitudes in row-major order.
577    pub fn slope_map(&self) -> Vec<f64> {
578        let mut out = Vec::with_capacity(self.rows * self.cols);
579        for row in 0..self.rows {
580            for col in 0..self.cols {
581                out.push(self.slope_at(col, row));
582            }
583        }
584        out
585    }
586    /// Return a flat `Vec`f64` of mean curvatures in row-major order.
587    pub fn curvature_map(&self) -> Vec<f64> {
588        let mut out = Vec::with_capacity(self.rows * self.cols);
589        for row in 0..self.rows {
590            for col in 0..self.cols {
591                out.push(self.curvature_at(col, row));
592            }
593        }
594        out
595    }
596    /// Find the closest surface point to query point `q` by brute-force search
597    /// over all grid vertices.
598    ///
599    /// Returns `(closest_world_point, row, col)`.
600    pub fn closest_vertex(&self, q: [f64; 3]) -> ([f64; 3], usize, usize) {
601        let mut best_dist2 = f64::INFINITY;
602        let mut best_pt = [0.0f64; 3];
603        let mut best_row = 0usize;
604        let mut best_col = 0usize;
605        for row in 0..self.rows {
606            for col in 0..self.cols {
607                let vx = col as f64 * self.scale_x;
608                let vy = self.height_at(row, col);
609                let vz = row as f64 * self.scale_z;
610                let dx = q[0] - vx;
611                let dy = q[1] - vy;
612                let dz = q[2] - vz;
613                let d2 = dx * dx + dy * dy + dz * dz;
614                if d2 < best_dist2 {
615                    best_dist2 = d2;
616                    best_pt = [vx, vy, vz];
617                    best_row = row;
618                    best_col = col;
619                }
620            }
621        }
622        (best_pt, best_row, best_col)
623    }
624    /// Resample the height field to new grid dimensions by bilinear interpolation.
625    ///
626    /// `new_cols` and `new_rows` must be ≥ 2.  Returns a new `HeightField` with
627    /// the same world extents but a different resolution.
628    pub fn resample(&self, new_cols: usize, new_rows: usize) -> HeightField {
629        assert!(new_cols >= 2 && new_rows >= 2);
630        let new_scale_x = (self.scale_x * (self.cols - 1) as f64) / (new_cols - 1) as f64;
631        let new_scale_z = (self.scale_z * (self.rows - 1) as f64) / (new_rows - 1) as f64;
632        let mut heights = Vec::with_capacity(new_rows * new_cols);
633        for nr in 0..new_rows {
634            for nc in 0..new_cols {
635                let x = nc as f64 * new_scale_x;
636                let z = nr as f64 * new_scale_z;
637                heights.push(self.height_at_world(x, z));
638            }
639        }
640        HeightField::new(heights, new_rows, new_cols, new_scale_x, new_scale_z)
641    }
642    /// Simple hydraulic erosion step: for each cell, water flows to the
643    /// steepest-descent neighbor and carries a fraction `sediment_rate` of
644    /// height difference.
645    ///
646    /// `iterations` controls how many passes are performed.  For correctness
647    /// the caller should keep `sediment_rate` ≤ 0.5.
648    pub fn hydraulic_erode(&mut self, sediment_rate: f64, iterations: usize) {
649        for _ in 0..iterations {
650            let mut delta = vec![0.0f64; self.rows * self.cols];
651            for row in 0..self.rows {
652                for col in 0..self.cols {
653                    let h = self.height_at(row, col);
654                    let neighbors: [(isize, isize); 4] = [
655                        (row as isize - 1, col as isize),
656                        (row as isize + 1, col as isize),
657                        (row as isize, col as isize - 1),
658                        (row as isize, col as isize + 1),
659                    ];
660                    let mut steepest_diff = 0.0f64;
661                    let mut steepest_nr = -1isize;
662                    let mut steepest_nc = -1isize;
663                    for (nr, nc) in neighbors {
664                        if nr >= 0 && nr < self.rows as isize && nc >= 0 && nc < self.cols as isize
665                        {
666                            let nh = self.height_at(nr as usize, nc as usize);
667                            let diff = h - nh;
668                            if diff > steepest_diff {
669                                steepest_diff = diff;
670                                steepest_nr = nr;
671                                steepest_nc = nc;
672                            }
673                        }
674                    }
675                    if steepest_nr >= 0 && steepest_diff > 0.0 {
676                        let transfer = sediment_rate * steepest_diff;
677                        delta[row * self.cols + col] -= transfer;
678                        delta[steepest_nr as usize * self.cols + steepest_nc as usize] += transfer;
679                    }
680                }
681            }
682            for (h, d) in self.heights.iter_mut().zip(delta.iter()) {
683                *h += d;
684            }
685        }
686    }
687    /// Compute a simple flow-accumulation map (watershed proxy).
688    ///
689    /// Each cell accumulates 1 unit plus the total of all upstream cells that
690    /// drain into it following steepest descent.  Returns a flat `Vec`f64`
691    /// in row-major order; larger values indicate valleys/channels.
692    pub fn flow_accumulation(&self) -> Vec<f64> {
693        let n = self.rows * self.cols;
694        let mut drain: Vec<Option<usize>> = vec![None; n];
695        for row in 0..self.rows {
696            for col in 0..self.cols {
697                let h = self.height_at(row, col);
698                let neighbors: [(isize, isize); 4] = [
699                    (row as isize - 1, col as isize),
700                    (row as isize + 1, col as isize),
701                    (row as isize, col as isize - 1),
702                    (row as isize, col as isize + 1),
703                ];
704                let mut best_diff = 0.0f64;
705                let mut best_idx: Option<usize> = None;
706                for (nr, nc) in neighbors {
707                    if nr >= 0 && nr < self.rows as isize && nc >= 0 && nc < self.cols as isize {
708                        let nh = self.height_at(nr as usize, nc as usize);
709                        let diff = h - nh;
710                        if diff > best_diff {
711                            best_diff = diff;
712                            best_idx = Some(nr as usize * self.cols + nc as usize);
713                        }
714                    }
715                }
716                drain[row * self.cols + col] = best_idx;
717            }
718        }
719        let mut order: Vec<usize> = (0..n).collect();
720        order.sort_by(|&a, &b| {
721            let ha = self.heights[a];
722            let hb = self.heights[b];
723            hb.partial_cmp(&ha).unwrap_or(std::cmp::Ordering::Equal)
724        });
725        let mut accum = vec![1.0f64; n];
726        for &idx in &order {
727            if let Some(target) = drain[idx] {
728                let val = accum[idx];
729                accum[target] += val;
730            }
731        }
732        accum
733    }
734    /// Clamp all height values to the range `\[min_h, max_h\]`.
735    pub fn clamp_heights(&mut self, min_h: f64, max_h: f64) {
736        for h in &mut self.heights {
737            *h = h.clamp(min_h, max_h);
738        }
739    }
740    /// Scale all heights by a uniform factor.
741    pub fn scale_heights(&mut self, factor: f64) {
742        for h in &mut self.heights {
743            *h *= factor;
744        }
745    }
746    /// Offset all heights by a constant value.
747    pub fn offset_heights(&mut self, offset: f64) {
748        for h in &mut self.heights {
749            *h += offset;
750        }
751    }
752    /// Normalize heights to the range `\[0, 1\]`.  If all heights are equal, all
753    /// become 0.
754    pub fn normalize_heights(&mut self) {
755        let (min_h, max_h) = self.height_bounds();
756        let range = max_h - min_h;
757        if range < 1e-15 {
758            for h in &mut self.heights {
759                *h = 0.0;
760            }
761        } else {
762            for h in &mut self.heights {
763                *h = (*h - min_h) / range;
764            }
765        }
766    }
767    /// Invert heights: each height `h` becomes `max_height - (h - min_height)`.
768    pub fn invert_heights(&mut self) {
769        let (min_h, max_h) = self.height_bounds();
770        for h in &mut self.heights {
771            *h = max_h - (*h - min_h);
772        }
773    }
774    /// Compute the average height over the entire grid.
775    pub fn mean_height(&self) -> f64 {
776        if self.heights.is_empty() {
777            return 0.0;
778        }
779        self.heights.iter().sum::<f64>() / self.heights.len() as f64
780    }
781    /// Compute the variance of heights.
782    pub fn height_variance(&self) -> f64 {
783        if self.heights.is_empty() {
784            return 0.0;
785        }
786        let mean = self.mean_height();
787        self.heights
788            .iter()
789            .map(|&h| (h - mean) * (h - mean))
790            .sum::<f64>()
791            / self.heights.len() as f64
792    }
793    /// Count the number of local maxima (peaks) in the grid.
794    ///
795    /// A cell is a peak if it is strictly higher than all 4-connected neighbors.
796    pub fn count_peaks(&self) -> usize {
797        let mut count = 0usize;
798        for row in 0..self.rows {
799            for col in 0..self.cols {
800                let h = self.height_at(row, col);
801                let neighbors: [(isize, isize); 4] = [
802                    (row as isize - 1, col as isize),
803                    (row as isize + 1, col as isize),
804                    (row as isize, col as isize - 1),
805                    (row as isize, col as isize + 1),
806                ];
807                let is_peak = neighbors.iter().all(|&(nr, nc)| {
808                    if nr < 0 || nr >= self.rows as isize || nc < 0 || nc >= self.cols as isize {
809                        true
810                    } else {
811                        self.height_at(nr as usize, nc as usize) < h
812                    }
813                });
814                if is_peak {
815                    count += 1;
816                }
817            }
818        }
819        count
820    }
821    /// Approximate volume under the height field (above y=0) using the trapezoidal rule.
822    ///
823    /// Each grid cell contributes `(average_height) * cell_area`.
824    pub fn volume(&self) -> f64 {
825        if self.rows < 2 || self.cols < 2 {
826            return 0.0;
827        }
828        let mut vol = 0.0_f64;
829        let cell_area = self.scale_x * self.scale_z;
830        for row in 0..self.rows - 1 {
831            for col in 0..self.cols - 1 {
832                let h00 = self.height_at(row, col);
833                let h10 = self.height_at(row, col + 1);
834                let h01 = self.height_at(row + 1, col);
835                let h11 = self.height_at(row + 1, col + 1);
836                let avg = (h00 + h10 + h01 + h11) * 0.25;
837                vol += avg * cell_area;
838            }
839        }
840        vol
841    }
842    /// Cast a ray against the height field. Returns hit information if intersection found.
843    ///
844    /// Uses the tessellated triangle mesh for accurate intersection.
845    pub fn ray_cast(
846        &self,
847        ray_origin: &oxiphysics_core::math::Vec3,
848        ray_direction: &oxiphysics_core::math::Vec3,
849        max_toi: f64,
850    ) -> Option<HeightfieldRayHit> {
851        let (verts, tris) = self.to_triangle_mesh();
852        let mut closest_toi = max_toi;
853        let mut hit = None;
854        for tri_idx in &tris {
855            let v0 = oxiphysics_core::math::Vec3::new(
856                verts[tri_idx[0]][0],
857                verts[tri_idx[0]][1],
858                verts[tri_idx[0]][2],
859            );
860            let v1 = oxiphysics_core::math::Vec3::new(
861                verts[tri_idx[1]][0],
862                verts[tri_idx[1]][1],
863                verts[tri_idx[1]][2],
864            );
865            let v2 = oxiphysics_core::math::Vec3::new(
866                verts[tri_idx[2]][0],
867                verts[tri_idx[2]][1],
868                verts[tri_idx[2]][2],
869            );
870            if let Some(ray_hit) =
871                ray_triangle(ray_origin, ray_direction, closest_toi, &v0, &v1, &v2)
872                && ray_hit.toi >= 0.0
873                && ray_hit.toi < closest_toi
874            {
875                closest_toi = ray_hit.toi;
876                let point = oxiphysics_core::math::Vec3::new(
877                    ray_origin.x + ray_direction.x * ray_hit.toi,
878                    ray_origin.y + ray_direction.y * ray_hit.toi,
879                    ray_origin.z + ray_direction.z * ray_hit.toi,
880                );
881                hit = Some(HeightfieldRayHit {
882                    toi: ray_hit.toi,
883                    point,
884                });
885            }
886        }
887        hit
888    }
889}
890/// A ray-hit result from `HeightField::ray_cast`.
891#[derive(Debug, Clone)]
892pub struct HeightfieldRayHit {
893    /// Time of impact (distance along ray).
894    pub toi: f64,
895    /// World-space hit point.
896    pub point: oxiphysics_core::math::Vec3,
897}
898/// Result of a heightfield ray traversal.
899#[derive(Debug, Clone)]
900pub struct HeightfieldRaycast {
901    /// Column index of the cell that was hit.
902    pub cell_ix: usize,
903    /// Row index of the cell that was hit.
904    pub cell_iz: usize,
905    /// Ray parameter t at the intersection.
906    pub t: f64,
907    /// Surface normal at the intersection.
908    pub normal: [f64; 3],
909}
910/// State for a DDA grid-traversal ray cast over a height field.
911///
912/// Call [`HeightfieldRayTraversal::new`] to set up the traversal, then call
913/// [`HeightfieldRayTraversal::next_cell`] repeatedly to visit each cell along
914/// the ray in XZ-projection order.
915#[derive(Debug, Clone)]
916pub struct HeightfieldRayTraversal {
917    /// Current cell column index.
918    pub cell_col: isize,
919    /// Current cell row index.
920    pub cell_row: isize,
921    /// Direction step in the column dimension (+1 or -1).
922    pub(super) step_col: isize,
923    /// Direction step in the row dimension (+1 or -1).
924    pub(super) step_row: isize,
925    /// Parameter t at the next X-boundary crossing.
926    pub(super) t_max_x: f64,
927    /// Parameter t at the next Z-boundary crossing.
928    pub(super) t_max_z: f64,
929    /// Change in t per cell step in X.
930    pub(super) t_delta_x: f64,
931    /// Change in t per cell step in Z.
932    pub(super) t_delta_z: f64,
933    /// Maximum t to traverse.
934    pub(super) t_max: f64,
935    /// True once the traversal has finished.
936    pub done: bool,
937}
938impl HeightfieldRayTraversal {
939    /// Initialize a DDA traversal over `hf` starting at `ray_origin` in direction
940    /// `ray_dir` (need not be normalised).  `max_t` bounds the traversal.
941    ///
942    /// Returns `None` if the ray does not enter the XZ footprint of the grid.
943    pub fn new(
944        hf: &HeightField,
945        ray_origin: [f64; 3],
946        ray_dir: [f64; 3],
947        max_t: f64,
948    ) -> Option<Self> {
949        if hf.rows < 2 || hf.cols < 2 {
950            return None;
951        }
952        let grid_w = (hf.cols - 1) as f64 * hf.scale_x;
953        let grid_h = (hf.rows - 1) as f64 * hf.scale_z;
954        let (t_enter, t_exit) = ray_aabb_xz(
955            ray_origin[0],
956            ray_origin[2],
957            ray_dir[0],
958            ray_dir[2],
959            grid_w,
960            grid_h,
961            max_t,
962        )?;
963        let t_start = t_enter.max(0.0);
964        if t_start > t_exit {
965            return None;
966        }
967        let px = ray_origin[0] + ray_dir[0] * t_start;
968        let pz = ray_origin[2] + ray_dir[2] * t_start;
969        let cell_col = ((px / hf.scale_x).floor() as isize).clamp(0, (hf.cols - 2) as isize);
970        let cell_row = ((pz / hf.scale_z).floor() as isize).clamp(0, (hf.rows - 2) as isize);
971        let step_col = if ray_dir[0] >= 0.0 { 1_isize } else { -1 };
972        let step_row = if ray_dir[2] >= 0.0 { 1_isize } else { -1 };
973        let t_delta_x = if ray_dir[0].abs() < 1e-12 {
974            f64::INFINITY
975        } else {
976            hf.scale_x / ray_dir[0].abs()
977        };
978        let t_delta_z = if ray_dir[2].abs() < 1e-12 {
979            f64::INFINITY
980        } else {
981            hf.scale_z / ray_dir[2].abs()
982        };
983        let x_boundary = if step_col > 0 {
984            (cell_col + 1) as f64 * hf.scale_x
985        } else {
986            cell_col as f64 * hf.scale_x
987        };
988        let z_boundary = if step_row > 0 {
989            (cell_row + 1) as f64 * hf.scale_z
990        } else {
991            cell_row as f64 * hf.scale_z
992        };
993        let t_max_x = if ray_dir[0].abs() < 1e-12 {
994            f64::INFINITY
995        } else {
996            t_start + (x_boundary - px) / ray_dir[0]
997        };
998        let t_max_z = if ray_dir[2].abs() < 1e-12 {
999            f64::INFINITY
1000        } else {
1001            t_start + (z_boundary - pz) / ray_dir[2]
1002        };
1003        Some(Self {
1004            cell_col,
1005            cell_row,
1006            step_col,
1007            step_row,
1008            t_max_x,
1009            t_max_z,
1010            t_delta_x,
1011            t_delta_z,
1012            t_max: t_exit.min(max_t),
1013            done: false,
1014        })
1015    }
1016    /// Advance to the next cell along the ray.
1017    ///
1018    /// Returns `(col, row, t_entry)` for the cell just entered, or `None`
1019    /// when the traversal exits the grid.
1020    pub fn next_cell(&mut self) -> Option<(usize, usize, f64)> {
1021        if self.done {
1022            return None;
1023        }
1024        let col = self.cell_col;
1025        let row = self.cell_row;
1026        let t_entry = self.t_max_x.min(self.t_max_z);
1027        if t_entry > self.t_max {
1028            self.done = true;
1029            return None;
1030        }
1031        if self.t_max_x < self.t_max_z {
1032            self.cell_col += self.step_col;
1033            self.t_max_x += self.t_delta_x;
1034        } else {
1035            self.cell_row += self.step_row;
1036            self.t_max_z += self.t_delta_z;
1037        }
1038        if col < 0 || row < 0 {
1039            self.done = true;
1040            return None;
1041        }
1042        Some((col as usize, row as usize, t_entry))
1043    }
1044}