Donjourno is a procedural terrain generator and renderer with WASM target support. It uses a layered architecture with a variadic template Pipeline that provides type-safe, compile-time composition of generation stages with lazy evaluation utilizing C++23 features (concepts, fold expressions, and variadic templates). The renderer supports real-time isometric 3D visualization with dynamic lighting, sub-grid tessellation, and procedural mesh texturing.
The Grid is the central data structure representing the terrain. Each Cell contains:
height: Elevation value (0.0 at sea level, scaled by peak height).roughness: Surface roughness generated by thermal erosion and ridge formation. Controls the amplitude of procedural mesh jitter during rendering.forest_mask: Probability or density mask for vegetation placement.terrain: Terrain classification (Grass,Mountain,Tree,Water).r,g,b: Surface color computed by the texturing pass.
A Layer is any struct that satisfies the GeneratorLayer concept. This requires two methods:
apply(Grid& grid): Performs the transformation.get_name(): Returns a string identifier for debugging/logging.
The Pipeline is a variadic template that composes multiple layers. It uses C++17 fold expressions and std::apply to iterate over layers at compile-time, ensuring zero-cost abstraction and short-circuiting error handling.
The terrain is generated through a multi-stage pipeline executed in order:
HybridTerrain → ForestMask → ForestPlacement → TerrainTexture
A coverage-controlled terrain layer that combines a noise-based allocation mask with Newtonian hydraulic erosion. Mountain regions emerge in organic, random shapes determined by the coverage percentage, while remaining terrain stays flat and walkable — designed for procedural cities, roads, and grid-based tabletop RPG play.
The implementation follows the Newtonian droplet model from SimpleErosion.
A low-frequency 2D noise field determines which parts of the map become mountainous. The mask uses 4-octave fbm_2d at a coarse scale (
To allocate exactly the desired coverage percentage of cells, all noise values are sorted and a threshold
Cells where
A 6-octave fBm heightmap is generated only inside the mask using a hash-based value noise function (zero dependencies):
where the edge blend
The noise scale
Water droplets spawn only within mask cells and are simulated with Newtonian physics. Each droplet has:
-
Position
$(p_x, p_y)$ — grid coordinates -
Velocity
$(s_x, s_y)$ — 2D speed vector -
Volume
$V$ — water quantity, decreasing via evaporation -
Sediment
$S$ — carried material
At each timestep
1. Surface normal acceleration. The droplet is accelerated by the terrain's surface normal, weighted by its mass:
where
2. Friction damping. Speed is reduced each step:
3. Unified erosion/deposition. Sediment capacity is proportional to speed, volume, and height differential:
Both erosion and deposition use a single formula that approaches equilibrium:
When
4. Boundary kill. Droplets that leave the coverage mask are immediately killed, preventing erosion from spilling into flat terrain.
5. Evaporation. Volume decreases each step:
The droplet dies when
Default parameters (tuned from SimpleErosion):
| Parameter | Default | Description |
|---|---|---|
dt |
1.2 | Integration timestep |
friction |
0.05 | Speed loss factor per step |
density |
1.0 | Droplet density (mass = V × ρ) |
depositionRate |
0.1 | Rate of approach to equilibrium sediment |
evaporationRate |
0.01 | Volume loss per step |
minVolume |
0.01 | Volume below which droplet dies |
Cells within the mask with height > 0.1 are classified as Mountain; everything else is Grass. Roughness is derived from cumulative erosion activity at each cell, normalized and scaled by elevation. Flat terrain outside the coverage mask has zero height and zero roughness.
The ForestMaskGenerator computes a fertility mask based on elevation, excluding mountain terrain entirely (forest_mask = 0 for all Mountain cells). For grass cells, the mask decays with height:
The ForestPlacementGenerator then stochastically places trees where random() < mask × density.
The TerrainTextureGenerator uses relative height thresholds (computed from the actual max height) so coloring adapts to any terrain configuration:
| Condition | Colour | RGB |
|---|---|---|
|
Mountain — flat, |
Snow | (240, 240, 250) |
| Mountain — steep slope | Grey rock | (80–130, grey) |
|
Mountain — flat, |
Brown rock | (110, 95, 75) |
| Mountain — flat, low altitude | Highland grass | (90, 130, 80) |
| Tree | Dark green | (30, 100, 30) |
|
Grass — steep ( |
Dirt | (139, 115, 85) |
| Grass — flat | Green | (60, 140, 60) |
Steepness is computed per-cell using central differences. Grey rock colour is altitude-dependent — darker at lower elevations, lighter near peaks.
Each grid cell is subdivided into
Each sub-vertex receives procedural height jitter driven by erosion-derived roughness and elevation:
Per-triangle normals are dot-producted with a sun direction vector (controlled by "Time of Day", 6:00–18:00):
All tessellation, noise, normal, and lighting calculations are cached in std::vector<CachedTriangle>. The cache is invalidated only when UI parameters change, reducing per-frame cost to a simple DrawTriangle iteration.
The project uses a recursive Make system. It requires a Clang toolchain (LLVM 18+) for native builds, and Emscripten for WebAssembly.
Native Targets:
make clean
make cli # Builds bin/Donjourno-cli
make gui # Builds bin/Donjourno-gui
make # Builds both cli and guiWebAssembly Target:
make setup-wasm # Bootstraps the local Emscripten sandbox and Python 3.11
make wasm # Builds the WebAssembly app to build-wasm/Native:
make run # Runs the GUI app
./bin/Donjourno-cli # Runs the CLI appWeb:
make serve # Starts a local server on port 8080 to host the Wasm buildauto pipeline = Pipeline(
HybridTerrainGenerator{}, // Coverage mask + fBm + droplet erosion
ForestMaskGenerator{0.6f, 2.0f}, // Create fertility mask
ForestPlacementGenerator{0.4f}, // Stochastic planting
TerrainTextureGenerator{} // Assign colours by biome
);
pipeline.execute(grid);


