# 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](https://h3geo.org)). 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](https://github.com/godosa/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 ```bash 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/ ──► worldmap-viewer └─► eras (config/eras.toml) → out/r/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/` 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 [--final]` | rebuild just one era | | `check [--final]` | sanity report (land share, rain belts, rain shadows, …) | | `compare --old ` | 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: ```toml [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: ```toml [[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): ```toml [[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. ```toml [[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/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](LICENSE). Third-party notices: [licenses.md](licenses.md). Credits: [CREDITS.md](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.