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/
- Sketch — where land roughly is. Warped a bit so it doesn't look hand-drawn.
- Plates grow from seed points; their motions decide where they collide (mountains, trenches, volcanic arcs), pull apart (rifts, mid-ocean ridges) or slide past.
- Crust/elevation — continental vs oceanic crust, orogens, shelves, ridges, hotspot chains; sea level is solved
so
land_fractionof the planet is dry. - Erosion carves valleys and fills basins.
- 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.
- Hydrology — rivers, lakes, salt flats. Ice — ice sheets and sea ice. Environment — biomes (Holdridge), landforms, ground, lithology, ores. Seabed — sediments, vents, trenches.
- Render — preview PNGs in
previews/r<res>/and viewer textures inout/.
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→ runnew-worldor drawsketch/*.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.