worldgen

git clone https://git.godosa.eu/worldgen

master

raw · 17683 bytes

worldgen

Status: early, working; used to build full planets, interfaces may still change.

Generates a whole planet — plate tectonics, mountains, erosion, climate, rivers, lakes, ice, biomes, ores, sea floor — on a hexagonal grid (H3). You steer it with a rough sketch of where land goes plus a few config files; physics fills in the rest. Look at the result with worldmap-viewer (a 3D globe in the browser you can fly down to ground level) or the preview PNGs it writes.

Everything runs locally.

Requirements

  • Python ≥ 3.11, pip install -r requirements.txt (numpy, scipy, pillow, h3)
  • Linux or macOS (Windows: use WSL). Multi-core CPU helps a lot.
  • RAM: ~2 GB for quick builds (res 3–4); res 5 on a big planet wants 16–30 GB.

Quick start

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp -r example ../myworld && cd ../myworld         # a world is just a folder
../worldgen/.venv/bin/python ../worldgen/mapgen.py new-world --seed 42   # random continents + plates + an example era
../worldgen/.venv/bin/python ../worldgen/mapgen.py build                 # quick build (res 3, ~1–3 min)

mapgen.py works on the current folder, or pass --world DIR. Don't like the world? new-world --seed 43 --force and build again. Like the shape but want to change it? Edit the files below and rebuild.

How it works

