use super::functions::*;
pub struct TerrainSampling;
impl TerrainSampling {
pub fn elevation_profile(
hf: &HeightfieldShape,
path: &[[f64; 2]],
samples_per_segment: usize,
) -> Vec<ElevationSample> {
let mut result = Vec::new();
let mut cumulative_dist = 0.0;
for seg in path.windows(2) {
let a = seg[0];
let b = seg[1];
let dx = b[0] - a[0];
let dz = b[1] - a[1];
let seg_len = (dx * dx + dz * dz).sqrt();
for i in 0..samples_per_segment {
let t = i as f64 / samples_per_segment as f64;
let x = a[0] + dx * t;
let z = a[1] + dz * t;
let h = hf.height_at(x, z);
let n = hf.normal_at(x, z);
let slope = n[1].clamp(-1.0, 1.0).acos();
let dist_along = t * seg_len;
result.push(ElevationSample {
position: [x, h, z],
distance: cumulative_dist + dist_along,
height: h,
slope,
});
}
cumulative_dist += seg_len;
}
result
}
pub fn slope_map(hf: &HeightfieldShape) -> Vec<f64> {
let mut slopes = Vec::with_capacity(hf.nx * hf.nz);
for iz in 0..hf.nz {
for ix in 0..hf.nx {
let x = ix as f64 * hf.scale_x;
let z = iz as f64 * hf.scale_z;
let n = hf.normal_at(x, z);
let slope = n[1].clamp(-1.0, 1.0).acos();
slopes.push(slope);
}
}
slopes
}
pub fn find_peak(
hf: &HeightfieldShape,
x_min: f64,
z_min: f64,
x_max: f64,
z_max: f64,
) -> [f64; 3] {
let ix_min = (x_min / hf.scale_x) as usize;
let iz_min = (z_min / hf.scale_z) as usize;
let ix_max = ((x_max / hf.scale_x) as usize).min(hf.nx - 1);
let iz_max = ((z_max / hf.scale_z) as usize).min(hf.nz - 1);
let mut best_h = f64::MIN;
let mut best_pos = [0.0; 3];
for iz in iz_min..=iz_max {
for ix in ix_min..=ix_max {
let h = hf.height_at_cell(ix, iz) + hf.y_offset;
if h > best_h {
best_h = h;
best_pos = [ix as f64 * hf.scale_x, h, iz as f64 * hf.scale_z];
}
}
}
best_pos
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SlopeCategory {
Flat,
Gentle,
Moderate,
Steep,
Cliff,
}
#[derive(Debug, Clone)]
pub struct HfBvhNode {
pub aabb_min: [f64; 3],
pub aabb_max: [f64; 3],
pub x0: usize,
pub z0: usize,
pub x1: usize,
pub z1: usize,
pub children: Option<[usize; 2]>,
}
#[derive(Debug, Clone)]
pub struct CachedTerrainContact {
pub query_point: [f64; 3],
pub contact: Option<TerrainContact>,
pub valid: bool,
pub frame: u64,
}
pub struct TerrainShadowMap {
pub shadow: Vec<f64>,
pub nx: usize,
pub nz: usize,
}
impl TerrainShadowMap {
pub fn compute(hf: &HeightfieldShape, light_dir: [f64; 3]) -> Self {
let ld = normalize3(light_dir);
let nx = hf.nx;
let nz = hf.nz;
let mut shadow = vec![1.0f64; nx * nz];
for iz in 0..nz {
for ix in 0..nx {
let x0 = ix as f64 * hf.scale_x;
let z0 = iz as f64 * hf.scale_z;
let y0 = hf.height_at_cell(ix, iz) + hf.y_offset + 0.01;
let max_dist = (hf.nx as f64 * hf.scale_x + hf.nz as f64 * hf.scale_z) * 0.5;
let n_steps = 20usize;
let mut occluded = false;
for step in 1..=n_steps {
let t = (step as f64 / n_steps as f64) * max_dist;
let rx = x0 + ld[0] * t;
let rz = z0 + ld[2] * t;
let ry = y0 + ld[1] * t;
if rx < 0.0 || rz < 0.0 {
break;
}
if rx > (nx - 1) as f64 * hf.scale_x || rz > (nz - 1) as f64 * hf.scale_z {
break;
}
let terrain_h = hf.height_at(rx, rz);
if ry < terrain_h {
occluded = true;
break;
}
}
shadow[iz * nx + ix] = if occluded { 0.0 } else { 1.0 };
}
}
TerrainShadowMap { shadow, nx, nz }
}
pub fn get(&self, ix: usize, iz: usize) -> f64 {
self.shadow[iz * self.nx + ix]
}
pub fn lit_fraction(&self) -> f64 {
let total = self.nx * self.nz;
if total == 0 {
return 0.0;
}
let lit: f64 = self.shadow.iter().sum();
lit / total as f64
}
}
#[derive(Debug, Clone)]
pub struct TerrainContact {
pub point: [f64; 3],
pub normal: [f64; 3],
pub depth: f64,
}
#[derive(Debug, Clone)]
pub struct ElevationSample {
pub position: [f64; 3],
pub distance: f64,
pub height: f64,
pub slope: f64,
}
pub struct LodTerrainCollider<'a> {
pub hf: &'a HeightfieldShape,
pub reference_point: [f64; 3],
}
impl<'a> LodTerrainCollider<'a> {
pub fn new(hf: &'a HeightfieldShape, reference_point: [f64; 3]) -> Self {
LodTerrainCollider {
hf,
reference_point,
}
}
pub fn lod_for(&self, query: [f64; 3]) -> LodLevel {
let dx = query[0] - self.reference_point[0];
let dz = query[2] - self.reference_point[2];
let dist = (dx * dx + dz * dz).sqrt();
LodLevel::from_distance(dist)
}
pub fn sphere_contact_lod(&self, center: [f64; 3], radius: f64) -> Option<TerrainContact> {
let lod = self.lod_for(center);
let stride = lod.stride() as f64;
let snap_x = (center[0] / (self.hf.scale_x * stride)).round() * self.hf.scale_x * stride;
let snap_z = (center[2] / (self.hf.scale_z * stride)).round() * self.hf.scale_z * stride;
let terrain_h = self.hf.height_at(snap_x, snap_z);
let depth = terrain_h + radius - center[1];
if depth > 0.0 {
let normal = self.hf.normal_at(snap_x, snap_z);
Some(TerrainContact {
point: [snap_x, terrain_h, snap_z],
normal,
depth,
})
} else {
None
}
}
pub fn coarse_height_grid(&self, lod: LodLevel) -> Vec<(f64, f64, f64)> {
let stride = lod.stride();
let mut samples = Vec::new();
let mut iz = 0;
while iz < self.hf.nz {
let mut ix = 0;
while ix < self.hf.nx {
let x = ix as f64 * self.hf.scale_x;
let z = iz as f64 * self.hf.scale_z;
let h = self.hf.height_at_cell(ix, iz) + self.hf.y_offset;
samples.push((x, z, h));
ix += stride;
}
iz += stride;
}
samples
}
}
pub struct HeightfieldBvh {
pub nodes: Vec<HfBvhNode>,
pub hf: HeightfieldShape,
}
impl HeightfieldBvh {
pub fn build(hf: HeightfieldShape) -> Self {
let nx = hf.nx;
let nz = hf.nz;
let mut nodes = Vec::new();
let (h_min, h_max) = hf.height_range();
let root = HfBvhNode {
aabb_min: [0.0, h_min, 0.0],
aabb_max: [
(nx - 1) as f64 * hf.scale_x,
h_max,
(nz - 1) as f64 * hf.scale_z,
],
x0: 0,
z0: 0,
x1: nx - 1,
z1: nz - 1,
children: None,
};
nodes.push(root);
HeightfieldBvh { nodes, hf }
}
pub fn ray_vs_root(&self, origin: [f64; 3], dir: [f64; 3]) -> bool {
let node = &self.nodes[0];
ray_aabb(origin, dir, node.aabb_min, node.aabb_max).is_some()
}
}
pub struct TerrainContactCache {
pub entries: Vec<CachedTerrainContact>,
pub position_tolerance: f64,
pub max_age_frames: u64,
pub current_frame: u64,
}
impl TerrainContactCache {
pub fn new(position_tolerance: f64, max_age_frames: u64) -> Self {
TerrainContactCache {
entries: Vec::new(),
position_tolerance,
max_age_frames,
current_frame: 0,
}
}
pub fn next_frame(&mut self) {
self.current_frame += 1;
}
pub fn lookup(&self, query: [f64; 3]) -> Option<&CachedTerrainContact> {
let tol_sq = self.position_tolerance * self.position_tolerance;
for entry in &self.entries {
if !entry.valid {
continue;
}
if self.current_frame - entry.frame > self.max_age_frames {
continue;
}
let dx = entry.query_point[0] - query[0];
let dy = entry.query_point[1] - query[1];
let dz = entry.query_point[2] - query[2];
if dx * dx + dy * dy + dz * dz < tol_sq {
return Some(entry);
}
}
None
}
pub fn insert(&mut self, query: [f64; 3], contact: Option<TerrainContact>) {
let tol_sq = self.position_tolerance * self.position_tolerance;
for entry in &mut self.entries {
let dx = entry.query_point[0] - query[0];
let dy = entry.query_point[1] - query[1];
let dz = entry.query_point[2] - query[2];
if dx * dx + dy * dy + dz * dz < tol_sq {
entry.query_point = query;
entry.contact = contact;
entry.valid = true;
entry.frame = self.current_frame;
return;
}
}
self.entries.push(CachedTerrainContact {
query_point: query,
contact,
valid: true,
frame: self.current_frame,
});
}
pub fn invalidate_old(&mut self) {
for entry in &mut self.entries {
if self.current_frame - entry.frame > self.max_age_frames {
entry.valid = false;
}
}
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn num_valid(&self) -> usize {
self.entries.iter().filter(|e| e.valid).count()
}
}
pub struct UnderwaterTerrainDetector<'a> {
pub hf: &'a HeightfieldShape,
pub water_level: f64,
}
impl<'a> UnderwaterTerrainDetector<'a> {
pub fn new(hf: &'a HeightfieldShape, water_level: f64) -> Self {
UnderwaterTerrainDetector { hf, water_level }
}
pub fn is_underwater(&self, x: f64, y: f64, z: f64) -> bool {
let terrain_h = self.hf.height_at(x, z);
y > terrain_h && y < self.water_level
}
pub fn water_depth_at(&self, x: f64, z: f64) -> f64 {
let terrain_h = self.hf.height_at(x, z);
(self.water_level - terrain_h).max(0.0)
}
pub fn buoyancy_force(
&self,
_x: f64,
y: f64,
_z: f64,
radius: f64,
rho_water: f64,
gravity: f64,
) -> f64 {
let depth = self.water_level - (y - radius);
if depth <= 0.0 {
return 0.0;
}
let submerged_fraction = (depth / (2.0 * radius)).clamp(0.0, 1.0);
let v_full = (4.0 / 3.0) * std::f64::consts::PI * radius * radius * radius;
let v_sub = v_full * submerged_fraction;
rho_water * gravity * v_sub
}
pub fn count_submerged_cells(&self) -> usize {
let mut count = 0;
for iz in 0..self.hf.nz {
for ix in 0..self.hf.nx {
let h = self.hf.height_at_cell(ix, iz) + self.hf.y_offset;
if h < self.water_level {
count += 1;
}
}
}
count
}
}
#[derive(Debug, Clone)]
pub struct TerrainRayHit {
pub point: [f64; 3],
pub normal: [f64; 3],
pub t: f64,
pub cell_x: usize,
pub cell_z: usize,
}
pub struct VoxelTerrain {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub voxel_size: f64,
pub origin: [f64; 3],
pub densities: Vec<f64>,
}
impl VoxelTerrain {
pub fn new(nx: usize, ny: usize, nz: usize, voxel_size: f64, origin: [f64; 3]) -> Self {
VoxelTerrain {
nx,
ny,
nz,
voxel_size,
origin,
densities: vec![0.0; nx * ny * nz],
}
}
fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
ix + self.nx * (iy + self.ny * iz)
}
pub fn set(&mut self, ix: usize, iy: usize, iz: usize, density: f64) {
let idx = self.index(ix, iy, iz);
self.densities[idx] = density;
}
pub fn get(&self, ix: usize, iy: usize, iz: usize) -> f64 {
self.densities[self.index(ix, iy, iz)]
}
pub fn is_solid(&self, ix: usize, iy: usize, iz: usize) -> bool {
self.get(ix, iy, iz) >= 0.0
}
pub fn aabb_query(&self, aabb_min: [f64; 3], aabb_max: [f64; 3]) -> Vec<(usize, usize, usize)> {
let vs = self.voxel_size;
let ix0 = (((aabb_min[0] - self.origin[0]) / vs).floor() as isize).max(0) as usize;
let iy0 = (((aabb_min[1] - self.origin[1]) / vs).floor() as isize).max(0) as usize;
let iz0 = (((aabb_min[2] - self.origin[2]) / vs).floor() as isize).max(0) as usize;
let ix1 = ((((aabb_max[0] - self.origin[0]) / vs).ceil() as isize).max(0) as usize)
.min(self.nx - 1);
let iy1 = ((((aabb_max[1] - self.origin[1]) / vs).ceil() as isize).max(0) as usize)
.min(self.ny - 1);
let iz1 = ((((aabb_max[2] - self.origin[2]) / vs).ceil() as isize).max(0) as usize)
.min(self.nz - 1);
let mut result = Vec::new();
for ix in ix0..=ix1 {
for iy in iy0..=iy1 {
for iz in iz0..=iz1 {
if self.is_solid(ix, iy, iz) {
result.push((ix, iy, iz));
}
}
}
}
result
}
pub fn extract_isosurface(&self) -> Vec<[[f64; 3]; 3]> {
let mut tris = Vec::new();
let vs = self.voxel_size;
for iz in 0..self.nz.saturating_sub(1) {
for iy in 0..self.ny.saturating_sub(1) {
for ix in 0..self.nx.saturating_sub(1) {
let d000 = self.get(ix, iy, iz);
let d100 = self.get(ix + 1, iy, iz);
let d010 = self.get(ix, iy + 1, iz);
let has_positive = d000 > 0.0 || d100 > 0.0 || d010 > 0.0;
let has_negative = d000 < 0.0 || d100 < 0.0 || d010 < 0.0;
if has_positive && has_negative {
let base = voxel_to_world(ix, iy, iz, vs, self.origin);
let v0 = base;
let v1 = add3(base, [vs, 0.0, 0.0]);
let v2 = add3(base, [0.0, vs, 0.0]);
tris.push([v0, v1, v2]);
}
}
}
}
tris
}
}
pub struct CliffDetector<'a> {
pub hf: &'a HeightfieldShape,
pub threshold_angle: f64,
}
impl<'a> CliffDetector<'a> {
pub fn new(hf: &'a HeightfieldShape, threshold_angle: f64) -> Self {
CliffDetector {
hf,
threshold_angle,
}
}
pub fn is_cliff(&self, x: f64, z: f64) -> bool {
let n = self.hf.normal_at(x, z);
let angle = n[1].clamp(-1.0, 1.0).acos();
angle >= self.threshold_angle
}
pub fn cliff_mask(&self) -> Vec<bool> {
let mut mask = Vec::with_capacity(self.hf.nx * self.hf.nz);
for iz in 0..self.hf.nz {
for ix in 0..self.hf.nx {
let x = ix as f64 * self.hf.scale_x;
let z = iz as f64 * self.hf.scale_z;
mask.push(self.is_cliff(x, z));
}
}
mask
}
pub fn num_cliffs(&self) -> usize {
self.cliff_mask().iter().filter(|&&c| c).count()
}
pub fn steepest_cell(
&self,
x_min: f64,
z_min: f64,
x_max: f64,
z_max: f64,
) -> (usize, usize, f64) {
let ix_min = (x_min / self.hf.scale_x) as usize;
let iz_min = (z_min / self.hf.scale_z) as usize;
let ix_max = ((x_max / self.hf.scale_x) as usize).min(self.hf.nx - 1);
let iz_max = ((z_max / self.hf.scale_z) as usize).min(self.hf.nz - 1);
let mut max_angle = 0.0_f64;
let mut best_ix = ix_min;
let mut best_iz = iz_min;
for iz in iz_min..=iz_max {
for ix in ix_min..=ix_max {
let x = ix as f64 * self.hf.scale_x;
let z = iz as f64 * self.hf.scale_z;
let angle = self.hf.normal_at(x, z)[1].clamp(-1.0, 1.0).acos();
if angle > max_angle {
max_angle = angle;
best_ix = ix;
best_iz = iz;
}
}
}
(best_ix, best_iz, max_angle)
}
}
pub struct TerrainDeformation {
pub hf: HeightfieldShape,
}
impl TerrainDeformation {
pub fn new(hf: HeightfieldShape) -> Self {
TerrainDeformation { hf }
}
pub fn apply_crater(&mut self, cx: f64, cz: f64, radius: f64, depth: f64) {
let inv_sx = 1.0 / self.hf.scale_x;
let inv_sz = 1.0 / self.hf.scale_z;
let cell_r = (radius * inv_sx.max(inv_sz)).ceil() as usize;
let ix_center = (cx * inv_sx) as usize;
let iz_center = (cz * inv_sz) as usize;
let nx = self.hf.nx;
let nz = self.hf.nz;
for dix in 0..=(2 * cell_r) {
for diz in 0..=(2 * cell_r) {
let ix = ix_center.saturating_sub(cell_r) + dix;
let iz = iz_center.saturating_sub(cell_r) + diz;
if ix >= nx || iz >= nz {
continue;
}
let wx = ix as f64 * self.hf.scale_x;
let wz = iz as f64 * self.hf.scale_z;
let dx = wx - cx;
let dz_ = wz - cz;
let dist = (dx * dx + dz_ * dz_).sqrt();
if dist < radius {
let factor = 1.0 - (dist / radius).powi(2);
self.hf.heights[iz * nx + ix] -= depth * factor;
}
}
}
}
pub fn apply_track(&mut self, start: [f64; 2], end: [f64; 2], width: f64, depth: f64) {
let d = [end[0] - start[0], end[1] - start[1]];
let len = (d[0] * d[0] + d[1] * d[1]).sqrt();
if len < 1e-14 {
return;
}
let nx = self.hf.nx;
let nz = self.hf.nz;
for ix in 0..nx {
for iz in 0..nz {
let wx = ix as f64 * self.hf.scale_x;
let wz = iz as f64 * self.hf.scale_z;
let px = wx - start[0];
let pz = wz - start[1];
let t = ((px * d[0] + pz * d[1]) / (len * len)).clamp(0.0, 1.0);
let proj_x = start[0] + t * d[0];
let proj_z = start[1] + t * d[1];
let dist = ((wx - proj_x).powi(2) + (wz - proj_z).powi(2)).sqrt();
if dist < width * 0.5 {
let factor = 1.0 - 2.0 * dist / width;
self.hf.heights[iz * nx + ix] -= depth * factor;
}
}
}
}
pub fn apply_mound(&mut self, cx: f64, cz: f64, radius: f64, height: f64) {
let inv_sx = 1.0 / self.hf.scale_x;
let inv_sz = 1.0 / self.hf.scale_z;
let cell_r = (radius * inv_sx.max(inv_sz)).ceil() as usize;
let ix_center = (cx * inv_sx) as usize;
let iz_center = (cz * inv_sz) as usize;
let nx = self.hf.nx;
let nz = self.hf.nz;
for dix in 0..=(2 * cell_r) {
for diz in 0..=(2 * cell_r) {
let ix = ix_center.saturating_sub(cell_r) + dix;
let iz = iz_center.saturating_sub(cell_r) + diz;
if ix >= nx || iz >= nz {
continue;
}
let wx = ix as f64 * self.hf.scale_x;
let wz = iz as f64 * self.hf.scale_z;
let dx = wx - cx;
let dz_ = wz - cz;
let dist = (dx * dx + dz_ * dz_).sqrt();
if dist < radius {
let factor = 1.0 - dist / radius;
self.hf.heights[iz * nx + ix] += height * factor;
}
}
}
}
}
pub struct HeightfieldShape {
pub nx: usize,
pub nz: usize,
pub scale_x: f64,
pub scale_z: f64,
pub heights: Vec<f64>,
pub y_offset: f64,
}
impl HeightfieldShape {
pub fn new(nx: usize, nz: usize, scale_x: f64, scale_z: f64, heights: Vec<f64>) -> Self {
assert_eq!(heights.len(), nx * nz, "heights length must be nx*nz");
HeightfieldShape {
nx,
nz,
scale_x,
scale_z,
heights,
y_offset: 0.0,
}
}
pub fn height_at_cell(&self, ix: usize, iz: usize) -> f64 {
self.heights[iz * self.nx + ix]
}
pub fn height_at(&self, x: f64, z: f64) -> f64 {
let cx = (x / self.scale_x).max(0.0);
let cz = (z / self.scale_z).max(0.0);
let ix = (cx as usize).min(self.nx - 2);
let iz = (cz as usize).min(self.nz - 2);
let tx = cx - ix as f64;
let tz = cz - iz as f64;
let h00 = self.height_at_cell(ix, iz);
let h10 = self.height_at_cell(ix + 1, iz);
let h01 = self.height_at_cell(ix, iz + 1);
let h11 = self.height_at_cell(ix + 1, iz + 1);
bilinear_height_interp(h00, h10, h01, h11, tx, tz) + self.y_offset
}
pub fn normal_at(&self, x: f64, z: f64) -> [f64; 3] {
let h = 0.5_f64.max(self.scale_x.min(self.scale_z));
let h_nx = self.height_at(x - h, z);
let h_px = self.height_at(x + h, z);
let h_nz = self.height_at(x, z - h);
let h_pz = self.height_at(x, z + h);
terrain_normal_from_heights(h_nx, h_px, h_nz, h_pz, h, h)
}
pub fn height_range(&self) -> (f64, f64) {
let mut mn = f64::MAX;
let mut mx = f64::MIN;
for &h in &self.heights {
mn = mn.min(h);
mx = mx.max(h);
}
(mn + self.y_offset, mx + self.y_offset)
}
pub fn world_extent(&self) -> ([f64; 3], [f64; 3]) {
let (h_min, h_max) = self.height_range();
(
[0.0, h_min, 0.0],
[
(self.nx - 1) as f64 * self.scale_x,
h_max,
(self.nz - 1) as f64 * self.scale_z,
],
)
}
}
pub struct TerrainSlopeAspect<'a> {
pub hf: &'a HeightfieldShape,
}
impl<'a> TerrainSlopeAspect<'a> {
pub fn new(hf: &'a HeightfieldShape) -> Self {
TerrainSlopeAspect { hf }
}
pub fn slope_angle(&self, x: f64, z: f64) -> f64 {
let n = self.hf.normal_at(x, z);
n[1].clamp(-1.0, 1.0).acos()
}
pub fn aspect_angle(&self, x: f64, z: f64) -> f64 {
let h = self.hf.scale_x.min(self.hf.scale_z) * 0.5;
let dhdx = (self.hf.height_at(x + h, z) - self.hf.height_at(x - h, z)) / (2.0 * h);
let dhdz = (self.hf.height_at(x, z + h) - self.hf.height_at(x, z - h)) / (2.0 * h);
let angle = dhdz.atan2(dhdx);
if angle < 0.0 {
angle + 2.0 * std::f64::consts::PI
} else {
angle
}
}
pub fn classify_slope(&self, x: f64, z: f64) -> SlopeCategory {
let deg = self.slope_angle(x, z).to_degrees();
if deg < 5.0 {
SlopeCategory::Flat
} else if deg < 15.0 {
SlopeCategory::Gentle
} else if deg < 30.0 {
SlopeCategory::Moderate
} else if deg < 45.0 {
SlopeCategory::Steep
} else {
SlopeCategory::Cliff
}
}
pub fn slope_category_map(&self) -> Vec<SlopeCategory> {
let mut cats = Vec::with_capacity(self.hf.nx * self.hf.nz);
for iz in 0..self.hf.nz {
for ix in 0..self.hf.nx {
let x = ix as f64 * self.hf.scale_x;
let z = iz as f64 * self.hf.scale_z;
cats.push(self.classify_slope(x, z));
}
}
cats
}
}
pub struct HeightfieldBvhFull {
pub nodes: Vec<HfBvhNode>,
pub hf: HeightfieldShape,
pub leaf_size: usize,
}
impl HeightfieldBvhFull {
pub fn build(hf: HeightfieldShape, leaf_size: usize) -> Self {
let ls = leaf_size.max(1);
let mut nodes = Vec::new();
let (h_min, h_max) = hf.height_range();
let nx = hf.nx;
let nz = hf.nz;
let root = HfBvhNode {
aabb_min: [0.0, h_min, 0.0],
aabb_max: [
(nx - 1) as f64 * hf.scale_x,
h_max,
(nz - 1) as f64 * hf.scale_z,
],
x0: 0,
z0: 0,
x1: nx - 1,
z1: nz - 1,
children: None,
};
nodes.push(root);
let mut work_stack: Vec<usize> = vec![0];
while let Some(idx) = work_stack.pop() {
let (x0, x1, z0, z1) = {
let n = &nodes[idx];
(n.x0, n.x1, n.z0, n.z1)
};
let x_span = x1.saturating_sub(x0);
let z_span = z1.saturating_sub(z0);
if x_span <= ls && z_span <= ls {
continue;
}
let (ca, cb) = if x_span >= z_span {
let xm = x0 + x_span / 2;
(
Self::make_node(&hf, x0, xm, z0, z1),
Self::make_node(&hf, xm, x1, z0, z1),
)
} else {
let zm = z0 + z_span / 2;
(
Self::make_node(&hf, x0, x1, z0, zm),
Self::make_node(&hf, x0, x1, zm, z1),
)
};
let ca_x = ca.x1.saturating_sub(ca.x0);
let ca_z = ca.z1.saturating_sub(ca.z0);
let cb_x = cb.x1.saturating_sub(cb.x0);
let cb_z = cb.z1.saturating_sub(cb.z0);
let ca_ok = ca_x < x_span || ca_z < z_span;
let cb_ok = cb_x < x_span || cb_z < z_span;
if !ca_ok || !cb_ok {
continue;
}
let ca_idx = nodes.len();
nodes.push(ca);
let cb_idx = nodes.len();
nodes.push(cb);
nodes[idx].children = Some([ca_idx, cb_idx]);
work_stack.push(ca_idx);
work_stack.push(cb_idx);
}
HeightfieldBvhFull {
nodes,
hf,
leaf_size: ls,
}
}
fn make_node(hf: &HeightfieldShape, x0: usize, x1: usize, z0: usize, z1: usize) -> HfBvhNode {
let mut h_min = f64::MAX;
let mut h_max = f64::MIN;
for iz in z0..=z1.min(hf.nz - 1) {
for ix in x0..=x1.min(hf.nx - 1) {
let h = hf.height_at_cell(ix, iz);
h_min = h_min.min(h);
h_max = h_max.max(h);
}
}
HfBvhNode {
aabb_min: [
x0 as f64 * hf.scale_x,
h_min + hf.y_offset,
z0 as f64 * hf.scale_z,
],
aabb_max: [
x1 as f64 * hf.scale_x,
h_max + hf.y_offset,
z1 as f64 * hf.scale_z,
],
x0,
z0,
x1,
z1,
children: None,
}
}
pub fn query_aabb(&self, qmin: [f64; 3], qmax: [f64; 3]) -> Vec<usize> {
let mut result = Vec::new();
self.traverse_aabb(0, qmin, qmax, &mut result);
result
}
fn traverse_aabb(&self, idx: usize, qmin: [f64; 3], qmax: [f64; 3], result: &mut Vec<usize>) {
let node = &self.nodes[idx];
for k in 0..3 {
if qmax[k] < node.aabb_min[k] || qmin[k] > node.aabb_max[k] {
return;
}
}
if let Some([ca, cb]) = node.children {
self.traverse_aabb(ca, qmin, qmax, result);
self.traverse_aabb(cb, qmin, qmax, result);
} else {
result.push(idx);
}
}
pub fn num_nodes(&self) -> usize {
self.nodes.len()
}
}
#[derive(Debug, Clone)]
pub struct TerrainCcdResult {
pub toi: f64,
pub point: [f64; 3],
pub normal: [f64; 3],
}
pub struct WaterSurface {
pub waves: Vec<(f64, f64, f64, f64, f64)>,
pub time: f64,
pub base_level: f64,
}
impl WaterSurface {
pub fn new(base_level: f64) -> Self {
WaterSurface {
waves: Vec::new(),
time: 0.0,
base_level,
}
}
pub fn add_wave(&mut self, amplitude: f64, freq_x: f64, freq_z: f64, phase: f64, speed: f64) {
self.waves.push((amplitude, freq_x, freq_z, phase, speed));
}
pub fn step(&mut self, dt: f64) {
self.time += dt;
}
pub fn height_at(&self, x: f64, z: f64) -> f64 {
let mut h = self.base_level;
for &(amp, fx, fz, phase, speed) in &self.waves {
h += amp * (fx * x + fz * z + phase + speed * self.time).sin();
}
h
}
pub fn float_test(&self, point: [f64; 3]) -> (bool, f64) {
let wh = self.height_at(point[0], point[2]);
let depth = wh - point[1];
(depth > 0.0, depth.max(0.0))
}
}
pub struct TerrainNormalBilinear<'a> {
pub hf: &'a HeightfieldShape,
}
impl<'a> TerrainNormalBilinear<'a> {
pub fn new(hf: &'a HeightfieldShape) -> Self {
TerrainNormalBilinear { hf }
}
pub fn corner_normal(&self, ix: usize, iz: usize) -> [f64; 3] {
let x = ix as f64 * self.hf.scale_x;
let z = iz as f64 * self.hf.scale_z;
self.hf.normal_at(x, z)
}
pub fn normal_at(&self, x: f64, z: f64) -> [f64; 3] {
let cx = (x / self.hf.scale_x).max(0.0);
let cz = (z / self.hf.scale_z).max(0.0);
let ix = (cx as usize).min(self.hf.nx.saturating_sub(2));
let iz = (cz as usize).min(self.hf.nz.saturating_sub(2));
let tx = cx - ix as f64;
let tz = cz - iz as f64;
let n00 = self.corner_normal(ix, iz);
let n10 = self.corner_normal((ix + 1).min(self.hf.nx - 1), iz);
let n01 = self.corner_normal(ix, (iz + 1).min(self.hf.nz - 1));
let n11 = self.corner_normal((ix + 1).min(self.hf.nx - 1), (iz + 1).min(self.hf.nz - 1));
let nx = n00[0] * (1.0 - tx) * (1.0 - tz)
+ n10[0] * tx * (1.0 - tz)
+ n01[0] * (1.0 - tx) * tz
+ n11[0] * tx * tz;
let ny = n00[1] * (1.0 - tx) * (1.0 - tz)
+ n10[1] * tx * (1.0 - tz)
+ n01[1] * (1.0 - tx) * tz
+ n11[1] * tx * tz;
let nz = n00[2] * (1.0 - tx) * (1.0 - tz)
+ n10[2] * tx * (1.0 - tz)
+ n01[2] * (1.0 - tx) * tz
+ n11[2] * tx * tz;
normalize3([nx, ny, nz])
}
}
pub struct TerrainCollider<'a> {
pub hf: &'a HeightfieldShape,
}
impl<'a> TerrainCollider<'a> {
pub fn new(hf: &'a HeightfieldShape) -> Self {
TerrainCollider { hf }
}
pub fn sphere_vs_terrain(&self, center: [f64; 3], radius: f64) -> Option<TerrainContact> {
let terrain_h = self.hf.height_at(center[0], center[2]);
let depth = terrain_h + radius - center[1];
if depth > 0.0 {
let normal = self.hf.normal_at(center[0], center[2]);
let point = [center[0], terrain_h, center[2]];
Some(TerrainContact {
point,
normal,
depth,
})
} else {
None
}
}
pub fn capsule_vs_terrain(
&self,
cap_a: [f64; 3],
cap_b: [f64; 3],
radius: f64,
) -> Vec<TerrainContact> {
let mut contacts = Vec::new();
for frac in [0.0, 0.5, 1.0] {
let center = add3(cap_a, scale3(sub3(cap_b, cap_a), frac));
if let Some(c) = self.sphere_vs_terrain(center, radius) {
contacts.push(c);
}
}
contacts
}
pub fn box_vs_terrain(&self, box_min: [f64; 3], box_max: [f64; 3]) -> Vec<TerrainContact> {
let mut contacts = Vec::new();
for &xi in &[box_min[0], box_max[0]] {
for &zi in &[box_min[2], box_max[2]] {
let terrain_h = self.hf.height_at(xi, zi);
if box_min[1] < terrain_h {
let depth = terrain_h - box_min[1];
let normal = self.hf.normal_at(xi, zi);
contacts.push(TerrainContact {
point: [xi, terrain_h, zi],
normal,
depth,
});
}
}
}
contacts
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TerrainLayer {
Rock,
Soil,
Vegetation,
Sand,
}
pub struct ProceduralCraterImpact;
impl ProceduralCraterImpact {
pub fn apply_crater_with_rim(
hf: &mut HeightfieldShape,
cx: f64,
cz: f64,
radius: f64,
depth: f64,
rim_height: f64,
rim_width_fraction: f64,
) {
let inv_sx = 1.0 / hf.scale_x;
let inv_sz = 1.0 / hf.scale_z;
let cell_r = ((radius * 1.5) * inv_sx.max(inv_sz)).ceil() as usize;
let ix_center = (cx * inv_sx) as usize;
let iz_center = (cz * inv_sz) as usize;
let nx = hf.nx;
let nz = hf.nz;
for dix in 0..=(2 * cell_r) {
for diz in 0..=(2 * cell_r) {
let ix = ix_center.saturating_sub(cell_r) + dix;
let iz = iz_center.saturating_sub(cell_r) + diz;
if ix >= nx || iz >= nz {
continue;
}
let wx = ix as f64 * hf.scale_x;
let wz = iz as f64 * hf.scale_z;
let dist = ((wx - cx).powi(2) + (wz - cz).powi(2)).sqrt();
let r_norm = dist / radius;
let delta = if r_norm < 1.0 {
-depth * (1.0 - r_norm * r_norm)
} else if r_norm < 1.0 + rim_width_fraction {
let rim_r = (r_norm - 1.0) / rim_width_fraction;
rim_height * (-2.0 * rim_r * rim_r).exp()
} else {
0.0
};
hf.heights[iz * nx + ix] += delta;
}
}
}
pub fn apply_multiple(
hf: &mut HeightfieldShape,
impactors: &[(f64, f64, f64, f64)],
rim_height_fraction: f64,
) {
for &(cx, cz, radius, depth) in impactors {
Self::apply_crater_with_rim(
hf,
cx,
cz,
radius,
depth,
depth * rim_height_fraction,
0.3,
);
}
}
}
pub struct TerrainRaycast<'a> {
pub hf: &'a HeightfieldShape,
}
impl<'a> TerrainRaycast<'a> {
pub fn new(hf: &'a HeightfieldShape) -> Self {
TerrainRaycast { hf }
}
pub fn cast(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<TerrainRayHit> {
let inv_sx = 1.0 / self.hf.scale_x;
let inv_sz = 1.0 / self.hf.scale_z;
let mut cx = ((origin[0] * inv_sx) as isize).max(0) as usize;
let mut cz = ((origin[2] * inv_sz) as isize).max(0) as usize;
let dx = dir[0] * inv_sx;
let dz = dir[2] * inv_sz;
let step_x: isize = if dx >= 0.0 { 1 } else { -1 };
let step_z: isize = if dz >= 0.0 { 1 } else { -1 };
let t_delta_x = if dx.abs() > 1e-14 {
1.0 / dx.abs()
} else {
f64::MAX
};
let t_delta_z = if dz.abs() > 1e-14 {
1.0 / dz.abs()
} else {
f64::MAX
};
let mut t_max_x = if dx.abs() > 1e-14 {
let boundary = if dx >= 0.0 {
((cx + 1) as f64 - origin[0] * inv_sx) / dx
} else {
(cx as f64 - origin[0] * inv_sx) / dx
};
boundary.abs()
} else {
f64::MAX
};
let mut t_max_z = if dz.abs() > 1e-14 {
let boundary = if dz >= 0.0 {
((cz + 1) as f64 - origin[2] * inv_sz) / dz
} else {
(cz as f64 - origin[2] * inv_sz) / dz
};
boundary.abs()
} else {
f64::MAX
};
let max_steps = (self.hf.nx + self.hf.nz) * 2;
for _ in 0..max_steps {
if cx >= self.hf.nx - 1 || cz >= self.hf.nz - 1 {
break;
}
let h = self.hf.height_at(
(cx as f64 + 0.5) * self.hf.scale_x,
(cz as f64 + 0.5) * self.hf.scale_z,
);
let t = if dir[1].abs() > 1e-14 {
(h - origin[1]) / dir[1]
} else {
continue;
};
if t >= 0.0 {
let hit_x = origin[0] + dir[0] * t;
let hit_z = origin[2] + dir[2] * t;
let cell_x = (hit_x * inv_sx) as usize;
let cell_z = (hit_z * inv_sz) as usize;
if cell_x == cx && cell_z == cz {
let point = [hit_x, h, hit_z];
let normal = self.hf.normal_at(hit_x, hit_z);
return Some(TerrainRayHit {
point,
normal,
t,
cell_x: cx,
cell_z: cz,
});
}
}
if t_max_x < t_max_z {
t_max_x += t_delta_x;
if step_x > 0 {
cx += 1;
} else if cx > 0 {
cx -= 1;
} else {
break;
}
} else {
t_max_z += t_delta_z;
if step_z > 0 {
cz += 1;
} else if cz > 0 {
cz -= 1;
} else {
break;
}
}
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LodLevel {
High,
Medium,
Low,
VeryLow,
}
impl LodLevel {
pub fn stride(&self) -> usize {
match self {
LodLevel::High => 1,
LodLevel::Medium => 2,
LodLevel::Low => 4,
LodLevel::VeryLow => 8,
}
}
pub fn from_distance(dist: f64) -> Self {
if dist < 20.0 {
LodLevel::High
} else if dist < 60.0 {
LodLevel::Medium
} else if dist < 150.0 {
LodLevel::Low
} else {
LodLevel::VeryLow
}
}
}
pub struct TerrainTriangle;
impl TerrainTriangle {
pub fn cell_triangles(
hf: &HeightfieldShape,
ix: usize,
iz: usize,
) -> ([[f64; 3]; 3], [[f64; 3]; 3]) {
let x0 = ix as f64 * hf.scale_x;
let z0 = iz as f64 * hf.scale_z;
let x1 = (ix + 1) as f64 * hf.scale_x;
let z1 = (iz + 1) as f64 * hf.scale_z;
let y00 = hf.height_at_cell(ix, iz) + hf.y_offset;
let y10 = hf.height_at_cell(ix + 1, iz) + hf.y_offset;
let y01 = hf.height_at_cell(ix, iz + 1) + hf.y_offset;
let y11 = hf.height_at_cell(ix + 1, iz + 1) + hf.y_offset;
let t0 = [[x0, y00, z0], [x1, y10, z0], [x0, y01, z1]];
let t1 = [[x1, y10, z0], [x1, y11, z1], [x0, y01, z1]];
(t0, t1)
}
pub fn select_triangle(
hf: &HeightfieldShape,
ix: usize,
iz: usize,
tx: f64,
tz: f64,
) -> [[f64; 3]; 3] {
let (t0, t1) = Self::cell_triangles(hf, ix, iz);
if tx + tz <= 1.0 { t0 } else { t1 }
}
}
pub struct MultiLayerTerrain {
pub hf: HeightfieldShape,
pub layers: Vec<TerrainLayer>,
}
impl MultiLayerTerrain {
pub fn new(hf: HeightfieldShape) -> Self {
let n = hf.nx * hf.nz;
let layers = vec![TerrainLayer::Rock; n];
MultiLayerTerrain { hf, layers }
}
fn cell_index(&self, ix: usize, iz: usize) -> usize {
iz * self.hf.nx + ix
}
pub fn set_layer(&mut self, ix: usize, iz: usize, layer: TerrainLayer) {
let i = self.cell_index(ix, iz);
self.layers[i] = layer;
}
pub fn layer_at_cell(&self, ix: usize, iz: usize) -> &TerrainLayer {
&self.layers[self.cell_index(ix, iz)]
}
pub fn layer_at(&self, x: f64, z: f64) -> &TerrainLayer {
let ix = ((x / self.hf.scale_x).round() as usize).min(self.hf.nx - 1);
let iz = ((z / self.hf.scale_z).round() as usize).min(self.hf.nz - 1);
self.layer_at_cell(ix, iz)
}
pub fn layer_friction(layer: &TerrainLayer) -> f64 {
match layer {
TerrainLayer::Rock => 0.7,
TerrainLayer::Soil => 0.5,
TerrainLayer::Vegetation => 0.4,
TerrainLayer::Sand => 0.35,
}
}
pub fn layer_restitution(layer: &TerrainLayer) -> f64 {
match layer {
TerrainLayer::Rock => 0.3,
TerrainLayer::Soil => 0.1,
TerrainLayer::Vegetation => 0.05,
TerrainLayer::Sand => 0.05,
}
}
pub fn friction_at(&self, x: f64, z: f64) -> f64 {
Self::layer_friction(self.layer_at(x, z))
}
pub fn count_layer(&self, target: &TerrainLayer) -> usize {
self.layers.iter().filter(|l| *l == target).count()
}
}
pub struct TerrainCcd<'a> {
pub hf: &'a HeightfieldShape,
pub steps: usize,
}
impl<'a> TerrainCcd<'a> {
pub fn new(hf: &'a HeightfieldShape, steps: usize) -> Self {
TerrainCcd { hf, steps }
}
pub fn sphere_ccd(&self, c0: [f64; 3], c1: [f64; 3], radius: f64) -> Option<TerrainCcdResult> {
let dc = sub3(c1, c0);
for i in 0..=self.steps {
let t = i as f64 / self.steps as f64;
let c = add3(c0, scale3(dc, t));
let h = self.hf.height_at(c[0], c[2]);
if c[1] - radius < h {
let normal = self.hf.normal_at(c[0], c[2]);
return Some(TerrainCcdResult {
toi: t,
point: [c[0], h, c[2]],
normal,
});
}
}
None
}
}
pub struct ErosionFriction {
pub nx: usize,
pub nz: usize,
pub hardness: Vec<f64>,
pub moisture: Vec<f64>,
}
impl ErosionFriction {
pub fn new(nx: usize, nz: usize) -> Self {
ErosionFriction {
nx,
nz,
hardness: vec![1.0; nx * nz],
moisture: vec![0.0; nx * nz],
}
}
fn index(&self, ix: usize, iz: usize) -> usize {
iz * self.nx + ix
}
pub fn set_hardness(&mut self, ix: usize, iz: usize, h: f64) {
let i = self.index(ix, iz);
self.hardness[i] = h.clamp(0.0, 1.0);
}
pub fn set_moisture(&mut self, ix: usize, iz: usize, m: f64) {
let i = self.index(ix, iz);
self.moisture[i] = m.clamp(0.0, 1.0);
}
pub fn friction_at(&self, ix: usize, iz: usize) -> f64 {
let mu_rock = 0.8_f64;
let i = self.index(ix, iz);
let h = self.hardness[i];
let m = self.moisture[i];
mu_rock * h * (1.0 - 0.7 * m)
}
pub fn friction_interp(&self, x: f64, z: f64, scale_x: f64, scale_z: f64) -> f64 {
let cx = (x / scale_x).max(0.0);
let cz = (z / scale_z).max(0.0);
let ix = (cx as usize).min(self.nx.saturating_sub(2));
let iz = (cz as usize).min(self.nz.saturating_sub(2));
let tx = cx - ix as f64;
let tz = cz - iz as f64;
let f00 = self.friction_at(ix, iz);
let f10 = self.friction_at((ix + 1).min(self.nx - 1), iz);
let f01 = self.friction_at(ix, (iz + 1).min(self.nz - 1));
let f11 = self.friction_at((ix + 1).min(self.nx - 1), (iz + 1).min(self.nz - 1));
bilinear_height_interp(f00, f10, f01, f11, tx, tz)
}
pub fn erode(&mut self, ix: usize, iz: usize, impulse: f64) {
let i = self.index(ix, iz);
self.hardness[i] = (self.hardness[i] - impulse * 0.01).max(0.0);
self.moisture[i] = (self.moisture[i] + impulse * 0.005).min(1.0);
}
}