use bevy::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct CoverBand {
pub amount: f32,
pub max_slope_deg: f32,
pub slope_soft_deg: f32,
pub min_height_m: Option<f32>,
pub max_height_m: Option<f32>,
pub height_soft_m: f32,
}
impl Default for CoverBand {
fn default() -> Self {
Self {
amount: 0.0,
max_slope_deg: 180.0,
slope_soft_deg: 0.0,
min_height_m: None,
max_height_m: None,
height_soft_m: 0.0,
}
}
}
fn ramp(value: f32, soft: f32) -> f32 {
if soft > 0.0 {
let t = (value / soft).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
} else if value >= 0.0 {
1.0
} else {
0.0
}
}
impl CoverBand {
pub fn snow(amount: f32) -> Self {
Self {
amount,
max_slope_deg: 45.0,
slope_soft_deg: 15.0,
..Self::default()
}
}
pub fn wet(amount: f32) -> Self {
Self {
amount,
max_slope_deg: 150.0,
slope_soft_deg: 30.0,
..Self::default()
}
}
pub fn coverage(&self, normal: Vec3, height_m: f32) -> f32 {
let amount = self.amount.clamp(0.0, 1.0);
if amount == 0.0 {
return 0.0;
}
let angle = normal
.normalize_or_zero()
.y
.clamp(-1.0, 1.0)
.acos()
.to_degrees();
let slope = ramp(self.max_slope_deg - angle, self.slope_soft_deg);
let above = self
.min_height_m
.map_or(1.0, |lo| ramp(height_m - lo, self.height_soft_m));
let below = self
.max_height_m
.map_or(1.0, |hi| ramp(hi - height_m, self.height_soft_m));
amount * slope * above * below
}
pub(crate) fn packed(&self) -> [Vec4; 2] {
[
Vec4::new(
self.amount.clamp(0.0, 1.0),
self.max_slope_deg,
self.slope_soft_deg.max(0.0),
self.height_soft_m.max(0.0),
),
Vec4::new(
self.min_height_m.unwrap_or(-1.0e30),
self.max_height_m.unwrap_or(1.0e30),
0.0,
0.0,
),
]
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct SurfaceCover {
pub snow: CoverBand,
pub wetness: CoverBand,
}
impl SurfaceCover {
pub fn new(snow: f32, wetness: f32) -> Self {
Self {
snow: CoverBand::snow(snow),
wetness: CoverBand::wet(wetness),
}
}
pub fn snow_at(&self, normal: Vec3, height_m: f32) -> f32 {
self.snow.coverage(normal, height_m)
}
pub fn wetness_at(&self, normal: Vec3, height_m: f32) -> f32 {
self.wetness.coverage(normal, height_m)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sphere_normal(polar_deg: f32) -> (Vec3, f32) {
let p = polar_deg.to_radians();
(Vec3::new(p.sin(), p.cos(), 0.0), 5.0 + 2.0 * p.cos())
}
#[test]
fn a_wet_sphere_is_fully_wet_on_top_and_dry_beneath() {
let cover = SurfaceCover {
wetness: CoverBand {
amount: 1.0,
max_slope_deg: 100.0,
..CoverBand::default()
},
..SurfaceCover::default()
};
let (up, h) = sphere_normal(0.0);
assert_eq!(cover.wetness_at(up, h), 1.0);
let (side, h) = sphere_normal(60.0);
assert_eq!(cover.wetness_at(side, h), 1.0);
let (under, h) = sphere_normal(135.0);
assert_eq!(cover.wetness_at(under, h), 0.0);
assert_eq!(cover.snow_at(up, h), 0.0, "a default band is bare");
}
#[test]
fn snow_sheds_smoothly_between_its_soft_edge_and_its_limit() {
let band = CoverBand::snow(0.8);
let at = |deg: f32| band.coverage(sphere_normal(deg).0, 0.0);
assert_eq!(at(0.0), 0.8);
assert_eq!(at(30.0), 0.8);
assert!(at(37.5) > 0.3 && at(37.5) < 0.5, "half way: {}", at(37.5));
assert_eq!(at(45.0), 0.0);
assert_eq!(at(90.0), 0.0);
assert!(at(34.0) > at(40.0), "monotone");
}
#[test]
fn a_height_window_bounds_the_cover_and_an_amount_is_clamped() {
let band = CoverBand {
amount: 3.0,
min_height_m: Some(10.0),
max_height_m: Some(20.0),
height_soft_m: 2.0,
..CoverBand::default()
};
let up = Vec3::Y;
assert_eq!(band.coverage(up, 9.0), 0.0);
assert_eq!(band.coverage(up, 10.0), 0.0);
assert_eq!(band.coverage(up, 12.0), 1.0);
assert_eq!(band.coverage(up, 15.0), 1.0);
assert_eq!(band.coverage(up, 19.0), 0.5);
assert_eq!(band.coverage(up, 21.0), 0.0);
assert_eq!(
band.coverage(up * 7.0, 15.0),
1.0,
"the normal's length does not matter"
);
}
#[cfg(feature = "serde")]
#[test]
fn a_cover_round_trips_and_missing_fields_are_the_defaults() {
let cover = SurfaceCover {
snow: CoverBand {
max_height_m: Some(40.0),
..CoverBand::snow(0.5)
},
wetness: CoverBand::wet(0.25),
};
let json = serde_json::to_string(&cover).unwrap();
assert_eq!(serde_json::from_str::<SurfaceCover>(&json).unwrap(), cover);
let partial: SurfaceCover = serde_json::from_str(r#"{"snow":{"amount":1.0}}"#).unwrap();
assert_eq!(partial.snow.amount, 1.0);
assert_eq!(partial.snow.max_slope_deg, 180.0);
assert_eq!(partial.wetness, CoverBand::default());
}
}