sketch/*.png ─┐
masks/*.png  ─┼─► grid → sketch → plates → crust → elevation → erosion → climate → hydrology
config/*.toml ┘      → seabed → environment → ice → fields → render   ──►  out/r<res>/  ──► worldmap-viewer
                                                                     └─► eras (config/eras.toml) → out/r<res>/eras/
  1. Sketch — where land roughly is. Warped a bit so it doesn't look hand-drawn.
  2. Plates grow from seed points; their motions decide where they collide (mountains, trenches, volcanic arcs), pull apart (rifts, mid-ocean ridges) or slide past.
  3. Crust/elevation — continental vs oceanic crust, orogens, shelves, ridges, hotspot chains; sea level is solved so land_fraction of the planet is dry.
  4. Erosion carves valleys and fills basins.
  5. Climate — insolation from tilt, wind belts, rain shadows, monsoons → temperature and rainfall per season. Ocean (inside climate): the annual-mean wind drives surface currents (gyres, fast western boundary currents, circumpolar currents where a passage is open); the currents carry heat, which sets the sea-surface temperature and warms or cools the coasts downstream; Ekman upwelling and a sea productivity index come from the same winds.
  6. Hydrology — rivers, lakes, salt flats. Ice — ice sheets and sea ice. Environment — biomes (Holdridge), landforms, ground, lithology, ores. Seabed — sediments, vents, trenches.
  7. Render — preview PNGs in previews/r<res>/ and viewer textures in out/.

Same seed + same inputs = same world. Builds are cached by stage; changing a file reruns only what depends on it.

Commands

Command What
new-world --seed N [--continents N] [--land F] [--toward LAT LON] [--mountains T] [--force] random sketch/, config/tectonics.toml, config/eras.toml
build [--final] [--res N] [--from STAGE] [--stop STAGE] build the world (res_dev, or res_final with --final), then every era
era <name> [--final] rebuild just one era
check [--final] sanity report (land share, rain belts, rain shadows, …)
compare --old <cells.npz> how a build differs from an earlier one

Regional refinement, sea-floor previews and terrain export for game engines live in worldmap-viewer (mapview.py).

Resolution and time

H3 resolution sets detail. Cell counts are the same on any planet; a bigger radius means bigger cells.

res cells cell size (Earth radius) build time*
3 41 k ~110 km ~2–3 min
4 288 k ~42 km ~5–10 min
5 2.0 M ~16 km ~30 min+, lots of RAM

*16 cores. Iterate at res 3, build --final once you like it. The viewer adds procedural detail below build resolution (clearly marked "procedural detail — not data"), and regions add real ≈ 5 km detail where you want it.

Low memory. build --low-memory / era --low-memory (or WORLDGEN_LOW_MEMORY=1) renders the big rasters a band of rows at a time: slower, a lower memory peak, and byte-identical output. The cache does not depend on the mode. Every build also writes each viewer layer as soon as it is coloured, and samples projections straight from the 8-bit image, rather than holding ~30 full-size images and float copies in memory. The ocean-current solve (a direct sparse solve, ≈ 1.3 GB at res 4; res 5 solves on the res-4 grid) is unchanged in both modes.

Configuring for the world you want

All in config/. Change, rebuild, look. Unknown keys are errors (typo guard).

world.toml — the planet

[planet] (required)

Key Effect
radius_km size. Earth 6371. Changes cell size, distances, horizon
gravity_g surface gravity; also a viewer field
day_hours, year_days sky panel: day length, seasons
tilt_deg axial tilt → seasons, tropics, polar circles. 0 = no seasons; 45+ = wild seasons
sea_level_pressure_bar, scale_height_km, o2_fraction air: pressure falls with height; O₂ partial pressure

[build] (required): seed, res_dev, res_final, raster_width, dev_raster_width, preview_width, land_fraction (0.29 Earth; 0.5 = land world; 0.1 = ocean world with islands).

[render] (optional): name (shown by the viewer), style (preview palette: default, tidal-lock, salt-mirror). [units] (optional): in-world units span_m, league_km, moment_s (metadata for the viewer).

Optional tuning sections ([climate], [elevation], [erosion], [hydrology], …) — see Tuning below.

sketch/ — where the land is

Five equirectangular PNGs, 2:1 (e.g. 2000 × 1000), white = yes, black = no. Left edge = 180° W, top = 90° N.

File Meaning
land.png continents. The main control of the map's look
mountains.png where you want mountain ranges (added on top of tectonic ones)
desert.png, rainforest.png, trench.png reserved hints; may be all black

Draw them in any paint program (GIMP, Krita, Photoshop), or start from new-world and edit. [sketch] in world.toml: warp_km (how loosely the drawing is followed; 0 = exactly, default 1000), warp_freq, detail_warp_km, detail_warp_freq, and moves to rotate a whole drawn landmass elsewhere without redrawing:

[sketch]
moves = [ { at = [10.0, 20.0], to = [25.0, 20.0] } ]   # the landmass under 10N 20E moves to 25N 20E

If you move continents, move their plate seeds too.

masks/ — painted overrides (optional)

Grayscale PNGs, any 2:1 size, mid-grey = no change. See masks/README.md: land_hint, mountain_hint, o2_zones, gravity_zones, lock (coast follows land_hint exactly).

tectonics.toml — plates and features

This is where mountains come from. Each plate:

[[plate]]
id = "big-east"
seed = [20.0, 60.0]           # [lat, lon] it grows from
kind = "continental"          # or "oceanic"
motion = [270.0, 4.0]         # [azimuth° clockwise from north, speed cm/yr (0–20)]
  • Put a continental plate under each continent (one seed near its middle). A big continent split into two continental plates moving toward each other gets a Himalaya-style belt along the seam.
  • Oceanic plates fill the oceans (10–15 total plates looks Earth-like).
  • An oceanic plate moving into a continent → coastal mountains + deep trench offshore (Andes).
  • Plates moving apart → rift valleys on land, mid-ocean ridges at sea.
  • Faster plates = sharper, higher boundaries. 2–5 continental, 3–8 oceanic is Earth-like.
  • [plates] land_cost (0.25): lower lets continents split across plates more easily.

Optional features (any number of each):

[[volcano]]                   # a single big volcano
name = "big-shield"
center = [5.0, -120.0]
radius_km = 250.0
height_m = 6000.0
scar_azimuths = [200.0]       # optional: flank collapses

[[hotspot]]                   # an island chain (Hawaii)
name = "chain"
center = [-10.0, 150.0]
length_km = 2000.0

[[lip]]                       # a flood-basalt province (old lava plateau)
name = "traps"
center = [40.0, 30.0]
radius_km = 900.0

[[microcontinent]]            # a sliver of continental crust at sea
name = "sliver"
center = [-30.0, 70.0]
radius_km = 300.0

[[plateau]]                   # sunken plateau (Kerguelen-type); ≥ 2500 km apart
name = "deep-plateau"
center = [-40.0, -20.0]
area_km2 = 2.0e6
elongation = 1.5              # optional, 1–10
azimuth_deg = 30.0            # optional
top_m = [1500.0, 2500.0]      # depth of its top below sea level [shallowest, deepest]
islands = true                # optional: a few volcanic peaks break the surface

[[land_patch]]                # force land in a disc (strength 0–2, edge_noise 0–1)
name = "extra-land"
center = [0.0, 0.0]
radius_km = 800.0
edge_noise = 0.4

[[zone]]                      # gentle regional O₂ / gravity change in the base world
name = "thin-air"
field = "o2"                  # or "gravity"
center = [30.0, 90.0]
radius_km = 2000.0
v = -0.5                      # −1..1: O₂ × (1 + 0.5 v), gravity × (1 + 0.7 v)

Low-gravity zones grow taller, spikier relief ("spires").

eras.toml — the same world at different times (optional)

Eras are versions of the world changed by local events. Only the areas around events are recomputed (climate, rivers, ice, … follow). The viewer's era menu switches between them.

[[event]]
name = "impact"
kind = "disintegrate"         # a crater/basin: land sinks
center = [20.0, 40.0]
radius_km = 600.0
depth_m = 3000.0

[[event]]
name = "new-volcano"
kind = "volcano"
center = [0.0, 10.0]
radius_km = 80.0
peak_m = 4000.0
shape = "cone"                # cone | shield | caldera
peak_mode = "above"           # height above the ground, or "absolute"

[[event]]
name = "strange-air"
kind = "zone"                 # change fields in an area
shape = "circle"              # or "landmass" with seed = [lat, lon], reach_km = [on land, out to sea]
center = [20.0, 40.0]
radius_km = 1500.0
profile = "smooth"            # or edge_km = 5.0 for a sharp border
fields = { gravity_g = 0.5, pressure_bar = 1.5, o2_fraction = 0.3, fire_reactivity = 0.6 }

[eras]
order = ["ancient", "modern"]  # oldest first; each era includes the events of the eras before it
default = "modern"             # what the viewer opens

[eras.ancient]
label = "Ancient times"
events = []                   # no events = the base world

[eras.modern]
label = "Modern times"
events = ["impact", "new-volcano", "strange-air"]
years = 5000                  # time since the era before (erosion/weathering)

[eras] in world.toml: mask_km (how far around an event things are recomputed, 1500), climate_blend_km, erosion_steps. Editing eras.toml never rebuilds the base world.

Tuning

Any key below goes in world.toml under its section; omit it to keep the default.

Section Useful keys (default)
[climate] t_a (−55.2): global temperature offset in °C — +5 = hot world, −8 = ice age. hadley_edge_deg (20) / ferrel_edge_deg (55): desert belt and westerlies latitudes. global_mean_mm (1000): overall rainfall. oro_rate (20): rain shadow strength. monsoon_k (1). current_c (4): old rule-of-thumb current warming/cooling °C (used only with [ocean] enabled = false). lapse_c_per_km (6.5): cooling with height. land_seasonal (0.45), ocean_seasonal (0.15): season swing
[elevation] continental_base_m (400), continental_noise_m (350): lowland height and roughness. coast_noise_m (900): coastline ruggedness. trench_depth_m (4000). abyss_m (6500): ocean depth. hint_m (2500): height from mountains.png. spire_m (2500): low-gravity spires
[crust] shelf_km (450): continental shelf width. orogen_width_km (700): mountain belt width. rift_width_km (200). edge_noise (0.5): coast fractalness
[erosion] steps (12), k (0.02): more = deeper valleys, lower mountains. hillslope_km (25). sediment_fill (0.15)
[hydrology] river_min_km3_yr (2): lower = more rivers drawn. lake_depth_k (12). salt_flat_max_p_mm (300)
[ice] melt_summer_c (0): higher = more ice. sea_ice_t_c (−1.8). sheet_max_m (3000)
[plates] land_cost (0.25), noise (0.35): plate boundary wiggle
[seabed] vent_field (0.8), sulfides (0.7), carbonate_depth_m (4500)
[ocean] enabled (true): currents, SST, upwelling, productivity (false = the old rule-of-thumb climate, all ocean fields zero). friction_days (5): 1/friction — longer = narrower, faster western boundary currents (width ≈ friction/β, never under one cell). stress_k (1): wind-stress multiplier — scales all current speeds. layer_m (150): depth of the wind-driven layer. land_friction (1000): how still land is. direct_max_cells (500000): bigger grids solve currents one resolution coarser. relax_days (300): how long sea water keeps its heat — longer = stronger warm/cold current anomalies. kappa_m2s (1000): heat mixing. ekman_min_lat (3), upwell_coast_km (100): upwelling near the equator / spread off coasts. prod_upwell (0.6), prod_shelf (0.4), prod_mix (0.3), upwell_ref_m_yr (100): productivity weights. prod_front (0.4), front_min (0.5), front_ref (1.0): SST fronts (where warm and cold currents meet) are productive — gradients over front_min °C/100 km count, saturating at front_ref above it. rho_air (1.2), drag (1.3e-3): bulk stress formula
[masks] o2_range (0.5), gravity_range (0.7): how strongly the painted masks act

Recipes

Want Do
More / less land [build] land_fraction
Pangaea one huge blob in land.png, 2–3 continental plates under it colliding
Big mountain range at a spot two continental plates converging there, or paint mountains.png, or masks/mountain_hint.png
Andes coast oceanic plate moving into the continent's coast
Island arcs two oceanic plates converging
Island chain [[hotspot]]
Hotter / colder world [climate] t_a ± a few °C
Deserts wider / narrower [climate] hadley_edge_deg, global_mean_mm
Wetter world [climate] global_mean_mm up
No seasons / extreme seasons [planet] tilt_deg 0 / 40+
Rugged vs smooth coasts [crust] edge_noise, [elevation] coast_noise_m, [sketch] warp_km
Exact coastline from your drawing [sketch] warp_km = 0, detail_warp_km = 0, or paint masks/lock.png
Crater, sunken land, magic zone later in history eras.toml events

Files

mapgen.py         CLI                 mapgen/     pipeline stages (mapgen/testing.py: test fixtures)
example/          a starter world: config/world.toml, masks/README.md (new-world adds the rest)
tests/            python -m unittest discover -s tests -t .

A world folder: config/ (world.toml tectonics.toml eras.toml), sketch/, optional masks/; generated: out/ previews/ (safe to delete; rebuild).

Ocean fields in out/r<res>/cells.npz (per cell; zero on land):

Field Unit Meaning
current m/s, (n,3) surface current vector, tangent to the sphere
current_speed m/s its length
sst °C annual-mean sea-surface temperature (≥ −1.8; T_mean on land)
upwelling m/yr Ekman vertical velocity, + = up (nutrients), − = down
productivity 0–1 sea life index: upwelling, shallow shelf, winter mixing, dimmed toward the poles

Viewer layers: Ocean currents (speed + arrows), Sea-surface temperature, Sea productivity.

Troubleshooting

  • error: sketch files missing → run new-world or draw sketch/*.png.
  • two plate seeds fall in the same cell → move one seed apart.
  • plateaus … are N km apart → plateaus need ≥ 2500 km between centres.
  • Out of memory → lower res_final, or cap it via the shared ledger: wf res run --mem 12G --for 1h --title "mapgen final" -- python mapgen.py build --final (busy → exit 3, retry later).

Licence and credits

MIT, see LICENSE. Third-party notices: licenses.md. Credits: CREDITS.md.

From the author

These projects are things that have been tumbling about in my head for a long time, and I now feel like trying to do something with. The bulk of my focus here is on some of the games that I love, but every time I boot them up, I just fiddle about in the menu and set up mods and fixes for a couple hours, with my drive to play them fizzling out. This is my hope to solve that, and while I am a professional software engineer this would not have happened were it not for the rise of (relatively) cheap AI that could do the bulk of the work with me. If you don't approve, that's fine. I did these things for me, sharing them is something I do in the hopes to help others in similar situations, that just want to play the games they love. Thank you to all that made these playable in the first place, with some luck this finds you, and can bring some joy.

Written with AI assistance (Claude, by Anthropic), used as an engineering tool.