use std::fmt;
use std::sync::Arc;
use bevy::math::{IVec2, Vec2};
use bevy_isomesh::VolumeField;
use isomesh::Sdf;
use crate::terrain::TerrainGrid;
#[derive(Clone)]
pub struct SnowyField {
base: Arc<dyn VolumeField>,
cell_size: f32,
top: Arc<[f32]>,
top_grid: TerrainGrid,
snow: Arc<[f32]>,
snow_grid: TerrainGrid,
}
impl SnowyField {
pub fn new(
base: Arc<dyn VolumeField>,
cell_size: f32,
top: Arc<[f32]>,
top_grid: TerrainGrid,
snow: Arc<[f32]>,
snow_grid: TerrainGrid,
) -> Self {
let cells = |g: &TerrainGrid| g.size.x as usize * g.size.y as usize;
assert_eq!(
top.len(),
cells(&top_grid),
"the column tops must cover their grid"
);
assert_eq!(
snow.len(),
cells(&snow_grid),
"the snow depths must cover their grid"
);
Self {
base,
cell_size,
top,
top_grid,
snow,
snow_grid,
}
}
pub fn base(&self) -> &Arc<dyn VolumeField> {
&self.base
}
pub fn snow(&self) -> &Arc<[f32]> {
&self.snow
}
pub fn snow_grid(&self) -> &TerrainGrid {
&self.snow_grid
}
pub fn top(&self) -> &Arc<[f32]> {
&self.top
}
pub fn top_grid(&self) -> &TerrainGrid {
&self.top_grid
}
fn top_and_snow(&self, xz: Vec2) -> (f32, f32) {
let g = &self.top_grid;
let base = ((xz - g.origin) / g.cell_m - 0.5).floor().as_ivec2();
let at = |dx: i32, dz: i32| self.top[g.index(base + IVec2::new(dx, dz))];
let taps = [at(0, 0), at(1, 0), at(0, 1), at(1, 1)];
let lo = taps.iter().copied().fold(f32::MAX, f32::min);
let hi = taps.iter().copied().fold(f32::MIN, f32::max);
if hi - lo <= self.cell_size {
return (
g.sample(&self.top, xz),
self.snow_grid.sample(&self.snow, xz),
);
}
let nearest =
|grid: &TerrainGrid| grid.index(((xz - grid.origin) / grid.cell_m).floor().as_ivec2());
(self.top[nearest(g)], self.snow[nearest(&self.snow_grid)])
}
}
impl fmt::Debug for SnowyField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SnowyField")
.field("cell_size", &self.cell_size)
.field("top_grid", &self.top_grid)
.field("snow_grid", &self.snow_grid)
.finish_non_exhaustive()
}
}
impl Sdf for SnowyField {
type Scalar = f32;
#[inline]
fn sample(&self, p: [f32; 3]) -> f32 {
let xz = Vec2::new(p[0], p[2]);
let (top, snow) = self.top_and_snow(xz);
let slab = (p[1] - (top + snow)).max((top - self.cell_size) - p[1]);
self.base.sample(p).min(slab)
}
}
#[cfg(test)]
mod tests {
use bevy::math::UVec2;
use isomesh::fields::Sphere;
use super::*;
pub(crate) fn centred_grid(n: u32, cell: f32) -> TerrainGrid {
TerrainGrid {
origin: Vec2::splat(-(n as f32) * cell * 0.5),
cell_m: cell,
size: UVec2::splat(n),
y0: 0.0,
}
}
fn constant(grid: &TerrainGrid, value: f32) -> Arc<[f32]> {
vec![value; (grid.size.x * grid.size.y) as usize].into()
}
fn sphere_slab(radius: f32, snow: f32) -> SnowyField {
let grid = centred_grid(8, 1.0);
SnowyField::new(
Arc::new(Sphere {
center: [0.0; 3],
radius,
}),
0.5,
constant(&grid, radius),
grid,
constant(&grid, snow),
grid,
)
}
#[test]
fn snowy_field_sphere_slab() {
let r = 2.0;
let field = sphere_slab(r, 0.2);
let inside_slab = field.sample([0.0, r + 0.1, 0.0]);
assert!(inside_slab < 0.0, "inside the slab: {inside_slab}");
assert!((inside_slab + 0.1).abs() < 1e-6, "{inside_slab}");
let above = field.sample([0.0, r + 0.3, 0.0]);
assert!(above > 0.0, "above the snow: {above}");
assert!((above - 0.1).abs() < 1e-6, "{above}");
let base = Sphere {
center: [0.0; 3],
radius: r,
};
let deep = field.sample([0.0, r - 0.7, 0.0]);
assert_eq!(deep, base.sample([0.0, r - 0.7, 0.0]));
assert!((deep + 0.7).abs() < 1e-6, "{deep}");
assert!(field.sample([0.0, r + 0.2 + 1e-3, 0.0]) > 0.0);
assert!(field.sample([0.0, r + 0.2 - 1e-3, 0.0]) < 0.0);
}
#[test]
fn snowy_field_interpolates_the_top_bilinearly() {
let r = 2.0_f32;
let grid = centred_grid(8, 1.0);
let tops: Arc<[f32]> = (0..grid.size.y)
.flat_map(|j| (0..grid.size.x).map(move |i| (i, j)))
.map(|(i, j)| {
let x = grid.origin.x + (i as f32 + 0.5) * grid.cell_m;
let z = grid.origin.y + (j as f32 + 0.5) * grid.cell_m;
(r * r - x * x - z * z).max(0.0).sqrt()
})
.collect();
let at_origin = grid.sample(&tops, Vec2::ZERO);
let want = (r * r - 0.5).sqrt();
assert!((at_origin - want).abs() < 1e-4, "{at_origin} vs {want}");
assert!((want - 1.8708).abs() < 1e-4);
}
#[test]
fn snowy_field_tunnel_is_snow_free() {
struct Tunnel;
impl Sdf for Tunnel {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
(p[1] - 10.0).max(1.0 - (p[1] - 5.0).abs())
}
}
let grid = centred_grid(8, 1.0);
let field = SnowyField::new(
Arc::new(Tunnel),
0.5,
constant(&grid, 10.0),
grid,
constant(&grid, 0.5),
grid,
);
assert_eq!(field.sample([0.0, 5.0, 0.0]), 1.0);
let mut y = 4.0_f32;
while y <= 6.0 {
assert_eq!(
field.sample([0.0, y, 0.0]),
Tunnel.sample([0.0, y, 0.0]),
"y = {y}"
);
y += 0.0625;
}
assert_eq!(field.sample([0.0, 9.0, 0.0]), -1.0);
assert!((field.sample([0.0, 10.25, 0.0]) + 0.25).abs() < 1e-6);
assert!((field.sample([0.0, 10.75, 0.0]) - 0.25).abs() < 1e-6);
}
#[test]
fn gradient_points_up_on_the_slab() {
struct Tunnel;
impl Sdf for Tunnel {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
(p[1] - 10.0).max(1.0 - (p[1] - 5.0).abs())
}
}
let grid = centred_grid(8, 1.0);
let field = SnowyField::new(
Arc::new(Tunnel),
0.5,
constant(&grid, 10.0),
grid,
constant(&grid, 0.5),
grid,
);
let g = field.gradient([0.0, 10.25, 0.0]);
assert!(
g[0].abs() < 1e-2 && (g[1] - 1.0).abs() < 1e-2 && g[2].abs() < 1e-2,
"{g:?}"
);
}
#[derive(Clone, Copy)]
struct Step;
impl Sdf for Step {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
p[1] - if p[0] < 0.0 { 5.0 } else { 6.0 }
}
}
#[test]
fn the_slab_sits_on_both_sides_of_a_cliff() {
let grid = centred_grid(8, 0.25);
let tops: Arc<[f32]> = (0..64)
.map(|i| if (i % 8) < 4 { 5.0 } else { 6.0 })
.collect();
let field = SnowyField::new(Arc::new(Step), 0.25, tops, grid, vec![0.3; 64].into(), grid);
assert!(
field.sample([-0.01, 5.1, 0.0]) < 0.0,
"snow on the low side at the foot"
);
assert!(
field.sample([-0.01, 5.4, 0.0]) > 0.0,
"air above the low side's snow"
);
assert!(
field.sample([0.01, 6.2, 0.0]) < 0.0,
"snow on the high side"
);
assert!(field.sample([-0.6, 5.25, 0.0]) < 0.0);
assert!(field.sample([-0.6, 5.35, 0.0]) > 0.0);
}
}