use bevy::prelude::*;
use bevy::render::render_resource::ShaderType;
pub(crate) fn area_per_perimeter(a: f32, b: f32) -> f32 {
let h = ((a - b) / (a + b)).powi(2);
let perimeter =
std::f32::consts::PI * (a + b) * (1.0 + 3.0 * h / (10.0 + (4.0 - 3.0 * h).sqrt()));
std::f32::consts::PI * a * b / perimeter
}
pub(crate) fn ring_width(a: f32, b: f32, texel_m: f32) -> f32 {
(2.0 * texel_m).max(0.3 * a.min(b))
}
pub(crate) fn window_origin(camera: Vec2, texel_m: f32, size: u32) -> IVec2 {
(camera / texel_m).floor().as_ivec2() - IVec2::splat((size / 2) as i32)
}
pub(crate) const MAX_IMPRINTS: usize = 64;
pub(crate) const STAMP_TILE: u32 = 128;
pub(crate) fn wall_share(half: Vec2, texel_m: f32) -> f32 {
let narrow = half.min_element();
(2.5 * texel_m).min(0.5 * narrow) / narrow
}
pub(crate) fn mean_carve(e: f32) -> f32 {
1.0 - e + 0.3 * e * e
}
pub(crate) const MAX_SWEEP_M: f32 = 1.0;
pub(crate) const MARCH_BOUND_M: f32 = 0.35;
pub(crate) const MAX_SINKAGE_M: f32 = 0.30;
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub(crate) struct ImprintGpu {
pub segment: Vec4,
pub shape: Vec4,
pub load: Vec4,
pub tile: Vec4,
}
impl ImprintGpu {
pub(crate) fn new(
half: Vec2,
yaw: f32,
load_n: f32,
from: Vec2,
to: Vec2,
sole_y: f32,
texel_m: f32,
) -> Option<Self> {
if !(half.x > 0.0 && half.y > 0.0 && load_n > 0.0 && sole_y.is_finite()) {
return None;
}
let ring = ring_width(half.x, half.y, texel_m);
let reach = half.max_element() + ring.max(MAX_SINKAGE_M) + 2.0 * texel_m;
let min = from.min(to) - Vec2::splat(reach);
let max = from.max(to) + Vec2::splat(reach);
let tile_min = (min / texel_m).floor().as_ivec2();
let tile_max = (max / texel_m).ceil().as_ivec2();
let edge = (tile_max - tile_min).max_element();
if edge > STAMP_TILE as i32 {
return None;
}
let pressure_kpa = load_n / (std::f32::consts::PI * half.x * half.y) / 1000.0;
Some(Self {
segment: Vec4::new(from.x, from.y, to.x, to.y),
shape: Vec4::new(half.x, half.y, yaw.cos(), yaw.sin()),
load: Vec4::new(
sole_y,
pressure_kpa,
2.0 * half.min_element(),
area_per_perimeter(half.x, half.y) * mean_carve(wall_share(half, texel_m)),
),
tile: Vec4::new(ring, tile_min.x as f32, tile_min.y as f32, edge as f32),
})
}
}
#[cfg(test)]
pub(crate) mod mirror {
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Bekker {
pub n: f32,
pub k_c: f32,
pub k_phi: f32,
}
impl Bekker {
pub(crate) fn sinkage(self, p_kpa: f32, b: f32) -> f32 {
let k = self.k_c / b.max(1e-3) + self.k_phi;
(p_kpa.max(0.0) / k).powf(1.0 / self.n)
}
}
pub(crate) const SNOW: Bekker = Bekker {
n: 1.6,
k_c: 4.37,
k_phi: 196.72,
};
pub(crate) const DRY_SAND: Bekker = Bekker {
n: 1.1,
k_c: 0.99,
k_phi: 1528.43,
};
pub(crate) const DRY_SOIL: Bekker = Bekker {
n: 0.7,
k_c: 5.27,
k_phi: 1515.04,
};
pub(crate) const MUD: Bekker = Bekker {
n: 0.5,
k_c: 0.77,
k_phi: 51.91,
};
pub(crate) const COMPACTED_SNOW_DENSITY: f32 = 500.0;
pub(crate) const LOOSE_LAYER_M: f32 = 0.30;
pub(crate) const SNOW_GROUND_M: f32 = 0.01;
pub(crate) fn snow_sinkage(p_kpa: f32, b: f32, depth: f32, density: f32) -> f32 {
let compaction = depth * (1.0 - density / COMPACTED_SNOW_DENSITY);
SNOW.sinkage(p_kpa, b)
.min(compaction.max(0.0))
.min(LOOSE_LAYER_M)
}
pub(crate) fn soil_sinkage(p_kpa: f32, b: f32, wetness: f32) -> f32 {
let dry = DRY_SOIL.sinkage(p_kpa, b).clamp(1e-6, LOOSE_LAYER_M);
let wet = MUD.sinkage(p_kpa, b).clamp(1e-6, LOOSE_LAYER_M);
(dry.ln() + (wet.ln() - dry.ln()) * wetness.clamp(0.0, 1.0)).exp()
}
fn smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct Ground {
pub snow_m: f32,
pub snow_density: f32,
pub sediment_m: f32,
pub slope_deg: f32,
pub wetness: f32,
}
impl Ground {
pub(crate) fn rock(self) -> f32 {
smoothstep(35.0, 45.0, self.slope_deg)
}
pub(crate) fn sand(self) -> f32 {
smoothstep(0.3, 0.7, self.sediment_m)
* (1.0 - smoothstep(26.0, 30.0, self.slope_deg))
* (1.0 - self.rock())
}
pub(crate) fn sinkage(self, p_kpa: f32, b: f32) -> f32 {
if self.snow_m > SNOW_GROUND_M {
return snow_sinkage(p_kpa, b, self.snow_m, self.snow_density);
}
let sand = self.sand();
let z = sand * DRY_SAND.sinkage(p_kpa, b).min(LOOSE_LAYER_M)
+ (1.0 - sand) * soil_sinkage(p_kpa, b, self.wetness);
z * (1.0 - self.rock())
}
}
}
#[cfg(test)]
mod tests {
use super::mirror::*;
use super::*;
fn walker() -> (f32, f32) {
let area = std::f32::consts::PI * 0.05 * 0.13;
(77.5 * 9.81 / area / 1000.0, 0.1)
}
fn close(actual: f32, expected: f32, tolerance: f32) -> bool {
(actual - expected).abs() <= tolerance * expected
}
#[test]
fn a_walker_sinks_as_the_note_computes() {
let (p, b) = walker();
assert!(close(p, 37.2, 0.01), "pressure {p}");
let sand = Ground {
sediment_m: 2.0,
..default()
};
assert!(
close(sand.sinkage(p, b), 0.0339, 0.01),
"sand {}",
sand.sinkage(p, b)
);
let soil = Ground::default();
assert!(
close(soil.sinkage(p, b), 0.0048, 0.02),
"soil {}",
soil.sinkage(p, b)
);
let mud = Ground {
wetness: 1.0,
..default()
};
assert!(close(MUD.sinkage(p, b), 0.390, 0.01));
assert!(close(mud.sinkage(p, b), LOOSE_LAYER_M, 0.001));
let fresh = Ground {
snow_m: 0.3,
snow_density: 100.0,
..default()
};
assert!(close(SNOW.sinkage(p, b), 0.312, 0.01));
assert!(close(fresh.sinkage(p, b), 0.240, 0.01));
let settled = Ground {
snow_density: 300.0,
..fresh
};
assert!(close(settled.sinkage(p, b), 0.120, 0.01));
}
#[test]
fn soil_sinks_deeper_the_wetter_it_is() {
let (p, b) = walker();
let mut last = 0.0;
for i in 0..=10 {
let z = soil_sinkage(p, b, i as f32 / 10.0);
assert!(z > last, "wetness {}: {z} after {last}", i as f32 / 10.0);
last = z;
}
}
#[test]
fn rock_takes_no_imprint() {
let (p, b) = walker();
let cliff = Ground {
slope_deg: 50.0,
..default()
};
assert_eq!(cliff.sinkage(p, b), 0.0);
}
#[test]
fn the_window_is_centred_on_the_camera_texel() {
let o = window_origin(Vec2::new(10.02, -3.0), 0.05, 1024);
assert_eq!(o, IVec2::new(200 - 512, -60 - 512));
assert_eq!(window_origin(Vec2::new(10.04, -2.99), 0.05, 1024), o);
}
#[test]
fn an_imprint_swept_beyond_a_tile_is_refused() {
let foot = |to: Vec2| {
ImprintGpu::new(Vec2::new(0.06, 0.14), 0.0, 760.0, Vec2::ZERO, to, 0.0, 0.05)
};
assert!(foot(Vec2::new(0.3, 0.0)).is_some());
assert!(foot(Vec2::new(8.0, 0.0)).is_none());
}
}