use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use bevy::math::{IVec2, Rect, UVec2, Vec2, Vec4};
use bevy::prelude::*;
use bevy::render::render_resource::ShaderType;
use super::{TerrainGpu, TerrainGrid, TerrainSnapshot};
use crate::setup::{SetupError, bad};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SculptKind {
Raise {
amount_m: f32,
},
Lower {
amount_m: f32,
},
Smooth {
blend: f32,
},
Flatten {
height_m: f32,
blend: f32,
},
Noise {
amplitude_m: f32,
feature_m: f32,
seed: u32,
},
}
#[derive(Message, Clone, Copy, Debug, PartialEq)]
pub struct SculptTerrain {
pub centre_m: Vec2,
pub radius_m: f32,
pub hardness: f32,
pub kind: SculptKind,
}
pub const DEFAULT_SCULPT_HARDNESS: f32 = 0.35;
impl SculptTerrain {
pub fn new(centre_m: Vec2, radius_m: f32, kind: SculptKind) -> Self {
Self {
centre_m,
radius_m,
hardness: DEFAULT_SCULPT_HARDNESS,
kind,
}
}
pub fn with_hardness(mut self, hardness: f32) -> Self {
self.hardness = hardness;
self
}
pub fn inverse(&self) -> Option<Self> {
let kind = match self.kind {
SculptKind::Raise { amount_m } => SculptKind::Lower { amount_m },
SculptKind::Lower { amount_m } => SculptKind::Raise { amount_m },
SculptKind::Smooth { .. } | SculptKind::Flatten { .. } | SculptKind::Noise { .. } => {
return None;
}
};
Some(Self { kind, ..*self })
}
pub fn validate(&self) -> Result<(), SetupError> {
if !self.centre_m.is_finite() {
return Err(bad("centre_m", "must be finite"));
}
if !(self.radius_m.is_finite() && self.radius_m > 0.0) {
return Err(bad("radius_m", "must be finite and above 0"));
}
if !(0.0..1.0).contains(&self.hardness) {
return Err(bad("hardness", "must be in 0.0..1.0"));
}
let finite_min = |v: f32| v.is_finite() && v >= 0.0;
let unit = |v: f32| (0.0..=1.0).contains(&v);
match self.kind {
SculptKind::Raise { amount_m } | SculptKind::Lower { amount_m } => {
if !finite_min(amount_m) {
return Err(bad("kind.amount_m", "must be finite and at least 0"));
}
}
SculptKind::Smooth { blend } => {
if !unit(blend) {
return Err(bad("kind.blend", "must be in 0.0..=1.0"));
}
}
SculptKind::Flatten { height_m, blend } => {
if !height_m.is_finite() {
return Err(bad("kind.height_m", "must be finite"));
}
if !unit(blend) {
return Err(bad("kind.blend", "must be in 0.0..=1.0"));
}
}
SculptKind::Noise {
amplitude_m,
feature_m,
..
} => {
if !finite_min(amplitude_m) {
return Err(bad("kind.amplitude_m", "must be finite and at least 0"));
}
if !(feature_m.is_finite() && feature_m > 0.0) {
return Err(bad("kind.feature_m", "must be finite and above 0"));
}
}
}
Ok(())
}
}
pub fn sculpt_weight(d_m: f32, radius_m: f32, hardness: f32) -> f32 {
let t = ((d_m / radius_m - hardness) / (1.0 - hardness)).clamp(0.0, 1.0);
let s = 1.0 - t;
s * s * (3.0 - 2.0 * s)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType)]
pub(crate) struct SculptParamsGpu {
origin: Vec2,
cell: f32,
pad0: f32,
size: bevy::math::UVec2,
pad1: bevy::math::UVec2,
area: Vec4,
shape: Vec4,
}
impl SculptParamsGpu {
fn new(stroke: &SculptTerrain, grid: &super::TerrainGrid) -> Self {
let (kind, amount, z, w) = match stroke.kind {
SculptKind::Raise { amount_m } => (0.0, amount_m, 0.0, 0.0),
SculptKind::Lower { amount_m } => (1.0, amount_m, 0.0, 0.0),
SculptKind::Smooth { blend } => (2.0, blend, 0.0, 0.0),
SculptKind::Flatten { height_m, blend } => (3.0, blend, height_m - grid.y0, 0.0),
SculptKind::Noise {
amplitude_m,
feature_m,
seed,
} => (
4.0,
amplitude_m,
feature_m,
(seed & 0xffff) as f32 * 17.0 + 0.5,
),
};
Self {
origin: grid.origin,
cell: grid.cell_m,
pad0: 0.0,
size: grid.size,
pad1: bevy::math::UVec2::ZERO,
area: Vec4::new(
stroke.centre_m.x,
stroke.centre_m.y,
stroke.radius_m,
stroke.hardness,
),
shape: Vec4::new(kind, amount, z, w),
}
}
}
#[derive(Message, Clone, Debug)]
pub struct RestoreTerrain {
pub snapshot: TerrainSnapshot,
pub region_m: Rect,
}
impl RestoreTerrain {
pub fn new(snapshot: TerrainSnapshot, region_m: Rect) -> Self {
Self { snapshot, region_m }
}
pub fn validate(&self) -> Result<(), SetupError> {
let r = self.region_m;
if !(r.min.is_finite() && r.max.is_finite()) {
return Err(bad("region_m", "must be finite"));
}
if !(r.max.x > r.min.x && r.max.y > r.min.y) {
return Err(bad("region_m", "must have a positive width and depth"));
}
let cells = self.snapshot.grid.size.x as usize * self.snapshot.grid.size.y as usize;
if self.snapshot.surface.len() != cells || self.snapshot.sediment.len() != cells {
return Err(bad(
"snapshot",
"surface and sediment must hold one value per cell of its grid",
));
}
Ok(())
}
}
pub(crate) fn region_cells(grid: &TerrainGrid, region: Rect) -> Option<(UVec2, UVec2)> {
let lo = ((region.min - grid.origin) / grid.cell_m - 0.5).ceil();
let hi = ((region.max - grid.origin) / grid.cell_m - 0.5).floor();
let size = grid.size.as_ivec2();
let first = lo.as_ivec2().max(IVec2::ZERO);
let last = hi.as_ivec2().min(size - 1);
(first.cmple(last).all()).then(|| (first.as_uvec2(), (last + 1).as_uvec2()))
}
fn same_grid(a: &TerrainGrid, b: &TerrainGrid) -> bool {
a.size == b.size
&& (a.cell_m - b.cell_m).abs() <= 1e-3 * b.cell_m
&& (a.origin - b.origin).abs().max_element() <= 1e-3 * b.cell_m
}
#[derive(Clone, Debug)]
pub(crate) struct RestoreGpu {
pub x0: u32,
pub z0: u32,
pub width: u32,
pub rows: u32,
pub bedrock: Arc<[f32]>,
pub sediment: Arc<[f32]>,
}
impl RestoreGpu {
fn new(restore: &RestoreTerrain) -> Option<Self> {
let snap = &restore.snapshot;
let (first, past) = region_cells(&snap.grid, restore.region_m)?;
let width = past.x - first.x;
let rows = past.y - first.y;
let mut bedrock = Vec::with_capacity((width * rows) as usize);
let mut sediment = Vec::with_capacity((width * rows) as usize);
for z in first.y..past.y {
let row = (z * snap.grid.size.x + first.x) as usize;
for i in row..row + width as usize {
bedrock.push(snap.surface[i] - snap.sediment[i]);
sediment.push(snap.sediment[i]);
}
}
Some(Self {
x0: first.x,
z0: first.y,
width,
rows,
bedrock: bedrock.into(),
sediment: sediment.into(),
})
}
pub(crate) fn write(
&self,
queue: &bevy::render::renderer::RenderQueue,
bedrock: &bevy::render::render_resource::Buffer,
sediment: &bevy::render::render_resource::Buffer,
size_x: u32,
) {
let w = self.width as usize;
for r in 0..self.rows as usize {
let offset = (u64::from(self.z0) + r as u64) * u64::from(size_x) + u64::from(self.x0);
let rows = r * w..(r + 1) * w;
queue.write_buffer(
bedrock,
offset * 4,
bytemuck::cast_slice(&self.bedrock[rows.clone()]),
);
queue.write_buffer(
sediment,
offset * 4,
bytemuck::cast_slice(&self.sediment[rows]),
);
}
}
}
enum Edit {
Stroke(SculptTerrain),
Restore(RestoreGpu),
}
#[derive(Resource, Default)]
pub(crate) struct SculptQueue(VecDeque<Edit>);
pub(crate) fn apply_sculpts(
mut strokes: MessageReader<SculptTerrain>,
mut restores: MessageReader<RestoreTerrain>,
mut queue: ResMut<SculptQueue>,
gpu: Option<ResMut<TerrainGpu>>,
) {
for stroke in strokes.read() {
match stroke.validate() {
Ok(()) => queue.0.push_back(Edit::Stroke(*stroke)),
Err(error) => warn!("a SculptTerrain stroke is dropped: {error}"),
}
}
let live = gpu.as_ref().map(|g| g.state.grid);
for restore in restores.read() {
if let Err(error) = restore.validate() {
warn!("a RestoreTerrain is dropped: {error}");
} else if live.is_some_and(|grid| !same_grid(&restore.snapshot.grid, &grid)) {
warn!(
"a RestoreTerrain is dropped: its snapshot was not read from the live terrain's grid"
);
} else if let Some(cut) = RestoreGpu::new(restore) {
queue.0.push_back(Edit::Restore(cut));
}
}
let Some(mut gpu) = gpu else {
return;
};
if !gpu.generated.load(Ordering::Relaxed)
|| gpu.stroked.load(Ordering::Relaxed) < gpu.stroke_epoch
|| gpu.restored.load(Ordering::Relaxed) < gpu.restore_epoch
{
return;
}
match queue.0.pop_front() {
Some(Edit::Stroke(stroke)) => {
gpu.stroke = Some(SculptParamsGpu::new(&stroke, &gpu.state.grid));
gpu.stroke_epoch += 1;
gpu.changes += 1;
}
Some(Edit::Restore(cut)) => {
gpu.restore = Some(cut);
gpu.restore_epoch += 1;
gpu.changes += 1;
}
None => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_weight_is_a_plateau_then_a_smooth_fall_to_nothing_at_the_radius() {
assert_eq!(sculpt_weight(0.0, 10.0, 0.5), 1.0);
assert_eq!(sculpt_weight(5.0, 10.0, 0.5), 1.0);
assert_eq!(sculpt_weight(10.0, 10.0, 0.5), 0.0);
assert_eq!(sculpt_weight(25.0, 10.0, 0.5), 0.0);
let mut last = 1.0;
for i in 50..=100 {
let w = sculpt_weight(i as f32 / 10.0, 10.0, 0.5);
assert!(w <= last, "monotone at {i}: {w} > {last}");
last = w;
}
assert!(sculpt_weight(9.99, 10.0, 0.5) < 1e-4);
}
#[test]
fn only_the_stroke_kinds_that_add_a_delta_have_an_inverse() {
let at = |kind| SculptTerrain::new(Vec2::new(3.0, 4.0), 12.0, kind).with_hardness(0.6);
let raise = at(SculptKind::Raise { amount_m: 2.5 });
let undo = raise.inverse().expect("a raise is undone by a lower");
assert_eq!(
undo,
at(SculptKind::Lower { amount_m: 2.5 }).with_hardness(0.6)
);
assert_eq!(undo.inverse(), Some(raise));
assert!(at(SculptKind::Smooth { blend: 1.0 }).inverse().is_none());
assert!(
at(SculptKind::Flatten {
height_m: 1.0,
blend: 1.0
})
.inverse()
.is_none()
);
assert!(
at(SculptKind::Noise {
amplitude_m: 1.0,
feature_m: 5.0,
seed: 1
})
.inverse()
.is_none()
);
}
#[test]
fn a_stroke_names_the_field_that_breaks_its_bound() {
let ok = SculptTerrain::new(Vec2::ZERO, 10.0, SculptKind::Raise { amount_m: 1.0 });
assert!(ok.validate().is_ok());
let field = |s: SculptTerrain| s.validate().expect_err("invalid").field;
assert_eq!(
field(SculptTerrain {
radius_m: 0.0,
..ok
}),
"radius_m"
);
assert_eq!(field(ok.with_hardness(1.0)), "hardness");
assert_eq!(field(ok.with_hardness(-0.1)), "hardness");
assert_eq!(
field(SculptTerrain {
kind: SculptKind::Lower { amount_m: -1.0 },
..ok
}),
"kind.amount_m"
);
assert_eq!(
field(SculptTerrain {
kind: SculptKind::Smooth { blend: 1.5 },
..ok
}),
"kind.blend"
);
assert_eq!(
field(SculptTerrain {
kind: SculptKind::Noise {
amplitude_m: 1.0,
feature_m: 0.0,
seed: 0
},
..ok
}),
"kind.feature_m"
);
assert_eq!(
field(SculptTerrain {
centre_m: Vec2::new(f32::NAN, 0.0),
..ok
}),
"centre_m"
);
}
fn grid() -> TerrainGrid {
TerrainGrid {
origin: Vec2::new(-4.0, -4.0),
cell_m: 1.0,
size: UVec2::splat(8),
y0: 0.0,
}
}
#[test]
fn a_region_takes_the_cells_whose_centres_it_contains() {
let (first, past) = region_cells(&grid(), Rect::new(-1.0, -1.0, 1.0, 1.0)).expect("cells");
assert_eq!((first, past), (UVec2::splat(3), UVec2::splat(5)));
let (first, past) =
region_cells(&grid(), Rect::new(-50.0, 2.0, 50.0, 50.0)).expect("cells");
assert_eq!((first, past), (UVec2::new(0, 6), UVec2::new(8, 8)));
assert!(region_cells(&grid(), Rect::new(0.1, 0.1, 0.4, 0.4)).is_none());
assert!(region_cells(&grid(), Rect::new(10.0, 10.0, 20.0, 20.0)).is_none());
}
#[test]
fn a_restore_is_cut_from_the_snapshots_bedrock_and_sediment() {
let surface: Vec<f32> = (0..64).map(|i| i as f32).collect();
let snapshot = TerrainSnapshot {
grid: grid(),
surface: Arc::from(surface),
sediment: Arc::from(vec![0.5; 64]),
version: 0,
};
let restore = RestoreTerrain::new(snapshot, Rect::new(-1.0, -1.0, 1.0, 1.0));
assert!(restore.validate().is_ok());
let cut = RestoreGpu::new(&restore).expect("a region");
assert_eq!((cut.x0, cut.z0, cut.width, cut.rows), (3, 3, 2, 2));
assert_eq!(&*cut.bedrock, &[26.5, 27.5, 34.5, 35.5]);
assert_eq!(&*cut.sediment, &[0.5; 4]);
let mut bad = restore.clone();
bad.region_m = Rect::new(1.0, 1.0, 1.0, 2.0);
assert!(bad.validate().is_err());
}
}