C++/raylib semi-realistic fantasy/sci-fi planet generator on a fixed icosphere grid (Eulerian: properties flow over fixed cells). World-creation stages: tectonics, continental drift & erosion, hydrology (rivers/lakes), climate (temperature + orographic precipitation), and biome classification. Engine in src/sim (raylib-free, headless-testable), viewer in src/render. See CLAUDE.md and docs/ (design-notes.md, fauna-flora-plan.md = next step). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
5.7 KiB
Markdown
95 lines
5.7 KiB
Markdown
# Design notes (durable context)
|
||
|
||
These are the non-obvious decisions/conventions that were previously only in Claude's
|
||
auto-memory (which lives under `~/.claude/` and does **not** travel with the repo). Captured
|
||
here so the context survives a move to another machine/server. `CLAUDE.md` has the
|
||
authoritative current-state changelog; this is the "why / where things live" summary.
|
||
|
||
## Framing: World Creation → Live World
|
||
|
||
The roadmap is no longer rigid numbered "phases". **World Creation** is a set of
|
||
continuous, overlapping stages on a geological clock (My): tectonics → continental drift &
|
||
erosion → hydrology → climate → biomes → (fauna & flora, next). The long-term goal is a
|
||
separate **Live World** mode that runs the *finished* planet at a much slower real-time
|
||
clock (hours/days/weeks/months) with dynamic weather (clouds, rain, storms) and living
|
||
ecosystems/civilization. **Internal code still uses `phase*` names** (`Planet::drifting`,
|
||
the `phase3` flag, `phase3AfterMy`/`phase3DtScale` config keys) for save/config
|
||
compatibility — only display strings and docs use the new framing.
|
||
|
||
## Code module layout
|
||
|
||
Split into a raylib-free **engine** (`src/sim/`, headless-testable) and a raylib **viewer**
|
||
(`src/render/`); `src/main.cpp` is a ~10-line entry point. CMake adds both dirs to the
|
||
include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
||
|
||
`Planet` is **one class implemented across several .cpp files** (all share `Planet.hpp`):
|
||
- `PlanetTypes.hpp` — `Cell` / `Plate` / `SubGrid` / `Biome` enum / `PlanetConfig`.
|
||
- `Planet.cpp` — generation, geometry, plate seeding, RNG + shared helpers, subgrid, min/max.
|
||
- `PlanetTectonics.cpp` — `step()` (stress→uplift→relax; orogeny boosts gated on `drifting`).
|
||
- `PlanetDrift.cpp` — `cflDtMy`/`advect` + plate lifecycle (fission/kick/baby/fuse/enclosed).
|
||
- `PlanetErosion.cpp` — `erode` + `adjustSeaLevel`.
|
||
- `PlanetHydrology.cpp` — `routeFlow`/`computeHydrology`/`hydrology` (depression-fill→lakes,
|
||
steepest-descent→rivers, mass-conserving stream-power incision).
|
||
- `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation).
|
||
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
||
- `PlanetIO.cpp` — text config + binary save/load.
|
||
- (`PlanetBiosphere.cpp` — fauna/flora, planned; see `fauna-flora-plan.md`.)
|
||
|
||
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
||
`ViewerInput.cpp` (camera/picking/keys), `ViewerRender.cpp` (globe/map/panels/HUD/prompt),
|
||
plus topical helpers `Colors` / `Map2D` / `Overlays` / `Picking` / `Panels`.
|
||
|
||
Per-tick order in `Viewer::refreshView()`: `computeHydrology()` (if hydrology on) →
|
||
`computeClimate()` → `classifyBiomes()` → (`computeBiosphere()` when added) → `recolor()`.
|
||
|
||
## Core principle (do not violate)
|
||
|
||
Geometry is **fixed** — cells (icosphere vertices) never move. Only per-cell *properties*
|
||
flow over the fixed grid + neighbor adjacency (Eulerian). New phenomena = new per-cell
|
||
fields flowed over the grid, never moving cells.
|
||
|
||
## Axial tilt render convention (non-obvious)
|
||
|
||
The 3D globe is rendered leaned by `cfg.axialTilt` via `rlRotatef(tilt,0,0,1)` wrapping all
|
||
3D content in `renderGlobe3D`. Because that rotation isn't in the data, anything mapping
|
||
between world and model space must compensate with `rotateZ(v, ±tilt)` (src/render/
|
||
Picking.cpp): 3D picking un-rotates the ray hit by `−tilt` before `nearestCell`; 3D plate
|
||
labels rotate by `+tilt` before projecting. The 2D map + biome/climate are tilt-independent.
|
||
|
||
## Save format (v6) — self-describing config
|
||
|
||
`planet.save` stores `PlanetConfig` as a **self-describing key=value text block** (not a raw
|
||
POD dump), parsed like `planet.cfg` (`writeConfigFields`/`parseConfigStream` shared in
|
||
PlanetIO.cpp), written at `precision(17)` so doubles round-trip exactly. Consequence:
|
||
**adding/removing PlanetConfig fields no longer breaks saves** (unknown keys ignored, missing
|
||
keys keep defaults). v6 cannot load pre-v6 saves (one-time break; a length guard fails it
|
||
gracefully). Per-cell `Cell.biome` is saved (a byte appended after `invader`).
|
||
|
||
## Climate + biome model (derived, not saved)
|
||
|
||
`computeClimate()` builds two derived per-cell fields:
|
||
- **Temperature** (°C) = latitude curve (`biomeEquatorTemp/PoleDrop/LatExp`, super-linear so
|
||
cold concentrates at poles) − `biomeElevLapse` × elevation.
|
||
- **Precipitation**: zonal prevailing winds (easterly tropics/poles, westerly mid-lat); ocean
|
||
cells are a moisture source; each land cell takes its **upwind** neighbour's moisture, rains
|
||
out more on windward upslopes (orographic), loses a multiplicative fraction per cell
|
||
(continentality) → leeward/interior drying. The raw field is near-binary, so it's
|
||
**diffused** (`climateMoistureSmooth` passes) into transition zones, then normalized to
|
||
`sMoist∈[0,1]` by anchoring the **median land precip → 0.5** (robust to orographic spikes).
|
||
|
||
`classifyBiomes()` reads `sTemp` + `sMoist` (not a latitude hack) → rain-shadow/interior
|
||
deserts emerge; 13 biomes incl. polar Ice; wetlands require water adjacency. All biome &
|
||
climate thresholds are tunable `biome*` / `climate*` keys in `planet.cfg`.
|
||
|
||
## Headless testing
|
||
|
||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||
```
|
||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
||
src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp \
|
||
src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||
```
|
||
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
||
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|