use crate::grid::Grid;
use crate::rng::Pcg32;
use crate::ErosionParams;
struct Droplet {
x: f32,
y: f32,
dir_x: f32,
dir_y: f32,
speed: f32,
water: f32,
sediment: f32,
}
pub fn erode_with_progress(
heightmap: &mut Grid<f32>,
params: &ErosionParams,
seed: u32,
on_progress: &mut dyn FnMut(usize, usize),
) {
if heightmap.width() < 3 || heightmap.height() < 3 {
on_progress(1, 1);
return;
}
let droplets = params.droplets.unwrap_or_else(|| heightmap.len());
let brush = Brush::new(params.radius);
let mut rng = Pcg32::new(seed as u64, 0x9e3779b97f4a7c15);
let max_x = (heightmap.width() - 1) as f32 - 1e-4;
let max_y = (heightmap.height() - 1) as f32 - 1e-4;
let chunk = (droplets / 100).max(1);
let mut done = 0;
while done < droplets {
on_progress(done, droplets);
let end = (done + chunk).min(droplets);
for _ in done..end {
let mut drop = Droplet {
x: rng.next_f32() * max_x,
y: rng.next_f32() * max_y,
dir_x: 0.0,
dir_y: 0.0,
speed: 1.0,
water: 1.0,
sediment: 0.0,
};
for _ in 0..params.max_lifetime {
if !step_droplet(heightmap, params, &brush, &mut drop, max_x, max_y) {
break;
}
}
}
done = end;
}
on_progress(droplets, droplets.max(1));
}
fn step_droplet(
heightmap: &mut Grid<f32>,
params: &ErosionParams,
brush: &Brush,
drop: &mut Droplet,
max_x: f32,
max_y: f32,
) -> bool {
let xi = drop.x as usize;
let yi = drop.y as usize;
let fx = drop.x - xi as f32;
let fy = drop.y - yi as f32;
let (h, gx, gy) = height_and_gradient(heightmap, xi, yi, fx, fy);
drop.dir_x = drop.dir_x * params.inertia - gx * (1.0 - params.inertia);
drop.dir_y = drop.dir_y * params.inertia - gy * (1.0 - params.inertia);
let len = (drop.dir_x * drop.dir_x + drop.dir_y * drop.dir_y).sqrt();
if len <= f32::EPSILON {
return false;
}
drop.dir_x /= len;
drop.dir_y /= len;
drop.x += drop.dir_x;
drop.y += drop.dir_y;
if drop.x < 0.0 || drop.x >= max_x || drop.y < 0.0 || drop.y >= max_y {
return false;
}
let new_h = sample_height(heightmap, drop.x, drop.y);
let dh = new_h - h;
let capacity = (-dh).max(params.min_capacity) * drop.speed * drop.water * params.capacity;
if drop.sediment > capacity || dh > 0.0 {
deposit(heightmap, drop, capacity, dh, params, xi, yi, fx, fy);
} else {
erode_around(heightmap, drop, capacity, dh, params, brush, xi, yi);
}
drop.speed = (drop.speed * drop.speed - dh * params.gravity)
.max(0.0)
.sqrt();
drop.water *= 1.0 - params.evaporate_speed;
true
}
#[allow(clippy::too_many_arguments)]
fn deposit(
heightmap: &mut Grid<f32>,
drop: &mut Droplet,
capacity: f32,
dh: f32,
params: &ErosionParams,
xi: usize,
yi: usize,
fx: f32,
fy: f32,
) {
let amount = if dh > 0.0 {
drop.sediment.min(dh)
} else {
(drop.sediment - capacity) * params.deposit_speed
};
drop.sediment -= amount;
heightmap[(xi, yi)] += amount * (1.0 - fx) * (1.0 - fy);
heightmap[(xi + 1, yi)] += amount * fx * (1.0 - fy);
heightmap[(xi, yi + 1)] += amount * (1.0 - fx) * fy;
heightmap[(xi + 1, yi + 1)] += amount * fx * fy;
}
#[allow(clippy::too_many_arguments)]
fn erode_around(
heightmap: &mut Grid<f32>,
drop: &mut Droplet,
capacity: f32,
dh: f32,
params: &ErosionParams,
brush: &Brush,
xi: usize,
yi: usize,
) {
let amount = ((capacity - drop.sediment) * params.erode_speed).min(-dh);
for &(ox, oy, w) in &brush.offsets {
let bx = xi as i32 + ox;
let by = yi as i32 + oy;
if bx < 0 || by < 0 || bx >= heightmap.width() as i32 || by >= heightmap.height() as i32 {
continue;
}
let cell = &mut heightmap[(bx as usize, by as usize)];
let taken = cell.min(amount * w);
*cell -= taken;
drop.sediment += taken;
}
}
fn height_and_gradient(
heightmap: &Grid<f32>,
xi: usize,
yi: usize,
fx: f32,
fy: f32,
) -> (f32, f32, f32) {
let h00 = heightmap[(xi, yi)];
let h10 = heightmap[(xi + 1, yi)];
let h01 = heightmap[(xi, yi + 1)];
let h11 = heightmap[(xi + 1, yi + 1)];
let gx = (h10 - h00) * (1.0 - fy) + (h11 - h01) * fy;
let gy = (h01 - h00) * (1.0 - fx) + (h11 - h10) * fx;
let h = h00 * (1.0 - fx) * (1.0 - fy)
+ h10 * fx * (1.0 - fy)
+ h01 * (1.0 - fx) * fy
+ h11 * fx * fy;
(h, gx, gy)
}
fn sample_height(heightmap: &Grid<f32>, px: f32, py: f32) -> f32 {
let xi = px as usize;
let yi = py as usize;
let fx = px - xi as f32;
let fy = py - yi as f32;
height_and_gradient(heightmap, xi, yi, fx, fy).0
}
struct Brush {
offsets: Vec<(i32, i32, f32)>,
}
impl Brush {
fn new(radius: usize) -> Self {
let r = radius.max(1) as i32;
let mut offsets = Vec::new();
let mut sum = 0.0f32;
for oy in -r..=r {
for ox in -r..=r {
let d = ((ox * ox + oy * oy) as f32).sqrt();
if d <= r as f32 {
let w = 1.0 - d / r as f32;
if w > 0.0 {
offsets.push((ox, oy, w));
sum += w;
}
}
}
}
for (_, _, w) in offsets.iter_mut() {
*w /= sum;
}
Self { offsets }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slope_map(n: usize) -> Grid<f32> {
let mut map = Grid::new(n, n, 0.0f32);
for y in 0..n {
for x in 0..n {
map[(x, y)] = x as f32 / (n - 1) as f32;
}
}
map
}
#[test]
fn erosion_changes_terrain() {
let mut map = slope_map(64);
let before = map.clone();
erode_with_progress(&mut map, &ErosionParams::default(), 1, &mut |_, _| {});
assert_ne!(map, before);
}
#[test]
fn erosion_is_deterministic() {
let mut a = slope_map(64);
let mut b = slope_map(64);
erode_with_progress(&mut a, &ErosionParams::default(), 5, &mut |_, _| {});
erode_with_progress(&mut b, &ErosionParams::default(), 5, &mut |_, _| {});
assert_eq!(a, b);
}
#[test]
fn erosion_roughly_conserves_mass() {
let mut map = slope_map(128);
let before: f32 = map.iter().sum();
erode_with_progress(&mut map, &ErosionParams::default(), 2, &mut |_, _| {});
let after: f32 = map.iter().sum();
assert!(after <= before);
assert!(after > before * 0.9);
}
#[test]
fn no_negative_heights() {
let mut map = slope_map(64);
erode_with_progress(&mut map, &ErosionParams::default(), 3, &mut |_, _| {});
assert!(map.iter().all(|v| *v >= 0.0));
}
#[test]
fn brush_weights_are_normalised() {
for radius in 1..=5 {
let brush = Brush::new(radius);
let sum: f32 = brush.offsets.iter().map(|(_, _, w)| w).sum();
assert!((sum - 1.0).abs() < 1e-5);
}
}
}