Packages
Procedural terrain generator: fractal noise, hydraulic erosion and rivers (Rust NIF)
Current section
Files
Jump to
Current section
Files
terrain_generator
README.md
README.md
# Terrain generator
[](https://github.com/fabio-t/terrain-generator/actions/workflows/ci.yml)
[](https://github.com/fabio-t/terrain-generator/releases/latest)
[](https://crates.io/crates/terrain-generator)
[](https://hex.pm/packages/terrain_generator)
[](https://docs.rs/terrain-generator)
Procedural terrain generator written in Rust. It builds a fractal-noise
heightmap, shapes it into an island with noisy coastlines, carves it with
particle-based **hydraulic erosion**, and routes water over the result so
that **rivers follow the eroded valleys** and depressions become lakes.

It is usable five ways:
- as a **CLI** that renders maps to PNG,
- as a **desktop GUI** for trying settings with a live preview,
- as a **Rust library** (`cargo` dependency),
- as an **Elixir library** via a Rustler NIF (`mix` dependency),
- as a **Java library** via the Foreign Function & Memory API — and, through
the same C ABI, from C, C++, C# or anything else with an FFI.
Generation is fully deterministic: the same seed and parameters always
produce the same terrain, from every entry point. It is single-threaded
and erosion dominates the cost: on a typical desktop core, expect roughly
a second for a 512×512 map and a few seconds for 1024×1024. Lower
`--droplets` for faster previews.
## Pipeline
1. **Base heightmap** — fractal (fBm) OpenSimplex noise from the
[`noise`](https://crates.io/crates/noise) crate.
2. **Land mask** — a single radial island, or an archipelago of several,
perturbed by a secondary noise field for natural coastlines.
3. **Hydraulic erosion** — droplet simulation (Beyer 2015): rain droplets
roll downhill, pick up sediment and deposit it on flats, carving
valleys and drainage networks.
4. **Basin breaching** *(opt-in)* — cuts outlets through the rims of
oversized closed basins so they drain instead of becoming inland seas.
5. **Hydrology** — priority-flood depression filling (Barnes et al. 2014)
plus D8 flow accumulation; high-flow cells become rivers, filled
depressions become lakes, and border-connected sea becomes the ocean
mask.
The output is not just an image: you get elevation, raw flow accumulation
(upstream catchment per cell), and river, lake and ocean masks as separate
raw layers for use in games or simulations.
## Installing the CLI
### Prebuilt binaries
Grab an archive for your platform from the
[latest release](https://github.com/fabio-t/terrain-generator/releases/latest):
| Platform | File |
|----------|------|
| Linux x86_64 (static, runs on any distro) | `terrain-generator-<tag>-x86_64-unknown-linux-musl.tar.gz` |
| macOS Apple Silicon | `terrain-generator-<tag>-aarch64-apple-darwin.tar.gz` |
| macOS Intel | `terrain-generator-<tag>-x86_64-apple-darwin.tar.gz` |
| Windows x86_64 | `terrain-generator-<tag>-x86_64-pc-windows-msvc.zip` |
Unpack it and run the `terrain-generator` binary inside — there are no
other files to install. `SHA256SUMS.txt` on the release page has the
checksums.
### With cargo
Requires a [Rust toolchain](https://rustup.rs) (1.91 or newer):
```sh
cargo install terrain-generator
```
Add `--git https://github.com/fabio-t/terrain-generator` for the
unreleased state of `main`.
### From a clone
```sh
git clone https://github.com/fabio-t/terrain-generator
cd terrain-generator
cargo run --release # generates map0.png in the current directory
```
## Using the CLI
```sh
terrain-generator # one 512x512 map, seed 0 -> map0.png
terrain-generator -s 10 -n 5 # five maps, seeds 10..14
terrain-generator -W 1024 -H 1024 # bigger map
terrain-generator --rivers 0.02 --island 0.7 --droplets 2000000
terrain-generator --grey -o heightmaps/ # greyscale heightmaps into a directory
terrain-generator --help # all options
```
Every pipeline stage can be tuned (`--octaves`, `--frequency`, `--island`,
`--coast-noise`, `--droplets`, `--rivers`, `--sea-level`, ...) or disabled
(`--no-island`, `--no-erosion`, `--no-rivers`). The same seed with the
same options always reproduces the same map.
## What the options do
Every map below uses **seed 1** at 512×512; each row changes a single
option against the defaults. The same knobs exist under the same names in
the Rust API (`IslandParams`, `ErosionParams`, ...) and the Elixir API
(`:island`, `:coast_noise`, `:erosion`, `:rivers`, `:sea_level`, ...).
### Island shape — `--coast-noise`
How strongly noise perturbs the radial island falloff: `0` is a perfectly
round island, `1` (the default) carves bays, peninsulas and offshore
islets.
<table><tr>
<td align="center"><img src="docs/options/coast-noise-0.png" width="240"><br><code>--coast-noise 0</code></td>
<td align="center"><img src="docs/options/coast-noise-05.png" width="240"><br><code>--coast-noise 0.5</code></td>
<td align="center"><img src="docs/options/default.png" width="240"><br><code>--coast-noise 1</code> (default)</td>
</tr></table>
### Island size — `--island` / `--no-island`
Island diameter as a fraction of the shorter map dimension, or no island
mask at all (terrain runs to the borders — useful for tiling a larger
world out of maps).
<table><tr>
<td align="center"><img src="docs/options/island-07.png" width="240"><br><code>--island 0.7</code></td>
<td align="center"><img src="docs/options/default.png" width="240"><br><code>--island 1.0</code> (default)</td>
<td align="center"><img src="docs/options/no-island.png" width="240"><br><code>--no-island</code></td>
</tr></table>
### Erosion — `--no-erosion` / `--droplets`
Without erosion the raw fractal terrain is smooth and blobby. The default
simulates one droplet per cell; more droplets keep carving — valleys
deepen, sediment builds plains, coastlines erode.
<table><tr>
<td align="center"><img src="docs/options/no-erosion.png" width="240"><br><code>--no-erosion</code></td>
<td align="center"><img src="docs/options/default.png" width="240"><br>default (262k droplets)</td>
<td align="center"><img src="docs/options/droplets-1m.png" width="240"><br><code>--droplets 1000000</code></td>
</tr></table>
### River density — `--rivers`
The approximate fraction of land covered by rivers (extracted from flow
accumulation, so they always follow the valleys). Shown as 1:1 crops so
the individual rivers are visible.
<table><tr>
<td align="center"><img src="docs/options/rivers-0003.png" width="240"><br><code>--rivers 0.003</code></td>
<td align="center"><img src="docs/options/rivers-001.png" width="240"><br><code>--rivers 0.01</code> (default)</td>
<td align="center"><img src="docs/options/rivers-004.png" width="240"><br><code>--rivers 0.04</code></td>
</tr></table>
A share of the land gives a different river network at every map size.
Two thresholds hold steady instead, and either replaces `--rivers`:
`--river-min-upstream N` makes a river wherever at least `N` cells drain
through, for when a cell has a fixed real-world size, and
`--river-per-edge K` wherever the catchment reaches `K` times the longer
map edge, for a same-looking network at every resolution.
### Sea level — `--sea-level`
Cells at or below this elevation are ocean. Raising it drowns the
lowlands and turns valley systems into fjord-like inlets.
<table><tr>
<td align="center"><img src="docs/options/sea-level-003.png" width="240"><br><code>--sea-level 0.03</code></td>
<td align="center"><img src="docs/options/default.png" width="240"><br><code>--sea-level 0.08</code> (default)</td>
<td align="center"><img src="docs/options/sea-level-02.png" width="240"><br><code>--sea-level 0.2</code></td>
</tr></table>
### Base noise — `--frequency` / `--octaves`
`--frequency` is the number of large features across the map (defaults to
`2.5`): lower gives one big landmass, higher gives busier, more broken
terrain. `--octaves` (default `8`) controls fine detail; fewer octaves
give smoother, softer relief. Between octaves, `--lacunarity` (default
`2`) multiplies the frequency and `--persistence` (default `0.5`) the
amplitude: a higher persistence keeps more of the fine detail.
<table><tr>
<td align="center"><img src="docs/options/frequency-15.png" width="240"><br><code>--frequency 1.5</code></td>
<td align="center"><img src="docs/options/frequency-4.png" width="240"><br><code>--frequency 4</code></td>
<td align="center"><img src="docs/options/octaves-4.png" width="240"><br><code>--octaves 4</code></td>
</tr></table>
### Archipelagos — `--archipelago`
`--archipelago N` places N islands instead of one, with `--islands-size`,
`--islands-spacing`, `--islands-spread` and `--islands-variation` to shape
the group. Placement comes from the seed, so it is reproducible; the count
is approximate, because neighbours can merge and coastline noise can split
an island in two. Erosion and rivers work per landmass.
<table><tr>
<td align="center"><img src="docs/options/default.png" width="240"><br>single island (default)</td>
<td align="center"><img src="docs/options/archipelago-5.png" width="240"><br><code>--archipelago 5</code></td>
<td align="center"><img src="docs/options/archipelago-9.png" width="240"><br><code>--archipelago 9 --islands-size 0.22</code></td>
</tr></table>
### Draining big basins — `--breach-basins`
Droplet erosion fills pits but cannot cut through a high rim, so large
interior depressions survive and become inland seas. `--breach-basins F`
carves an outlet from every closed basin bigger than fraction `F` of the
map, along the path where water would wear through first, leaving valleys
instead of lakes.
<table><tr>
<td align="center"><img src="docs/options/breach-off.png" width="240"><br>default (basins stay lakes)</td>
<td align="center"><img src="docs/options/breach-on.png" width="240"><br><code>--breach-basins 0.002</code></td>
</tr></table>
### Output style — `--grey`
The shaded render is for looking at; the greyscale render is the raw
heightmap (black = 0, white = 1), ready to import into game engines and
other tools.
<table><tr>
<td align="center"><img src="docs/options/default.png" width="240"><br>default (shaded)</td>
<td align="center"><img src="docs/options/greyscale.png" width="240"><br><code>--grey</code></td>
</tr></table>
## Getting the data out — `--layers`
One generation can emit every layer as its own file, for feeding into
other tools:
```sh
terrain-generator --layers height,flow,rivers,lakes,ocean -o out/
```
This writes `map0_height.png`, `map0_flow.png` and so on. Elevation and
flow are written as **16-bit** greyscale so nothing is lost to
quantisation (8 bits would terrace the elevation); the masks are 8-bit
black/white. `shaded` is also available as a layer name, and remains the
default when `--layers` is not given.
## Desktop GUI

```sh
cargo run --release --manifest-path gui/Cargo.toml
```
Every CLI generation option as a control, a preview that shows the height
and water type of the cell under the pointer, and export of the picture,
the 16-bit heightmap or all layers. **Generate** builds the map on a
background thread; the *CLI command* panel gives the `terrain-generator`
invocation for the current settings, and its exports match the CLI's
byte for byte. Use `--release`: erosion is many times slower in a debug
build.
On Linux the file dialogs go through the desktop portal
(`xdg-desktop-portal`), which every desktop session runs.
Contributed by [@romangrigorii](https://github.com/romangrigorii) in
[#8](https://github.com/fabio-t/terrain-generator/pull/8).
## Rust library
```toml
[dependencies]
# add features = ["png"] if you want PNG file output; without it the
# library's only dependency is the `noise` crate
terrain-generator = { version = "0.3", default-features = false }
```
API documentation is on [docs.rs](https://docs.rs/terrain-generator).
```rust
use terrain_generator::{RenderStyle, Terrain};
let terrain = Terrain::builder().size(512, 512).seed(42).generate();
let elevation = terrain.heightmap[(100, 100)]; // [0, 1]
let moisture = terrain.flow_at(100, 100); // [0, 1], from flow accumulation
let river = terrain.is_river(100, 100);
let lake = terrain.is_lake(100, 100);
// with the `png` feature:
terrain.save_png("map.png", RenderStyle::Shaded)?;
```
Consumers who run their own hydrology can call `.no_rivers()`: flow
routing is skipped entirely and `heightmap` is the single source of truth.
The `ocean` mask is still computed, so "sea or inland basin?" stays
answerable without it.
Full reference: [docs/rust_api.md](docs/rust_api.md), or `cargo doc --open`.
### Determinism and stored worlds
The same seed and parameters always produce the same terrain. If you
persist worlds **as seeds** and regenerate them on load, pin an exact version
of this crate and commit your `Cargo.lock`. Any change to the output is a
breaking change for your saves. The output also depends on the `noise`
crate, which this crate pins to an exact version, so `noise` changes
only when this crate does.
One exception to cross-platform bit-exactness: the normalised `flow` layer
is computed with `f32::ln`, whose last-bit rounding can differ between
platforms. `heightmap`, `flow_accumulation`, `rivers`, `lakes` and `ocean`
use only IEEE-exact operations and are identical everywhere.
## Elixir library
A [Hex package](https://hex.pm/packages/terrain_generator) wraps the same
Rust core through [Rustler](https://hexdocs.pm/rustler). The NIF compiles
from source when you fetch it, so Erlang/OTP 24+, Elixir 1.15+ and Rust
1.91+ (required by Rustler 0.38) are needed at compile time.
```elixir
def deps do
[{:terrain_generator, "~> 0.3"}]
end
```
```elixir
{:ok, terrain} = TerrainGenerator.generate(width: 512, height: 512, seed: 42)
TerrainGenerator.Terrain.height_at(terrain, 100, 100)
#=> 0.53
:ok = TerrainGenerator.render_png("map.png", seed: 42)
```
Heightmap, flow, river and lake buffers are returned as plain binaries
(32-bit floats / bytes), cheap to pass around the BEAM. The NIF runs on a
dirty CPU scheduler, so generation never blocks your schedulers.
Full reference: [HexDocs](https://hexdocs.pm/terrain_generator), or
[docs/elixir_api.md](docs/elixir_api.md).
## Java library
The generator exposes a plain **C ABI**, which Java 22+ binds directly
through the [Foreign Function & Memory API][ffm] — no JNI shim, and no
Java-specific code inside the Rust library. The Java sources have no
dependencies, so they drop into Maven, Gradle or a plain classpath.
### Using the released jar
Every release attaches `terrain-generator-<version>.jar`, which carries a
native library for each supported platform inside it:
| | |
|---|---|
| Linux | x86-64 (glibc 2.35+) |
| macOS | Apple Silicon and Intel |
| Windows | x86-64 |
Put it on the classpath and you are done — **no Rust toolchain, nothing to
install, nothing to build**:
```sh
java --enable-native-access=ALL-UNNAMED -cp terrain-generator-0.3.0.jar:. YourApp
```
The binding loads its native library from the first of these that works:
1. the path in the `tergen.library` system property, if set;
2. the default library search path;
3. the copy bundled in the jar for this platform, extracted to a
temporary file.
So a locally built library still wins while you are working on the Rust
side, and everyone else gets step 3 without knowing it exists. The jar is
compiled for Java 22, and mismatched jar/native combinations are caught at
class-init by an ABI-version check rather than by misreading memory.
### Building from source
```sh
./java/build.sh # builds the native library, packs the jar, runs the binding tests
```
That runs the tests twice: once against the freshly built library and once
against the jar alone, so the extraction path is covered too.
```java
import com.github.fabioticconi.tergen.*;
TerrainConfig config = TerrainConfig.defaults().size(512, 512).seed(42);
try (Terrain terrain = TerrainGenerator.generate(config)) {
float elevation = terrain.heightAt(100, 100);
boolean river = terrain.isRiver(100, 100);
terrain.savePng(Layer.SHADED, Path.of("map.png"));
}
```
Layers can be copied into Java arrays or borrowed as a zero-copy
`MemorySegment`. The binding tests assert that a map generated from Java
hashes to the same checksum as the Rust test suite pins, so the three
language bindings are known to produce identical terrain.
Full reference: [docs/java_api.md](docs/java_api.md). The C interface
itself is [`include/terrain_generator.h`](include/terrain_generator.h), if
you would rather use `jextract` or call it from C/C++.
[ffm]: https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html
## Development
```sh
cargo test --workspace # Rust unit, integration and doc tests
cargo clippy --workspace # lints
mix test # Elixir tests (needs Elixir + Rust)
./java/build.sh # Java jar + binding tests (needs JDK 22+ and Rust)
```
CI (formatting, clippy, both test suites) runs automatically on pushes
to `main`, and on demand for any other branch via **Run workflow** on
the [Actions tab](https://github.com/fabio-t/terrain-generator/actions/workflows/ci.yml)
— pull requests are not built automatically. Releases
are cut by pushing a tag: `v1.2.3` (or `1.2.3`) publishes a release with
CLI binaries for Linux, macOS and Windows attached, plus the Java jar with
every platform's native library inside it, and release notes taken from
that version's CHANGELOG section; a pre-release tag like `v1.2.3-rc1`
publishes a GitHub pre-release.
## License
Apache-2.0