terrain_generator/terrain.rs
1//! The [`Terrain`] type and its builder — the main entry point of the crate.
2
3use crate::grid::Grid;
4use crate::render::{self, RenderStyle};
5use crate::{base, breach, erosion, hydrology};
6
7/// The most cells a map can have: `width * height` must not exceed it.
8///
9/// That is `65535 x 65535`, or 16 GiB for each float layer. Hydrology and
10/// basin breaching index cells with `u32`, which sets the limit.
11pub const MAX_CELLS: usize = u32::MAX as usize;
12
13/// An invalid generation parameter.
14///
15/// Returned by [`TerrainBuilder::try_generate`]; [`TerrainBuilder::generate`]
16/// panics with the same message instead.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct ParamError {
19 /// The offending parameter, e.g. `"sea level"` or `"noise octaves"`.
20 pub param: &'static str,
21 /// Human-readable description of the valid range.
22 pub message: String,
23}
24
25impl ParamError {
26 fn new(param: &'static str, message: impl Into<String>) -> Self {
27 Self {
28 param,
29 message: message.into(),
30 }
31 }
32}
33
34impl std::fmt::Display for ParamError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{} {}", self.param, self.message)
37 }
38}
39
40impl std::error::Error for ParamError {}
41
42fn check(ok: bool, param: &'static str, message: &str) -> Result<(), ParamError> {
43 if ok {
44 Ok(())
45 } else {
46 Err(ParamError::new(param, message))
47 }
48}
49
50/// Fractal (fBm) noise parameters for the base heightmap.
51#[derive(Clone, Debug)]
52pub struct NoiseParams {
53 /// Number of noise octaves. More octaves add finer detail. Default: `8`.
54 pub octaves: usize,
55 /// Number of large features across the map (resolution-independent).
56 /// Default: `2.5`.
57 pub frequency: f64,
58 /// Frequency multiplier between octaves. Default: `2.0`.
59 pub lacunarity: f64,
60 /// Amplitude multiplier between octaves, in `(0, 1)`. Default: `0.5`.
61 pub persistence: f64,
62}
63
64impl Default for NoiseParams {
65 fn default() -> Self {
66 Self {
67 octaves: 8,
68 frequency: 2.5,
69 lacunarity: 2.0,
70 persistence: 0.5,
71 }
72 }
73}
74
75impl NoiseParams {
76 fn validate(&self) -> Result<(), ParamError> {
77 check(self.octaves > 0, "noise octaves", "must be positive")?;
78 check(
79 self.frequency > 0.0 && self.frequency.is_finite(),
80 "noise frequency",
81 "must be positive and finite",
82 )?;
83 check(
84 self.lacunarity > 0.0 && self.lacunarity.is_finite(),
85 "noise lacunarity",
86 "must be positive and finite",
87 )?;
88 check(
89 self.persistence > 0.0 && self.persistence.is_finite(),
90 "noise persistence",
91 "must be positive and finite",
92 )
93 }
94}
95
96/// Radial island mask parameters.
97#[derive(Clone, Debug)]
98pub struct IslandParams {
99 /// Island diameter as a fraction of the shorter map dimension, in
100 /// `(0, ~1.2]`. Default: `1.0`.
101 pub size: f32,
102 /// Strength of the coastline perturbation noise, relative to the
103 /// radial gradient: `0` gives a perfectly round island, `1` lets the
104 /// noise carve bays, peninsulas and offshore islets. Default: `1.0`.
105 pub coast_noise: f32,
106}
107
108impl Default for IslandParams {
109 fn default() -> Self {
110 Self {
111 size: 1.0,
112 coast_noise: 1.0,
113 }
114 }
115}
116
117impl IslandParams {
118 fn validate(&self) -> Result<(), ParamError> {
119 check(
120 self.size > 0.0 && self.size.is_finite(),
121 "island size",
122 "must be positive and finite",
123 )?;
124 check(
125 self.coast_noise >= 0.0 && self.coast_noise.is_finite(),
126 "island coast noise",
127 "must be non-negative and finite",
128 )
129 }
130}
131
132/// Archipelago mask parameters: several radial islands instead of one.
133///
134/// Placement is derived from the generation seed on its own random stream,
135/// so enabling archipelago mode cannot change the output of any
136/// single-island or mask-less generation.
137///
138/// # Why radial islands rather than a noise threshold
139///
140/// A common alternative is to threshold a noise field and call whatever
141/// stays above the cut an island. That yields organic chains, but the
142/// caller gets no say in how many islands appear or how big they are — the
143/// count falls out of the noise, and small threshold changes make it jump
144/// around. Placing explicit radial falloffs keeps `islands` and `size` as
145/// real, predictable controls, and reuses the same coastline noise that
146/// shapes single islands, so an archipelago's islands look like the
147/// single-island ones rather than like a different generator. The trade-off
148/// is that neighbours can merge into one landmass when they overlap, which
149/// is why the count is approximate — see
150/// [`min_spacing`](Self::min_spacing).
151#[derive(Clone, Debug)]
152pub struct ArchipelagoParams {
153 /// How many islands to place. Default: `5`.
154 pub islands: usize,
155 /// Mean island diameter as a fraction of the shorter map dimension.
156 /// Default: `0.35`.
157 pub size: f32,
158 /// Per-island random size variation, in `[0, 1)`: `0.35` means radii
159 /// vary by ±35% around [`size`](Self::size). Default: `0.35`.
160 pub size_variation: f32,
161 /// How much of the map centres may spread over, in `[0, 1]`: `1.0`
162 /// uses the whole map, smaller values cluster islands centrally.
163 /// Default: `0.85`.
164 pub spread: f32,
165 /// Minimum distance between island centres, as a fraction of the
166 /// shorter map dimension. Best-effort — see the type docs.
167 /// Default: `0.22`.
168 pub min_spacing: f32,
169 /// Coastline perturbation strength, as in
170 /// [`IslandParams::coast_noise`]. Default: `1.0`.
171 pub coast_noise: f32,
172}
173
174impl Default for ArchipelagoParams {
175 fn default() -> Self {
176 Self {
177 islands: 5,
178 size: 0.35,
179 size_variation: 0.35,
180 spread: 0.85,
181 min_spacing: 0.22,
182 coast_noise: 1.0,
183 }
184 }
185}
186
187impl ArchipelagoParams {
188 /// An archipelago of `islands` islands, other settings default.
189 pub fn with_islands(islands: usize) -> Self {
190 Self {
191 islands,
192 ..Default::default()
193 }
194 }
195
196 fn validate(&self) -> Result<(), ParamError> {
197 check(self.islands > 0, "archipelago islands", "must be positive")?;
198 check(
199 self.size > 0.0 && self.size.is_finite(),
200 "archipelago size",
201 "must be positive and finite",
202 )?;
203 check(
204 (0.0..1.0).contains(&self.size_variation),
205 "archipelago size variation",
206 "must be in range [0,1)",
207 )?;
208 check(
209 (0.0..=1.0).contains(&self.spread),
210 "archipelago spread",
211 "must be in range [0,1]",
212 )?;
213 check(
214 self.min_spacing >= 0.0 && self.min_spacing.is_finite(),
215 "archipelago min spacing",
216 "must be non-negative and finite",
217 )?;
218 check(
219 self.coast_noise >= 0.0 && self.coast_noise.is_finite(),
220 "archipelago coast noise",
221 "must be non-negative and finite",
222 )
223 }
224}
225
226/// Hydraulic erosion parameters. The defaults are good for maps between
227/// 256² and 2048²; usually only [`droplets`](Self::droplets) needs tuning.
228#[derive(Clone, Debug)]
229pub struct ErosionParams {
230 /// Number of simulated droplets. `None` (default) uses one droplet per
231 /// cell, which is a good baseline.
232 pub droplets: Option<usize>,
233 /// Maximum steps a droplet lives. Default: `30`.
234 pub max_lifetime: usize,
235 /// How much a droplet keeps its momentum on slope changes, in `[0, 1)`.
236 /// Default: `0.05`.
237 pub inertia: f32,
238 /// Sediment capacity multiplier. Higher values carve deeper valleys.
239 /// Default: `4.0`.
240 pub capacity: f32,
241 /// Minimum sediment capacity, prevents dead droplets on flats.
242 /// Default: `0.01`.
243 pub min_capacity: f32,
244 /// Fraction of free capacity eroded per step, in `(0, 1]`. Default: `0.3`.
245 pub erode_speed: f32,
246 /// Fraction of surplus sediment deposited per step, in `(0, 1]`.
247 /// Default: `0.3`.
248 pub deposit_speed: f32,
249 /// Fraction of water lost per step, in `[0, 1)`. Default: `0.01`.
250 pub evaporate_speed: f32,
251 /// Acceleration applied on descent. Default: `4.0`.
252 pub gravity: f32,
253 /// Radius (in cells) of the erosion brush. Default: `3`.
254 pub radius: usize,
255}
256
257impl Default for ErosionParams {
258 fn default() -> Self {
259 Self {
260 droplets: None,
261 max_lifetime: 30,
262 inertia: 0.05,
263 capacity: 4.0,
264 min_capacity: 0.01,
265 erode_speed: 0.3,
266 deposit_speed: 0.3,
267 evaporate_speed: 0.01,
268 gravity: 4.0,
269 radius: 3,
270 }
271 }
272}
273
274impl ErosionParams {
275 fn validate(&self) -> Result<(), ParamError> {
276 check(
277 self.max_lifetime > 0,
278 "erosion max lifetime",
279 "must be positive",
280 )?;
281 check(
282 (0.0..1.0).contains(&self.inertia),
283 "erosion inertia",
284 "must be in range [0,1)",
285 )?;
286 check(self.capacity > 0.0, "erosion capacity", "must be positive")?;
287 check(
288 self.min_capacity > 0.0,
289 "erosion min capacity",
290 "must be positive",
291 )?;
292 check(
293 self.erode_speed > 0.0 && self.erode_speed <= 1.0,
294 "erosion erode speed",
295 "must be in range (0,1]",
296 )?;
297 check(
298 self.deposit_speed > 0.0 && self.deposit_speed <= 1.0,
299 "erosion deposit speed",
300 "must be in range (0,1]",
301 )?;
302 check(
303 (0.0..1.0).contains(&self.evaporate_speed),
304 "erosion evaporate speed",
305 "must be in range [0,1)",
306 )?;
307 check(self.gravity > 0.0, "erosion gravity", "must be positive")?;
308 check(self.radius > 0, "erosion radius", "must be positive")
309 }
310}
311
312/// How the river network is cut out of the flow accumulation map.
313///
314/// Which one to use:
315///
316/// * [`Density`](Self::Density) asks for an *area share* — "about 1% of the
317/// land should be river". It adapts to any terrain, but it does not
318/// transfer between map sizes: catchments grow with area while channels
319/// grow with length, so the same share yields a visually denser network
320/// on a small map than on a large one. Good for one-off renders.
321/// * [`MinUpstreamCells`](Self::MinUpstreamCells) is absolute — "a channel
322/// starts once 500 cells drain through". Physically meaningful and
323/// comparable across maps *of the same cell size*, so it is the right
324/// choice when cells have a fixed real-world size (a game world tiled
325/// from several maps).
326/// * [`PerEdge`](Self::PerEdge) scales the absolute threshold with the map
327/// edge, i.e. required upstream cells = `factor * max(width, height)`.
328/// Use it when a map always represents "one island", whatever its
329/// resolution, and you want the same-looking network at every resolution.
330#[derive(Clone, Copy, Debug, PartialEq)]
331pub enum RiverThreshold {
332 /// Approximate fraction of land cells covered by rivers, in `[0, 1]`.
333 Density(f32),
334 /// Minimum upstream catchment, in cells, for a cell to be a river.
335 MinUpstreamCells(f32),
336 /// Minimum upstream catchment as a multiple of the longer map edge.
337 PerEdge(f32),
338}
339
340/// River extraction parameters.
341#[derive(Clone, Debug)]
342pub struct RiverParams {
343 /// How the river network is thresholded out of flow accumulation.
344 pub threshold: RiverThreshold,
345}
346
347impl Default for RiverParams {
348 fn default() -> Self {
349 Self {
350 threshold: RiverThreshold::Density(0.01),
351 }
352 }
353}
354
355impl RiverParams {
356 /// Rivers covering approximately `density` of the land area.
357 pub fn density(density: f32) -> Self {
358 Self {
359 threshold: RiverThreshold::Density(density),
360 }
361 }
362
363 /// Rivers wherever at least `cells` of catchment drain through.
364 pub fn min_upstream_cells(cells: f32) -> Self {
365 Self {
366 threshold: RiverThreshold::MinUpstreamCells(cells),
367 }
368 }
369
370 /// Rivers wherever at least `factor * max(width, height)` cells of
371 /// catchment drain through.
372 pub fn per_edge(factor: f32) -> Self {
373 Self {
374 threshold: RiverThreshold::PerEdge(factor),
375 }
376 }
377
378 fn validate(&self) -> Result<(), ParamError> {
379 match self.threshold {
380 RiverThreshold::Density(d) => check(
381 (0.0..=1.0).contains(&d),
382 "river density",
383 "must be in range [0,1]",
384 ),
385 RiverThreshold::MinUpstreamCells(c) => check(
386 c >= 0.0 && c.is_finite(),
387 "river min upstream cells",
388 "must be non-negative and finite",
389 ),
390 RiverThreshold::PerEdge(f) => check(
391 f >= 0.0 && f.is_finite(),
392 "river per-edge factor",
393 "must be non-negative and finite",
394 ),
395 }
396 }
397}
398
399/// Basin breaching parameters. See
400/// [`TerrainBuilder::breach_basins`] for what the stage does.
401#[derive(Clone, Debug)]
402pub struct BreachParams {
403 /// Largest closed basin, as a fraction of total map cells, that may
404 /// remain a lake. Bigger basins get an outlet carved. Default: `0.005`
405 /// (0.5% of the map — on a 512² map, about 1300 cells).
406 pub max_lake_fraction: f32,
407}
408
409impl Default for BreachParams {
410 fn default() -> Self {
411 Self {
412 max_lake_fraction: 0.005,
413 }
414 }
415}
416
417impl BreachParams {
418 fn validate(&self) -> Result<(), ParamError> {
419 check(
420 (0.0..=1.0).contains(&self.max_lake_fraction),
421 "breach max lake fraction",
422 "must be in range [0,1]",
423 )
424 }
425}
426
427/// A generated terrain: elevation plus the hydrology layers derived from it.
428///
429/// All per-cell buffers are [`Grid`]s, indexed as `grid[(x, y)]`; their
430/// flat row-major storage is available through [`Grid::as_slice`] for
431/// zero-copy hand-off to FFI, GPUs or file formats.
432///
433/// The layers are independent and raw on purpose: this crate decides what
434/// the terrain *is*, not what it means. Consumers that classify water for
435/// their own rules (salinity, drinkability, navigability) should build on
436/// [`ocean`](Self::ocean), [`lakes`](Self::lakes) and
437/// [`flow_accumulation`](Self::flow_accumulation) rather than on rendered
438/// colours or on elevation thresholds.
439#[derive(Clone, Debug)]
440pub struct Terrain {
441 /// Sea level the terrain was generated with, in `[0, 1]`.
442 pub sea_level: f32,
443 /// Elevation per cell, in `[0, 1]`.
444 pub heightmap: Grid<f32>,
445 /// Raw D8 flow accumulation per cell, in cells of upstream catchment
446 /// (each cell contributes 1, including itself). Use this to size
447 /// channels — river width typically scales with the square root of
448 /// catchment area. All-zero when river extraction is disabled.
449 pub flow_accumulation: Grid<f32>,
450 /// [`flow_accumulation`](Self::flow_accumulation) log-normalised into
451 /// `[0, 1]`, which is the more useful form as a moisture proxy for
452 /// biome placement. All-zero when river extraction is disabled.
453 pub flow: Grid<f32>,
454 /// River mask per cell: `1` where a river runs, `0` elsewhere.
455 pub rivers: Grid<u8>,
456 /// Lake mask per cell: `1` where an inland depression holds standing
457 /// water, `0` elsewhere. Rivers vanish beneath lakes and continue from
458 /// their outlet. A basin whose floor lies below sea level but which is
459 /// cut off from the sea counts as a lake, not ocean.
460 pub lakes: Grid<u8>,
461 /// Ocean mask per cell: `1` where the cell is at or below sea level
462 /// *and* connected to the map border through other such cells. Always
463 /// computed, including when river extraction is disabled, because it
464 /// is a fact about the heightmap rather than a hydrology result.
465 pub ocean: Grid<u8>,
466}
467
468impl Terrain {
469 /// Starts building a terrain with default parameters.
470 pub fn builder() -> TerrainBuilder {
471 TerrainBuilder::default()
472 }
473
474 /// Map width in cells.
475 pub fn width(&self) -> usize {
476 self.heightmap.width()
477 }
478
479 /// Map height in cells.
480 pub fn height(&self) -> usize {
481 self.heightmap.height()
482 }
483
484 /// Elevation at `(x, y)`, in `[0, 1]`.
485 pub fn height_at(&self, x: usize, y: usize) -> f32 {
486 self.heightmap[(x, y)]
487 }
488
489 /// Normalised flow accumulation at `(x, y)`, in `[0, 1]`.
490 pub fn flow_at(&self, x: usize, y: usize) -> f32 {
491 self.flow[(x, y)]
492 }
493
494 /// Raw upstream catchment at `(x, y)`, in cells.
495 pub fn flow_accumulation_at(&self, x: usize, y: usize) -> f32 {
496 self.flow_accumulation[(x, y)]
497 }
498
499 /// Whether a river runs through `(x, y)`.
500 pub fn is_river(&self, x: usize, y: usize) -> bool {
501 self.rivers[(x, y)] == 1
502 }
503
504 /// Whether `(x, y)` lies in a lake.
505 pub fn is_lake(&self, x: usize, y: usize) -> bool {
506 self.lakes[(x, y)] == 1
507 }
508
509 /// Whether `(x, y)` is ocean: at or below sea level and connected to
510 /// the map border. Enclosed low ground is *not* ocean.
511 pub fn is_ocean(&self, x: usize, y: usize) -> bool {
512 self.ocean[(x, y)] == 1
513 }
514
515 /// Whether `(x, y)` is covered by water of any kind: ocean, lake or
516 /// river.
517 ///
518 /// Note this is a union of the three masks, not an elevation test — a
519 /// closed basin below sea level counts as lake, not ocean.
520 pub fn is_water(&self, x: usize, y: usize) -> bool {
521 self.is_ocean(x, y) || self.is_lake(x, y) || self.is_river(x, y)
522 }
523
524 /// Renders the terrain to a row-major RGB byte buffer
525 /// (`width * height * 3`).
526 pub fn to_rgb(&self, style: RenderStyle) -> Vec<u8> {
527 render::to_rgb(self, style)
528 }
529
530 /// Writes a single [`Layer`](crate::Layer) as a PNG file: the shaded
531 /// render, or one of the raw data layers (elevation and flow as 16-bit
532 /// greyscale, masks as 8-bit black/white).
533 ///
534 /// Requires the `png` cargo feature (enabled by default).
535 #[cfg(feature = "png")]
536 pub fn save_layer_png<P: AsRef<std::path::Path>>(
537 &self,
538 layer: crate::Layer,
539 path: P,
540 ) -> Result<(), render::RenderError> {
541 render::save_layer_png(self, layer, path)
542 }
543
544 /// Renders the terrain and writes it as a PNG file.
545 ///
546 /// Requires the `png` cargo feature (enabled by default).
547 #[cfg(feature = "png")]
548 pub fn save_png<P: AsRef<std::path::Path>>(
549 &self,
550 path: P,
551 style: RenderStyle,
552 ) -> Result<(), render::RenderError> {
553 render::save_png(self, path, style)
554 }
555}
556
557/// Builder for [`Terrain`]. Obtained via [`Terrain::builder`].
558///
559/// Island shaping, erosion and rivers are all enabled by default; disable
560/// them with [`no_island`](Self::no_island), [`no_erosion`](Self::no_erosion)
561/// and [`no_rivers`](Self::no_rivers). Basin breaching is opt-in via
562/// [`breach_basins`](Self::breach_basins).
563///
564/// ```
565/// use terrain_generator::Terrain;
566///
567/// let terrain = Terrain::builder().size(128, 128).seed(42).generate();
568/// assert_eq!(terrain.heightmap.len(), 128 * 128);
569/// ```
570///
571/// # Heightmap-only generation
572///
573/// Consumers that run their own hydrology should turn river extraction off.
574/// This skips depression filling, flow routing and river extraction
575/// entirely — it is the intended, supported path, not a degraded mode:
576///
577/// ```
578/// use terrain_generator::Terrain;
579///
580/// let terrain = Terrain::builder().size(128, 128).seed(7).no_rivers().generate();
581/// let elevation: &[f32] = terrain.heightmap.as_slice(); // row-major, [0, 1]
582/// assert!(terrain.flow.iter().all(|f| *f == 0.0)); // not computed
583/// # let _ = elevation;
584/// ```
585///
586/// The [`ocean`](Terrain::ocean) mask is still computed in that mode (it is
587/// a linear-time flood fill over the heightmap, not hydrology), so
588/// "is this cell sea or an inland basin" remains answerable.
589#[derive(Clone, Debug)]
590pub struct TerrainBuilder {
591 width: usize,
592 height: usize,
593 seed: u32,
594 sea_level: f32,
595 noise: NoiseParams,
596 island: Option<IslandParams>,
597 archipelago: Option<ArchipelagoParams>,
598 erosion: Option<ErosionParams>,
599 breach: Option<BreachParams>,
600 rivers: Option<RiverParams>,
601}
602
603impl Default for TerrainBuilder {
604 fn default() -> Self {
605 Self {
606 width: 512,
607 height: 512,
608 seed: 0,
609 sea_level: 0.08,
610 noise: NoiseParams::default(),
611 island: Some(IslandParams::default()),
612 archipelago: None,
613 erosion: Some(ErosionParams::default()),
614 breach: None,
615 rivers: Some(RiverParams::default()),
616 }
617 }
618}
619
620impl TerrainBuilder {
621 /// Sets the map dimensions in cells. Default: `512 x 512`.
622 ///
623 /// Both must be positive, and `width * height` at most
624 /// [`MAX_CELLS`].
625 pub fn size(mut self, width: usize, height: usize) -> Self {
626 self.width = width;
627 self.height = height;
628 self
629 }
630
631 /// Sets the generation seed. The same seed and parameters always
632 /// produce the same terrain. Default: `0`.
633 pub fn seed(mut self, seed: u32) -> Self {
634 self.seed = seed;
635 self
636 }
637
638 /// Sets the sea level, in `[0, 1)`. Cells at or below it are ocean if
639 /// they connect to the map border. Default: `0.08`.
640 pub fn sea_level(mut self, sea_level: f32) -> Self {
641 self.sea_level = sea_level;
642 self
643 }
644
645 /// Sets the fractal noise parameters for the base heightmap.
646 pub fn noise(mut self, noise: NoiseParams) -> Self {
647 self.noise = noise;
648 self
649 }
650
651 /// Enables single-island shaping with the given parameters (enabled
652 /// with defaults unless [`no_island`](Self::no_island) or
653 /// [`archipelago`](Self::archipelago) is called).
654 pub fn island(mut self, island: IslandParams) -> Self {
655 self.island = Some(island);
656 self.archipelago = None;
657 self
658 }
659
660 /// Shapes the land into several islands instead of one. Replaces
661 /// single-island shaping.
662 ///
663 /// Erosion and hydrology are mask-agnostic and need no changes for
664 /// this: each landmass gets its own drainage, and the sea between
665 /// islands stays sea.
666 ///
667 /// ```
668 /// use terrain_generator::{ArchipelagoParams, Terrain};
669 ///
670 /// let terrain = Terrain::builder()
671 /// .size(256, 256)
672 /// .seed(3)
673 /// .archipelago(ArchipelagoParams::with_islands(4))
674 /// .generate();
675 /// # let _ = terrain;
676 /// ```
677 pub fn archipelago(mut self, archipelago: ArchipelagoParams) -> Self {
678 self.archipelago = Some(archipelago);
679 self.island = None;
680 self
681 }
682
683 /// Disables land shaping entirely: terrain reaches the map borders.
684 pub fn no_island(mut self) -> Self {
685 self.island = None;
686 self.archipelago = None;
687 self
688 }
689
690 /// Enables hydraulic erosion with the given parameters (enabled with
691 /// defaults unless [`no_erosion`](Self::no_erosion) is called).
692 pub fn erosion(mut self, erosion: ErosionParams) -> Self {
693 self.erosion = Some(erosion);
694 self
695 }
696
697 /// Disables hydraulic erosion.
698 pub fn no_erosion(mut self) -> Self {
699 self.erosion = None;
700 self
701 }
702
703 /// Enables basin breaching between erosion and hydrology: closed
704 /// basins bigger than
705 /// [`max_lake_fraction`](BreachParams::max_lake_fraction) get an outlet
706 /// carved so they drain instead of becoming inland seas. Off by
707 /// default.
708 pub fn breach_basins(mut self, breach: BreachParams) -> Self {
709 self.breach = Some(breach);
710 self
711 }
712
713 /// Disables basin breaching (the default).
714 pub fn no_breach(mut self) -> Self {
715 self.breach = None;
716 self
717 }
718
719 /// Enables river extraction with the given parameters (enabled with
720 /// defaults unless [`no_rivers`](Self::no_rivers) is called).
721 pub fn rivers(mut self, rivers: RiverParams) -> Self {
722 self.rivers = Some(rivers);
723 self
724 }
725
726 /// Disables flow routing, river and lake extraction: the `flow`,
727 /// `flow_accumulation`, `rivers` and `lakes` layers come back all-zero
728 /// and none of that work is done. The [`ocean`](Terrain::ocean) mask is
729 /// still computed. See the type-level docs for the heightmap-only
730 /// contract.
731 pub fn no_rivers(mut self) -> Self {
732 self.rivers = None;
733 self
734 }
735
736 /// Runs the full pipeline: noise, island mask, erosion, optional basin
737 /// breaching, then flow routing.
738 ///
739 /// # Panics
740 ///
741 /// Panics with a descriptive message if any parameter is out of range:
742 /// zero dimensions, a sea level outside `[0, 1)`, non-positive noise
743 /// parameters, or erosion/river parameters outside the ranges
744 /// documented on their fields. Use
745 /// [`try_generate`](Self::try_generate) to get a [`ParamError`] instead
746 /// — preferable when the parameters come from user input.
747 pub fn generate(&self) -> Terrain {
748 match self.try_generate() {
749 Ok(terrain) => terrain,
750 Err(e) => panic!("{e}"),
751 }
752 }
753
754 /// Same as [`generate`](Self::generate) but reports invalid parameters
755 /// as a [`ParamError`] instead of panicking.
756 ///
757 /// ```
758 /// use terrain_generator::Terrain;
759 ///
760 /// let err = Terrain::builder().sea_level(1.5).try_generate().unwrap_err();
761 /// assert_eq!(err.param, "sea level");
762 /// ```
763 pub fn try_generate(&self) -> Result<Terrain, ParamError> {
764 self.try_generate_with_progress(|_, _| {})
765 }
766
767 /// Same as [`try_generate`](Self::try_generate), and reports how far the
768 /// pipeline has got. `fraction` moves from `0.0` to `1.0` and never
769 /// backwards. `stage` is a short label such as `"Erosion"` or `"Rivers"`.
770 ///
771 /// Erosion is almost all of the cost, so most of the fraction is spent
772 /// there, updated about a hundred times. The callback is not required
773 /// to be fast, but it should not do heavy work.
774 pub fn try_generate_with_progress(
775 &self,
776 mut report: impl FnMut(f32, &'static str),
777 ) -> Result<Terrain, ParamError> {
778 self.validate()?;
779
780 // Weights, not timings. Erosion dominates a normal map; the other
781 // stages are short and only need a visible slice of the bar.
782 let noise_w = 1.0;
783 let erosion_w = if self.erosion.is_some() { 20.0 } else { 0.0 };
784 let breach_w = if self.breach.is_some() { 2.0 } else { 0.0 };
785 let river_w = if self.rivers.is_some() { 2.0 } else { 0.0 };
786 let total = noise_w + erosion_w + breach_w + river_w;
787 let mut cursor = 0.0f32;
788 let mut take = |weight: f32| {
789 let start = cursor;
790 cursor += weight / total;
791 (start, cursor)
792 };
793
794 let mask = match (&self.archipelago, &self.island) {
795 (Some(arch), _) => Some(base::Mask::Archipelago(arch)),
796 (None, Some(island)) => Some(base::Mask::Island(island)),
797 (None, None) => None,
798 };
799
800 let (n0, n1) = take(noise_w);
801 report(n0, "Heightmap");
802 let mut heightmap = base::generate(self.width, self.height, self.seed, &self.noise, mask);
803 report(n1, "Heightmap");
804
805 if let Some(erosion_params) = &self.erosion {
806 let (e0, e1) = take(erosion_w);
807 erosion::erode_with_progress(
808 &mut heightmap,
809 erosion_params,
810 self.seed,
811 &mut |done, droplets| {
812 let t = if droplets == 0 {
813 1.0
814 } else {
815 done as f32 / droplets as f32
816 };
817 report(e0 + (e1 - e0) * t, "Erosion");
818 },
819 );
820 }
821
822 if let Some(breach_params) = &self.breach {
823 let (b0, b1) = take(breach_w);
824 report(b0, "Basins");
825 // breaching reshapes the coastline as little as possible, but
826 // it needs to know what is already sea
827 let ocean = hydrology::ocean_mask(&heightmap, self.sea_level);
828 breach::breach(&mut heightmap, &ocean, breach_params);
829 report(b1, "Basins");
830 }
831
832 let ocean = hydrology::ocean_mask(&heightmap, self.sea_level);
833
834 let (flow_accumulation, flow, rivers, lakes) = match &self.rivers {
835 Some(river_params) => {
836 let (r0, r1) = take(river_w);
837 report(r0, "Rivers");
838 let hydro = hydrology::compute(&heightmap, &ocean, river_params);
839 report(r1, "Rivers");
840 (hydro.accumulation, hydro.flow, hydro.rivers, hydro.lakes)
841 }
842 None => (
843 Grid::new(self.width, self.height, 0.0),
844 Grid::new(self.width, self.height, 0.0),
845 Grid::new(self.width, self.height, 0),
846 Grid::new(self.width, self.height, 0),
847 ),
848 };
849
850 report(1.0, "Done");
851
852 Ok(Terrain {
853 sea_level: self.sea_level,
854 heightmap,
855 flow_accumulation,
856 flow,
857 rivers,
858 lakes,
859 ocean,
860 })
861 }
862
863 fn validate(&self) -> Result<(), ParamError> {
864 check(
865 self.width > 0 && self.height > 0,
866 "width and height",
867 "must be positive",
868 )?;
869 // Hydrology and basin breaching index cells with u32, and breaching
870 // keeps u32::MAX free to mean "no cell". A bigger map would wrap
871 // those indices, and in a release build `width * height` itself can
872 // wrap, so either would panic mid-generation instead of failing
873 // here. The limit allows 65535 x 65535, 16 GiB per float layer.
874 check(
875 self.width
876 .checked_mul(self.height)
877 .is_some_and(|cells| cells <= MAX_CELLS),
878 "width and height",
879 "must give at most 4294967295 cells (2^32 - 1)",
880 )?;
881 check(
882 (0.0..1.0).contains(&self.sea_level),
883 "sea level",
884 "must be in range [0,1)",
885 )?;
886 self.noise.validate()?;
887 if let Some(island) = &self.island {
888 island.validate()?;
889 }
890 if let Some(archipelago) = &self.archipelago {
891 archipelago.validate()?;
892 }
893 if let Some(erosion) = &self.erosion {
894 erosion.validate()?;
895 }
896 if let Some(breach) = &self.breach {
897 breach.validate()?;
898 }
899 if let Some(rivers) = &self.rivers {
900 rivers.validate()?;
901 }
902 Ok(())
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909
910 #[test]
911 fn full_pipeline_is_deterministic() {
912 let build = || {
913 Terrain::builder()
914 .size(64, 64)
915 .seed(11)
916 .erosion(ErosionParams {
917 droplets: Some(2000),
918 ..Default::default()
919 })
920 .generate()
921 };
922 let a = build();
923 let b = build();
924 assert_eq!(a.heightmap, b.heightmap);
925 assert_eq!(a.rivers, b.rivers);
926 assert_eq!(a.flow, b.flow);
927 assert_eq!(a.flow_accumulation, b.flow_accumulation);
928 assert_eq!(a.ocean, b.ocean);
929 assert_eq!(a.lakes, b.lakes);
930 }
931
932 #[test]
933 fn different_seeds_differ() {
934 let a = Terrain::builder().size(64, 64).seed(1).generate();
935 let b = Terrain::builder().size(64, 64).seed(2).generate();
936 assert_ne!(a.heightmap, b.heightmap);
937 }
938
939 #[test]
940 fn rivers_are_not_on_water() {
941 let t = Terrain::builder()
942 .size(128, 128)
943 .seed(3)
944 .rivers(RiverParams::density(0.05))
945 .generate();
946 for y in 0..t.height() {
947 for x in 0..t.width() {
948 if t.is_river(x, y) {
949 assert!(!t.is_ocean(x, y) && !t.is_lake(x, y));
950 }
951 }
952 }
953 }
954
955 #[test]
956 fn lakes_are_never_ocean() {
957 let t = Terrain::builder().size(128, 128).seed(3).generate();
958 for y in 0..t.height() {
959 for x in 0..t.width() {
960 if t.is_lake(x, y) {
961 assert!(!t.is_ocean(x, y));
962 }
963 }
964 }
965 }
966
967 #[test]
968 fn ocean_reaches_the_border_but_not_the_peaks() {
969 let t = Terrain::builder().size(128, 128).seed(3).generate();
970 assert!(t.is_ocean(0, 0), "map corner should be sea");
971 // the highest cell is certainly not ocean
972 let (mut best, mut best_v) = ((0, 0), -1.0f32);
973 for y in 0..t.height() {
974 for x in 0..t.width() {
975 if t.height_at(x, y) > best_v {
976 best_v = t.height_at(x, y);
977 best = (x, y);
978 }
979 }
980 }
981 assert!(!t.is_ocean(best.0, best.1));
982 }
983
984 #[test]
985 fn heightmap_only_mode_skips_hydrology() {
986 let t = Terrain::builder()
987 .size(64, 64)
988 .seed(5)
989 .no_rivers()
990 .generate();
991 assert!(t.flow.iter().all(|v| *v == 0.0));
992 assert!(t.flow_accumulation.iter().all(|v| *v == 0.0));
993 assert!(t.rivers.iter().all(|v| *v == 0));
994 assert!(t.lakes.iter().all(|v| *v == 0));
995 // ocean is still available
996 assert!(t.ocean.iter().any(|v| *v == 1));
997 // and the heightmap is the same as with hydrology on
998 let full = Terrain::builder().size(64, 64).seed(5).generate();
999 assert_eq!(t.heightmap, full.heightmap);
1000 }
1001
1002 #[test]
1003 fn raw_accumulation_backs_the_normalised_flow() {
1004 let t = Terrain::builder().size(96, 96).seed(8).generate();
1005 assert!(t.flow_accumulation.iter().all(|a| *a >= 1.0));
1006 // both orderings agree: normalisation is monotonic
1007 let idx_max_acc = (0..t.flow_accumulation.len())
1008 .max_by(|a, b| {
1009 t.flow_accumulation.as_slice()[*a].total_cmp(&t.flow_accumulation.as_slice()[*b])
1010 })
1011 .unwrap();
1012 let idx_max_flow = (0..t.flow.len())
1013 .max_by(|a, b| t.flow.as_slice()[*a].total_cmp(&t.flow.as_slice()[*b]))
1014 .unwrap();
1015 assert_eq!(idx_max_acc, idx_max_flow);
1016 }
1017
1018 #[test]
1019 fn output_stays_in_unit_range() {
1020 let t = Terrain::builder().size(64, 64).seed(4).generate();
1021 assert!(t.heightmap.iter().all(|v| (0.0..=1.0).contains(v)));
1022 assert!(t.flow.iter().all(|v| (0.0..=1.0).contains(v)));
1023 }
1024
1025 #[test]
1026 fn try_generate_reports_errors_instead_of_panicking() {
1027 let err = Terrain::builder()
1028 .sea_level(1.5)
1029 .try_generate()
1030 .unwrap_err();
1031 assert_eq!(err.param, "sea level");
1032 assert!(err.to_string().contains("[0,1)"));
1033
1034 let err = Terrain::builder().size(0, 8).try_generate().unwrap_err();
1035 assert_eq!(err.param, "width and height");
1036
1037 let err = Terrain::builder()
1038 .size(16, 16)
1039 .rivers(RiverParams::density(2.0))
1040 .try_generate()
1041 .unwrap_err();
1042 assert_eq!(err.param, "river density");
1043
1044 assert!(Terrain::builder().size(16, 16).try_generate().is_ok());
1045
1046 // rejected before anything is allocated, so these cost nothing
1047 let err = Terrain::builder()
1048 .size(65536, 65536)
1049 .try_generate()
1050 .unwrap_err();
1051 assert_eq!(err.param, "width and height");
1052 let err = Terrain::builder()
1053 .size(usize::MAX, 2)
1054 .try_generate()
1055 .unwrap_err();
1056 assert_eq!(err.param, "width and height");
1057 }
1058
1059 #[test]
1060 fn progress_advances_to_the_end() {
1061 let mut last = -1.0f32;
1062 let mut saw_erosion = false;
1063 let terrain = Terrain::builder()
1064 .size(32, 32)
1065 .erosion(ErosionParams {
1066 droplets: Some(300),
1067 ..Default::default()
1068 })
1069 .try_generate_with_progress(|fraction, stage| {
1070 assert!(
1071 (0.0..=1.0).contains(&fraction),
1072 "fraction {fraction} out of range"
1073 );
1074 assert!(
1075 fraction + 1e-4 >= last,
1076 "progress went backwards from {last} to {fraction}"
1077 );
1078 last = fraction;
1079 if stage == "Erosion" {
1080 saw_erosion = true;
1081 }
1082 })
1083 .unwrap();
1084 assert!(saw_erosion);
1085 assert!((last - 1.0).abs() < 1e-4);
1086 assert_eq!(terrain.width(), 32);
1087 }
1088
1089 #[test]
1090 #[should_panic(expected = "sea level")]
1091 fn invalid_sea_level_panics() {
1092 let _ = Terrain::builder().sea_level(1.5).generate();
1093 }
1094
1095 #[test]
1096 #[should_panic(expected = "octaves")]
1097 fn invalid_noise_params_panic() {
1098 let _ = Terrain::builder()
1099 .size(16, 16)
1100 .noise(NoiseParams {
1101 octaves: 0,
1102 ..Default::default()
1103 })
1104 .generate();
1105 }
1106
1107 #[test]
1108 #[should_panic(expected = "width and height")]
1109 fn zero_size_panics() {
1110 let _ = Terrain::builder().size(0, 64).generate();
1111 }
1112}