use std::f32::consts::TAU;
use std::fmt;
use std::sync::Arc;
use bevy::ecs::resource::Resource;
use bevy::math::{Vec2, Vec3};
const FOOTING_SAMPLES: usize = 16;
const SLOPE_STEP: f32 = 0.25;
#[derive(Resource, Clone)]
pub struct FloraGround {
height: Arc<dyn Fn(Vec2) -> f32 + Send + Sync>,
}
impl FloraGround {
pub fn new(height: impl Fn(Vec2) -> f32 + Send + Sync + 'static) -> Self {
Self { height: Arc::new(height) }
}
pub fn flat(height: f32) -> Self {
Self::new(move |_| height)
}
pub fn height(&self, at: Vec2) -> f32 {
(self.height)(at)
}
pub fn normal(&self, at: Vec2) -> Vec3 {
let dx = self.height(at + Vec2::X * SLOPE_STEP) - self.height(at - Vec2::X * SLOPE_STEP);
let dz = self.height(at + Vec2::Y * SLOPE_STEP) - self.height(at - Vec2::Y * SLOPE_STEP);
Vec3::new(-dx, 2.0 * SLOPE_STEP, -dz).normalize_or(Vec3::Y)
}
pub fn slope(&self, at: Vec2) -> f32 {
self.normal(at).y.clamp(-1.0, 1.0).acos()
}
pub fn footing(&self, at: Vec2, radius: f32) -> f32 {
self.around(at, radius).fold(f32::INFINITY, f32::min)
}
pub(crate) fn top(&self, at: Vec2, radius: f32) -> f32 {
self.around(at, radius).fold(f32::NEG_INFINITY, f32::max)
}
fn around(&self, at: Vec2, radius: f32) -> impl Iterator<Item = f32> + '_ {
let ring = (0..FOOTING_SAMPLES).map(move |k| {
let (sin, cos) = (k as f32 * TAU / FOOTING_SAMPLES as f32).sin_cos();
self.height(at + Vec2::new(cos, sin) * radius)
});
std::iter::once(self.height(at)).chain(ring)
}
}
impl Default for FloraGround {
fn default() -> Self {
Self::flat(0.0)
}
}
impl fmt::Debug for FloraGround {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FloraGround").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_trunk_on_a_slope_stands_where_its_downhill_side_meets_the_ground() {
let ground = FloraGround::new(|at: Vec2| 10.0 - 0.3 * at.x);
let at = Vec2::new(4.0, -2.0);
let radius = 0.5;
let footing = ground.footing(at, radius);
assert!((footing - (ground.height(at) - 0.3 * radius)).abs() < 1.0e-4, "footing {footing}");
assert!((ground.top(at, radius) - (ground.height(at) + 0.3 * radius)).abs() < 1.0e-4);
for k in 0..64 {
let (sin, cos) = (k as f32 * TAU / 64.0).sin_cos();
let edge = at + Vec2::new(cos, sin) * radius;
assert!(footing <= ground.height(edge) + 1.0e-4);
}
assert!((ground.slope(at) - 0.3f32.atan()).abs() < 1.0e-4);
let normal = ground.normal(at);
assert!(normal.x > 0.0 && normal.z.abs() < 1.0e-6 && (normal.length() - 1.0).abs() < 1.0e-5);
let flat = FloraGround::flat(2.0);
assert_eq!((flat.footing(at, radius), flat.slope(at)), (2.0, 0.0));
}
}