Skip to main content

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}