use phyz_math::Vec3;
#[derive(Debug, Clone, PartialEq)]
pub struct Heightfield {
pub origin: Vec3,
pub cell: f64,
pub nx: usize,
pub ny: usize,
pub heights: Vec<f32>,
}
impl Heightfield {
pub fn new(origin: Vec3, cell: f64, nx: usize, ny: usize) -> Self {
assert!(nx >= 1 && ny >= 1, "heightfield needs at least one node");
assert!(
cell.is_finite() && cell > 0.0,
"heightfield cell must be positive, got {cell}"
);
Self {
origin,
cell,
nx,
ny,
heights: vec![0.0; nx * ny],
}
}
pub fn flat(height: f64) -> Self {
Self::new(Vec3::new(0.0, 0.0, height), 1.0, 1, 1)
}
fn locate(&self, w: f64, o: f64, n: usize) -> (usize, f64) {
if n < 2 {
return (0, 0.0);
}
let u = ((w - o) / self.cell).clamp(0.0, (n - 1) as f64);
let i = (u as usize).min(n - 2);
(i, u - i as f64)
}
fn node(&self, ix: usize, iy: usize) -> f64 {
self.origin.z + self.heights[iy * self.nx + ix] as f64
}
pub fn height(&self, x: f64, y: f64) -> f64 {
let (ix, tx) = self.locate(x, self.origin.x, self.nx);
let (iy, ty) = self.locate(y, self.origin.y, self.ny);
if self.nx < 2 && self.ny < 2 {
return self.node(0, 0);
}
let (ix1, iy1) = ((ix + 1).min(self.nx - 1), (iy + 1).min(self.ny - 1));
let h00 = self.node(ix, iy);
let h10 = self.node(ix1, iy);
let h01 = self.node(ix, iy1);
let h11 = self.node(ix1, iy1);
(h00 * (1.0 - tx) + h10 * tx) * (1.0 - ty) + (h01 * (1.0 - tx) + h11 * tx) * ty
}
pub fn normal(&self, x: f64, y: f64) -> Vec3 {
let (ix, tx) = self.locate(x, self.origin.x, self.nx);
let (iy, ty) = self.locate(y, self.origin.y, self.ny);
let inside = |w: f64, o: f64, n: usize| w >= o && w <= o + (n - 1) as f64 * self.cell;
let (mut dhdx, mut dhdy) = (0.0, 0.0);
if self.nx >= 2 && inside(x, self.origin.x, self.nx) {
let iy1 = (iy + 1).min(self.ny - 1);
let d0 = self.node(ix + 1, iy) - self.node(ix, iy);
let d1 = self.node(ix + 1, iy1) - self.node(ix, iy1);
dhdx = (d0 * (1.0 - ty) + d1 * ty) / self.cell;
}
if self.ny >= 2 && inside(y, self.origin.y, self.ny) {
let ix1 = (ix + 1).min(self.nx - 1);
let d0 = self.node(ix, iy + 1) - self.node(ix, iy);
let d1 = self.node(ix1, iy + 1) - self.node(ix1, iy);
dhdy = (d0 * (1.0 - tx) + d1 * tx) / self.cell;
}
Vec3::new(-dhdx, -dhdy, 1.0).normalize()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_field_is_the_plane_everywhere() {
let hf = Heightfield::flat(0.25);
for &(x, y) in &[(0.0, 0.0), (-1e3, 42.0), (7.5, -7.5)] {
assert_eq!(hf.height(x, y), 0.25);
assert_eq!(hf.normal(x, y), Vec3::z());
}
}
#[test]
fn bilinear_interpolates_between_nodes() {
let mut hf = Heightfield::new(Vec3::zeros(), 1.0, 2, 2);
hf.heights = vec![0.0, 1.0, 2.0, 3.0]; assert!((hf.height(0.5, 0.5) - 1.5).abs() < 1e-12);
assert!((hf.height(0.25, 0.75) - 1.75).abs() < 1e-12);
assert!((hf.height(5.0, 0.0) - 1.0).abs() < 1e-12);
assert!((hf.height(-5.0, -5.0) - 0.0).abs() < 1e-12);
}
#[test]
fn normal_matches_the_slope() {
let mut hf = Heightfield::new(Vec3::zeros(), 1.0, 3, 2);
hf.heights = vec![0.0, 1.0, 2.0, 0.0, 1.0, 2.0];
let n = hf.normal(1.0, 0.5);
let expect = Vec3::new(-1.0, 0.0, 1.0).normalize();
assert!((n - expect).norm() < 1e-12, "normal {n:?}");
assert_eq!(hf.normal(100.0, 0.0), Vec3::z());
}
#[test]
fn origin_offsets_the_whole_surface() {
let hf = Heightfield::new(Vec3::new(10.0, -3.0, 2.0), 0.5, 1, 1);
assert_eq!(hf.height(0.0, 0.0), 2.0);
}
}