Live World stage: clock, day/night, seasons, moons, tides & ocean currents
Adds the slow real-time "Live World" mode (key W on a settled world) that runs the finished planet on an hours->weeks/months clock (liveTime/liveRate, [ / ] ramp the rate) with geology frozen. Everything new is a derived per-cell field flowed over the fixed grid. Day/night & seasons (PlanetLive.cpp): a raylib-free per-cell insolation field (computeInsolation) drives a moving day/night terminator (N) from time-of-day rotation + seasonal declination (axialTilt); a live seasonal temperature cycles the static summer/winter fields over the year (computeLiveSeason); a moving snow/sea-ice line tracks it. Day/night + snow are render overlays over any colour mode (3D + 2D). Save v8 stores the live clock. Sky & tides (PlanetOcean.cpp): 1-3 random moons (separate RNG, saved v9) orbit on the clock and, with the now small/distant sun, raise an equilibrium tide (computeTides -> sTide), shown as a tide-coloured coastline (T, buildCoastline + tideColor). Moons render with sun-lit phases, orbit rings and eclipses (solar shadow spot in the day/night overlay, lunar dimming). The 2D map is left-aligned; the freed space holds a Live-World "Sky & tides" panel (per-moon phase + a selected coastal tile's tidal phase). Ocean currents + climate feedback (PlanetOcean.cpp): computeOceanCurrents builds a per-ocean-cell tangent velocity from wind stress + Coriolis deflection + coast-following (gyres); computeClimate feeds warm (poleward) / cold (equatorward) currents back into sTemp as a bounded coastal anomaly (climateCurrentFactor) before seasons, so biomes shift. Rendered as warm/cold current arrows (O). Config: dayLengthHours/yearLengthDays/snowTemp/seaIceTemp/tideAmplitude/tideSunFactor/ climateCurrentFactor. Save header v7->v9 (version-gated; older saves load fine). Headless test_live.cpp + test_ocean.cpp; test_logic/test_biota still pass; GUI build clean. Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8ed9ae4515
commit
aa32e38dad
33
BUILD.md
33
BUILD.md
@ -47,8 +47,12 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
||||
J toggle rivers (Phase 2.5 hydrology)
|
||||
H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion)
|
||||
L generate biota population (flora/fauna/funga; settled world; re-press regenerates)
|
||||
W enter / leave Live World (slow real-time clock; settled world)
|
||||
N toggle the day/night terminator (Live World)
|
||||
T toggle the tide-coloured coastline (Live World)
|
||||
O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue)
|
||||
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
||||
[ / ] drift speed (My per real second, Phase 2/3)
|
||||
[ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month)
|
||||
S single tectonic tick
|
||||
F fast-forward Phase-1 forming to settled (instant)
|
||||
R reseed planet (restart forming)
|
||||
@ -67,8 +71,9 @@ CLI flags (applied before the first load/generate):
|
||||
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
||||
auto-created on first run, reload live with F2. Range-checked on
|
||||
load; an invalid file reverts to safe defaults (not overwritten).
|
||||
planet.save binary snapshot (versioned, currently v6): seed + config + full planet
|
||||
state; F5 writes it, F9 reloads and resumes deterministically. As of v6
|
||||
planet.save binary snapshot (versioned, currently v9: +moons; v8 +Live World clock;
|
||||
v7 +biota): seed + config + full planet state; F5 writes it, F9 reloads and
|
||||
resumes deterministically. As of v6
|
||||
the config is stored as a self-describing key=value block (like
|
||||
planet.cfg), so adding/removing config fields no longer breaks saves
|
||||
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
|
||||
@ -161,6 +166,7 @@ shadow, dry interiors) then diffuses it. Color modes 6 (temperature) / 7 (precip
|
||||
climateContinentality 0.05 fractional moisture lost per inland cell (dries interiors)
|
||||
climateWindPasses 50 moisture-advection iterations (steady state)
|
||||
climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest)
|
||||
climateCurrentFactor 4 C max coastal warming/cooling from ocean currents (0 = off; O shows arrows)
|
||||
|
||||
Biota (PlanetConfig): flora/fauna/funga. Density scalars drive color modes 8/9/0;
|
||||
the discrete slot/point population is generated on demand (L) and saved (v7).
|
||||
@ -182,17 +188,32 @@ the discrete slot/point population is generated on demand (L) and saved (v7).
|
||||
bioFaunaPoints 16 fauna point budget at full density
|
||||
bioFungaPoints 14 funga point budget at full density
|
||||
|
||||
Live World (PlanetConfig): the slow real-time clock (hours -> weeks/months) over the
|
||||
finished planet -- a moving day/night terminator, a live seasonal temperature cycle and a
|
||||
moving snow line. axialTilt (above) drives the seasonal declination. Press W to enter.
|
||||
|
||||
dayLengthHours 24 hours in one planetary day (rotation -> day/night)
|
||||
yearLengthDays 365.25 days in one planetary year (orbit -> seasons)
|
||||
snowTemp 0 C land below the live temperature shows snow
|
||||
seaIceTemp -2 C ocean below the live temperature shows sea ice
|
||||
tideAmplitude 0.6 m equilibrium-tide scale per unit tide-raising weight
|
||||
tideSunFactor 0.46 sun's tide weight relative to a unit moon (Earth ~0.46)
|
||||
|
||||
Moons (1-3, randomized in generateMoons() + saved v9): orbit on the live clock, raise the
|
||||
tides with the sun, and render as small lit spheres with phases, orbit rings and eclipses.
|
||||
|
||||
## Headless logic test (no display)
|
||||
|
||||
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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \
|
||||
src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||
-o /tmp/t && /tmp/t
|
||||
|
||||
# Biota suite: same source list, swap test_logic.cpp -> test_biota.cpp
|
||||
# Biota / Live World / Ocean suites: same source list, swap test_logic.cpp ->
|
||||
# test_biota.cpp, test_live.cpp or test_ocean.cpp
|
||||
|
||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||
determinism. Run after changing Planet::step().
|
||||
|
||||
125
CLAUDE.md
125
CLAUDE.md
@ -56,10 +56,33 @@ dynamic weather and life.
|
||||
> lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not
|
||||
> travel with the repo.
|
||||
|
||||
**Live World (future):** with planet creation finished, run the world at a slow real-time
|
||||
**Live World (in progress):** with planet creation finished, run the world at a slow real-time
|
||||
scale with dynamic **weather** (clouds, rain, storms, fronts), day/night, and living
|
||||
**ecosystems / civilization** evolving in real time. This is a separate large effort;
|
||||
the fixed-grid Eulerian model + the climate fields are the groundwork for it.
|
||||
- **Clock + day/night groundwork** *(done — see `PlanetLive.cpp`)* — a slow real-time clock
|
||||
(hours → weeks/months, `liveTime`/`liveRate`, key `W` to enter once settled, `[`/`]` ramp
|
||||
the rate), a moving **day/night terminator** (sun from time-of-day rotation + seasonal
|
||||
declination via `axialTilt`; key `N`), a raylib-free per-cell **insolation field**
|
||||
(`computeInsolation`, the weather hook), a **live seasonal temperature** cycling the
|
||||
static summer/winter fields over the year (`computeLiveSeason`), and a **moving snow / sea-ice
|
||||
line**. Day/night + snow are render overlays over any colour mode (3D + 2D). Save **v8** adds
|
||||
the live-clock state. Still **future:** actual weather, day/night temperature swing,
|
||||
precipitation/evaporation, living/evolving ecosystems.
|
||||
- **Moons + tides + distant sun** *(done — see `PlanetOcean.cpp`)* — **1–3 moons** generated per
|
||||
world from a separate RNG (no tectonic perturbation) and **saved (v9)**; they orbit on the live
|
||||
clock and, with the sun, raise an **equilibrium tide** (`computeTides` → `sTide`, two bulges via
|
||||
`cosθ²−⅓`, semidiurnal). Tides show as a **tide-coloured coastline** (`buildCoastline` + a
|
||||
diverging `tideColor`, key `T`, 3D + 2D). The 3D **sun** is now small + far with a faint halo;
|
||||
**moons** render with sun-lit **phases** (offset-dark-sphere) + faint **orbit rings**, plus
|
||||
**eclipses** — solar (a moon transiting the sun darkens a shadow spot in the day/night overlay),
|
||||
lunar (a moon in the planet's shadow dims red). Knobs `tideAmplitude`/`tideSunFactor`.
|
||||
- **Ocean currents (+ climate feedback)** *(done — see `PlanetOcean.cpp`)* — `computeOceanCurrents()`
|
||||
builds a per-ocean-cell tangent velocity from wind stress (`sWind`) + Coriolis deflection
|
||||
(right N / left S) + coast-following (gyres) + smoothing. `computeClimate()` calls it and feeds
|
||||
**warm (poleward) / cold (equatorward)** currents back into `sTemp` as a bounded coastal anomaly
|
||||
(`climateCurrentFactor`), so biomes shift naturally. Rendered as warm/cold **current arrows**
|
||||
over the sea (key `O`, 3D + 2D). This completes the Live World ocean/sky pass.
|
||||
|
||||
## Current state
|
||||
|
||||
@ -299,6 +322,54 @@ Working and verified (logic tested headless):
|
||||
organism list. `bio*` config knobs. Headless `test_biota.cpp`: density ranges/zeros, fauna≤
|
||||
capacity, carnivore gating, slot/point budgets, determinism + RNG isolation, v7 round-trip,
|
||||
pre-v7 loads empty. v7 reads v6-and-older (no biota block → empty population; press `L`).
|
||||
- **Live World — clock + day/night + live seasons + snow line:** the first **Live World** stage
|
||||
(the slow real-time arc after World Creation). Engine (`src/sim/PlanetLive.cpp`, raylib-free,
|
||||
derived/not-saved): `computeInsolation(dayOfYear01, timeOfDay01)` → `sInsolation` (0..1 cosine
|
||||
solar incidence; declination `axialTilt·sin(2π·doy)`, sub-solar longitude sweeps once per day;
|
||||
the foundation the future weather sim reads) and `computeLiveSeason(doy)` → `sLiveTemp` (the
|
||||
annual-mean `sTemp` swung toward the existing `summerTemp`/`winterTemp` by the seasonal phase,
|
||||
anti-phased across hemispheres). Viewer: key `W` (settled world) toggles **Live World** — drift
|
||||
freezes and `liveTime` advances at `liveRate` (sim hours/real-second), `[`/`]` ramp it
|
||||
hour→month; the HUD shows a `Year/Day/HH:MM` calendar (`dayLengthHours`/`yearLengthDays`).
|
||||
`rebuildLiveOverlay()` builds a per-cell day/night brightness (`illum`, soft terminator, dim
|
||||
night floor) + `shadedColors` (base colour → snow on cold land / sea-ice on cold ocean via
|
||||
`snowTemp`/`seaIceTemp` → day/night dim); both the 3D globe and 2D map draw `displayColors()`
|
||||
(the overlay over **any** colour mode), `N` toggles the terminator, a sun marker sits over the
|
||||
lit hemisphere. Cell-info adds a `live temp / day-night / snow` line. Save **v8** appends the
|
||||
Live World flag + `liveTime` (version-gated; older saves load with it off). New `PlanetLive.cpp`
|
||||
in CMake + the headless list; `test_live.cpp`: insolation range, lit/dark hemispheres,
|
||||
declination tracks `axialTilt` (polar day/night at solstice), live temp within the
|
||||
summer/winter band + anti-phased, snow line advances in winter, determinism.
|
||||
- **Live World — moons, tides & a distant sun:** engine `src/sim/PlanetOcean.cpp` (raylib-free):
|
||||
`generateMoons()` seeds **1–3 moons** (`Moon` struct in PlanetTypes) from a separate RNG
|
||||
(`cfg.seed ^ 0x900D5EED`, tectonic stream untouched); `sunDirection`/`moonDirection`/
|
||||
`moonOrbitNormal` give model-space sky geometry (one source of truth — `computeInsolation` now
|
||||
calls `sunDirection`). `computeTides(doy,tod,days)` → `sTide` (m), equilibrium two-bulge tide
|
||||
(`Σ w·(cosθ²−⅓)`, moons + sun weighted `tideSunFactor`, scaled `tideAmplitude`); derived/not
|
||||
saved. Viewer: `T` colours the **coastline** (`buildCoastline` dual-contour + `tideColor`
|
||||
diverging amber↔cyan, per-segment in 3D + `drawColoredSegments2D` in 2D), auto-scaled to the
|
||||
tide extent; `stepSim` computes tides + `moonDirs`/`moonNormals` each live frame. 3D render:
|
||||
small **distant sun** (`sunDist`≈9) + halo; **moons** at a visible orbit band with sun-lit
|
||||
**phase** (offset-dark-sphere), faint **orbit rings**, and **eclipses** — solar shadow folded
|
||||
into `rebuildLiveOverlay`'s `illum` near the sub-solar point, lunar dimming (reddish) when a
|
||||
moon is in the planet's shadow. Cell-info adds a tide line; stats shows the moon count. **Save
|
||||
v9** appends the moons block (`writeState`/`readState(...,hasMoons)`; pre-v9 saves synthesize
|
||||
moons from the seed). `test_ocean.cpp`: moon count/determinism + RNG isolation, unit sweeping
|
||||
sky dirs, zero-mean two-bulge tide (high under moon + antipode, low at 90°, moves with time),
|
||||
save v9 round-trip.
|
||||
- **Live World — ocean currents + climate feedback:** `Planet::computeOceanCurrents()`
|
||||
(PlanetOcean.cpp) builds a per-ocean-cell tangent velocity `sCurrent` (derived/not saved):
|
||||
wind stress (`sWind`) rotated by a **Coriolis** deflection (right N / left S about the cell
|
||||
normal), the across-shore component removed at land neighbours so flow **follows coasts**
|
||||
(gyres), then 3 smoothing passes (re-projected to the tangent plane; zero on land).
|
||||
`computeClimate()` calls it right after the wind pass and feeds it back: a **bounded coastal
|
||||
temperature anomaly** = `climateCurrentFactor · (poleward speed / max)` on ocean cells (warm
|
||||
poleward, cold equatorward), smoothed onto the coasts and added to `sTemp` **before** seasons,
|
||||
so summer/winter + biomes shift with it. Render: `buildCurrents` emits subsampled warm/cold
|
||||
**arrows** over the sea (warm = poleward/red, cold = equatorward/blue), key `O` (3D + 2D), built
|
||||
in `refreshView`. New knob `climateCurrentFactor` (4 °C). `test_ocean.cpp` adds: currents
|
||||
tangent + zero on land + widespread, feedback bounded by the knob and produces both warming and
|
||||
cooling, deterministic. Live-World ocean/sky pass complete.
|
||||
- Mouse hover (in either view) shows per-cell info. Clicking a tile opens a
|
||||
right-side detail panel: tile info header + the tile's subgrid drawn as a
|
||||
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
|
||||
@ -330,6 +401,8 @@ src/
|
||||
PlanetErosion.cpp erode() + adjustSeaLevel()
|
||||
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
||||
PlanetClimate.cpp computeClimate() (temperature + orographic precipitation)
|
||||
PlanetLive.cpp computeInsolation/computeLiveSeason (Live World: day/night + live seasons)
|
||||
PlanetOcean.cpp moons (generate/orbit) + computeTides (Live World sky & tides)
|
||||
PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate)
|
||||
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
||||
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
||||
@ -394,12 +467,13 @@ raylib 5.5 is fetched automatically — do not vendor it.
|
||||
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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \
|
||||
src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||
-o /tmp/t && /tmp/t
|
||||
```
|
||||
(Swap `test_logic.cpp` for `test_biota.cpp` to run the Biota suite — same source list.)
|
||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp` or `test_ocean.cpp` to run the
|
||||
Biota / Live World / Ocean suites — same source list.)
|
||||
|
||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||
the window (the engine lives in `src/sim` and is raylib-free, so it links without
|
||||
@ -429,10 +503,13 @@ elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
|
||||
(`8`/`9`/`0` = biota density; `6` **cycles** temperature → summer → winter → seasonality;
|
||||
active mode shown top-center of the globe) ·
|
||||
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
||||
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
||||
all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) ·
|
||||
`O` ocean-current arrows (warm/cold) · `SPACE` or on-screen button pause ·
|
||||
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
|
||||
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
||||
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
||||
on a settled world; re-press regenerates) · `R` reseed ·
|
||||
on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) ·
|
||||
`R` reseed ·
|
||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
||||
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
||||
|
||||
@ -441,6 +518,13 @@ Phase 2** / **Start Phase 3**; `H` starts/stops it manually. In Phase 3 drift
|
||||
keeps running at a finer timestep (`cflDtMy()*phase3DtScale`) while rivers, lakes
|
||||
and fluvial erosion evolve.
|
||||
|
||||
Live World (`W`, on a settled world): geological drift freezes and a slow real-time clock runs
|
||||
(`liveTime` in hours, `liveRate` = sim hours/real-second, ramped hour→month with `[`/`]`). A
|
||||
moving day/night terminator (`N`), a live seasonal temperature cycle and a moving snow/sea-ice
|
||||
line animate over whatever colour mode is active; the HUD shows a `Year/Day/HH:MM` calendar.
|
||||
**1–3 moons** orbit (sun-lit phases, orbit rings, solar/lunar eclipses) and, with the distant
|
||||
sun, raise tides — `T` colours the coastline by the live tide level (amber low ↔ cyan high).
|
||||
|
||||
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
||||
file (both applied before the initial load/generate).
|
||||
|
||||
@ -450,11 +534,13 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||
save header is versioned (currently **7**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
save header is versioned (currently **9**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
||||
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
||||
block — three Organism lists per cell, gated by a flag byte); newer-than-supported is
|
||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`).
|
||||
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
||||
clock — a flag byte + `liveTime`, v9 appends the **moons** block); newer-than-supported is
|
||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
||||
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed.
|
||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||
config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults),
|
||||
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —
|
||||
@ -462,8 +548,10 @@ a one-time break; a length guard makes that fail gracefully.) `writeConfigFields
|
||||
`parseConfigStream` in PlanetIO.cpp are shared by `planet.cfg` and the save.
|
||||
|
||||
Layout (1920x1080): left column 70% wide = 3D globe (top, 60% h, RenderTexture
|
||||
1344x648) + 2D Equal Earth map (bottom, 40% h); right column 30% wide = cell info
|
||||
(top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the
|
||||
1344x648) + 2D Equal Earth map (bottom, 40% h, **left-aligned**, with the freed space at
|
||||
its right holding the Live-World **"Sky & tides" panel** — `liveInfoRect`, `renderLiveInfo`:
|
||||
per-moon phase discs + a selected coastal tile's tidal phase); right column 30% wide = cell
|
||||
info (top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the
|
||||
3D viewport (1344x648 at the origin -- GetScreenToWorldRay assumes the full
|
||||
screen, wrong here); 2D hover uses EqualEarth::inverse (minus the `mapLon` pan).
|
||||
Borders (`B`), drift vectors (`D`) and a lat/lon graticule (`G`) draw in BOTH the
|
||||
@ -511,6 +599,9 @@ triangles (plates are fixed in phase 1).
|
||||
`climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes →
|
||||
wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`,
|
||||
`climateOroRefHeight`, `climateWindPasses`. Temperature uses the `biome*` temp params.
|
||||
`climateCurrentFactor` (4 °C) is the max coastal warming/cooling from ocean currents (0 = off;
|
||||
ocean-current arrows toggle with `O`). Current deflection angle + smoothing passes are
|
||||
constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config.
|
||||
- Seasons (`season*` + `axialTilt` + `biomeSeasonWeight`, `planet.cfg`) — `axialTilt` is the
|
||||
master driver (0 = no seasons); `seasonAmpMax` (18 °C max seasonal half-range at full
|
||||
tilt/lat/interior), `seasonLatExp` (1.2, push swing toward poles), `seasonContinentRings`
|
||||
@ -526,6 +617,18 @@ triangles (plates are fixed in phase 1).
|
||||
it; Tiny=1…Huge=5), `bioRegionBonus` (how strongly a cell copies same-biome neighbours →
|
||||
homogeneity vs variety). To add organisms, append to `biotaArchetypes()` in PlanetBiota.cpp
|
||||
(append-only — indices are serialized in v7 saves).
|
||||
- **Live World (`dayLengthHours`/`yearLengthDays`/`snowTemp`/`seaIceTemp`, `planet.cfg`):**
|
||||
`dayLengthHours` (24) sets the day/night period (and the `d/s` rate unit), `yearLengthDays`
|
||||
(365.25) the season period; `axialTilt` drives the seasonal declination (0 = no day/night
|
||||
tilt). `snowTemp` (0 °C) is the land snow line, `seaIceTemp` (-2 °C) the ocean sea-ice line —
|
||||
raise either to grow the white caps. Render constants (night-floor brightness, terminator
|
||||
softness, snow-blend ramp) live in `rebuildLiveOverlay()` (src/render/Viewer.cpp), not config.
|
||||
- **Moons & tides (`tideAmplitude`/`tideSunFactor`, `planet.cfg`):** `tideAmplitude` (0.6 m)
|
||||
scales the equilibrium tide per unit tide-raising weight (raise for a more dramatic coastline
|
||||
swing); `tideSunFactor` (0.46) is the sun's tide weight vs a unit moon. Moon count (1–3) and
|
||||
per-moon orbit/period/inclination/mass are randomized in `generateMoons()` (PlanetOcean.cpp);
|
||||
the 3D sun distance/size, moon orbit-render band and eclipse angles are render constants
|
||||
(ViewerRender.cpp / `rebuildLiveOverlay`), not config.
|
||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||
knob for how fast/high relief builds.
|
||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||
|
||||
@ -26,6 +26,8 @@ add_executable(planetsim
|
||||
src/sim/PlanetHydrology.cpp
|
||||
src/sim/PlanetBiomes.cpp
|
||||
src/sim/PlanetClimate.cpp
|
||||
src/sim/PlanetLive.cpp
|
||||
src/sim/PlanetOcean.cpp
|
||||
src/sim/PlanetBiota.cpp
|
||||
src/sim/PlanetFloraGen.cpp
|
||||
src/sim/PlanetFaunaGen.cpp
|
||||
|
||||
@ -31,6 +31,10 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
||||
- `PlanetHydrology.cpp` — `routeFlow`/`computeHydrology`/`hydrology` (depression-fill→lakes,
|
||||
steepest-descent→rivers, mass-conserving stream-power incision).
|
||||
- `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation).
|
||||
- `PlanetLive.cpp` — `computeInsolation()`/`computeLiveSeason()` (Live World: day/night + live
|
||||
seasonal temperature; derived, not saved).
|
||||
- `PlanetOcean.cpp` — moons (`generateMoons`, `moonDirection`/`sunDirection`/`moonOrbitNormal`) +
|
||||
`computeTides()` (Live World sky & equilibrium tides). Moons are saved (v9); tides derived.
|
||||
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
||||
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
||||
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
||||
@ -99,6 +103,8 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede
|
||||
thermal inertia; interiors swing most). `classifyBiomes()` blends winter temp into the
|
||||
Tundra/Taiga cold cutoffs via `biomeSeasonWeight` (0 = mean only → unchanged biomes), so
|
||||
cold-winter continental interiors turn boreal/tundra. Seasonal fields are derived/not-saved.
|
||||
**Ocean currents** add a bounded coastal warm/cold anomaly to this mean before the seasons pass
|
||||
(`climateCurrentFactor`; see the Ocean section).
|
||||
- **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
|
||||
@ -110,6 +116,61 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede
|
||||
deserts emerge; 13 biomes incl. polar Ice; wetlands require water adjacency. All biome &
|
||||
climate thresholds are tunable `biome*` / `climate*` keys in `planet.cfg`.
|
||||
|
||||
## Live World (slow real-time clock) — day/night + live seasons (derived, not saved)
|
||||
|
||||
The arc after World Creation: the finished planet runs on a slow real-time clock instead of
|
||||
the geological My clock. `PlanetLive.cpp` (raylib-free) builds two derived per-cell fields,
|
||||
recomputed each frame like climate (never saved):
|
||||
- `computeInsolation(dayOfYear01, timeOfDay01)` → `sInsolation` ∈ [0,1], the instantaneous
|
||||
solar incidence `max(0, cell.unit · sunDir)`. `sunDir = lonLatToDir(λ, δ)` with declination
|
||||
`δ = axialTilt·sin(2π·dayOfYear01)` (0 at equinox, ±tilt at solstice → polar day/night) and
|
||||
sub-solar longitude `λ = π·(1−2·timeOfDay01)` sweeping once per day. **This is the hook the
|
||||
future weather sim reads** (daytime heating). Computed in **model space** (the fixed cell
|
||||
units) so it stays consistent with both the tilted 3D globe (the lit pattern rotates with the
|
||||
globe; the seasonal lean is carried by `δ`, not the render tilt) and the model-space 2D map.
|
||||
- `computeLiveSeason(dayOfYear01)` → `sLiveTemp`, the annual-mean `sTemp` swung toward the
|
||||
static `summerTemp`/`winterTemp` by the seasonal phase `g = sin(2π·doy)·sign(lat)`
|
||||
(`liveTemp = mean + A·g`, `A = (summer−winter)/2`), anti-phased across hemispheres.
|
||||
|
||||
Viewer (Eulerian, geometry fixed — all overlays are per-cell render passes): key `W` (settled
|
||||
world) toggles `liveWorld`; drift freezes and `liveTime` (hours) advances at `liveRate` (sim
|
||||
hours/real-second, ramped hour→month with `[`/`]`). `rebuildLiveOverlay()` builds `illum` (soft
|
||||
day/night terminator over `sInsolation`, dim night floor) + `shadedColors` (base colour → snow
|
||||
on cold land / sea-ice on cold ocean via `snowTemp`/`seaIceTemp` → day/night dim); both the 3D
|
||||
globe and 2D map draw `displayColors()` (the overlay over **any** colour mode). `N` toggles the
|
||||
terminator. Save **v8** appends a Live World flag + `liveTime` (header, version-gated). Knobs:
|
||||
`dayLengthHours`/`yearLengthDays`/`snowTemp`/`seaIceTemp` in `planet.cfg`.
|
||||
|
||||
## Moons & tides (Live World sky/oceans)
|
||||
|
||||
`PlanetOcean.cpp` (raylib-free): `generateMoons()` seeds **1–3 `Moon`s** from a **separate RNG**
|
||||
(`cfg.seed ^ 0x900D5EED`) so it never touches the tectonic `rngState` — moons are world objects
|
||||
(not cells) and are **saved (v9)** via `writeState`/`readState(..., hasMoons)` (pre-v9 saves
|
||||
synthesize them from the seed). Sky geometry is one source of truth: `sunDirection(doy,tod)` =
|
||||
celestial dir leaned by declination then spun `-2π·tod` about +Y; `moonDirection(i,tod,days)` =
|
||||
inclined orbit circle `Ω=2π·days/period+phase` then the same spin (so a fixed cell sees ≈one
|
||||
lunar pass/day). `computeInsolation` now calls `sunDirection`. **Tides** (`computeTides` →
|
||||
`sTide`, derived/not saved): equilibrium two-bulge potential `Σ_body w·(cosθ²−⅓)` over the moons
|
||||
(weight `tideWeight`) + sun (`tideSunFactor`), scaled `tideAmplitude` — zero-mean, high under a
|
||||
body and its antipode, low at 90°, sweeping ≈twice/day.
|
||||
|
||||
Render (Viewer): the coastline is traced once per terrain change (`buildCoastline`, dual-contour
|
||||
on the land/ocean split, recording the adjacent ocean cell per segment) and coloured by
|
||||
`tideColor(sTide[oceanCell])` (`T`; auto-scaled), in 3D + 2D. The 3D sun is small/distant with a
|
||||
halo; moons render at a visible orbit band with a sun-lit **phase** (offset-dark-sphere trick),
|
||||
faint **orbit rings** (great circle ⟂ `moonOrbitNormal`), and **eclipses** — solar darkens a spot
|
||||
in `rebuildLiveOverlay`'s `illum` near the sub-solar point when a moon transits the sun; lunar
|
||||
dims a moon reddish in the planet's shadow.
|
||||
|
||||
**Ocean currents** (`computeOceanCurrents`, also `PlanetOcean.cpp`): a per-ocean-cell tangent
|
||||
velocity `sCurrent` from wind stress (`sWind`) rotated by a **Coriolis** deflection (right N /
|
||||
left S about the cell normal), with the across-shore component removed at land neighbours so the
|
||||
stream follows the coast (gyres), then smoothed and re-tangented (zero on land). `computeClimate`
|
||||
calls it right after the wind pass and feeds **warm (poleward) / cold (equatorward)** currents
|
||||
back into `sTemp` as a bounded coastal anomaly (`climateCurrentFactor`, smoothed onto coasts,
|
||||
applied before seasons → biomes shift with it). Rendered as warm/cold arrows over the sea
|
||||
(`buildCurrents`, key `O`). Currents/feedback are derived (not saved).
|
||||
|
||||
## Headless testing
|
||||
|
||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||
@ -117,6 +178,7 @@ 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/PlanetLive.cpp src/sim/PlanetOcean.cpp \
|
||||
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||
src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
# test_biota.cpp uses the same source list (Biota suite).
|
||||
|
||||
@ -149,6 +149,19 @@ Color seasonColor(double rangeC) {
|
||||
return Color{ L(0), L(1), L(2), 255 };
|
||||
}
|
||||
|
||||
// Tide: diverging around mid-tide. Low (negative) -> amber, high (positive) -> cyan.
|
||||
Color tideColor(double level, double range) {
|
||||
static const unsigned char loC[3] = { 235, 175, 80 }; // low tide (amber)
|
||||
static const unsigned char midC[3] = { 150, 175, 185 }; // mid tide (pale)
|
||||
static const unsigned char hiC[3] = { 55, 165, 235 }; // high tide (cyan)
|
||||
double t = std::clamp(level / std::max(1e-6, range), -1.0, 1.0);
|
||||
const unsigned char* a = t < 0.0 ? loC : midC;
|
||||
const unsigned char* b = t < 0.0 ? midC : hiC;
|
||||
double f = std::fabs(t);
|
||||
auto L = [&](int c){ return (unsigned char)(a[c] + (b[c] - a[c]) * f); };
|
||||
return Color{ L(0), L(1), L(2), 255 };
|
||||
}
|
||||
|
||||
// Precipitation ramp over normalized [0,1]: tan (dry) -> green -> teal/blue (wet).
|
||||
Color precipColor(double moist01) {
|
||||
double t = std::clamp(moist01, 0.0, 1.0);
|
||||
|
||||
@ -25,6 +25,9 @@ Color tempColor(double celsius);
|
||||
Color precipColor(double moist01);
|
||||
// Seasonality: summer-winter temperature range in deg C (grey calm -> orange extreme).
|
||||
Color seasonColor(double rangeC);
|
||||
// Tide level (m), diverging around 0: low tide amber -> high tide cyan. `range` sets the
|
||||
// saturation scale (meters at which the colour is fully amber/cyan).
|
||||
Color tideColor(double level, double range);
|
||||
// Biota density ramps (0..1): flora barren->lush green, fauna pale->amber/red,
|
||||
// funga pale->violet/brown.
|
||||
Color floraColor(double d01);
|
||||
|
||||
@ -97,6 +97,71 @@ void buildRivers(const Planet& p, float radius,
|
||||
}
|
||||
}
|
||||
|
||||
void buildCoastline(const Planet& p, float radius,
|
||||
std::vector<Vector3>& segs, std::vector<int>& oceanCell) {
|
||||
segs.clear(); oceanCell.clear();
|
||||
const double sea = p.cfg.seaLevel;
|
||||
auto isLand = [&](int i) { return p.cells[i].elevation > sea; };
|
||||
auto midV = [&](int i, int j) -> Vector3 {
|
||||
Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius;
|
||||
return Vector3{ (float)m.x, (float)m.y, (float)m.z };
|
||||
};
|
||||
// The ocean cell nearest a coast segment, for sampling tide. Prefer an ocean endpoint of
|
||||
// the cut edge; fall back to the lone/other cell that is ocean.
|
||||
auto emit = [&](const Vector3& a, const Vector3& b, int oc) {
|
||||
segs.push_back(a); segs.push_back(b); oceanCell.push_back(oc);
|
||||
};
|
||||
const std::vector<int>& tri = p.triIndices();
|
||||
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
||||
int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2];
|
||||
bool la = isLand(ia), lb = isLand(ib), lc = isLand(ic);
|
||||
if (la == lb && lb == lc) continue; // all land or all ocean: no coast
|
||||
// Exactly one vertex differs from the other two -> one cut separating it.
|
||||
int lone, o1, o2;
|
||||
if (la == lb) { lone = ic; o1 = ia; o2 = ib; }
|
||||
else if (lb == lc) { lone = ia; o1 = ib; o2 = ic; }
|
||||
else { lone = ib; o1 = ia; o2 = ic; }
|
||||
int oc = isLand(lone) ? o1 : lone; // the ocean side of the cut
|
||||
emit(midV(lone, o1), midV(lone, o2), oc);
|
||||
}
|
||||
}
|
||||
|
||||
void buildCurrents(const Planet& p, float radius,
|
||||
std::vector<Vector3>& segs, std::vector<Color>& cols) {
|
||||
segs.clear(); cols.clear();
|
||||
const std::vector<Vec3>& cur = p.current();
|
||||
if (cur.empty()) return;
|
||||
const double sea = p.cfg.seaLevel;
|
||||
double maxSp = 1e-9;
|
||||
for (const Vec3& v : cur) maxSp = std::max(maxSp, v.length());
|
||||
const Vec3 up{0, 1, 0};
|
||||
const Color warm{235, 120, 90, 255}, cold{90, 160, 235, 255};
|
||||
auto V = [](const Vec3& q) { return Vector3{ (float)q.x, (float)q.y, (float)q.z }; };
|
||||
for (int i = 0; i < (int)p.cells.size(); i += 3) { // subsample for readability
|
||||
if (p.cells[i].elevation > sea) continue;
|
||||
const Vec3& vel = cur[i];
|
||||
double sp = vel.length();
|
||||
if (sp < 0.18 * maxSp) continue; // skip the slack water
|
||||
const Vec3& nrm = p.cells[i].unit;
|
||||
Vec3 dir = vel * (1.0 / sp);
|
||||
double len = 0.02 + 0.03 * (sp / maxSp);
|
||||
Vec3 base = nrm * (double)radius;
|
||||
Vec3 tip = base + dir * len;
|
||||
Vec3 perp = dir.cross(nrm).normalized();
|
||||
Vec3 back = dir * -1.0; double hl = len * 0.35;
|
||||
Vec3 h1 = tip + (back * 0.8 + perp * 0.6) * hl;
|
||||
Vec3 h2 = tip + (back * 0.8 - perp * 0.6) * hl;
|
||||
// Warm if flowing poleward (toward the nearer pole), cold if equatorward.
|
||||
Vec3 northT = up - nrm * up.dot(nrm); double nl = northT.length();
|
||||
double pw = 0.0;
|
||||
if (nl > 1e-9) { northT = northT * (1.0 / nl); pw = dir.dot(northT) * (nrm.y >= 0 ? 1.0 : -1.0); }
|
||||
Color c = pw >= 0.0 ? warm : cold;
|
||||
segs.push_back(V(base)); segs.push_back(V(tip)); cols.push_back(c);
|
||||
segs.push_back(V(tip)); segs.push_back(V(h1)); cols.push_back(c);
|
||||
segs.push_back(V(tip)); segs.push_back(V(h2)); cols.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<Vector2>> buildGraticule() {
|
||||
std::vector<std::vector<Vector2>> g;
|
||||
const double D = M_PI / 180.0;
|
||||
@ -171,6 +236,23 @@ void drawSegments2D(const std::vector<Vector3>& segs, Color col, float width,
|
||||
rlEnd(); rlSetLineWidth(1.0f);
|
||||
}
|
||||
|
||||
void drawColoredSegments2D(const std::vector<Vector3>& segs, const std::vector<Color>& cols,
|
||||
float width, Rectangle r, double lonOffset) {
|
||||
if (segs.empty()) return;
|
||||
rlSetLineWidth(width); rlBegin(RL_LINES);
|
||||
for (size_t i = 0, c = 0; i + 1 < segs.size(); i += 2, ++c) {
|
||||
Vec3 a = Vec3{segs[i].x, segs[i].y, segs[i].z}.normalized();
|
||||
Vec3 b = Vec3{segs[i + 1].x, segs[i + 1].y, segs[i + 1].z}.normalized();
|
||||
double alo, ala, blo, bla; dirToLonLat(a, alo, ala); dirToLonLat(b, blo, bla);
|
||||
Vector2 pa = projLonLat(alo, ala, lonOffset, r), pb = projLonLat(blo, bla, lonOffset, r);
|
||||
if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue;
|
||||
const Color& col = cols[c < cols.size() ? c : cols.size() - 1];
|
||||
rlColor4ub(col.r, col.g, col.b, 255);
|
||||
rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y);
|
||||
}
|
||||
rlEnd(); rlSetLineWidth(1.0f);
|
||||
}
|
||||
|
||||
void drawSubgrids(const std::vector<std::shared_ptr<SubGrid>>& sgs,
|
||||
float visBase, float elevExagg, double seaLevel, float eps) {
|
||||
for (const auto& sg : sgs) {
|
||||
|
||||
@ -25,6 +25,20 @@ void buildDriftArrows(const Planet& p, float radius,
|
||||
void buildRivers(const Planet& p, float radius,
|
||||
std::vector<Vector3>& rivers, std::vector<Vector3>& bigRivers);
|
||||
|
||||
// ---- Coastline (land/ocean boundary, dual contour through triangles) --------
|
||||
// Like buildBorders but on the elevation-vs-seaLevel split. For each emitted segment
|
||||
// (pairs in `segs`) records a representative adjacent OCEAN cell in `oceanCell` (one
|
||||
// entry per segment, i.e. per 2 points) so the viewer can colour it by that cell's tide.
|
||||
void buildCoastline(const Planet& p, float radius,
|
||||
std::vector<Vector3>& segs, std::vector<int>& oceanCell);
|
||||
|
||||
// ---- Ocean currents (subsampled arrows, warm/cold) --------------------------
|
||||
// Short arrows along Planet::current() over a subsample of ocean cells. `cols` has one
|
||||
// colour per segment (warm = poleward/red, cold = equatorward/blue). Needs computeClimate
|
||||
// (which computes the current field) to have run.
|
||||
void buildCurrents(const Planet& p, float radius,
|
||||
std::vector<Vector3>& segs, std::vector<Color>& cols);
|
||||
|
||||
// ---- Lat/lon graticule ------------------------------------------------------
|
||||
// Polylines of (lon,lat) radians (Vector2.x=lon, .y=lat).
|
||||
std::vector<std::vector<Vector2>> buildGraticule();
|
||||
@ -38,6 +52,11 @@ void drawGraticuleLabels2D(Rectangle r, double lonOffset);
|
||||
void drawSegments2D(const std::vector<Vector3>& segs, Color col, float width,
|
||||
Rectangle r, double lonOffset);
|
||||
|
||||
// As drawSegments2D but with a per-segment colour (cols has one entry per segment,
|
||||
// i.e. per 2 points). Used for the tide-shaded coastline.
|
||||
void drawColoredSegments2D(const std::vector<Vector3>& segs, const std::vector<Color>& cols,
|
||||
float width, Rectangle r, double lonOffset);
|
||||
|
||||
// ---- Subgrid overlay (high-res patch over a selected cell) ------------------
|
||||
void drawSubgrids(const std::vector<std::shared_ptr<SubGrid>>& sgs,
|
||||
float visBase, float elevExagg, double seaLevel, float eps);
|
||||
|
||||
@ -53,6 +53,17 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
||||
if (sized(p.summerTemp()) && sized(p.winterTemp()))
|
||||
L.push_back(std::string(TextFormat(" summer %.0f C / winter %.0f C",
|
||||
p.summerTemp()[i], p.winterTemp()[i])));
|
||||
// Live World: current-season temperature + whether it's day or night + snow cover.
|
||||
if (sized(p.liveTemp())) {
|
||||
bool day = sized(p.insolation()) && p.insolation()[i] > 0.05;
|
||||
bool snow = (elev > p.cfg.seaLevel) ? (p.liveTemp()[i] < p.cfg.snowTemp)
|
||||
: (p.liveTemp()[i] < p.cfg.seaIceTemp);
|
||||
L.push_back(std::string(TextFormat("live %.1f C %s%s", p.liveTemp()[i],
|
||||
day ? "day" : "night", snow ? " snow" : "")));
|
||||
}
|
||||
if (sized(p.tide()))
|
||||
L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i],
|
||||
p.tide()[i] >= 0.0 ? "high" : "low")));
|
||||
L.push_back(std::string(TextFormat("geoAge %.0f My neighbors %d", age, (int)c.neighbors.size())));
|
||||
// Hydrology (derived; present once routeFlow()/hydrology() has run).
|
||||
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)
|
||||
@ -166,7 +177,8 @@ void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected) {
|
||||
}
|
||||
}
|
||||
|
||||
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting) {
|
||||
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
||||
bool live, double liveHours) {
|
||||
DrawRectangleRec(r, Color{12, 14, 22, 235});
|
||||
DrawRectangleLinesEx(r, 1, Color{120, 120, 150, 255});
|
||||
int x = (int)r.x + 16, y = (int)r.y + 12;
|
||||
@ -198,6 +210,7 @@ void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting) {
|
||||
L(TextFormat("Surface area: %.0f M km2", surfKm2 / 1.0e6));
|
||||
L(TextFormat("Plates: %d active %d continental / %d oceanic", used, contPlates, used - contPlates));
|
||||
L(TextFormat("Young ridges: %d strips %d cells", babyPlates, babyCells));
|
||||
L(TextFormat("Moons: %d", (int)p.getMoons().size()));
|
||||
int water = N - landGeo;
|
||||
double wlRatio = landGeo > 0 ? (double)water / landGeo : 0.0;
|
||||
L(TextFormat("Land %.0f%% Ocean %.0f%% (water:land %.2f:1)",
|
||||
@ -205,7 +218,10 @@ void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting) {
|
||||
L(TextFormat("Sea level: %+.0f m", seaLvl));
|
||||
L(TextFormat("Crust: %.0f%% continental / %.0f%% oceanic", 100.0 * cont / N, 100.0 * (N - cont) / N));
|
||||
L(TextFormat("Elevation: %.0f .. %.0f m mean %.0f m", mn, mx, sum / N));
|
||||
if (drifting) L(TextFormat("Sim time: %.0f My", elapsedMy));
|
||||
if (live) L(TextFormat("Live World: year %ld, day %.1f",
|
||||
(long)(liveHours / p.cfg.dayLengthHours / p.cfg.yearLengthDays) + 1,
|
||||
std::fmod(liveHours / p.cfg.dayLengthHours, p.cfg.yearLengthDays)));
|
||||
else if (drifting) L(TextFormat("Sim time: %.0f My", elapsedMy));
|
||||
y += 8;
|
||||
DrawText("plate cells size area speed land", x, y, 15, Color{150, 155, 170, 255}); y += 21;
|
||||
|
||||
|
||||
@ -17,4 +17,5 @@ void drawDetailPanel(const Planet& p, const std::shared_ptr<SubGrid>& sg,
|
||||
void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected);
|
||||
|
||||
// World statistics panel (shown in the subareas quadrant when no tile selected).
|
||||
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting);
|
||||
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
||||
bool live = false, double liveHours = 0.0);
|
||||
|
||||
@ -35,9 +35,13 @@ bool Viewer::init(int argc, char** argv) {
|
||||
int mapH = mapAreaH - 30;
|
||||
int mapW = (int)(mapH * (EqualEarth::halfWidth() / EqualEarth::halfHeight()));
|
||||
if (mapW > leftW - 30) { mapW = leftW - 30; mapH = (int)(mapW / (EqualEarth::halfWidth() / EqualEarth::halfHeight())); }
|
||||
mapRect = Rectangle{ (float)((leftW - mapW) / 2),
|
||||
// Left-align the 2D map (was centered) so the freed space at right holds the live sky panel.
|
||||
const float mapMargin = 16.0f;
|
||||
mapRect = Rectangle{ mapMargin,
|
||||
(float)(mapAreaY + (mapAreaH - mapH) / 2),
|
||||
(float)mapW, (float)mapH };
|
||||
float liveX = mapRect.x + mapRect.width + 16.0f;
|
||||
liveInfoRect = Rectangle{ liveX, mapRect.y, (float)leftW - liveX - 8.0f, mapRect.height };
|
||||
|
||||
// Right column.
|
||||
hoverRect = Rectangle{ (float)rightX + 8, 8.0f, (float)rightW - 16, (float)rightH - 16 };
|
||||
@ -136,6 +140,71 @@ void Viewer::recolor() {
|
||||
minE = planet.minElevation(); maxE = planet.maxElevation();
|
||||
}
|
||||
|
||||
// Live World: from the sim's insolation + live-temperature fields, build the per-cell
|
||||
// day/night brightness (illum) and the shaded draw colours (base colour -> snow/ice tint ->
|
||||
// day/night dim). Cheap O(n); called every frame while in Live World.
|
||||
void Viewer::rebuildLiveOverlay() {
|
||||
const size_t n = planet.cells.size();
|
||||
const std::vector<double>& sun = planet.insolation();
|
||||
const std::vector<double>& lt = planet.liveTemp();
|
||||
const double sea = planet.cfg.seaLevel;
|
||||
const double snowT = planet.cfg.snowTemp;
|
||||
const double iceT = planet.cfg.seaIceTemp;
|
||||
const float nightFloor = 0.18f; // night side dim (not black) so colours read
|
||||
|
||||
illum.assign(n, 1.0f);
|
||||
shadedColors.resize(n);
|
||||
auto smoothstep = [](double e0, double e1, double x) {
|
||||
double t = (e1 > e0) ? (x - e0) / (e1 - e0) : 0.0;
|
||||
t = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
};
|
||||
// Solar eclipse: a moon roughly between the sun and the planet (its model-space direction
|
||||
// near the sun's) casts a shadow around the sub-solar point. Strength ramps with alignment.
|
||||
const Vec3 sd{ sunDir.x, sunDir.y, sunDir.z };
|
||||
const double eclipseReach = 0.13; // rad: how close a moon must be to the sun to eclipse
|
||||
const double umbra = 0.10; // rad: angular radius of the shadow spot
|
||||
double eclipseStrength = 0.0;
|
||||
for (const auto& md : moonDirs) {
|
||||
double d = std::acos(std::clamp((double)(md.x*sd.x + md.y*sd.y + md.z*sd.z), -1.0, 1.0));
|
||||
if (d < eclipseReach) eclipseStrength = std::max(eclipseStrength, 1.0 - d / eclipseReach);
|
||||
}
|
||||
auto blend = [](unsigned char c, unsigned char to, double a) {
|
||||
return (unsigned char)(c + (to - c) * a);
|
||||
};
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
// Day/night: soft sunrise band over the clamped cosine incidence.
|
||||
float f = nightFloor;
|
||||
if (!sun.empty()) f = nightFloor + (1.0f - nightFloor) * (float)smoothstep(0.0, 0.12, sun[i]);
|
||||
// Eclipse shadow: darken cells near the sub-solar point while a moon transits the sun.
|
||||
if (eclipseStrength > 0.0 && !sun.empty()) {
|
||||
double dd = std::acos(std::clamp(planet.cells[i].unit.x*sd.x + planet.cells[i].unit.y*sd.y
|
||||
+ planet.cells[i].unit.z*sd.z, -1.0, 1.0));
|
||||
double sh = eclipseStrength * std::exp(-(dd / umbra) * (dd / umbra));
|
||||
f *= (float)std::max(0.10, 1.0 - 0.85 * sh);
|
||||
}
|
||||
illum[i] = f;
|
||||
|
||||
Color c = vcolors[i];
|
||||
// Snow on cold land, sea ice on cold ocean (live seasonal temperature).
|
||||
if (!lt.empty()) {
|
||||
double e = planet.cells[i].elevation;
|
||||
if (e > sea) {
|
||||
double a = (snowT - lt[i]) / 8.0; // fully snow ~8 C below freezing
|
||||
if (a > 0.0) { a = a > 0.85 ? 0.85 : a;
|
||||
c = Color{ blend(c.r, 242, a), blend(c.g, 246, a), blend(c.b, 250, a), 255 }; }
|
||||
} else {
|
||||
double a = (iceT - lt[i]) / 6.0; // sea ice
|
||||
if (a > 0.0) { a = a > 0.9 ? 0.9 : a;
|
||||
c = Color{ blend(c.r, 212, a), blend(c.g, 226, a), blend(c.b, 236, a), 255 }; }
|
||||
}
|
||||
}
|
||||
// Day/night dimming over the (possibly snow-tinted) colour.
|
||||
if (dayNightOn) c = Color{ (unsigned char)(c.r * f), (unsigned char)(c.g * f), (unsigned char)(c.b * f), 255 };
|
||||
shadedColors[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
void Viewer::refreshView() {
|
||||
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
||||
planet.computeClimate(); // temperature + precipitation fields
|
||||
@ -147,6 +216,8 @@ void Viewer::refreshView() {
|
||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||
}
|
||||
if (phase3) buildRivers(planet, riverR, rivers, bigRivers);
|
||||
buildCoastline(planet, riverR, coast, coastOcean); // land/ocean boundary (for tide lines)
|
||||
buildCurrents(planet, driftR, currentSegs, currentCols); // ocean current arrows (warm/cold)
|
||||
if (selectedCell >= 0) rebuildSub();
|
||||
}
|
||||
|
||||
@ -180,12 +251,15 @@ void Viewer::saveGame(const char* path) {
|
||||
std::ofstream os(path, std::ios::binary);
|
||||
if (!os) { setStatus("Save failed"); return; }
|
||||
uint32_t ver = SAVE_VERSION; uint8_t st = settled ? 1 : 0; uint8_t p3 = phase3 ? 1 : 0;
|
||||
uint8_t lw = liveWorld ? 1 : 0;
|
||||
os.write("PLSV", 4);
|
||||
os.write(reinterpret_cast<const char*>(&ver), sizeof ver);
|
||||
os.write(reinterpret_cast<const char*>(&elapsedMy), sizeof elapsedMy);
|
||||
os.write(reinterpret_cast<const char*>(&st), sizeof st);
|
||||
os.write(reinterpret_cast<const char*>(&driftRate), sizeof driftRate);
|
||||
os.write(reinterpret_cast<const char*>(&p3), sizeof p3); // v3: Phase-3 flag
|
||||
os.write(reinterpret_cast<const char*>(&lw), sizeof lw); // v8: Live World flag
|
||||
os.write(reinterpret_cast<const char*>(&liveTime), sizeof liveTime); // v8: live clock (hours)
|
||||
planet.writeState(os);
|
||||
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
||||
}
|
||||
@ -194,20 +268,24 @@ void Viewer::loadGame(const char* path) {
|
||||
std::ifstream is(path, std::ios::binary);
|
||||
if (!is) { setStatus(std::string("No ") + path); return; }
|
||||
char magic[4] = {0}; uint32_t ver = 0; double em = 0; uint8_t st = 0; double dr = 4.0; uint8_t p3 = 0;
|
||||
uint8_t lw = 0; double lh = 0.0;
|
||||
is.read(magic, 4);
|
||||
is.read(reinterpret_cast<char*>(&ver), sizeof ver);
|
||||
is.read(reinterpret_cast<char*>(&em), sizeof em);
|
||||
is.read(reinterpret_cast<char*>(&st), sizeof st);
|
||||
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
||||
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
||||
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
|
||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
||||
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||
if (!planet.readState(is, ver >= 4, ver >= 7)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota
|
||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota, v9: moons
|
||||
cfg = planet.cfg; // adopt the loaded config
|
||||
elapsedMy = em; settled = (st != 0);
|
||||
planet.drifting = settled; // resume drift boosts iff mid-drift
|
||||
phase3 = (p3 != 0); phase3Prompt = false;
|
||||
phase3PromptAt = phase3 ? elapsedMy : (elapsedMy + planet.cfg.phase3AfterMy);
|
||||
driftRate = dr;
|
||||
liveWorld = (lw != 0); liveTime = lh; // v8: resume the Live World clock
|
||||
settleRun = settleNeed; // keep the settled latch consistent
|
||||
dtMy = settled ? planet.cflDtMy() : 0.0;
|
||||
driftAccum = 0.0; formAccum = 0.0;
|
||||
@ -222,6 +300,28 @@ void Viewer::loadGame(const char* path) {
|
||||
// Advance the simulation this frame: Phase-1 forming (paced ticks toward
|
||||
// equilibrium), or Phase-2 drift / Phase-3 drift+hydrology at a finer dt.
|
||||
void Viewer::stepSim() {
|
||||
if (liveWorld) {
|
||||
// --- Live World: advance the slow clock; geology is frozen --------
|
||||
if (!paused) liveTime += liveRate * GetFrameTime(); // hours
|
||||
double days = liveTime / planet.cfg.dayLengthHours;
|
||||
double dayOfYear01 = days / planet.cfg.yearLengthDays;
|
||||
dayOfYear01 -= std::floor(dayOfYear01);
|
||||
double timeOfDay01 = days - std::floor(days);
|
||||
planet.computeInsolation(dayOfYear01, timeOfDay01);
|
||||
planet.computeLiveSeason(dayOfYear01);
|
||||
planet.computeTides(dayOfYear01, timeOfDay01, days);
|
||||
Vec3 s = planet.sunDirection(dayOfYear01, timeOfDay01); // shared sky geometry
|
||||
sunDir = Vector3{ (float)s.x, (float)s.y, (float)s.z };
|
||||
moonDirs.clear(); moonNormals.clear();
|
||||
for (int m = 0; m < (int)planet.getMoons().size(); ++m) {
|
||||
Vec3 md = planet.moonDirection(m, timeOfDay01, days);
|
||||
Vec3 mn = planet.moonOrbitNormal(m, timeOfDay01);
|
||||
moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z });
|
||||
moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z });
|
||||
}
|
||||
rebuildLiveOverlay();
|
||||
return;
|
||||
}
|
||||
if (!paused && !settled) {
|
||||
// --- Phase 1: forming, paced ticks toward equilibrium -------------
|
||||
formAccum += GetFrameTime() * formRate;
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||
struct Viewer {
|
||||
// ---- Files / save format ------------------------------------------------
|
||||
static constexpr uint32_t SAVE_VERSION = 7; // v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
||||
static constexpr uint32_t SAVE_VERSION = 9; // v9: +moons; v8: +Live World clock; v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
||||
const char* CONFIG_PATH = "planet.cfg";
|
||||
const char* SAVE_PATH = "planet.save";
|
||||
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
||||
@ -26,6 +26,7 @@ struct Viewer {
|
||||
int view3DW = 0, view3DH = 0;
|
||||
RenderTexture2D rt3d{};
|
||||
Rectangle mapRect{}, hoverRect{}, panelRect{}, gridRect{};
|
||||
Rectangle liveInfoRect{}; // free space right of the (left-aligned) 2D map: moon/tide phase
|
||||
Rectangle pauseBtn{}, p3ContinueBtn{}, p3StartBtn{};
|
||||
float pbCx = 0.0f, pbCy = 0.0f;
|
||||
|
||||
@ -72,6 +73,24 @@ struct Viewer {
|
||||
bool phase3 = false, phase3Prompt = false;
|
||||
double phase3PromptAt = 0.0;
|
||||
|
||||
// Live World: a slow real-time clock (hours -> weeks/months) over the finished planet.
|
||||
// Geological drift freezes while it runs; a moving day/night terminator, a live seasonal
|
||||
// temperature cycle and a moving snow line animate. liveRate is sim hours per real second.
|
||||
bool liveWorld = false, dayNightOn = true;
|
||||
double liveTime = 0.0; // hours since the live clock started
|
||||
double liveRate = 1.0; // sim hours advanced per real second (ramps hour->month)
|
||||
std::vector<float> illum; // per-cell day/night brightness (1 = day, floor = night)
|
||||
std::vector<Color> shadedColors;// vcolors + snow/ice tint + day/night dim (live overlay)
|
||||
Vector3 sunDir{0.0f, 0.0f, 1.0f}; // model-space sub-solar direction (for the 3D sun marker)
|
||||
std::vector<Vector3> moonDirs; // model-space sub-lunar directions (one per moon, for render)
|
||||
std::vector<Vector3> moonNormals;// model-space orbit-plane normals (one per moon, for the ring)
|
||||
std::vector<Vector3> coast; // coastline segments (land/ocean boundary, rebuilt with terrain)
|
||||
std::vector<int> coastOcean; // ocean cell per coast segment (to sample tide)
|
||||
std::vector<Color> coastCols; // per-segment tide colour (filled each frame when showTides)
|
||||
bool showTides = false; // colour the coastline by the live tide level (key T)
|
||||
std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows
|
||||
bool showCurrents = false; // ocean current arrows, warm/cold (key O)
|
||||
|
||||
// Selection + subgrid (phase 4/5 preview).
|
||||
int selectedCell = -1;
|
||||
double selectedThresh = 0.06;
|
||||
@ -102,6 +121,10 @@ struct Viewer {
|
||||
void selectCell(int idx);
|
||||
void recolor();
|
||||
void refreshView();
|
||||
void rebuildLiveOverlay(); // Live World: fill illum + shadedColors from sim fields
|
||||
// Colors the 3D globe + 2D map actually draw: the live overlay when in Live World, else the
|
||||
// plain per-cell colours.
|
||||
const std::vector<Color>& displayColors() const { return liveWorld ? shadedColors : vcolors; }
|
||||
void regenWorld(); // after generate(): geometry changed
|
||||
void regen(); // generate(cfg) + regenWorld()
|
||||
void stepOnce(); // one tick + settle bookkeeping
|
||||
@ -118,6 +141,7 @@ struct Viewer {
|
||||
void renderFrame();
|
||||
void renderGlobe3D();
|
||||
void renderMap2D();
|
||||
void renderLiveInfo(); // Live World: moon phases + (coastal) tidal phase, beside the 2D map
|
||||
void renderPanels();
|
||||
void renderHUD();
|
||||
void renderPrompt();
|
||||
|
||||
@ -131,6 +131,20 @@ void Viewer::handleInput() {
|
||||
{ mode = ColorMode::FloraDensity; recolor(); }
|
||||
setStatus("Biota generated (flora/fauna/funga)");
|
||||
}
|
||||
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
|
||||
liveWorld = !liveWorld;
|
||||
if (liveWorld) {
|
||||
phase3Prompt = false; paused = false;
|
||||
refreshView(); // fresh base colours; overlay builds in stepSim
|
||||
setStatus("Live World started");
|
||||
} else {
|
||||
paused = true; refreshView(); // back to World Creation (drift), paused
|
||||
setStatus("Live World stopped");
|
||||
}
|
||||
}
|
||||
if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator
|
||||
if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline
|
||||
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
|
||||
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
||||
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
||||
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
||||
@ -152,7 +166,14 @@ void Viewer::handleInput() {
|
||||
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
||||
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
||||
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
||||
// Drift speed (Phase 2): My simulated per real second.
|
||||
if (IsKeyPressed(KEY_RIGHT_BRACKET)) driftRate = std::min(driftRate * 1.5, 80.0);
|
||||
if (IsKeyPressed(KEY_LEFT_BRACKET)) driftRate = std::max(driftRate / 1.5, 0.5);
|
||||
// Speed control: Live World ramps the live clock (sim hours/s, ~hour -> month);
|
||||
// otherwise it sets the drift rate (My simulated per real second).
|
||||
if (IsKeyPressed(KEY_RIGHT_BRACKET)) {
|
||||
if (liveWorld) liveRate = std::min(liveRate * 1.5, 720.0);
|
||||
else driftRate = std::min(driftRate * 1.5, 80.0);
|
||||
}
|
||||
if (IsKeyPressed(KEY_LEFT_BRACKET)) {
|
||||
if (liveWorld) liveRate = std::max(liveRate / 1.5, 0.25);
|
||||
else driftRate = std::max(driftRate / 1.5, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ void Viewer::renderGlobe3D() {
|
||||
rlPushMatrix();
|
||||
rlRotatef((float)planet.cfg.axialTilt, 0.0f, 0.0f, 1.0f);
|
||||
const std::vector<int>& tri = planet.triIndices();
|
||||
const std::vector<Color>& dc = displayColors(); // live overlay (day/night + snow) or plain
|
||||
rlBegin(RL_TRIANGLES);
|
||||
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
||||
int idx[3] = { tri[k], tri[k + 1], tri[k + 2] };
|
||||
@ -27,7 +28,7 @@ void Viewer::renderGlobe3D() {
|
||||
const Cell& cc = planet.cells[idx[j]];
|
||||
const Vec3& u = cc.unit;
|
||||
float r = visBase + (float)cc.elevation * elevExagg;
|
||||
const Color& col = vcolors[idx[j]];
|
||||
const Color& col = dc[idx[j]];
|
||||
rlColor4ub(col.r, col.g, col.b, 255);
|
||||
rlVertex3f((float)(u.x * r), (float)(u.y * r), (float)(u.z * r));
|
||||
}
|
||||
@ -72,6 +73,36 @@ void Viewer::renderGlobe3D() {
|
||||
};
|
||||
drawRiv(rivers, 1.5f); drawRiv(bigRivers, 3.0f);
|
||||
}
|
||||
// Live World tide: colour the coastline by the local tide level (per-segment colour cached
|
||||
// in coastCols so the 2D map reuses it). Auto-scaled to the current tide extent.
|
||||
coastCols.clear();
|
||||
if (liveWorld && showTides && !coast.empty()) {
|
||||
const std::vector<double>& td = planet.tide();
|
||||
double range = 1e-6;
|
||||
for (int oc : coastOcean) if (oc >= 0 && oc < (int)td.size()) range = std::max(range, std::fabs(td[oc]));
|
||||
coastCols.reserve(coastOcean.size());
|
||||
for (int oc : coastOcean)
|
||||
coastCols.push_back((oc >= 0 && oc < (int)td.size()) ? tideColor(td[oc], range) : Color{150,175,185,255});
|
||||
rlSetLineWidth(3.0f); rlBegin(RL_LINES);
|
||||
for (size_t i = 0, c = 0; i + 1 < coast.size(); i += 2, ++c) {
|
||||
const Color& col = coastCols[c];
|
||||
rlColor4ub(col.r, col.g, col.b, 255);
|
||||
rlVertex3f(coast[i].x, coast[i].y, coast[i].z);
|
||||
rlVertex3f(coast[i + 1].x, coast[i + 1].y, coast[i + 1].z);
|
||||
}
|
||||
rlEnd(); rlSetLineWidth(1.0f);
|
||||
}
|
||||
// Ocean currents: warm/cold arrows over the sea (per-segment colour).
|
||||
if (showCurrents && !currentSegs.empty()) {
|
||||
rlSetLineWidth(2.0f); rlBegin(RL_LINES);
|
||||
for (size_t i = 0, c = 0; i + 1 < currentSegs.size(); i += 2, ++c) {
|
||||
const Color& col = currentCols[c];
|
||||
rlColor4ub(col.r, col.g, col.b, 255);
|
||||
rlVertex3f(currentSegs[i].x, currentSegs[i].y, currentSegs[i].z);
|
||||
rlVertex3f(currentSegs[i + 1].x, currentSegs[i + 1].y, currentSegs[i + 1].z);
|
||||
}
|
||||
rlEnd(); rlSetLineWidth(1.0f);
|
||||
}
|
||||
if (showGrat) drawGraticule3D(graticule, gratR);
|
||||
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
||||
if (selectedCell >= 0) {
|
||||
@ -97,6 +128,57 @@ void Viewer::renderGlobe3D() {
|
||||
DrawSphere(Vector3{0.0f, ax, 0.0f}, 0.05f, Color{230, 90, 80, 255}); // north
|
||||
DrawSphere(Vector3{0.0f, -ax, 0.0f}, 0.05f, Color{80, 140, 230, 255}); // south
|
||||
}
|
||||
// Live World sky: a small, distant sun (bright core + faint halo) and the orbiting moons
|
||||
// (sun-lit phase + orbit ring; dimmed reddish during a lunar eclipse). All inside the tilted
|
||||
// matrix so they stay consistent with the model-space lit pattern.
|
||||
if (liveWorld) {
|
||||
// Sun: far away + small, with a couple of translucent halo shells so it still reads.
|
||||
const float sunDist = 9.0f;
|
||||
Vector3 sp{ sunDir.x * sunDist, sunDir.y * sunDist, sunDir.z * sunDist };
|
||||
DrawSphere(sp, 0.60f, Color{255, 240, 180, 26});
|
||||
DrawSphere(sp, 0.34f, Color{255, 238, 170, 55});
|
||||
DrawSphere(sp, 0.17f, Color{255, 246, 205, 255});
|
||||
|
||||
const auto& mns = planet.getMoons();
|
||||
for (size_t m = 0; m < mns.size() && m < moonDirs.size(); ++m) {
|
||||
const Vector3& dir = moonDirs[m];
|
||||
float dist = visBase + 0.8f + (float)(mns[m].orbitRadius / 30.0) * 4.0f; // visible band
|
||||
Vector3 mp{ dir.x * dist, dir.y * dist, dir.z * dist };
|
||||
float rr = (float)mns[m].dispRadius;
|
||||
|
||||
// Faint orbit ring: the great circle perpendicular to the orbit-plane normal.
|
||||
if (m < moonNormals.size()) {
|
||||
Vec3 nrm = Vec3{moonNormals[m].x, moonNormals[m].y, moonNormals[m].z}.normalized();
|
||||
Vec3 u = std::fabs(nrm.y) < 0.9 ? nrm.cross(Vec3{0,1,0}).normalized()
|
||||
: nrm.cross(Vec3{1,0,0}).normalized();
|
||||
Vec3 v = nrm.cross(u);
|
||||
rlBegin(RL_LINES); rlColor4ub(120, 130, 160, 90);
|
||||
const int seg = 64;
|
||||
for (int k = 0; k < seg; ++k) {
|
||||
double a0 = 2.0 * M_PI * k / seg, a1 = 2.0 * M_PI * (k + 1) / seg;
|
||||
Vec3 p0 = (u * std::cos(a0) + v * std::sin(a0)) * dist;
|
||||
Vec3 p1 = (u * std::cos(a1) + v * std::sin(a1)) * dist;
|
||||
rlVertex3f((float)p0.x, (float)p0.y, (float)p0.z);
|
||||
rlVertex3f((float)p1.x, (float)p1.y, (float)p1.z);
|
||||
}
|
||||
rlEnd();
|
||||
}
|
||||
|
||||
// Lunar eclipse: moon near the anti-solar point (in the planet's shadow) -> dim red.
|
||||
double antiAlign = -(dir.x*sunDir.x + dir.y*sunDir.y + dir.z*sunDir.z); // dot(dir,-sun)
|
||||
bool eclipsed = antiAlign > std::cos(0.13);
|
||||
Color lit = eclipsed ? Color{90, 35, 30, 255} : Color{210, 210, 215, 255};
|
||||
DrawSphere(mp, rr, lit);
|
||||
// Phase via the offset-dark-sphere trick: lit fraction k = (1 - cos(phase))/2, with
|
||||
// cos(phase)=dot(moonDir,sunDir) (new moon when aligned with the sun). Shift a dark
|
||||
// sphere toward the unlit (anti-sun) side to occlude it; offset 0 = new, ~2r = full.
|
||||
double cosPhase = dir.x*sunDir.x + dir.y*sunDir.y + dir.z*sunDir.z;
|
||||
double k = (1.0 - cosPhase) * 0.5; // 0 = new, 1 = full
|
||||
float off = (float)(k * 2.2 * rr);
|
||||
Vector3 dp{ mp.x - sunDir.x * off, mp.y - sunDir.y * off, mp.z - sunDir.z * off };
|
||||
DrawSphere(dp, rr * 1.02f, Color{12, 12, 16, 255});
|
||||
}
|
||||
}
|
||||
rlPopMatrix();
|
||||
EndMode3D();
|
||||
EndTextureMode();
|
||||
@ -106,11 +188,13 @@ void Viewer::renderGlobe3D() {
|
||||
void Viewer::renderMap2D() {
|
||||
DrawRectangleRec(mapRect, Color{6, 8, 14, 255});
|
||||
BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height);
|
||||
drawMap2D(planet, vcolors, map2D, mapRect, mapLon);
|
||||
drawMap2D(planet, displayColors(), map2D, mapRect, mapLon);
|
||||
if (showGrat) { drawGraticule2D(graticule, mapRect, mapLon); drawGraticuleLabels2D(mapRect, mapLon); }
|
||||
if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, mapRect, mapLon);
|
||||
if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, mapRect, mapLon);
|
||||
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, mapRect, mapLon);
|
||||
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, mapRect, mapLon);
|
||||
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon);
|
||||
if (phase3 && showRivers) {
|
||||
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon);
|
||||
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon);
|
||||
@ -131,6 +215,95 @@ void Viewer::renderMap2D() {
|
||||
DrawText("2D Equal Earth (hover, drag to pan)", (int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255});
|
||||
}
|
||||
|
||||
// Live World "Sky & tides" panel in the freed space right of the (left-aligned) 2D map:
|
||||
// the current phase of every moon, and the tidal phase of a selected coastal tile.
|
||||
void Viewer::renderLiveInfo() {
|
||||
if (!liveWorld) return;
|
||||
Rectangle r = liveInfoRect;
|
||||
DrawRectangleRec(r, Color{10, 12, 20, 235});
|
||||
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
||||
int x = (int)r.x + 14, y = (int)r.y + 10;
|
||||
DrawText("Sky & tides", x, y, 20, RAYWHITE); y += 30;
|
||||
|
||||
// Sky geometry at the current clock (recomputed here so the panel is self-contained).
|
||||
const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays;
|
||||
double days = liveTime / dayH;
|
||||
double doy = days / yrD; doy -= std::floor(doy);
|
||||
double tod = days - std::floor(days);
|
||||
const double dStep = 0.03; // ~ for waxing/waning + rising/falling
|
||||
double days2 = days + dStep, doy2 = days2 / yrD - std::floor(days2 / yrD), tod2 = days2 - std::floor(days2);
|
||||
Vec3 sun = planet.sunDirection(doy, tod);
|
||||
Vec3 sun2 = planet.sunDirection(doy2, tod2);
|
||||
auto illumFrac = [](const Vec3& moon, const Vec3& s) { return (1.0 - moon.dot(s)) * 0.5; };
|
||||
auto phaseName = [](double f, bool wax) -> const char* {
|
||||
if (f < 0.04) return "New";
|
||||
if (f > 0.96) return "Full";
|
||||
if (f > 0.46 && f < 0.54) return wax ? "First quarter" : "Last quarter";
|
||||
if (f < 0.5) return wax ? "Waxing crescent" : "Waning crescent";
|
||||
return wax ? "Waxing gibbous" : "Waning gibbous";
|
||||
};
|
||||
// A small 2D phase disc: dark circle with the lit fraction filled (terminator ellipse).
|
||||
auto drawPhase = [](float cx, float cy, float rad, double f, bool wax) {
|
||||
DrawCircle((int)cx, (int)cy, rad, Color{26, 28, 36, 255});
|
||||
double cosphi = 1.0 - 2.0 * f; // terminator x = w * cosphi
|
||||
for (int dy = -(int)rad; dy <= (int)rad; ++dy) {
|
||||
double w = std::sqrt(std::max(0.0, (double)rad * rad - (double)dy * dy));
|
||||
double xt = w * cosphi, xa, xb;
|
||||
if (wax) { xa = xt; xb = w; } else { xa = -w; xb = -xt; }
|
||||
if (xb > xa) DrawLine((int)(cx + xa), (int)(cy + dy), (int)(cx + xb), (int)(cy + dy), Color{226, 226, 232, 255});
|
||||
}
|
||||
DrawCircleLines((int)cx, (int)cy, rad, Color{120, 124, 145, 255});
|
||||
};
|
||||
|
||||
const auto& mns = planet.getMoons();
|
||||
for (size_t m = 0; m < mns.size(); ++m) {
|
||||
Vec3 md = planet.moonDirection((int)m, tod, days);
|
||||
Vec3 md2 = planet.moonDirection((int)m, tod2, days2);
|
||||
double f = illumFrac(md, sun);
|
||||
bool wax = illumFrac(md2, sun2) >= f;
|
||||
float cy = (float)y + 20.0f;
|
||||
drawPhase((float)x + 22.0f, cy, 20.0f, f, wax);
|
||||
DrawText(TextFormat("Moon %d: %s", (int)m + 1, phaseName(f, wax)), x + 52, y + 6, 17, Color{210, 215, 225, 255});
|
||||
DrawText(TextFormat("%.0f%% lit period %.0f d", f * 100.0, mns[m].periodDays), x + 52, y + 27, 15, Color{150, 160, 175, 255});
|
||||
y += 50;
|
||||
}
|
||||
if (mns.empty()) { DrawText("(no moons)", x, y, 16, Color{150, 155, 170, 255}); y += 24; }
|
||||
|
||||
// Tidal phase for a selected coastal tile (placeholder: high/low + rising/falling).
|
||||
y += 8;
|
||||
DrawText("Tidal phase", x, y, 18, Color{200, 205, 220, 255}); y += 26;
|
||||
if (selectedCell >= 0 && selectedCell < (int)planet.cells.size()) {
|
||||
const Cell& c = planet.cells[selectedCell];
|
||||
const double sea = planet.cfg.seaLevel;
|
||||
bool selLand = c.elevation > sea, coastal = false;
|
||||
for (int nb : c.neighbors) if ((planet.cells[nb].elevation > sea) != selLand) { coastal = true; break; }
|
||||
if (coastal) {
|
||||
// Single-cell tide now vs a step ahead -> rising/falling (the field itself is the
|
||||
// equilibrium tide; a richer coastal/resonant model is future work).
|
||||
auto cellTide = [&](double dy, double td, double dd) {
|
||||
double h = 0.0;
|
||||
for (int mm = 0; mm < (int)mns.size(); ++mm) {
|
||||
double cc = c.unit.dot(planet.moonDirection(mm, td, dd));
|
||||
h += mns[mm].tideWeight * (cc * cc - 1.0 / 3.0);
|
||||
}
|
||||
double cs = c.unit.dot(planet.sunDirection(dy, td));
|
||||
h += planet.cfg.tideSunFactor * (cs * cs - 1.0 / 3.0);
|
||||
return h * planet.cfg.tideAmplitude;
|
||||
};
|
||||
double t0 = cellTide(doy, tod, days), t1 = cellTide(doy2, tod2, days2);
|
||||
bool rising = t1 >= t0;
|
||||
DrawText(TextFormat("coastal cell #%d", selectedCell), x, y, 15, Color{160, 170, 185, 255}); y += 21;
|
||||
DrawText(TextFormat("%+.2f m %s, %s", t0, t0 >= 0.0 ? "high" : "low", rising ? "rising" : "falling"),
|
||||
x, y, 17, tideColor(t0, std::max(0.05, std::fabs(t0)))); y += 24;
|
||||
DrawText("(equilibrium model - placeholder)", x, y, 13, Color{120, 125, 140, 255});
|
||||
} else {
|
||||
DrawText("selected tile is inland", x, y, 15, Color{150, 155, 170, 255});
|
||||
}
|
||||
} else {
|
||||
DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255});
|
||||
}
|
||||
}
|
||||
|
||||
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
||||
void Viewer::renderPanels() {
|
||||
drawHoverPanel(planet, hoverRect, hovered, selectedCell);
|
||||
@ -139,7 +312,7 @@ void Viewer::renderPanels() {
|
||||
planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge,
|
||||
panelRect, gridRect, hoveredSubIdx);
|
||||
else
|
||||
drawStats(planet, panelRect, elapsedMy, settled);
|
||||
drawStats(planet, panelRect, elapsedMy, settled, liveWorld, liveTime);
|
||||
}
|
||||
|
||||
// Top-left HUD text + the clickable pause button.
|
||||
@ -154,15 +327,34 @@ void Viewer::renderHUD() {
|
||||
int y = 10;
|
||||
auto line = [&](const std::string& s){ DrawText(s.c_str(), 12, y, 18, RAYWHITE); y += 22; };
|
||||
double fastest = 0.0; for (const auto& pl : planet.plates) fastest = std::max(fastest, pl.speedCmYr);
|
||||
line(!settled ? "Planet Sim - World Creation: forming"
|
||||
: phase3 ? "Planet Sim - World Creation: hydrology"
|
||||
: "Planet Sim - World Creation: drift & erosion");
|
||||
line(liveWorld ? "Planet Sim - Live World"
|
||||
: !settled ? "Planet Sim - World Creation: forming"
|
||||
: phase3 ? "Planet Sim - World Creation: hydrology"
|
||||
: "Planet Sim - World Creation: drift & erosion");
|
||||
line(TextFormat("Cells: %d Subdiv: %d CellWidth: %.0f km",
|
||||
(int)planet.cells.size(), cfg.subdivisions, planet.cellWidthMeters() / 1000.0));
|
||||
line(TextFormat("Elevation: %.0f .. %.0f m", minE, maxE));
|
||||
if (!settled)
|
||||
line(TextFormat("Forming terrain tick %lld max change %.1f m/tick%s",
|
||||
stepCount, maxChange, paused ? " [PAUSED]" : ""));
|
||||
else if (liveWorld) {
|
||||
const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays;
|
||||
double days = liveTime / dayH;
|
||||
long year = (long)std::floor(days / yrD) + 1;
|
||||
long doy = (long)std::floor(days - std::floor(days / yrD) * yrD) + 1;
|
||||
double hod = liveTime - std::floor(days) * dayH; // hours into the current day
|
||||
int hh = (int)hod, mm = (int)((hod - hh) * 60.0);
|
||||
line(TextFormat("Live World Year %ld Day %ld %02d:%02d%s",
|
||||
year, doy, hh, mm, paused ? " [PAUSED]" : ""));
|
||||
const double weekH = 7.0 * dayH, monthH = 30.0 * dayH;
|
||||
const char* rl; double rv;
|
||||
if (liveRate >= monthH) { rl = "mo/s"; rv = liveRate / monthH; }
|
||||
else if (liveRate >= weekH) { rl = "wk/s"; rv = liveRate / weekH; }
|
||||
else if (liveRate >= dayH) { rl = "d/s"; rv = liveRate / dayH; }
|
||||
else { rl = "h/s"; rv = liveRate; }
|
||||
line(TextFormat("rate %.1f %s day/night %s ([ / ] speed, N toggle, W exit)",
|
||||
rv, rl, dayNightOn ? "on" : "off"));
|
||||
}
|
||||
else {
|
||||
line(TextFormat("%s %.1f My elapsed %.1f My/s%s",
|
||||
phase3 ? "Hydrology - drift, rivers & erosion" : "Drift & erosion",
|
||||
@ -180,10 +372,10 @@ void Viewer::renderHUD() {
|
||||
y += 8;
|
||||
line("hover: cell info | click tile: open detail panel | C close");
|
||||
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga (*6 cycles mean/summer/winter/season)");
|
||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s]",
|
||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off"));
|
||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | R reseed | +/-",
|
||||
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off"));
|
||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
|
||||
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
||||
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
||||
@ -255,6 +447,7 @@ void Viewer::renderFrame() {
|
||||
}
|
||||
|
||||
renderMap2D();
|
||||
renderLiveInfo();
|
||||
renderPanels();
|
||||
renderHUD();
|
||||
renderPrompt();
|
||||
|
||||
@ -33,6 +33,7 @@ void Planet::generate(const PlanetConfig& c) {
|
||||
|
||||
assignPlates();
|
||||
seedInitialRelief();
|
||||
generateMoons(); // Live World satellites (separate RNG; does not perturb tectonics)
|
||||
computeClimate(); // temperature + precipitation fields (biomes read these)
|
||||
classifyBiomes(); // give the fresh world an initial biome per cell
|
||||
computeBiotaDensity(); // derived flora/fauna/funga density (population is on-demand)
|
||||
|
||||
@ -14,6 +14,7 @@ public:
|
||||
PlanetConfig cfg;
|
||||
std::vector<Cell> cells;
|
||||
std::vector<Plate> plates;
|
||||
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved)
|
||||
|
||||
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
||||
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
||||
@ -57,6 +58,37 @@ public:
|
||||
const std::vector<double>& summerTemp() const { return sTempSummer; } // deg C, warmest month
|
||||
const std::vector<double>& winterTemp() const { return sTempWinter; } // deg C, coldest month
|
||||
|
||||
// Live World stage (PlanetLive.cpp): the slow real-time clock's derived fields.
|
||||
// computeInsolation() = instantaneous solar incidence cos(sun angle), 0..1 -- the
|
||||
// physical foundation for live weather + the day/night terminator. computeLiveSeason()
|
||||
// = the live temperature cycling between winterTemp/summerTemp over the year (drives the
|
||||
// live temperature view + the moving snow line). Both derived/not-saved; call after
|
||||
// computeClimate(). dayOfYear01/timeOfDay01 are fractions in [0,1).
|
||||
void computeInsolation(double dayOfYear01, double timeOfDay01);
|
||||
void computeLiveSeason(double dayOfYear01);
|
||||
const std::vector<double>& insolation() const { return sInsolation; } // 0..1 cos incidence
|
||||
const std::vector<double>& liveTemp() const { return sLiveTemp; } // deg C, current season
|
||||
|
||||
// Live World sky + oceans (PlanetOcean.cpp). Sub-solar / sub-lunar directions in model
|
||||
// space at a clock fraction (drive insolation, tides and the 3D sun/moons -- one source of
|
||||
// truth). generateMoons() seeds 1-3 moons from a separate RNG (tectonic determinism intact).
|
||||
Vec3 sunDirection(double dayOfYear01, double timeOfDay01) const;
|
||||
Vec3 moonDirection(int moonIdx, double timeOfDay01, double timeDays) const;
|
||||
Vec3 moonOrbitNormal(int moonIdx, double timeOfDay01) const; // orbit-plane normal (for the ring)
|
||||
const std::vector<Moon>& getMoons() const { return moons; }
|
||||
void generateMoons();
|
||||
// Tides: equilibrium tidal height (m) per cell from the moons + sun at the given clock.
|
||||
// Derived/not saved; recomputed each frame like insolation.
|
||||
void computeTides(double dayOfYear01, double timeOfDay01, double timeDays);
|
||||
const std::vector<double>& tide() const { return sTide; }
|
||||
|
||||
// Ocean surface currents (PlanetOcean.cpp): a per-ocean-cell tangent velocity from wind
|
||||
// stress + Coriolis deflection + coast-following (gyres). Derived/not saved; needs sWind
|
||||
// (computeClimate() computes it, then calls this and feeds warm/cold currents back into
|
||||
// sTemp -- see climateCurrentFactor). Zero on land cells.
|
||||
void computeOceanCurrents();
|
||||
const std::vector<Vec3>& current() const { return sCurrent; }
|
||||
|
||||
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
|
||||
// fields (temperature + normalized precipitation). Derived + written back into
|
||||
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
|
||||
@ -91,7 +123,9 @@ public:
|
||||
// hasBiome: whether the stream carries the per-cell biome byte (save v4+). For
|
||||
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
||||
// hasBiota: whether the stream carries the biota population block (save v7+).
|
||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true);
|
||||
// hasMoons: whether the stream carries the moons block (save v9+); older saves
|
||||
// synthesize moons from the seed instead.
|
||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool hasMoons = true);
|
||||
|
||||
// Helpers for rendering / info.
|
||||
double cellWidthMeters() const; // approx lateral cell spacing
|
||||
@ -160,6 +194,12 @@ private:
|
||||
std::vector<Vec3> sWind;
|
||||
std::vector<int> sUpwind;
|
||||
|
||||
// Live World scratch (derived each frame in Live World; not saved). sInsolation is the
|
||||
// instantaneous solar incidence; sLiveTemp is the temperature for the current day-of-year;
|
||||
// sTide is the equilibrium tidal height (m) from the moons + sun.
|
||||
std::vector<double> sInsolation, sLiveTemp, sTide;
|
||||
std::vector<Vec3> sCurrent; // ocean surface current velocity (tangent; zero on land)
|
||||
|
||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
||||
|
||||
@ -63,6 +63,38 @@ void Planet::computeClimate() {
|
||||
sUpwind[i] = best;
|
||||
}
|
||||
|
||||
// 1b. Ocean currents + their temperature feedback. Currents need the wind just computed.
|
||||
// Warm (poleward-flowing) currents carry equatorial heat toward the poles and cold
|
||||
// (equatorward) currents the reverse; this raises/lowers coastal temperatures. We build a
|
||||
// bounded anomaly on ocean cells (sign = poleward speed) and smooth it onto the coasts.
|
||||
computeOceanCurrents();
|
||||
if (cfg.climateCurrentFactor > 0.0 && (int)sCurrent.size() == n) {
|
||||
double maxSp = 1e-9;
|
||||
for (int i = 0; i < n; ++i) maxSp = std::max(maxSp, sCurrent[i].length());
|
||||
std::vector<double> dT(n, 0.0);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (cells[i].elevation > sea) continue; // ocean cells only
|
||||
const Vec3& nrm = cells[i].unit;
|
||||
Vec3 northT = up - nrm * up.dot(nrm); // tangent toward +latitude
|
||||
double nl = northT.length(); if (nl < 1e-9) continue;
|
||||
northT = northT * (1.0 / nl);
|
||||
double sign = (nrm.y >= 0.0) ? 1.0 : -1.0; // toward the nearer pole
|
||||
double poleward = sCurrent[i].dot(northT) * sign / maxSp; // -1..1
|
||||
dT[i] = cfg.climateCurrentFactor * std::clamp(poleward, -1.0, 1.0);
|
||||
}
|
||||
// Spread the anomaly onto coasts and let it decay inland (neighbour averaging).
|
||||
std::vector<double> tmp(n);
|
||||
for (int p = 0; p < 3; ++p) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double s = dT[i]; int c = 1;
|
||||
for (int nb : cells[i].neighbors) { s += dT[nb]; ++c; }
|
||||
tmp[i] = s / c;
|
||||
}
|
||||
dT.swap(tmp);
|
||||
}
|
||||
for (int i = 0; i < n; ++i) sTemp[i] += dT[i];
|
||||
}
|
||||
|
||||
// 2. Steady-state moisture advection along the wind (iterative upwind differencing,
|
||||
// double-buffered -> deterministic). Ocean cells are a moisture source; land
|
||||
// cells take their upwind moisture, rain part of it out (more on windward upslopes)
|
||||
|
||||
@ -29,11 +29,13 @@
|
||||
D(biomeWetlandMoist) D(biomeDesertMoist) D(biomeGrassMoist) D(biomeTaigaMoist) \
|
||||
D(biomeLakeMinDepth) D(biomeSeasonWeight) \
|
||||
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
||||
D(climateOroRefHeight) D(climateContinentality) \
|
||||
D(climateOroRefHeight) D(climateContinentality) D(climateCurrentFactor) \
|
||||
D(seasonAmpMax) D(seasonLatExp) D(seasonOceanFactor) \
|
||||
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
|
||||
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
|
||||
D(bioFungaTempMin) D(bioRegionBonus) \
|
||||
D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \
|
||||
D(tideAmplitude) D(tideSunFactor) \
|
||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \
|
||||
@ -169,6 +171,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
|
||||
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
|
||||
E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality"));
|
||||
E(rng(cfg.climateCurrentFactor, 0.0, 30.0, "climateCurrentFactor"));
|
||||
E(rng(cfg.seasonAmpMax, 0.0, 60.0, "seasonAmpMax"));
|
||||
E(rng(cfg.seasonLatExp, 0.1, 6.0, "seasonLatExp"));
|
||||
E(rng(cfg.seasonOceanFactor, 0.0, 1.0, "seasonOceanFactor"));
|
||||
@ -182,6 +185,12 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight"));
|
||||
E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin"));
|
||||
E(rng(cfg.bioRegionBonus, 0.0, 10.0, "bioRegionBonus"));
|
||||
E(rng(cfg.dayLengthHours, 0.1, 1.0e5, "dayLengthHours"));
|
||||
E(rng(cfg.yearLengthDays, 1.0, 1.0e7, "yearLengthDays"));
|
||||
E(rng(cfg.snowTemp, -60.0, 30.0, "snowTemp"));
|
||||
E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp"));
|
||||
E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude"));
|
||||
E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor"));
|
||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||
@ -260,6 +269,7 @@ void Planet::writeState(std::ostream& os) const {
|
||||
writeVec(os, sPrevCount);
|
||||
writeVec(os, sStaleStreak);
|
||||
writeVec(os, sFreePlateIds);
|
||||
writeVec(os, moons); // v9: natural satellites (Live World)
|
||||
// v7: discrete biota population (sBiota). A flag byte gates the block so a
|
||||
// not-yet-populated world stays compact; otherwise three Organism lists per cell.
|
||||
uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio);
|
||||
@ -271,7 +281,7 @@ void Planet::writeState(std::ostream& os) const {
|
||||
}
|
||||
}
|
||||
|
||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) {
|
||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons) {
|
||||
// Read the length-prefixed key=value config block (see writeState). A default
|
||||
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
||||
// current defaults. The length guard rejects pre-v6 (raw-POD-config) saves.
|
||||
@ -300,6 +310,8 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) {
|
||||
readVec(is, sPrevCount);
|
||||
readVec(is, sStaleStreak);
|
||||
readVec(is, sFreePlateIds);
|
||||
if (hasMoons) readVec(is, moons); // v9: natural satellites
|
||||
else generateMoons(); // pre-v9 save: synthesize moons from the seed
|
||||
if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state
|
||||
// v7: discrete biota population. buildGeometry() already sized sBiota empty;
|
||||
// older saves (hasBiota=false) just keep the empty population (press L to fill).
|
||||
|
||||
52
src/sim/PlanetLive.cpp
Normal file
52
src/sim/PlanetLive.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "Planet.hpp"
|
||||
#include "Projection.hpp" // lonLatToDir, dirToLonLat
|
||||
#include <cmath>
|
||||
|
||||
// Live World stage: the slow real-time clock (hours -> weeks/months). Geometry is
|
||||
// fixed, so -- like climate -- these are derived per-cell fields flowed over the grid,
|
||||
// recomputed each frame and never saved. They are the foundation the future weather
|
||||
// simulation will read.
|
||||
//
|
||||
// Sun geometry is computed in MODEL space (the fixed cell unit vectors). The viewer
|
||||
// renders the globe leaned by axialTilt about world Z, but since the lit pattern is
|
||||
// baked per cell in model space it rotates with the globe automatically; the seasonal
|
||||
// lean of day/night is carried by the solar declination below, not the render tilt.
|
||||
// The 2D Equal Earth map also works in model-space lon/lat, so both views stay
|
||||
// consistent without any tilt bookkeeping here.
|
||||
|
||||
// Instantaneous solar incidence cos(theta) in [0,1] per cell.
|
||||
// declination delta = axialTilt * sin(2*pi * dayOfYear01) (0 at equinox, +/-tilt at solstice)
|
||||
// sub-solar lon lambda sweeps the globe once per day (westward as the planet spins east)
|
||||
// sunDir = lonLatToDir(lambda, delta); incidence = max(0, cell.unit . sunDir)
|
||||
void Planet::computeInsolation(double dayOfYear01, double timeOfDay01) {
|
||||
const int n = (int)cells.size();
|
||||
sInsolation.assign(n, 0.0);
|
||||
|
||||
const Vec3 sun = sunDirection(dayOfYear01, timeOfDay01); // shared sky geometry (PlanetOcean.cpp)
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double c = cells[i].unit.dot(sun);
|
||||
sInsolation[i] = c > 0.0 ? c : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Live temperature for the current day-of-year: the annual mean sTemp swung toward the
|
||||
// summer or winter extreme by the seasonal phase, opposite across the two hemispheres.
|
||||
// g = sin(2*pi * dayOfYear01) * sign(lat) in [-1,1] (+1 = this hemisphere's summer)
|
||||
// liveTemp = mean + A * g, A = (summerTemp - winterTemp)/2 (the seasonal half-amplitude)
|
||||
// At g=+1 -> summer, g=-1 -> winter, equator (g~0) -> mean. Requires computeClimate() first.
|
||||
void Planet::computeLiveSeason(double dayOfYear01) {
|
||||
const int n = (int)cells.size();
|
||||
sLiveTemp.assign(n, 0.0);
|
||||
if ((int)sTemp.size() != n) return; // no climate yet
|
||||
const bool haveSeason = ((int)sTempSummer.size() == n && (int)sTempWinter.size() == n);
|
||||
|
||||
const double season = std::sin(2.0 * M_PI * dayOfYear01);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double mean = sTemp[i];
|
||||
if (!haveSeason) { sLiveTemp[i] = mean; continue; }
|
||||
double A = 0.5 * (sTempSummer[i] - sTempWinter[i]); // >= 0
|
||||
double g = season * (cells[i].unit.y >= 0.0 ? 1.0 : -1.0);
|
||||
sLiveTemp[i] = mean + A * g;
|
||||
}
|
||||
}
|
||||
152
src/sim/PlanetOcean.cpp
Normal file
152
src/sim/PlanetOcean.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include "Planet.hpp"
|
||||
#include "Projection.hpp" // lonLatToDir
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
// Live World sky & oceans. Geometry is fixed, so the moons are world objects (not cells) and
|
||||
// the tide is a derived per-cell field flowed over the grid -- both animated by the live clock.
|
||||
// Stage 1: moon generation, sub-solar/sub-lunar directions (one source of truth, shared by
|
||||
// insolation/tides/render), and the equilibrium tide. (Stage 2 adds ocean currents here.)
|
||||
|
||||
namespace {
|
||||
// Rotations about the model axes (Y = north/spin axis). rotateY by a advances longitude by
|
||||
// a: lonLatToDir(lon,lat) rotated by a -> lonLatToDir(lon+a,lat).
|
||||
inline Vec3 rotateX(const Vec3& v, double a) {
|
||||
double c = std::cos(a), s = std::sin(a);
|
||||
return Vec3{ v.x, c * v.y - s * v.z, s * v.y + c * v.z };
|
||||
}
|
||||
inline Vec3 rotateY(const Vec3& v, double a) {
|
||||
double c = std::cos(a), s = std::sin(a);
|
||||
return Vec3{ c * v.x - s * v.z, v.y, s * v.x + c * v.z };
|
||||
}
|
||||
}
|
||||
|
||||
// 1-3 satellites from a SEPARATE RNG (seeded from cfg.seed) so generating moons never touches
|
||||
// the tectonic stream (rngState) -- mirrors the biota RNG isolation.
|
||||
void Planet::generateMoons() {
|
||||
moons.clear();
|
||||
uint32_t s = cfg.seed ? (cfg.seed ^ 0x900D5EEDu) : 0x900D5EEDu;
|
||||
auto next = [&]() { s ^= s << 13; s ^= s >> 17; s ^= s << 5; return s; };
|
||||
auto rf = [&]() { return (next() & 0xFFFFFFu) / double(0x1000000); };
|
||||
int count = 1 + (int)(next() % 3u); // 1..3
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Moon m;
|
||||
m.orbitRadius = 8.0 + rf() * 22.0; // 8..30 planet radii (render scale)
|
||||
m.periodDays = 6.0 + rf() * 54.0; // 6..60 days
|
||||
m.phase = rf() * 2.0 * M_PI;
|
||||
m.inclination = (rf() * 2.0 - 1.0) * (30.0 * M_PI / 180.0); // +/-30 deg
|
||||
m.tideWeight = 0.3 + rf() * 0.9; // 0.3..1.2 (Moon mass proxy)
|
||||
m.dispRadius = 0.05 + m.tideWeight * 0.06; // visual size ~ mass
|
||||
moons.push_back(m);
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-solar direction (model space). Celestial dir at a reference longitude, leaned by the
|
||||
// seasonal declination, then spun by the planet's rotation (-2*pi*timeOfDay). Equivalent to the
|
||||
// old lonLatToDir(pi*(1-2t), decl), so day/night is unchanged.
|
||||
Vec3 Planet::sunDirection(double dayOfYear01, double timeOfDay01) const {
|
||||
double decl = cfg.axialTilt * M_PI / 180.0 * std::sin(2.0 * M_PI * dayOfYear01);
|
||||
Vec3 C = lonLatToDir(M_PI, decl);
|
||||
return rotateY(C, -2.0 * M_PI * timeOfDay01);
|
||||
}
|
||||
|
||||
// Sub-lunar direction (model space): the moon orbits in an inclined plane, then the planet spin
|
||||
// sweeps it across the sky (so a fixed cell sees ~one lunar pass per day).
|
||||
Vec3 Planet::moonDirection(int moonIdx, double timeOfDay01, double timeDays) const {
|
||||
const Moon& m = moons[moonIdx];
|
||||
double Om = 2.0 * M_PI * timeDays / std::max(1e-9, m.periodDays) + m.phase;
|
||||
Vec3 eq{ std::cos(Om), 0.0, std::sin(Om) }; // equatorial orbit circle
|
||||
Vec3 C = rotateX(eq, m.inclination); // incline the orbit plane
|
||||
return rotateY(C, -2.0 * M_PI * timeOfDay01); // planet spin
|
||||
}
|
||||
|
||||
// Ocean surface currents: a per-ocean-cell tangent velocity. Wind drags the surface, Coriolis
|
||||
// deflects it (right in the N hemisphere, left in the S), and coasts block the across-shore
|
||||
// component so the flow turns to follow the shore -- which closes into gyres. A few smoothing
|
||||
// passes make the field coherent. Approximate at 223 km/cell, but it gives plausible streams +
|
||||
// the warm/cold pattern the climate feedback reads. Needs sWind (computeClimate sets it).
|
||||
void Planet::computeOceanCurrents() {
|
||||
const int n = (int)cells.size();
|
||||
sCurrent.assign(n, Vec3{0, 0, 0});
|
||||
if ((int)sWind.size() != n) return;
|
||||
const double sea = cfg.seaLevel;
|
||||
auto isOcean = [&](int i) { return cells[i].elevation <= sea; };
|
||||
|
||||
// 1. Wind-driven base velocity, rotated by a Coriolis deflection about the local normal.
|
||||
const double deflect = 35.0 * M_PI / 180.0;
|
||||
std::vector<Vec3> v(n, Vec3{0, 0, 0});
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!isOcean(i)) continue;
|
||||
const Vec3& w = sWind[i];
|
||||
if (w.length() < 1e-9) continue;
|
||||
const Vec3& nrm = cells[i].unit;
|
||||
double lat = std::asin(std::clamp(nrm.y, -1.0, 1.0));
|
||||
double th = deflect * (lat >= 0.0 ? -1.0 : 1.0); // deflect right (N) / left (S)
|
||||
v[i] = w * std::cos(th) + nrm.cross(w) * std::sin(th); // rotate w about the normal
|
||||
}
|
||||
|
||||
// 2. Coast deflection: remove any component flowing INTO a land neighbour, so the stream
|
||||
// bends to run along the coast (the seed of gyre circulation).
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!isOcean(i) || v[i].length() < 1e-12) continue;
|
||||
const Vec3& nrm = cells[i].unit;
|
||||
for (int nb : cells[i].neighbors) {
|
||||
if (isOcean(nb)) continue;
|
||||
Vec3 d = cells[nb].unit - cells[i].unit;
|
||||
d = d - nrm * d.dot(nrm); // tangent direction to the land
|
||||
double dl = d.length(); if (dl < 1e-12) continue;
|
||||
d = d * (1.0 / dl);
|
||||
double into = v[i].dot(d);
|
||||
if (into > 0.0) v[i] = v[i] - d * into;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Smooth over the ocean (neighbour average, re-projected to the tangent plane).
|
||||
std::vector<Vec3> tmp(n);
|
||||
for (int p = 0; p < 3; ++p) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!isOcean(i)) { tmp[i] = Vec3{0, 0, 0}; continue; }
|
||||
Vec3 s = v[i]; int c = 1;
|
||||
for (int nb : cells[i].neighbors) if (isOcean(nb)) { s = s + v[nb]; ++c; }
|
||||
Vec3 a = s * (1.0 / c);
|
||||
const Vec3& nrm = cells[i].unit;
|
||||
tmp[i] = a - nrm * a.dot(nrm); // re-tangent
|
||||
}
|
||||
v.swap(tmp);
|
||||
}
|
||||
sCurrent.swap(v);
|
||||
}
|
||||
|
||||
// Orbit-plane normal (model space) for moon i at the current spin -- the orbit is the great
|
||||
// circle perpendicular to this, used to draw the faint orbit ring.
|
||||
Vec3 Planet::moonOrbitNormal(int moonIdx, double timeOfDay01) const {
|
||||
Vec3 nrm = rotateX(Vec3{ 0.0, 1.0, 0.0 }, moons[moonIdx].inclination);
|
||||
return rotateY(nrm, -2.0 * M_PI * timeOfDay01);
|
||||
}
|
||||
|
||||
// Equilibrium tide (meters) per cell from every tide-raising body (moons + sun). Each body
|
||||
// raises two bulges -- toward it and its antipode -- via the (cos^2 - 1/3) tidal potential, so
|
||||
// the field is zero-mean and a cell rises/falls ~twice a day as the bulges sweep past.
|
||||
void Planet::computeTides(double dayOfYear01, double timeOfDay01, double timeDays) {
|
||||
const int n = (int)cells.size();
|
||||
sTide.assign(n, 0.0);
|
||||
|
||||
struct Body { Vec3 dir; double w; };
|
||||
std::vector<Body> bodies;
|
||||
bodies.reserve(moons.size() + 1);
|
||||
for (int i = 0; i < (int)moons.size(); ++i)
|
||||
bodies.push_back({ moonDirection(i, timeOfDay01, timeDays), moons[i].tideWeight });
|
||||
bodies.push_back({ sunDirection(dayOfYear01, timeOfDay01), cfg.tideSunFactor });
|
||||
|
||||
const double amp = cfg.tideAmplitude;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double h = 0.0;
|
||||
for (const auto& b : bodies) {
|
||||
double c = cells[i].unit.dot(b.dir);
|
||||
h += b.w * (c * c - 1.0 / 3.0);
|
||||
}
|
||||
sTide[i] = amp * h;
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,20 @@ enum class Biome : uint8_t {
|
||||
Desert, Forest, Taiga, Tundra, Hills, Mountains
|
||||
};
|
||||
|
||||
// A natural satellite (Live World). Geometry is fixed, so the moon is a world object
|
||||
// (not a cell): it orbits on the live clock, raises tides, and renders as a small sphere.
|
||||
// Generated 1-3 per world from a separate RNG (so it never perturbs tectonic determinism)
|
||||
// and saved (v9). orbitRadius is in planet radii (render scale); tideWeight is the tide-
|
||||
// raising mass proxy; inclination tilts the orbit plane off the equator.
|
||||
struct Moon {
|
||||
double orbitRadius = 16.0; // planet radii (visual orbit distance)
|
||||
double periodDays = 20.0; // orbital period (planetary days)
|
||||
double phase = 0.0; // orbital phase offset (radians)
|
||||
double inclination = 0.0; // orbit-plane tilt off the equator (radians)
|
||||
double tideWeight = 1.0; // tide-raising strength (Moon mass proxy)
|
||||
double dispRadius = 0.10; // display sphere radius (visual size)
|
||||
};
|
||||
|
||||
struct Plate {
|
||||
int id = 0;
|
||||
PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells
|
||||
@ -200,6 +214,8 @@ struct PlanetConfig {
|
||||
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
||||
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
||||
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
|
||||
double climateCurrentFactor = 4.0; // C: max coastal warming/cooling from ocean currents
|
||||
// (warm poleward currents raise, cold equatorward lower)
|
||||
|
||||
// --- Seasons (obliquity) -- see PlanetClimate.cpp -----------------------
|
||||
// axialTilt (above) drives a per-cell seasonal temperature range around the annual
|
||||
@ -230,4 +246,15 @@ struct PlanetConfig {
|
||||
int bioFloraPoints = 20; // flora point budget at full density (scaled by density)
|
||||
int bioFaunaPoints = 16; // fauna point budget at full density
|
||||
int bioFungaPoints = 14; // funga point budget at full density
|
||||
|
||||
// --- Live World (slow real-time clock) -- see PlanetLive.cpp -------------
|
||||
// The finished planet can run on a slow real-time clock (hours -> weeks/months) with a
|
||||
// moving day/night terminator, a live seasonal temperature cycle and a moving snow line.
|
||||
// dayLengthHours/yearLengthDays set the calendar; snowTemp/seaIceTemp the freezing lines.
|
||||
double dayLengthHours = 24.0; // hours in one planetary day (rotation -> day/night)
|
||||
double yearLengthDays = 365.25; // days in one planetary year (orbit -> seasons)
|
||||
double snowTemp = 0.0; // C: land below the live temperature shows snow
|
||||
double seaIceTemp = -2.0; // C: ocean below the live temperature shows sea ice
|
||||
double tideAmplitude = 0.6; // m: equilibrium-tide scale per unit tide-raising weight
|
||||
double tideSunFactor = 0.46; // sun's tide weight relative to a unit moon (Earth ~0.46)
|
||||
};
|
||||
|
||||
137
test_live.cpp
Normal file
137
test_live.cpp
Normal file
@ -0,0 +1,137 @@
|
||||
// Headless test for the Live World stage (insolation + live seasonal temperature).
|
||||
// No display / raylib needed.
|
||||
//
|
||||
// g++ -std=c++17 -O2 -Isrc/sim test_live.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/PlanetLive.cpp \
|
||||
// src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||
// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tl && /tmp/tl
|
||||
//
|
||||
// Verifies: insolation range; the sub-solar hemisphere is lit and the night side dark;
|
||||
// the solar declination tracks axialTilt (poles lit/dark at solstice, neutral at equinox);
|
||||
// live temperature stays within the summer/winter band and is anti-phased across the
|
||||
// hemispheres; the snow line advances in the winter hemisphere; and determinism.
|
||||
|
||||
#include "Planet.hpp"
|
||||
#include "Projection.hpp"
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(bool cond, const char* what) {
|
||||
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||
if (!cond) ++failures;
|
||||
}
|
||||
|
||||
// Index of the cell whose unit direction is closest to dir.
|
||||
static int nearestCell(const Planet& p, const Vec3& dir) {
|
||||
int best = 0; double bd = -2.0;
|
||||
for (int i = 0; i < (int)p.cells.size(); ++i) {
|
||||
double d = p.cells[i].unit.dot(dir);
|
||||
if (d > bd) { bd = d; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
int main() {
|
||||
Planet planet;
|
||||
PlanetConfig cfg;
|
||||
cfg.subdivisions = 5;
|
||||
cfg.seed = 1337;
|
||||
planet.generate(cfg);
|
||||
planet.computeClimate(); // live season needs the climate fields
|
||||
const int n = (int)planet.cells.size();
|
||||
|
||||
// North/south pole cells (extreme latitude) for declination checks.
|
||||
int north = 0, south = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (planet.cells[i].unit.y > planet.cells[north].unit.y) north = i;
|
||||
if (planet.cells[i].unit.y < planet.cells[south].unit.y) south = i;
|
||||
}
|
||||
const double tilt = cfg.axialTilt * M_PI / 180.0;
|
||||
|
||||
std::printf("Live World: insolation\n");
|
||||
// --- Range + lit/dark hemispheres (arbitrary time, summer solstice) -------
|
||||
planet.computeInsolation(0.25, 0.30);
|
||||
const std::vector<double>& sun = planet.insolation();
|
||||
bool inRange = true; int lit = 0; double mx = 0.0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (sun[i] < 0.0 || sun[i] > 1.0) inRange = false;
|
||||
if (sun[i] > 0.0) ++lit;
|
||||
mx = std::max(mx, sun[i]);
|
||||
}
|
||||
check(inRange, "insolation in [0,1]");
|
||||
check(lit > (int)(0.40 * n) && lit < (int)(0.60 * n), "about half the planet is in daylight");
|
||||
// Sub-solar cell is fully lit; its antipode is dark.
|
||||
double decl = tilt * std::sin(2.0 * M_PI * 0.25);
|
||||
double lon = M_PI * (1.0 - 2.0 * 0.30);
|
||||
Vec3 sd = lonLatToDir(lon, decl);
|
||||
int subsolar = nearestCell(planet, sd);
|
||||
int antipode = nearestCell(planet, sd * -1.0);
|
||||
check(sun[subsolar] > 0.99, "sub-solar cell is fully lit");
|
||||
check(sun[antipode] == 0.0, "antipodal (midnight) cell is dark");
|
||||
|
||||
std::printf("Live World: declination tracks axial tilt\n");
|
||||
// Equinox (decl=0): both poles near the terminator (~0). Solstice: summer pole lit,
|
||||
// winter pole in polar night.
|
||||
planet.computeInsolation(0.0, 0.0); // equinox
|
||||
double npEq = planet.insolation()[north], spEq = planet.insolation()[south];
|
||||
planet.computeInsolation(0.25, 0.0); // northern summer solstice
|
||||
double npSol = planet.insolation()[north], spSol = planet.insolation()[south];
|
||||
check(npEq < 0.1 && spEq < 0.1, "equinox: both poles near the terminator");
|
||||
check(npSol > 0.3, "summer solstice: summer pole sees the midnight sun");
|
||||
check(spSol == 0.0, "summer solstice: winter pole is in polar night");
|
||||
check(std::fabs(npSol - std::sin(tilt)) < 0.05, "polar insolation ~ sin(axialTilt)");
|
||||
|
||||
std::printf("Live World: seasonal temperature\n");
|
||||
// At northern summer (doy=0.25) live temp = summer in the north, winter in the south;
|
||||
// everywhere it stays within the [winter, summer] band.
|
||||
planet.computeLiveSeason(0.25);
|
||||
const std::vector<double>& lt = planet.liveTemp();
|
||||
const std::vector<double>& summ = planet.summerTemp();
|
||||
const std::vector<double>& wint = planet.winterTemp();
|
||||
bool inBand = true; double nLiveSummer = 0.0; int nN = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (lt[i] < wint[i] - 1e-6 || lt[i] > summ[i] + 1e-6) inBand = false;
|
||||
if (planet.cells[i].unit.y > 0.3) { nLiveSummer += lt[i]; ++nN; }
|
||||
}
|
||||
nLiveSummer /= std::max(1, nN);
|
||||
check(inBand, "live temp within [winter, summer] for every cell");
|
||||
check(std::fabs(lt[north] - summ[north]) < 0.5, "northern summer solstice -> north at its summer temp");
|
||||
check(std::fabs(lt[south] - wint[south]) < 0.5, "northern summer solstice -> south at its winter temp");
|
||||
// Half a year later the northern hemisphere is colder (anti-phase).
|
||||
planet.computeLiveSeason(0.75);
|
||||
double nLiveWinter = 0.0; nN = 0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (planet.cells[i].unit.y > 0.3) { nLiveWinter += planet.liveTemp()[i]; ++nN; }
|
||||
nLiveWinter /= std::max(1, nN);
|
||||
check(nLiveWinter < nLiveSummer - 1.0, "northern hemisphere colder in its winter than its summer");
|
||||
|
||||
std::printf("Live World: snow line advances in winter\n");
|
||||
auto snowCountNorth = [&](double doy) {
|
||||
planet.computeLiveSeason(doy); const std::vector<double>& t = planet.liveTemp();
|
||||
int c = 0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (planet.cells[i].unit.y > 0.0 && planet.cells[i].elevation > cfg.seaLevel
|
||||
&& t[i] < cfg.snowTemp) ++c;
|
||||
return c;
|
||||
};
|
||||
int snowSummer = snowCountNorth(0.25), snowWinter = snowCountNorth(0.75);
|
||||
std::printf(" north land snow cells: summer %d, winter %d\n", snowSummer, snowWinter);
|
||||
check(snowWinter > snowSummer, "more northern land under snow in winter than summer");
|
||||
|
||||
std::printf("Live World: determinism\n");
|
||||
Planet p2; p2.generate(cfg); p2.computeClimate();
|
||||
p2.computeInsolation(0.37, 0.61); p2.computeLiveSeason(0.37);
|
||||
planet.computeInsolation(0.37, 0.61); planet.computeLiveSeason(0.37);
|
||||
bool same = true;
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (p2.insolation()[i] != planet.insolation()[i] || p2.liveTemp()[i] != planet.liveTemp()[i])
|
||||
same = false;
|
||||
check(same, "same seed + time -> identical insolation & live temp");
|
||||
|
||||
std::printf(failures ? "\nSOME LIVE CHECKS FAILED (%d)\n" : "\nALL LIVE CHECKS PASSED\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
144
test_ocean.cpp
Normal file
144
test_ocean.cpp
Normal file
@ -0,0 +1,144 @@
|
||||
// Headless test for the Live World ocean/sky stage (moons + tides). No display needed.
|
||||
//
|
||||
// g++ -std=c++17 -O2 -Isrc/sim test_ocean.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/PlanetLive.cpp \
|
||||
// src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||
// -o /tmp/to && /tmp/to
|
||||
//
|
||||
// Verifies: 1-3 moons generated deterministically; moon/sun directions are unit vectors that
|
||||
// sweep with the clock; the equilibrium tide is a zero-mean two-bulge field (high under a body
|
||||
// and its antipode, low at 90 deg); and save v9 round-trips the moons.
|
||||
|
||||
#include "Planet.hpp"
|
||||
#include "Projection.hpp"
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(bool cond, const char* what) {
|
||||
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||
if (!cond) ++failures;
|
||||
}
|
||||
static int nearestCell(const Planet& p, const Vec3& dir) {
|
||||
int best = 0; double bd = -2.0;
|
||||
for (int i = 0; i < (int)p.cells.size(); ++i) {
|
||||
double d = p.cells[i].unit.dot(dir);
|
||||
if (d > bd) { bd = d; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
int main() {
|
||||
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 1337;
|
||||
Planet planet; planet.generate(cfg);
|
||||
const int n = (int)planet.cells.size();
|
||||
|
||||
std::printf("Ocean: moons\n");
|
||||
int nm = (int)planet.getMoons().size();
|
||||
std::printf(" moon count: %d\n", nm);
|
||||
check(nm >= 1 && nm <= 3, "1-3 moons generated");
|
||||
|
||||
// Determinism: same seed -> identical moons (and tectonic state unchanged by moon RNG).
|
||||
Planet p2; p2.generate(cfg);
|
||||
bool sameMoons = ((int)p2.getMoons().size() == nm);
|
||||
for (int i = 0; i < nm && sameMoons; ++i) {
|
||||
const Moon& a = planet.getMoons()[i]; const Moon& b = p2.getMoons()[i];
|
||||
if (a.orbitRadius != b.orbitRadius || a.periodDays != b.periodDays || a.phase != b.phase
|
||||
|| a.inclination != b.inclination || a.tideWeight != b.tideWeight) sameMoons = false;
|
||||
}
|
||||
check(sameMoons, "moon generation is deterministic for a seed");
|
||||
bool sameCells = true;
|
||||
for (int i = 0; i < n; ++i) if (planet.cells[i].elevation != p2.cells[i].elevation) sameCells = false;
|
||||
check(sameCells, "moon RNG does not perturb tectonic determinism");
|
||||
|
||||
std::printf("Ocean: sky directions\n");
|
||||
Vec3 s0 = planet.sunDirection(0.25, 0.10), s1 = planet.sunDirection(0.25, 0.60);
|
||||
check(std::fabs(s0.length() - 1.0) < 1e-9, "sun direction is a unit vector");
|
||||
check((s0 - s1).length() > 0.1, "sun sweeps as the day advances (time of day)");
|
||||
if (nm > 0) {
|
||||
Vec3 m0 = planet.moonDirection(0, 0.10, 3.0), m1 = planet.moonDirection(0, 0.60, 3.0);
|
||||
check(std::fabs(m0.length() - 1.0) < 1e-9, "moon direction is a unit vector");
|
||||
check((m0 - m1).length() > 0.1, "moon sweeps across the sky with the clock");
|
||||
}
|
||||
|
||||
std::printf("Ocean: tides (single equatorial moon, sun off)\n");
|
||||
// Isolate one equatorial moon so the two-bulge shape is unambiguous.
|
||||
Planet t; t.generate(cfg);
|
||||
t.cfg.tideSunFactor = 0.0;
|
||||
t.moons.clear();
|
||||
Moon m; m.orbitRadius = 20; m.periodDays = 27; m.phase = 0; m.inclination = 0; m.tideWeight = 1; m.dispRadius = 0.1;
|
||||
t.moons.push_back(m);
|
||||
t.computeTides(0.0, 0.25, 5.0);
|
||||
const std::vector<double> tide = t.tide(); // copy (t.tide() is overwritten on recompute)
|
||||
double sum = 0.0, mx = -1e30, mn = 1e30;
|
||||
for (int i = 0; i < n; ++i) { sum += tide[i]; mx = std::max(mx, tide[i]); mn = std::min(mn, tide[i]); }
|
||||
double mean = sum / n;
|
||||
check(std::fabs(mean) < 0.02, "tide field is ~zero-mean");
|
||||
check(mx > 0.0 && mn < 0.0, "tide has highs and lows (bulges + troughs)");
|
||||
Vec3 md = t.moonDirection(0, 0.25, 5.0);
|
||||
int sub = nearestCell(t, md);
|
||||
int anti = nearestCell(t, md * -1.0);
|
||||
int pole = nearestCell(t, Vec3{0, 1, 0}); // 90 deg from an equatorial moon
|
||||
check(tide[sub] > 0.0 && tide[anti] > 0.0, "high tide under the moon AND its antipode");
|
||||
check(tide[pole] < 0.0, "low tide at 90 degrees from the moon");
|
||||
|
||||
// The bulge moves with the clock. (Use a quarter-ish step, not 0.5: half a day apart the
|
||||
// moon is at the antipode and the tide -- being cos^2 symmetric -- is correctly identical.)
|
||||
t.computeTides(0.0, 0.40, 5.0);
|
||||
bool moved = false;
|
||||
for (int i = 0; i < n; ++i) if (std::fabs(t.tide()[i] - tide[i]) > 1e-6) { moved = true; break; }
|
||||
check(moved, "the tidal bulge moves as time of day advances");
|
||||
|
||||
std::printf("Ocean: currents\n");
|
||||
const std::vector<Vec3>& cur = planet.current(); // populated by generate()'s computeClimate
|
||||
check((int)cur.size() == n, "current field sized to the grid");
|
||||
bool tangent = true, landZero = true; int flowing = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (planet.cells[i].elevation <= cfg.seaLevel) {
|
||||
if (std::fabs(cur[i].dot(planet.cells[i].unit)) > 1e-6) tangent = false;
|
||||
if (cur[i].length() > 1e-9) ++flowing;
|
||||
} else if (cur[i].length() > 1e-12) landZero = false;
|
||||
}
|
||||
check(tangent, "ocean currents are tangent to the surface");
|
||||
check(landZero, "currents are zero on land");
|
||||
check(flowing > 50, "many ocean cells carry a current");
|
||||
|
||||
// Climate feedback: identical terrain, current factor on vs off.
|
||||
PlanetConfig cfgOff = cfg; cfgOff.climateCurrentFactor = 0.0;
|
||||
Planet conOn; conOn.generate(cfg);
|
||||
Planet conOff; conOff.generate(cfgOff);
|
||||
double maxAbs = 0.0, maxWarm = -1e9, maxCold = 1e9;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double d = conOn.temperature()[i] - conOff.temperature()[i];
|
||||
maxAbs = std::max(maxAbs, std::fabs(d));
|
||||
maxWarm = std::max(maxWarm, d); maxCold = std::min(maxCold, d);
|
||||
}
|
||||
check(maxAbs <= cfg.climateCurrentFactor + 1e-6, "current temp feedback bounded by climateCurrentFactor");
|
||||
check(maxWarm > 0.1 && maxCold < -0.1, "currents both warm and cool coasts");
|
||||
Planet conOn2; conOn2.generate(cfg);
|
||||
bool det = true;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (conOn2.temperature()[i] != conOn.temperature()[i]) det = false;
|
||||
if ((conOn2.current()[i] - conOn.current()[i]).length() != 0.0) det = false;
|
||||
}
|
||||
check(det, "currents + feedback deterministic for a seed");
|
||||
|
||||
std::printf("Ocean: save v9\n");
|
||||
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||
planet.writeState(ss);
|
||||
Planet q;
|
||||
bool ok = q.readState(ss, true, true, true);
|
||||
bool moonsRT = ok && (int)q.getMoons().size() == nm;
|
||||
for (int i = 0; i < nm && moonsRT; ++i)
|
||||
if (q.getMoons()[i].periodDays != planet.getMoons()[i].periodDays
|
||||
|| q.getMoons()[i].tideWeight != planet.getMoons()[i].tideWeight) moonsRT = false;
|
||||
check(moonsRT, "save v9 round-trips the moons");
|
||||
|
||||
std::printf(failures ? "\nSOME OCEAN CHECKS FAILED (%d)\n" : "\nALL OCEAN CHECKS PASSED\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user