use std::collections::{HashMap, HashSet};
use bevy::math::{UVec2, Vec2};
use bevy::prelude::Entity;
use bevy_isomesh::VoxelChunk;
use isomesh::Sdf;
use isomesh::chunk::{ChunkId, ChunkLayout};
use crate::terrain::TerrainGrid;
pub(crate) const BISECTIONS: u32 = 8;
pub(crate) const TOP_EPSILON_M: f32 = 1e-3;
pub(crate) const SWAP_CELLS: f32 = 0.5;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Columns {
pub grid: TerrainGrid,
pub y_lo: f32,
pub y_hi: f32,
pub cell: f32,
}
impl Columns {
pub fn centre(&self, index: usize) -> Vec2 {
let w = self.grid.size.x as usize;
let at = Vec2::new((index % w) as f32, (index / w) as f32);
self.grid.origin + (at + 0.5) * self.grid.cell_m
}
pub fn no_top(&self) -> f32 {
self.y_lo - 2.0 * self.cell
}
pub fn has_top(&self, top: f32) -> bool {
top > self.y_lo - self.cell
}
pub fn len(&self) -> usize {
self.grid.size.x as usize * self.grid.size.y as usize
}
pub fn columns_in(
&self,
layout: &ChunkLayout<f32>,
chunk: ChunkId,
) -> Option<(u32, u32, u32, u32)> {
let n = layout.cells() as i64;
let lo = layout.world_of_sample(layout.base_sample(chunk));
let hi_sample = layout.base_sample(chunk).map(|s| s + n);
let hi = layout.world_of_sample(hi_sample);
let axis = |lo: f32, hi: f32, origin: f32, count: u32| {
let first = ((lo - origin) / self.grid.cell_m - 0.5).ceil().max(0.0);
let last = ((hi - origin) / self.grid.cell_m - 0.5).floor();
(last >= first && first < count as f32)
.then(|| (first as u32, (last as u32).min(count - 1)))
};
let (i0, i1) = axis(lo[0], hi[0], self.grid.origin.x, self.grid.size.x)?;
let (j0, j1) = axis(lo[2], hi[2], self.grid.origin.y, self.grid.size.y)?;
Some((i0, i1, j0, j1))
}
}
pub(crate) fn volume_box<'a>(
layout: &ChunkLayout<f32>,
chunks: impl IntoIterator<Item = &'a VoxelChunk>,
) -> Option<([f32; 3], [f32; 3])> {
let n = layout.cells() as i64;
let mut bounds: Option<([i64; 3], [i64; 3])> = None;
for chunk in chunks {
let lo = layout.base_sample(chunk.id);
let hi = lo.map(|s| s + n);
bounds = Some(match bounds {
None => (lo, hi),
Some((min, max)) => (
std::array::from_fn(|a| lo[a].min(min[a])),
std::array::from_fn(|a| hi[a].max(max[a])),
),
});
}
let (min, max) = bounds?;
Some((layout.world_of_sample(min), layout.world_of_sample(max)))
}
pub(crate) fn column_top<S: Sdf<Scalar = f32> + ?Sized>(
base: &S,
x: f32,
z: f32,
y_hi: f32,
y_lo: f32,
c: f32,
) -> Option<f32> {
let mut above = y_hi;
if base.sample([x, above, z]) < 0.0 {
return Some(y_hi);
}
let steps = ((y_hi - y_lo) / c).round() as u32;
for k in 1..=steps {
let y = y_hi - k as f32 * c;
let f = base.sample([x, y, z]);
if f < 0.0 {
let (mut lo, mut hi) = (y, above);
for _ in 0..BISECTIONS {
let mid = 0.5 * (lo + hi);
if base.sample([x, mid, z]) >= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
return Some(0.5 * (lo + hi));
}
above = y;
}
None
}
pub(crate) fn scan_columns<S: Sdf<Scalar = f32> + ?Sized>(
base: &S,
columns: &Columns,
indices: impl IntoIterator<Item = usize>,
tops: &mut [f32],
) -> u64 {
let mut scanned = 0;
for index in indices {
let at = columns.centre(index);
tops[index] = column_top(base, at.x, at.y, columns.y_hi, columns.y_lo, columns.cell)
.unwrap_or_else(|| columns.no_top());
scanned += 1;
}
scanned
}
pub(crate) fn box_indices(
size: UVec2,
(i0, i1, j0, j1): (u32, u32, u32, u32),
) -> impl Iterator<Item = usize> {
(j0..=j1).flat_map(move |j| (i0..=i1).map(move |i| (j * size.x + i) as usize))
}
pub(crate) fn slab_span(columns: &Columns, old: (f32, f32), new: (f32, f32)) -> Option<(f32, f32)> {
let c = columns.cell;
let mut span: Option<(f32, f32)> = None;
for (top, snow) in [old, new] {
if !columns.has_top(top) {
continue;
}
let (lo, hi) = (top - c - c, top + snow + c);
span = Some(match span {
None => (lo, hi),
Some((a, b)) => (a.min(lo), b.max(hi)),
});
}
span
}
pub(crate) fn snow_span(columns: &Columns, top: f32, old: f32, new: f32) -> Option<(f32, f32)> {
columns.has_top(top).then(|| {
(
top + old.min(new) - columns.cell,
top + old.max(new) + columns.cell,
)
})
}
pub(crate) fn chunks_of_column(
layout: &ChunkLayout<f32>,
columns: &Columns,
index: usize,
(y0, y1): (f32, f32),
) -> ([i32; 3], [i32; 3]) {
let at = columns.centre(index);
let reach = columns.grid.cell_m + columns.cell;
let lo = layout.chunk_of([at.x - reach, y0, at.y - reach]).coords;
let hi = layout.chunk_of([at.x + reach, y1, at.y + reach]).coords;
(lo, hi)
}
#[derive(Default)]
pub(crate) struct DirtyChunks {
ids: HashSet<ChunkId>,
last: Option<([i32; 3], [i32; 3])>,
}
impl DirtyChunks {
pub fn add(
&mut self,
layout: &ChunkLayout<f32>,
columns: &Columns,
index: usize,
span: (f32, f32),
) {
let boxed = chunks_of_column(layout, columns, index, span);
if self.last == Some(boxed) {
return;
}
self.last = Some(boxed);
let (lo, hi) = boxed;
for x in lo[0]..=hi[0] {
for y in lo[1]..=hi[1] {
for z in lo[2]..=hi[2] {
self.ids.insert(ChunkId::new([x, y, z]));
}
}
}
}
pub fn into_ids(self) -> HashSet<ChunkId> {
self.ids
}
}
pub(crate) fn chunk_entities<'a>(
volume: Entity,
chunks: impl IntoIterator<Item = (Entity, &'a VoxelChunk)>,
) -> HashMap<ChunkId, Entity> {
chunks
.into_iter()
.filter(|(_, chunk)| chunk.volume == volume)
.map(|(entity, chunk)| (chunk.id, entity))
.collect()
}
#[derive(Debug)]
pub(crate) struct Swap {
pub snow: Vec<f32>,
pub changed: Vec<(usize, f32, f32)>,
}
pub(crate) fn hold_swap(
columns: &Columns,
tops: &[f32],
held: &[f32],
fresh: impl Fn(usize) -> f32,
) -> Option<Swap> {
let threshold = SWAP_CELLS * columns.cell;
let over =
|i: usize| columns.has_top(tops[i]) && (fresh(i).max(0.0) - held[i]).abs() > threshold;
if !(0..held.len()).any(over) {
return None;
}
let mut snow = held.to_vec();
let mut changed = Vec::new();
for i in 0..held.len() {
if over(i) {
let new = fresh(i).max(0.0);
changed.push((i, held[i], new));
snow[i] = new;
}
}
Some(Swap { snow, changed })
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use isomesh::fields::Sphere;
use super::*;
fn layout() -> ChunkLayout<f32> {
ChunkLayout::new(16, 0.5, [0.0; 3]).expect("a valid layout")
}
fn columns(layout: &ChunkLayout<f32>, n: u32, y: (f32, f32)) -> Columns {
Columns {
grid: TerrainGrid {
origin: Vec2::ZERO,
cell_m: 0.5,
size: UVec2::splat(n),
y0: 0.0,
},
y_lo: y.0,
y_hi: y.1,
cell: layout.cell_size(),
}
}
struct Counting<S>(S, Cell<u32>);
impl<S: Sdf<Scalar = f32>> Sdf for Counting<S> {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
self.1.set(self.1.get() + 1);
self.0.sample(p)
}
}
struct Layers<F: Fn([f32; 3]) -> f32>(F);
impl<F: Fn([f32; 3]) -> f32> Sdf for Layers<F> {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
(self.0)(p)
}
}
#[test]
fn column_top_extraction() {
let sphere = Counting(
Sphere {
center: [0.0; 3],
radius: 2.0,
},
Cell::new(0),
);
let top = column_top(&sphere, 0.0, 0.0, 4.0, -4.0, 0.5).expect("a top");
assert!((top - 2.0).abs() <= 2e-3, "{top}");
assert!(
sphere.1.get() <= 6 + BISECTIONS,
"{} evaluations",
sphere.1.get()
);
assert!(column_top(&sphere, 3.0, 3.0, 4.0, -4.0, 0.5).is_none());
let roof = Layers(|p: [f32; 3]| {
let below = p[1] - 4.0;
let roof = (p[1] - 10.0).max(8.0 - p[1]);
below.min(roof)
});
let top = column_top(&roof, 0.0, 0.0, 12.0, 0.0, 0.5).expect("a top");
assert!((top - 10.0).abs() <= 2e-3, "a roof over a cave: {top}");
let pit = Layers(|p: [f32; 3]| {
let in_pit = p[0].abs().max(p[2].abs()) < 1.0;
if in_pit { p[1] - 4.0 } else { p[1] - 10.0 }
});
let pit_top = column_top(&pit, 0.0, 0.0, 12.0, 0.0, 0.5).expect("a top");
assert!((pit_top - 4.0).abs() <= 2e-3, "the pit floor: {pit_top}");
let rim = column_top(&pit, 3.0, 0.0, 12.0, 0.0, 0.5).expect("a top");
assert!((rim - 10.0).abs() <= 2e-3, "{rim}");
assert_eq!(column_top(&pit, 3.0, 0.0, 8.0, 0.0, 0.5), Some(8.0));
}
#[test]
fn scan_columns_marks_no_top() {
let layout = layout();
let columns = columns(&layout, 8, (-4.0, 4.0));
let sphere = Sphere {
center: [2.0, 0.0, 2.0],
radius: 2.0,
};
let mut tops = vec![0.0; columns.len()];
scan_columns(&sphere, &columns, 0..columns.len(), &mut tops);
assert_eq!(tops[7 * 8 + 7], -5.0);
assert!(!columns.has_top(tops[7 * 8 + 7]));
assert!(columns.has_top(tops[4 * 8 + 4]));
}
#[test]
fn dirty_chunks_from_columns() {
let layout = layout();
let columns = columns(&layout, 32, (-8.0, 16.0));
let at = |i: usize, j: usize| j * 32 + i;
let dirty = |index: usize, t: f32, s0: f32, s1: f32| {
let span = snow_span(&columns, t, s0, s1).expect("a top");
let mut set = DirtyChunks::default();
set.add(&layout, &columns, index, span);
set.into_ids()
};
let set = dirty(at(15, 7), 3.0, 0.0, 0.3);
let xs: Vec<i32> = {
let mut v: Vec<_> = set.iter().map(|id| id.coords[0]).collect();
v.sort_unstable();
v.dedup();
v
};
assert_eq!(xs, vec![0, 1], "x chunks");
assert!(set.iter().all(|id| id.coords[2] == 0), "one z chunk");
assert_eq!(set.len(), 2);
assert_eq!(dirty(at(7, 7), 3.0, 0.0, 0.3).len(), 1);
let set = dirty(at(15, 15), 7.5, 0.0, 0.4);
assert_eq!(set.len(), 8, "{set:?}");
}
#[test]
fn a_new_slab_spans_its_halo() {
let layout = layout();
let columns = columns(&layout, 32, (-8.0, 16.0));
let (lo, hi) = slab_span(&columns, (3.0, 0.0), (3.0, 0.3)).expect("a top");
assert!(
(lo - 2.0).abs() < 1e-6 && (hi - 3.8).abs() < 1e-6,
"{lo} {hi}"
);
let none = columns.no_top();
let (lo, hi) = slab_span(&columns, (none, 0.0), (3.0, 0.3)).expect("a top");
assert!(
(lo - 2.0).abs() < 1e-6 && (hi - 3.8).abs() < 1e-6,
"{lo} {hi}"
);
assert!(slab_span(&columns, (none, 0.0), (none, 0.0)).is_none());
}
#[test]
fn no_changed_column_marks_nothing() {
let set = DirtyChunks::default().into_ids();
assert!(set.is_empty());
}
#[test]
fn hold_swap_is_per_column() {
let layout = layout();
let columns = columns(&layout, 4, (-8.0, 16.0));
let tops = vec![3.0; columns.len()];
let held = vec![0.0; columns.len()];
assert!(hold_swap(&columns, &tops, &held, |_| 0.24).is_none());
let swap = hold_swap(&columns, &tops, &held, |i| if i == 5 { 0.26 } else { 0.10 })
.expect("a swap");
assert_eq!(swap.changed, vec![(5, 0.0, 0.26)]);
assert_eq!(swap.snow[5], 0.26);
assert!(
swap.snow
.iter()
.enumerate()
.all(|(i, s)| i == 5 || *s == 0.0)
);
assert!(
hold_swap(&columns, &tops, &swap.snow, |i| if i == 5 {
0.26
} else {
0.10
})
.is_none()
);
assert!(
hold_swap(&columns, &tops, &swap.snow, |_| 0.50).is_some_and(|s| s.changed.len() == 15)
);
let only_5 = |i: usize| if i == 5 { 0.50 } else { 0.10 };
assert!(hold_swap(&columns, &tops, &swap.snow, only_5).is_none());
let only_5 = |i: usize| if i == 5 { 0.52 } else { 0.10 };
assert_eq!(
hold_swap(&columns, &tops, &swap.snow, only_5).map(|s| s.changed),
Some(vec![(5, 0.26, 0.52)])
);
let mut tops = tops;
tops[3] = columns.no_top();
let swap = hold_swap(&columns, &tops, &held, |_| 0.8).expect("a swap");
assert_eq!(swap.snow[3], 0.0);
assert_eq!(swap.changed.len(), 15);
}
#[test]
fn a_chunk_box_selects_its_columns() {
let layout = layout();
let columns = columns(&layout, 32, (-8.0, 16.0));
let (i0, i1, j0, j1) = columns
.columns_in(&layout, ChunkId::new([1, 0, 0]))
.expect("inside");
assert_eq!((i0, i1, j0, j1), (16, 31, 0, 15));
assert!(
columns
.columns_in(&layout, ChunkId::new([2, 0, 0]))
.is_none()
);
}
}