use crate::grid::Grid;
use crate::render::{self, RenderStyle};
use crate::{base, breach, erosion, hydrology};
pub const MAX_CELLS: usize = u32::MAX as usize;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamError {
pub param: &'static str,
pub message: String,
}
impl ParamError {
fn new(param: &'static str, message: impl Into<String>) -> Self {
Self {
param,
message: message.into(),
}
}
}
impl std::fmt::Display for ParamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.param, self.message)
}
}
impl std::error::Error for ParamError {}
fn check(ok: bool, param: &'static str, message: &str) -> Result<(), ParamError> {
if ok {
Ok(())
} else {
Err(ParamError::new(param, message))
}
}
#[derive(Clone, Debug)]
pub struct NoiseParams {
pub octaves: usize,
pub frequency: f64,
pub lacunarity: f64,
pub persistence: f64,
}
impl Default for NoiseParams {
fn default() -> Self {
Self {
octaves: 8,
frequency: 2.5,
lacunarity: 2.0,
persistence: 0.5,
}
}
}
impl NoiseParams {
fn validate(&self) -> Result<(), ParamError> {
check(self.octaves > 0, "noise octaves", "must be positive")?;
check(
self.frequency > 0.0 && self.frequency.is_finite(),
"noise frequency",
"must be positive and finite",
)?;
check(
self.lacunarity > 0.0 && self.lacunarity.is_finite(),
"noise lacunarity",
"must be positive and finite",
)?;
check(
self.persistence > 0.0 && self.persistence.is_finite(),
"noise persistence",
"must be positive and finite",
)
}
}
#[derive(Clone, Debug)]
pub struct IslandParams {
pub size: f32,
pub coast_noise: f32,
}
impl Default for IslandParams {
fn default() -> Self {
Self {
size: 1.0,
coast_noise: 1.0,
}
}
}
impl IslandParams {
fn validate(&self) -> Result<(), ParamError> {
check(
self.size > 0.0 && self.size.is_finite(),
"island size",
"must be positive and finite",
)?;
check(
self.coast_noise >= 0.0 && self.coast_noise.is_finite(),
"island coast noise",
"must be non-negative and finite",
)
}
}
#[derive(Clone, Debug)]
pub struct ArchipelagoParams {
pub islands: usize,
pub size: f32,
pub size_variation: f32,
pub spread: f32,
pub min_spacing: f32,
pub coast_noise: f32,
}
impl Default for ArchipelagoParams {
fn default() -> Self {
Self {
islands: 5,
size: 0.35,
size_variation: 0.35,
spread: 0.85,
min_spacing: 0.22,
coast_noise: 1.0,
}
}
}
impl ArchipelagoParams {
pub fn with_islands(islands: usize) -> Self {
Self {
islands,
..Default::default()
}
}
fn validate(&self) -> Result<(), ParamError> {
check(self.islands > 0, "archipelago islands", "must be positive")?;
check(
self.size > 0.0 && self.size.is_finite(),
"archipelago size",
"must be positive and finite",
)?;
check(
(0.0..1.0).contains(&self.size_variation),
"archipelago size variation",
"must be in range [0,1)",
)?;
check(
(0.0..=1.0).contains(&self.spread),
"archipelago spread",
"must be in range [0,1]",
)?;
check(
self.min_spacing >= 0.0 && self.min_spacing.is_finite(),
"archipelago min spacing",
"must be non-negative and finite",
)?;
check(
self.coast_noise >= 0.0 && self.coast_noise.is_finite(),
"archipelago coast noise",
"must be non-negative and finite",
)
}
}
#[derive(Clone, Debug)]
pub struct ErosionParams {
pub droplets: Option<usize>,
pub max_lifetime: usize,
pub inertia: f32,
pub capacity: f32,
pub min_capacity: f32,
pub erode_speed: f32,
pub deposit_speed: f32,
pub evaporate_speed: f32,
pub gravity: f32,
pub radius: usize,
}
impl Default for ErosionParams {
fn default() -> Self {
Self {
droplets: None,
max_lifetime: 30,
inertia: 0.05,
capacity: 4.0,
min_capacity: 0.01,
erode_speed: 0.3,
deposit_speed: 0.3,
evaporate_speed: 0.01,
gravity: 4.0,
radius: 3,
}
}
}
impl ErosionParams {
fn validate(&self) -> Result<(), ParamError> {
check(
self.max_lifetime > 0,
"erosion max lifetime",
"must be positive",
)?;
check(
(0.0..1.0).contains(&self.inertia),
"erosion inertia",
"must be in range [0,1)",
)?;
check(self.capacity > 0.0, "erosion capacity", "must be positive")?;
check(
self.min_capacity > 0.0,
"erosion min capacity",
"must be positive",
)?;
check(
self.erode_speed > 0.0 && self.erode_speed <= 1.0,
"erosion erode speed",
"must be in range (0,1]",
)?;
check(
self.deposit_speed > 0.0 && self.deposit_speed <= 1.0,
"erosion deposit speed",
"must be in range (0,1]",
)?;
check(
(0.0..1.0).contains(&self.evaporate_speed),
"erosion evaporate speed",
"must be in range [0,1)",
)?;
check(self.gravity > 0.0, "erosion gravity", "must be positive")?;
check(self.radius > 0, "erosion radius", "must be positive")
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RiverThreshold {
Density(f32),
MinUpstreamCells(f32),
PerEdge(f32),
}
#[derive(Clone, Debug)]
pub struct RiverParams {
pub threshold: RiverThreshold,
}
impl Default for RiverParams {
fn default() -> Self {
Self {
threshold: RiverThreshold::Density(0.01),
}
}
}
impl RiverParams {
pub fn density(density: f32) -> Self {
Self {
threshold: RiverThreshold::Density(density),
}
}
pub fn min_upstream_cells(cells: f32) -> Self {
Self {
threshold: RiverThreshold::MinUpstreamCells(cells),
}
}
pub fn per_edge(factor: f32) -> Self {
Self {
threshold: RiverThreshold::PerEdge(factor),
}
}
fn validate(&self) -> Result<(), ParamError> {
match self.threshold {
RiverThreshold::Density(d) => check(
(0.0..=1.0).contains(&d),
"river density",
"must be in range [0,1]",
),
RiverThreshold::MinUpstreamCells(c) => check(
c >= 0.0 && c.is_finite(),
"river min upstream cells",
"must be non-negative and finite",
),
RiverThreshold::PerEdge(f) => check(
f >= 0.0 && f.is_finite(),
"river per-edge factor",
"must be non-negative and finite",
),
}
}
}
#[derive(Clone, Debug)]
pub struct BreachParams {
pub max_lake_fraction: f32,
}
impl Default for BreachParams {
fn default() -> Self {
Self {
max_lake_fraction: 0.005,
}
}
}
impl BreachParams {
fn validate(&self) -> Result<(), ParamError> {
check(
(0.0..=1.0).contains(&self.max_lake_fraction),
"breach max lake fraction",
"must be in range [0,1]",
)
}
}
#[derive(Clone, Debug)]
pub struct Terrain {
pub sea_level: f32,
pub heightmap: Grid<f32>,
pub flow_accumulation: Grid<f32>,
pub flow: Grid<f32>,
pub rivers: Grid<u8>,
pub lakes: Grid<u8>,
pub ocean: Grid<u8>,
}
impl Terrain {
pub fn builder() -> TerrainBuilder {
TerrainBuilder::default()
}
pub fn width(&self) -> usize {
self.heightmap.width()
}
pub fn height(&self) -> usize {
self.heightmap.height()
}
pub fn height_at(&self, x: usize, y: usize) -> f32 {
self.heightmap[(x, y)]
}
pub fn flow_at(&self, x: usize, y: usize) -> f32 {
self.flow[(x, y)]
}
pub fn flow_accumulation_at(&self, x: usize, y: usize) -> f32 {
self.flow_accumulation[(x, y)]
}
pub fn is_river(&self, x: usize, y: usize) -> bool {
self.rivers[(x, y)] == 1
}
pub fn is_lake(&self, x: usize, y: usize) -> bool {
self.lakes[(x, y)] == 1
}
pub fn is_ocean(&self, x: usize, y: usize) -> bool {
self.ocean[(x, y)] == 1
}
pub fn is_water(&self, x: usize, y: usize) -> bool {
self.is_ocean(x, y) || self.is_lake(x, y) || self.is_river(x, y)
}
pub fn to_rgb(&self, style: RenderStyle) -> Vec<u8> {
render::to_rgb(self, style)
}
#[cfg(feature = "png")]
pub fn save_layer_png<P: AsRef<std::path::Path>>(
&self,
layer: crate::Layer,
path: P,
) -> Result<(), render::RenderError> {
render::save_layer_png(self, layer, path)
}
#[cfg(feature = "png")]
pub fn save_png<P: AsRef<std::path::Path>>(
&self,
path: P,
style: RenderStyle,
) -> Result<(), render::RenderError> {
render::save_png(self, path, style)
}
}
#[derive(Clone, Debug)]
pub struct TerrainBuilder {
width: usize,
height: usize,
seed: u32,
sea_level: f32,
noise: NoiseParams,
island: Option<IslandParams>,
archipelago: Option<ArchipelagoParams>,
erosion: Option<ErosionParams>,
breach: Option<BreachParams>,
rivers: Option<RiverParams>,
}
impl Default for TerrainBuilder {
fn default() -> Self {
Self {
width: 512,
height: 512,
seed: 0,
sea_level: 0.08,
noise: NoiseParams::default(),
island: Some(IslandParams::default()),
archipelago: None,
erosion: Some(ErosionParams::default()),
breach: None,
rivers: Some(RiverParams::default()),
}
}
}
impl TerrainBuilder {
pub fn size(mut self, width: usize, height: usize) -> Self {
self.width = width;
self.height = height;
self
}
pub fn seed(mut self, seed: u32) -> Self {
self.seed = seed;
self
}
pub fn sea_level(mut self, sea_level: f32) -> Self {
self.sea_level = sea_level;
self
}
pub fn noise(mut self, noise: NoiseParams) -> Self {
self.noise = noise;
self
}
pub fn island(mut self, island: IslandParams) -> Self {
self.island = Some(island);
self.archipelago = None;
self
}
pub fn archipelago(mut self, archipelago: ArchipelagoParams) -> Self {
self.archipelago = Some(archipelago);
self.island = None;
self
}
pub fn no_island(mut self) -> Self {
self.island = None;
self.archipelago = None;
self
}
pub fn erosion(mut self, erosion: ErosionParams) -> Self {
self.erosion = Some(erosion);
self
}
pub fn no_erosion(mut self) -> Self {
self.erosion = None;
self
}
pub fn breach_basins(mut self, breach: BreachParams) -> Self {
self.breach = Some(breach);
self
}
pub fn no_breach(mut self) -> Self {
self.breach = None;
self
}
pub fn rivers(mut self, rivers: RiverParams) -> Self {
self.rivers = Some(rivers);
self
}
pub fn no_rivers(mut self) -> Self {
self.rivers = None;
self
}
pub fn generate(&self) -> Terrain {
match self.try_generate() {
Ok(terrain) => terrain,
Err(e) => panic!("{e}"),
}
}
pub fn try_generate(&self) -> Result<Terrain, ParamError> {
self.try_generate_with_progress(|_, _| {})
}
pub fn try_generate_with_progress(
&self,
mut report: impl FnMut(f32, &'static str),
) -> Result<Terrain, ParamError> {
self.validate()?;
let noise_w = 1.0;
let erosion_w = if self.erosion.is_some() { 20.0 } else { 0.0 };
let breach_w = if self.breach.is_some() { 2.0 } else { 0.0 };
let river_w = if self.rivers.is_some() { 2.0 } else { 0.0 };
let total = noise_w + erosion_w + breach_w + river_w;
let mut cursor = 0.0f32;
let mut take = |weight: f32| {
let start = cursor;
cursor += weight / total;
(start, cursor)
};
let mask = match (&self.archipelago, &self.island) {
(Some(arch), _) => Some(base::Mask::Archipelago(arch)),
(None, Some(island)) => Some(base::Mask::Island(island)),
(None, None) => None,
};
let (n0, n1) = take(noise_w);
report(n0, "Heightmap");
let mut heightmap = base::generate(self.width, self.height, self.seed, &self.noise, mask);
report(n1, "Heightmap");
if let Some(erosion_params) = &self.erosion {
let (e0, e1) = take(erosion_w);
erosion::erode_with_progress(
&mut heightmap,
erosion_params,
self.seed,
&mut |done, droplets| {
let t = if droplets == 0 {
1.0
} else {
done as f32 / droplets as f32
};
report(e0 + (e1 - e0) * t, "Erosion");
},
);
}
if let Some(breach_params) = &self.breach {
let (b0, b1) = take(breach_w);
report(b0, "Basins");
let ocean = hydrology::ocean_mask(&heightmap, self.sea_level);
breach::breach(&mut heightmap, &ocean, breach_params);
report(b1, "Basins");
}
let ocean = hydrology::ocean_mask(&heightmap, self.sea_level);
let (flow_accumulation, flow, rivers, lakes) = match &self.rivers {
Some(river_params) => {
let (r0, r1) = take(river_w);
report(r0, "Rivers");
let hydro = hydrology::compute(&heightmap, &ocean, river_params);
report(r1, "Rivers");
(hydro.accumulation, hydro.flow, hydro.rivers, hydro.lakes)
}
None => (
Grid::new(self.width, self.height, 0.0),
Grid::new(self.width, self.height, 0.0),
Grid::new(self.width, self.height, 0),
Grid::new(self.width, self.height, 0),
),
};
report(1.0, "Done");
Ok(Terrain {
sea_level: self.sea_level,
heightmap,
flow_accumulation,
flow,
rivers,
lakes,
ocean,
})
}
fn validate(&self) -> Result<(), ParamError> {
check(
self.width > 0 && self.height > 0,
"width and height",
"must be positive",
)?;
check(
self.width
.checked_mul(self.height)
.is_some_and(|cells| cells <= MAX_CELLS),
"width and height",
"must give at most 4294967295 cells (2^32 - 1)",
)?;
check(
(0.0..1.0).contains(&self.sea_level),
"sea level",
"must be in range [0,1)",
)?;
self.noise.validate()?;
if let Some(island) = &self.island {
island.validate()?;
}
if let Some(archipelago) = &self.archipelago {
archipelago.validate()?;
}
if let Some(erosion) = &self.erosion {
erosion.validate()?;
}
if let Some(breach) = &self.breach {
breach.validate()?;
}
if let Some(rivers) = &self.rivers {
rivers.validate()?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_pipeline_is_deterministic() {
let build = || {
Terrain::builder()
.size(64, 64)
.seed(11)
.erosion(ErosionParams {
droplets: Some(2000),
..Default::default()
})
.generate()
};
let a = build();
let b = build();
assert_eq!(a.heightmap, b.heightmap);
assert_eq!(a.rivers, b.rivers);
assert_eq!(a.flow, b.flow);
assert_eq!(a.flow_accumulation, b.flow_accumulation);
assert_eq!(a.ocean, b.ocean);
assert_eq!(a.lakes, b.lakes);
}
#[test]
fn different_seeds_differ() {
let a = Terrain::builder().size(64, 64).seed(1).generate();
let b = Terrain::builder().size(64, 64).seed(2).generate();
assert_ne!(a.heightmap, b.heightmap);
}
#[test]
fn rivers_are_not_on_water() {
let t = Terrain::builder()
.size(128, 128)
.seed(3)
.rivers(RiverParams::density(0.05))
.generate();
for y in 0..t.height() {
for x in 0..t.width() {
if t.is_river(x, y) {
assert!(!t.is_ocean(x, y) && !t.is_lake(x, y));
}
}
}
}
#[test]
fn lakes_are_never_ocean() {
let t = Terrain::builder().size(128, 128).seed(3).generate();
for y in 0..t.height() {
for x in 0..t.width() {
if t.is_lake(x, y) {
assert!(!t.is_ocean(x, y));
}
}
}
}
#[test]
fn ocean_reaches_the_border_but_not_the_peaks() {
let t = Terrain::builder().size(128, 128).seed(3).generate();
assert!(t.is_ocean(0, 0), "map corner should be sea");
let (mut best, mut best_v) = ((0, 0), -1.0f32);
for y in 0..t.height() {
for x in 0..t.width() {
if t.height_at(x, y) > best_v {
best_v = t.height_at(x, y);
best = (x, y);
}
}
}
assert!(!t.is_ocean(best.0, best.1));
}
#[test]
fn heightmap_only_mode_skips_hydrology() {
let t = Terrain::builder()
.size(64, 64)
.seed(5)
.no_rivers()
.generate();
assert!(t.flow.iter().all(|v| *v == 0.0));
assert!(t.flow_accumulation.iter().all(|v| *v == 0.0));
assert!(t.rivers.iter().all(|v| *v == 0));
assert!(t.lakes.iter().all(|v| *v == 0));
assert!(t.ocean.iter().any(|v| *v == 1));
let full = Terrain::builder().size(64, 64).seed(5).generate();
assert_eq!(t.heightmap, full.heightmap);
}
#[test]
fn raw_accumulation_backs_the_normalised_flow() {
let t = Terrain::builder().size(96, 96).seed(8).generate();
assert!(t.flow_accumulation.iter().all(|a| *a >= 1.0));
let idx_max_acc = (0..t.flow_accumulation.len())
.max_by(|a, b| {
t.flow_accumulation.as_slice()[*a].total_cmp(&t.flow_accumulation.as_slice()[*b])
})
.unwrap();
let idx_max_flow = (0..t.flow.len())
.max_by(|a, b| t.flow.as_slice()[*a].total_cmp(&t.flow.as_slice()[*b]))
.unwrap();
assert_eq!(idx_max_acc, idx_max_flow);
}
#[test]
fn output_stays_in_unit_range() {
let t = Terrain::builder().size(64, 64).seed(4).generate();
assert!(t.heightmap.iter().all(|v| (0.0..=1.0).contains(v)));
assert!(t.flow.iter().all(|v| (0.0..=1.0).contains(v)));
}
#[test]
fn try_generate_reports_errors_instead_of_panicking() {
let err = Terrain::builder()
.sea_level(1.5)
.try_generate()
.unwrap_err();
assert_eq!(err.param, "sea level");
assert!(err.to_string().contains("[0,1)"));
let err = Terrain::builder().size(0, 8).try_generate().unwrap_err();
assert_eq!(err.param, "width and height");
let err = Terrain::builder()
.size(16, 16)
.rivers(RiverParams::density(2.0))
.try_generate()
.unwrap_err();
assert_eq!(err.param, "river density");
assert!(Terrain::builder().size(16, 16).try_generate().is_ok());
let err = Terrain::builder()
.size(65536, 65536)
.try_generate()
.unwrap_err();
assert_eq!(err.param, "width and height");
let err = Terrain::builder()
.size(usize::MAX, 2)
.try_generate()
.unwrap_err();
assert_eq!(err.param, "width and height");
}
#[test]
fn progress_advances_to_the_end() {
let mut last = -1.0f32;
let mut saw_erosion = false;
let terrain = Terrain::builder()
.size(32, 32)
.erosion(ErosionParams {
droplets: Some(300),
..Default::default()
})
.try_generate_with_progress(|fraction, stage| {
assert!(
(0.0..=1.0).contains(&fraction),
"fraction {fraction} out of range"
);
assert!(
fraction + 1e-4 >= last,
"progress went backwards from {last} to {fraction}"
);
last = fraction;
if stage == "Erosion" {
saw_erosion = true;
}
})
.unwrap();
assert!(saw_erosion);
assert!((last - 1.0).abs() < 1e-4);
assert_eq!(terrain.width(), 32);
}
#[test]
#[should_panic(expected = "sea level")]
fn invalid_sea_level_panics() {
let _ = Terrain::builder().sea_level(1.5).generate();
}
#[test]
#[should_panic(expected = "octaves")]
fn invalid_noise_params_panic() {
let _ = Terrain::builder()
.size(16, 16)
.noise(NoiseParams {
octaves: 0,
..Default::default()
})
.generate();
}
#[test]
#[should_panic(expected = "width and height")]
fn zero_size_panics() {
let _ = Terrain::builder().size(0, 64).generate();
}
}