use std::sync::Arc;
use bevy::math::{UVec2, Vec2, Vec3, Vec3Swizzles};
pub(crate) const LEAF_CELLS: u32 = 32;
pub(crate) const LACUNARITY: f32 = 2.0;
pub(crate) const GAIN: f32 = 0.5;
pub(crate) const OCTAVE_ROTATION: f32 = 2.399_96;
pub(crate) const F_HALF: f32 = 0.33;
pub(crate) const R_LOW: f32 = 0.21;
pub(crate) const R_SPAN: f32 = 0.71;
pub(crate) const AABB_PAD_M: f32 = 2.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TerrainGrid {
pub origin: Vec2,
pub cell_m: f32,
pub size: UVec2,
pub y0: f32,
}
impl TerrainGrid {
pub fn extent(&self) -> Vec2 {
self.size.as_vec2() * self.cell_m
}
pub fn contains(&self, p: Vec2) -> bool {
let local = p - self.origin;
local.cmpge(Vec2::ZERO).all() && local.cmple(self.extent()).all()
}
pub fn index(&self, c: bevy::math::IVec2) -> usize {
let m = self.size.as_ivec2() - 1;
let k = c.clamp(bevy::math::IVec2::ZERO, m).as_uvec2();
(k.y * self.size.x + k.x) as usize
}
pub fn sample(&self, layer: &[f32], p: Vec2) -> f32 {
let g = (p - self.origin) / self.cell_m - 0.5;
let base = g.floor();
let w = g - base;
let b = base.as_ivec2();
let at = |dx: i32, dz: i32| layer[self.index(b + bevy::math::IVec2::new(dx, dz))];
let row0 = at(0, 0) * (1.0 - w.x) + at(1, 0) * w.x;
let row1 = at(0, 1) * (1.0 - w.x) + at(1, 1) * w.x;
row0 * (1.0 - w.y) + row1 * w.y
}
pub fn drawn_height(&self, layer: &[f32], p: Vec2) -> f32 {
let g = (p - self.origin) / self.cell_m - 0.5;
let base = g.floor();
let w = g - base;
let b = base.as_ivec2();
let at = |dx: i32, dz: i32| layer[self.index(b + bevy::math::IVec2::new(dx, dz))];
if w.x + w.y <= 1.0 {
let h00 = at(0, 0);
h00 + w.x * (at(1, 0) - h00) + w.y * (at(0, 1) - h00)
} else {
let h11 = at(1, 1);
h11 + (1.0 - w.x) * (at(0, 1) - h11) + (1.0 - w.y) * (at(1, 0) - h11)
}
}
}
pub(crate) fn resolve_resolution(extent: Vec2, resolution: UVec2, max: u32) -> (UVec2, f32, bool) {
let snap = |r: u32| {
let r = r.clamp(LEAF_CELLS, max.max(LEAF_CELLS));
1 << r.ilog2()
};
let res = UVec2::new(snap(resolution.x), snap(resolution.y));
let cells = extent / res.as_vec2();
let cell = cells.max_element();
let adjusted = (cells - cell).abs().max_element() > cell * 1e-4;
if !adjusted {
return (res, cell, false);
}
let derive = |e: f32| ((e / cell / LEAF_CELLS as f32).round().max(1.0) as u32) * LEAF_CELLS;
(UVec2::new(derive(extent.x), derive(extent.y)), cell, true)
}
pub(crate) fn octave_count(requested: u32, feature_m: f32, cell_m: f32) -> u32 {
let nyquist = (feature_m / (2.0 * cell_m)).max(1.0).log2().floor() as u32 + 1;
requested.min(nyquist).max(1)
}
pub(crate) fn seed_mix(seed: u64) -> u32 {
(seed.wrapping_mul(2_654_435_761) % 4096) as u32
}
#[derive(Clone, Debug)]
pub struct TerrainSnapshot {
pub grid: TerrainGrid,
pub surface: Arc<[f32]>,
pub sediment: Arc<[f32]>,
pub version: u32,
}
impl TerrainSnapshot {
pub fn height(&self, p: Vec2) -> Option<f32> {
self.grid.contains(p).then(|| self.height_clamped(p))
}
pub fn height_clamped(&self, p: Vec2) -> f32 {
self.grid.y0 + self.grid.sample(&self.surface, p)
}
pub fn drawn_height(&self, p: Vec2) -> Option<f32> {
self.grid
.contains(p)
.then(|| self.grid.y0 + self.grid.drawn_height(&self.surface, p))
}
pub fn sediment(&self, p: Vec2) -> Option<f32> {
self.grid
.contains(p)
.then(|| self.grid.sample(&self.sediment, p))
}
pub(crate) fn slope_deg(&self, p: Vec2) -> Option<f32> {
if !self.grid.contains(p) {
return None;
}
let g = (p - self.grid.origin) / self.grid.cell_m - 0.5;
let base = g.floor();
let w = g - base;
let b = base.as_ivec2();
let last = self.grid.size.as_ivec2() - 1;
let gradient = |dx: i32, dz: i32| {
let c = (b + bevy::math::IVec2::new(dx, dz)).clamp(bevy::math::IVec2::ZERO, last);
let lo = (c - 1).max(bevy::math::IVec2::ZERO);
let hi = (c + 1).min(last);
let at = |x: i32, z: i32| self.surface[self.grid.index(bevy::math::IVec2::new(x, z))];
Vec2::new(
(at(hi.x, c.y) - at(lo.x, c.y)) / ((hi.x - lo.x) as f32 * self.grid.cell_m),
(at(c.x, hi.y) - at(c.x, lo.y)) / ((hi.y - lo.y) as f32 * self.grid.cell_m),
)
};
let row0 = gradient(0, 0).lerp(gradient(1, 0), w.x);
let row1 = gradient(0, 1).lerp(gradient(1, 1), w.x);
Some(row0.lerp(row1, w.y).length().atan().to_degrees())
}
pub(crate) fn ray_hit(&self, origin: Vec3, dir: Vec3, low: f32, high: f32) -> Option<Vec3> {
const MAX_STEPS: f32 = 4096.0;
if dir.y >= 0.0 {
return None;
}
let at = |t: f32| origin + dir * t;
let above = |t: f32| {
let p = at(t);
self.height(p.xz()).map_or(f32::INFINITY, |h| p.y - h)
};
let (t0, t1) = (
((origin.y - high) / -dir.y).max(0.0),
(origin.y - low) / -dir.y,
);
if t1 < t0 {
return None;
}
let step = (0.5 * self.grid.cell_m).max((t1 - t0) / MAX_STEPS);
let mut prev = t0;
if above(prev) <= 0.0 {
return Some(at(prev));
}
loop {
let t = (prev + step).min(t1);
if above(t) <= 0.0 {
let (mut a, mut b) = (prev, t);
for _ in 0..16 {
let mid = 0.5 * (a + b);
if above(mid) <= 0.0 {
b = mid;
} else {
a = mid;
}
}
return Some(at(b));
}
if t >= t1 {
return None;
}
prev = t;
}
}
}
pub(crate) mod surface;
#[cfg(test)]
pub(crate) mod mirror;
#[cfg(test)]
mod tests {
use super::*;
fn ramp() -> TerrainSnapshot {
TerrainSnapshot {
grid: TerrainGrid {
origin: Vec2::ZERO,
cell_m: 1.0,
size: UVec2::splat(16),
y0: 0.0,
},
surface: (0..16 * 16).map(|i| 0.5 * (i % 16) as f32).collect(),
sediment: Arc::from(vec![0.0; 16 * 16]),
version: 1,
}
}
#[test]
fn the_slope_is_the_gradients_angle_everywhere_on_a_ramp() {
let ramp = ramp();
let angle = 0.5_f32.atan().to_degrees();
for at in [
Vec2::new(8.3, 4.1),
Vec2::new(5.5, 5.5),
Vec2::new(0.2, 9.0),
Vec2::new(15.9, 1.0),
] {
let slope = ramp.slope_deg(at).expect("inside the grid");
assert!((slope - angle).abs() < 1e-3, "{at}: {slope}");
}
assert_eq!(ramp.slope_deg(Vec2::new(-0.1, 4.0)), None);
assert_eq!(ramp.slope_deg(Vec2::new(4.0, 16.1)), None);
}
#[test]
fn the_resolution_is_clamped_and_rounded_to_a_power_of_two() {
let e = Vec2::splat(2048.0);
assert_eq!(
resolve_resolution(e, UVec2::splat(1024), 1024),
(UVec2::splat(1024), 2.0, false)
);
assert_eq!(
resolve_resolution(e, UVec2::splat(4096), 2048).0,
UVec2::splat(2048)
);
assert_eq!(
resolve_resolution(e, UVec2::splat(1024), 256).0,
UVec2::splat(256)
);
assert_eq!(
resolve_resolution(e, UVec2::splat(1000), 2048).0,
UVec2::splat(512)
);
assert_eq!(
resolve_resolution(e, UVec2::splat(1), 2048).0,
UVec2::splat(32)
);
}
#[test]
fn square_cells_are_kept_by_rederiving_the_rows() {
let (res, cell, adjusted) =
resolve_resolution(Vec2::new(2048.0, 1024.0), UVec2::splat(1024), 1024);
assert_eq!(res, UVec2::new(1024, 512));
assert_eq!(cell, 2.0);
assert!(adjusted);
let (res, cell, adjusted) =
resolve_resolution(Vec2::new(1000.0, 1500.0), UVec2::splat(256), 256);
assert!(adjusted);
assert_eq!(res, UVec2::new(160, 256));
assert_eq!(cell, 1500.0 / 256.0);
assert_eq!(
resolve_resolution(Vec2::new(256.0, 4096.0), UVec2::splat(1024), 512),
(UVec2::new(32, 512), 8.0, true)
);
}
#[test]
fn octaves_stop_at_the_nyquist_limit() {
assert_eq!(octave_count(8, 600.0, 2.0), 8);
assert_eq!(octave_count(8, 300.0, 2.0), 7);
assert_eq!(octave_count(8, 1500.0, 2.0), 8);
assert_eq!(octave_count(8, 800.0, 2.0), 8);
assert_eq!(octave_count(8, 600.0, 8.0), 6);
assert_eq!(octave_count(0, 600.0, 2.0), 1);
}
#[test]
fn sampling_is_bilinear_with_cell_centres_at_half() {
let grid = TerrainGrid {
origin: Vec2::new(-10.0, -10.0),
cell_m: 2.0,
size: UVec2::new(4, 4),
y0: 5.0,
};
let layer: Vec<f32> = (0..16)
.map(|i| (i % 4) as f32 + (i / 4) as f32 * 10.0)
.collect();
assert!((grid.sample(&layer, Vec2::new(-7.0, -5.0)) - 21.0).abs() < 1e-5);
assert!((grid.sample(&layer, Vec2::new(-6.0, -5.0)) - 21.5).abs() < 1e-5);
let snap = TerrainSnapshot {
grid,
surface: layer.into(),
sediment: vec![0.25; 16].into(),
version: 1,
};
assert_eq!(snap.height(Vec2::new(-7.0, -5.0)), Some(26.0));
assert_eq!(snap.sediment(Vec2::new(-7.0, -5.0)), Some(0.25));
assert_eq!(snap.height(Vec2::new(-10.1, 0.0)), None);
assert!(snap.height(Vec2::new(-2.0, -2.0)).is_some());
assert_eq!(snap.height(Vec2::new(-1.9, -2.0)), None);
}
fn snapshot(h: impl Fn(f32, f32) -> f32) -> TerrainSnapshot {
let (n, cell) = (256u32, 2.0);
let origin = -0.5 * n as f32 * cell;
let surface: Vec<f32> = (0..n * n)
.map(|i| {
let (x, z) = ((i % n) as f32 + 0.5, (i / n) as f32 + 0.5);
h(origin + x * cell, origin + z * cell)
})
.collect();
TerrainSnapshot {
grid: TerrainGrid {
origin: Vec2::splat(origin),
cell_m: cell,
size: UVec2::splat(n),
y0: 3.0,
},
sediment: Arc::from(vec![0.0; surface.len()]),
surface: Arc::from(surface),
version: 1,
}
}
#[test]
fn a_view_ray_meets_the_ground_where_it_first_crosses_it() {
let forward = Vec3::new(0.5, -0.5, 0.5).normalize();
let flat = snapshot(|_, _| 0.0);
let target = Vec3::new(20.0, 3.0, -30.0);
let hit = flat
.ray_hit(target - forward * 457.0, forward, 3.0, 3.0)
.expect("the ray meets the plane");
assert!(hit.distance(target) < 1e-2, "{hit} vs {target}");
let ridge = snapshot(|x, z| {
if (x + z + 320.0).abs() < 15.0 {
40.0
} else {
0.0
}
});
let eye = Vec3::new(-150.0, 0.0, -150.0) - forward * 300.0;
let hit = ridge
.ray_hit(eye, forward, 3.0, 43.0)
.expect("the ray meets the ridge");
let ground = ridge.height(hit.xz()).expect("inside the grid");
assert!(
(hit.y - ground).abs() < 0.05,
"{hit} is not on the ground ({ground})"
);
assert!(hit.y > 10.0, "{hit} passed the ridge");
assert_eq!(flat.ray_hit(target, Vec3::X, 3.0, 3.0), None);
assert_eq!(
flat.ray_hit(target, Vec3::new(0.0, 0.6, 0.8), 3.0, 3.0),
None
);
let shallow = Vec3::new(1.0, -0.01, 0.0).normalize();
assert_eq!(
flat.ray_hit(Vec3::new(0.0, 10.0, 0.0), shallow, 3.0, 3.0),
None
);
}
}