use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use bevy::math::{Vec2, Vec4};
use bevy::prelude::*;
use bevy::render::render_resource::ShaderType;
use super::TerrainGpu;
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(Resource, Default)]
pub(crate) struct SculptQueue(VecDeque<SculptTerrain>);
pub(crate) fn apply_sculpts(
mut strokes: MessageReader<SculptTerrain>,
mut queue: ResMut<SculptQueue>,
gpu: Option<ResMut<TerrainGpu>>,
) {
for stroke in strokes.read() {
match stroke.validate() {
Ok(()) => queue.0.push_back(*stroke),
Err(error) => warn!("a SculptTerrain stroke is dropped: {error}"),
}
}
let Some(mut gpu) = gpu else {
return;
};
if !gpu.generated.load(Ordering::Relaxed)
|| gpu.stroked.load(Ordering::Relaxed) < gpu.stroke_epoch
{
return;
}
if let Some(stroke) = queue.0.pop_front() {
gpu.stroke = Some(SculptParamsGpu::new(&stroke, &gpu.state.grid));
gpu.stroke_epoch += 1;
gpu.changes += 1;
}
}
#[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"
);
}
}