Compare commits
No commits in common. "aa32e38dad2186b04a20c2b9ce8fd6e7fc8686ef" and "53e371bb2f2c50946421b74d25549b3ca39e2b9b" have entirely different histories.
aa32e38dad
...
53e371bb2f
47
BUILD.md
47
BUILD.md
@ -47,12 +47,8 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
J toggle rivers (Phase 2.5 hydrology)
|
J toggle rivers (Phase 2.5 hydrology)
|
||||||
H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion)
|
H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion)
|
||||||
L generate biota population (flora/fauna/funga; settled world; re-press regenerates)
|
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)
|
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
||||||
[ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month)
|
[ / ] drift speed (My per real second, Phase 2/3)
|
||||||
S single tectonic tick
|
S single tectonic tick
|
||||||
F fast-forward Phase-1 forming to settled (instant)
|
F fast-forward Phase-1 forming to settled (instant)
|
||||||
R reseed planet (restart forming)
|
R reseed planet (restart forming)
|
||||||
@ -71,9 +67,8 @@ CLI flags (applied before the first load/generate):
|
|||||||
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
||||||
auto-created on first run, reload live with F2. Range-checked on
|
auto-created on first run, reload live with F2. Range-checked on
|
||||||
load; an invalid file reverts to safe defaults (not overwritten).
|
load; an invalid file reverts to safe defaults (not overwritten).
|
||||||
planet.save binary snapshot (versioned, currently v9: +moons; v8 +Live World clock;
|
planet.save binary snapshot (versioned, currently v6): seed + config + full planet
|
||||||
v7 +biota): seed + config + full planet state; F5 writes it, F9 reloads and
|
state; F5 writes it, F9 reloads and resumes deterministically. As of v6
|
||||||
resumes deterministically. As of v6
|
|
||||||
the config is stored as a self-describing key=value block (like
|
the config is stored as a self-describing key=value block (like
|
||||||
planet.cfg), so adding/removing config fields no longer breaks saves
|
planet.cfg), so adding/removing config fields no longer breaks saves
|
||||||
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
|
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
|
||||||
@ -104,9 +99,6 @@ Phase 1 forms as before; tall mountains grow during Phase-2 drift.
|
|||||||
arcFactor 1.4 continental subduction-arc uplift (Andes)
|
arcFactor 1.4 continental subduction-arc uplift (Andes)
|
||||||
isostaticPersist 0.85 how strongly high crust resists relax (0..<1)
|
isostaticPersist 0.85 how strongly high crust resists relax (0..<1)
|
||||||
rootScale 2500 m above continentBase where persistence saturates
|
rootScale 2500 m above continentBase where persistence saturates
|
||||||
peakSoftCapStart 7000 m below this a tick's uplift always takes (P=1)
|
|
||||||
peakSoftCapEnd 12000 m at/above this uplift never takes (P=0); also the elev clamp
|
|
||||||
peakFailDrop 200 m max random drop when the grow roll loses (drift-only)
|
|
||||||
ridgeDepth -2500 m shallow elevation of brand-new crust at a ridge
|
ridgeDepth -2500 m shallow elevation of brand-new crust at a ridge
|
||||||
seafloorSubsidence 280 m per sqrt(My): seafloor deepens with crustal age
|
seafloorSubsidence 280 m per sqrt(My): seafloor deepens with crustal age
|
||||||
seafloorSeedAge 80 My initial oceanic age spread at generation
|
seafloorSeedAge 80 My initial oceanic age spread at generation
|
||||||
@ -118,7 +110,7 @@ Drift keeps running at a finer timestep during hydrology. (Config keys keep the
|
|||||||
phase3AfterMy 300 My drift before the "Start Phase 2.5?" prompt
|
phase3AfterMy 300 My drift before the "Start Phase 2.5?" prompt
|
||||||
phase3DtScale 0.2 hydrology timestep = cflDtMy()*this (finer = slower drift)
|
phase3DtScale 0.2 hydrology timestep = cflDtMy()*this (finer = slower drift)
|
||||||
rainfall 1.0 uniform precip per cell (drainage-area unit)
|
rainfall 1.0 uniform precip per cell (drainage-area unit)
|
||||||
riverThreshold 25 discharge above which a cell is a river (lower = richer network)
|
riverThreshold 50 discharge above which a cell is a river
|
||||||
riverIncision 0.02 K in stream-power incision K*Q^m*S^n*dt (raise = carve faster)
|
riverIncision 0.02 K in stream-power incision K*Q^m*S^n*dt (raise = carve faster)
|
||||||
riverDischargeExp 0.5 m (discharge exponent)
|
riverDischargeExp 0.5 m (discharge exponent)
|
||||||
riverSlopeExp 1.0 n (slope exponent)
|
riverSlopeExp 1.0 n (slope exponent)
|
||||||
@ -145,15 +137,6 @@ Biomes (PlanetConfig, Phase 3): per-cell biome classification thresholds. Temper
|
|||||||
biomeGrassMoist 0.50 moisture below this -> Grassland/Savanna, else Forest
|
biomeGrassMoist 0.50 moisture below this -> Grassland/Savanna, else Forest
|
||||||
biomeTaigaMoist 0.40 cool + above this -> Taiga (else Tundra)
|
biomeTaigaMoist 0.40 cool + above this -> Taiga (else Tundra)
|
||||||
biomeLakeMinDepth 20 m filled-basin depth above sea level counting as a Lake
|
biomeLakeMinDepth 20 m filled-basin depth above sea level counting as a Lake
|
||||||
biomeSeasonWeight 0.6 winter temp weight in the Tundra/Taiga cutoffs (0 = annual mean only)
|
|
||||||
|
|
||||||
Seasons (PlanetConfig): axialTilt (above) drives per-cell summer/winter temps; color key 6
|
|
||||||
cycles temperature -> summer -> winter -> seasonality.
|
|
||||||
|
|
||||||
seasonAmpMax 18 C max seasonal half-range at full tilt/latitude/interior
|
|
||||||
seasonLatExp 1.2 latitude shape exponent (>1 pushes swing toward the poles)
|
|
||||||
seasonOceanFactor 0.15 ocean/coast seasonal-swing floor (thermal inertia)
|
|
||||||
seasonContinentRings 6 ocean-distance rings to reach full continentality (1 ~ 223 km)
|
|
||||||
|
|
||||||
Climate (PlanetConfig, Phase 3): temperature uses the biome* temp params above;
|
Climate (PlanetConfig, Phase 3): temperature uses the biome* temp params above;
|
||||||
precipitation advects ocean moisture along zonal winds (windward rain, leeward rain
|
precipitation advects ocean moisture along zonal winds (windward rain, leeward rain
|
||||||
@ -166,7 +149,6 @@ shadow, dry interiors) then diffuses it. Color modes 6 (temperature) / 7 (precip
|
|||||||
climateContinentality 0.05 fractional moisture lost per inland cell (dries interiors)
|
climateContinentality 0.05 fractional moisture lost per inland cell (dries interiors)
|
||||||
climateWindPasses 50 moisture-advection iterations (steady state)
|
climateWindPasses 50 moisture-advection iterations (steady state)
|
||||||
climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest)
|
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;
|
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).
|
the discrete slot/point population is generated on demand (L) and saved (v7).
|
||||||
@ -188,32 +170,17 @@ the discrete slot/point population is generated on demand (L) and saved (v7).
|
|||||||
bioFaunaPoints 16 fauna point budget at full density
|
bioFaunaPoints 16 fauna point budget at full density
|
||||||
bioFungaPoints 14 funga 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)
|
## Headless logic test (no display)
|
||||||
|
|
||||||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
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/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
||||||
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
||||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \
|
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||||
-o /tmp/t && /tmp/t
|
-o /tmp/t && /tmp/t
|
||||||
|
|
||||||
# Biota / Live World / Ocean suites: same source list, swap test_logic.cpp ->
|
# Biota suite: same source list, swap test_logic.cpp -> test_biota.cpp
|
||||||
# test_biota.cpp, test_live.cpp or test_ocean.cpp
|
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
determinism. Run after changing Planet::step().
|
determinism. Run after changing Planet::step().
|
||||||
|
|||||||
176
CLAUDE.md
176
CLAUDE.md
@ -47,42 +47,16 @@ dynamic weather and life.
|
|||||||
generated **on demand** (`L`) and **saved** (save v7). Fauna is a herbivore/carnivore/
|
generated **on demand** (`L`) and **saved** (save v7). Fauna is a herbivore/carnivore/
|
||||||
omnivore food chain (predators gated on local prey); funga uses a flora-like but
|
omnivore food chain (predators gated on local prey); funga uses a flora-like but
|
||||||
moisture/organic-matter-led rule. The living/evolving ecosystem is reserved for Live World.
|
moisture/organic-matter-led rule. The living/evolving ecosystem is reserved for Live World.
|
||||||
- **Seasons (obliquity)** *(done)* — `axialTilt` drives per-cell summer/winter temperatures
|
Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
|
||||||
(`computeClimate`: `sTempSummer`/`sTempWinter` = annual mean ± a tilt/latitude/continentality
|
|
||||||
amplitude); winter temp feeds the Tundra/Taiga biome cutoffs (`biomeSeasonWeight`). Static
|
|
||||||
fields (the live yearly cycle is reserved for Live World). Color key `6` cycles the temp views.
|
|
||||||
|
|
||||||
> Durable design context (module layout, save format, climate/biome model, conventions)
|
> Durable design context (module layout, save format, climate/biome model, conventions)
|
||||||
> lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not
|
> lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not
|
||||||
> travel with the repo.
|
> travel with the repo.
|
||||||
|
|
||||||
**Live World (in progress):** with planet creation finished, run the world at a slow real-time
|
**Live World (future):** with planet creation finished, run the world at a slow real-time
|
||||||
scale with dynamic **weather** (clouds, rain, storms, fronts), day/night, and living
|
scale with dynamic **weather** (clouds, rain, storms, fronts), day/night, and living
|
||||||
**ecosystems / civilization** evolving in real time. This is a separate large effort;
|
**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.
|
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
|
## Current state
|
||||||
|
|
||||||
@ -278,20 +252,6 @@ Working and verified (logic tested headless):
|
|||||||
runs before `classifyBiomes()` in `generate()` and `refreshView()`. New `climate*` config
|
runs before `classifyBiomes()` in `generate()` and `refreshView()`. New `climate*` config
|
||||||
knobs (planet.cfg). Headless: equator warm/poles cold, lapse, coastal wetter than interior,
|
knobs (planet.cfg). Headless: equator warm/poles cold, lapse, coastal wetter than interior,
|
||||||
deserts present, deterministic.
|
deserts present, deterministic.
|
||||||
- **Seasons (obliquity):** `axialTilt` (previously visual-only) now drives a per-cell seasonal
|
|
||||||
temperature range. `computeClimate()` adds derived `sTempSummer`/`sTempWinter` (= annual mean
|
|
||||||
`sTemp` ± a half-amplitude `A = seasonAmpMax·tiltFactor·latShape·continentality`), where
|
|
||||||
`tiltFactor = sin(axialTilt)/sin(23.44°)` (0 tilt → no seasons) and **continentality** comes
|
|
||||||
from a multi-source BFS ring-distance from ocean cells (coasts/oceans muted by thermal
|
|
||||||
inertia, interiors swing most). Big swings at high-latitude continental interiors, ~0 at the
|
|
||||||
equatorial coast. `classifyBiomes()` blends **winter** temp into the Tundra/Taiga cold cutoffs
|
|
||||||
via `biomeSeasonWeight` (0 = annual-mean-only/old behaviour, default 0.6) so cold-winter
|
|
||||||
interiors turn boreal/tundra (Siberia effect) — the amplitude is geographically shaped, so
|
|
||||||
this expands cold biomes only where seasons bite. Derived/not-saved (no save bump). Color key
|
|
||||||
`6` now **cycles** mean→summer→winter→seasonality; cell-info shows summer/winter. New `season*`
|
|
||||||
+ `biomeSeasonWeight` config knobs. Headless: equator swing ≈1.6 °C vs ≈20 °C at high latitude,
|
|
||||||
interior land ≫ ocean, tilt=0 → no seasons, higher tilt → bigger swing, `biomeSeasonWeight=0`
|
|
||||||
leaves biomes unchanged, cold-biome count rises with seasons, deterministic.
|
|
||||||
- **UI polish (full cell info + view label + framing):** the cell-info panel
|
- **UI polish (full cell info + view label + framing):** the cell-info panel
|
||||||
(`cellInfo`, src/render/Panels.cpp) now shows everything per cell — crust type, **biome**
|
(`cellInfo`, src/render/Panels.cpp) now shows everything per cell — crust type, **biome**
|
||||||
(`biomeName`), **temperature** + **precipitation %**, and **river/lake** when hydrology is
|
(`biomeName`), **temperature** + **precipitation %**, and **river/lake** when hydrology is
|
||||||
@ -322,54 +282,6 @@ Working and verified (logic tested headless):
|
|||||||
organism list. `bio*` config knobs. Headless `test_biota.cpp`: density ranges/zeros, fauna≤
|
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,
|
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`).
|
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
|
- 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
|
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
|
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
|
||||||
@ -401,8 +313,6 @@ src/
|
|||||||
PlanetErosion.cpp erode() + adjustSeaLevel()
|
PlanetErosion.cpp erode() + adjustSeaLevel()
|
||||||
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
||||||
PlanetClimate.cpp computeClimate() (temperature + orographic precipitation)
|
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)
|
PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate)
|
||||||
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
||||||
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
||||||
@ -467,13 +377,12 @@ raylib 5.5 is fetched automatically — do not vendor it.
|
|||||||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
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/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
||||||
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
||||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \
|
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||||
-o /tmp/t && /tmp/t
|
-o /tmp/t && /tmp/t
|
||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp` or `test_ocean.cpp` to run the
|
(Swap `test_logic.cpp` for `test_biota.cpp` to run the Biota suite — same source list.)
|
||||||
Biota / Live World / Ocean suites — same source list.)
|
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
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
|
the window (the engine lives in `src/sim` and is raylib-free, so it links without
|
||||||
@ -500,16 +409,12 @@ LMB drag orbit · wheel zoom · hover for cell info (3D or map) ·
|
|||||||
click a tile to open its detail panel (subtiles) · `C` close panel ·
|
click a tile to open its detail panel (subtiles) · `C` close panel ·
|
||||||
drag the 2D map to pan it east/west · `1`..`0` color by
|
drag the 2D map to pan it east/west · `1`..`0` color by
|
||||||
elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
|
elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
|
||||||
(`8`/`9`/`0` = biota density; `6` **cycles** temperature → summer → winter → seasonality;
|
(`8`/`9`/`0` = biota density; active mode shown top-center of the globe) ·
|
||||||
active mode shown top-center of the globe) ·
|
|
||||||
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
||||||
all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) ·
|
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
||||||
`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 ·
|
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
||||||
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
||||||
on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) ·
|
on a settled world; re-press regenerates) · `R` reseed ·
|
||||||
`R` reseed ·
|
|
||||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
||||||
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
||||||
|
|
||||||
@ -518,13 +423,6 @@ 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
|
keeps running at a finer timestep (`cflDtMy()*phase3DtScale`) while rivers, lakes
|
||||||
and fluvial erosion evolve.
|
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
|
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
||||||
file (both applied before the initial load/generate).
|
file (both applied before the initial load/generate).
|
||||||
|
|
||||||
@ -534,13 +432,11 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
|||||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
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
|
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||||
save header is versioned (currently **9**; v2 adds the `[`/`]` drift rate, v3 a
|
save header is versioned (currently **7**; v2 adds the `[`/`]` drift rate, v3 a
|
||||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
`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**
|
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, v8 appends the **Live World**
|
block — three Organism lists per cell, gated by a flag byte); newer-than-supported is
|
||||||
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`).
|
||||||
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
|
**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),
|
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 —
|
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —
|
||||||
@ -548,10 +444,8 @@ a one-time break; a length guard makes that fail gracefully.) `writeConfigFields
|
|||||||
`parseConfigStream` in PlanetIO.cpp are shared by `planet.cfg` and the save.
|
`parseConfigStream` in PlanetIO.cpp are shared by `planet.cfg` and the save.
|
||||||
|
|
||||||
Layout (1920x1080): left column 70% wide = 3D globe (top, 60% h, RenderTexture
|
Layout (1920x1080): left column 70% wide = 3D globe (top, 60% h, RenderTexture
|
||||||
1344x648) + 2D Equal Earth map (bottom, 40% h, **left-aligned**, with the freed space at
|
1344x648) + 2D Equal Earth map (bottom, 40% h); right column 30% wide = cell info
|
||||||
its right holding the Live-World **"Sky & tides" panel** — `liveInfoRect`, `renderLiveInfo`:
|
(top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the
|
||||||
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
|
3D viewport (1344x648 at the origin -- GetScreenToWorldRay assumes the full
|
||||||
screen, wrong here); 2D hover uses EqualEarth::inverse (minus the `mapLon` pan).
|
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
|
Borders (`B`), drift vectors (`D`) and a lat/lon graticule (`G`) draw in BOTH the
|
||||||
@ -599,15 +493,6 @@ triangles (plates are fixed in phase 1).
|
|||||||
`climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes →
|
`climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes →
|
||||||
wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`,
|
wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`,
|
||||||
`climateOroRefHeight`, `climateWindPasses`. Temperature uses the `biome*` temp params.
|
`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`
|
|
||||||
(6, ocean-distance to full continentality; lower = coasts go continental sooner),
|
|
||||||
`seasonOceanFactor` (0.15, ocean/coast swing floor). `biomeSeasonWeight` (0.6) sets how much
|
|
||||||
winter temp drives the Tundra/Taiga cutoffs (0 = annual-mean only, restores pre-seasons biomes).
|
|
||||||
- Biota (`bio*` in PlanetConfig / `planet.cfg`) — density: `bioVegTempMin`/`bioVegTempOpt`/
|
- Biota (`bio*` in PlanetConfig / `planet.cfg`) — density: `bioVegTempMin`/`bioVegTempOpt`/
|
||||||
`bioVegMoistRef` (flora temp/moisture limits), `bioFaunaProductivity` (animals per unit
|
`bioVegMoistRef` (flora temp/moisture limits), `bioFaunaProductivity` (animals per unit
|
||||||
flora), `bioCarnPreyMin`/`bioCarnScale` (carnivore prey gate + ramp), `bioFungaMoistRef`/
|
flora), `bioCarnPreyMin`/`bioCarnScale` (carnivore prey gate + ramp), `bioFungaMoistRef`/
|
||||||
@ -617,18 +502,6 @@ triangles (plates are fixed in phase 1).
|
|||||||
it; Tiny=1…Huge=5), `bioRegionBonus` (how strongly a cell copies same-biome neighbours →
|
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
|
homogeneity vs variety). To add organisms, append to `biotaArchetypes()` in PlanetBiota.cpp
|
||||||
(append-only — indices are serialized in v7 saves).
|
(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
|
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||||
knob for how fast/high relief builds.
|
knob for how fast/high relief builds.
|
||||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||||
@ -662,24 +535,9 @@ triangles (plates are fixed in phase 1).
|
|||||||
toward 1 = ranges barely erode and continents stay high; lower = more dynamic
|
toward 1 = ranges barely erode and continents stay high; lower = more dynamic
|
||||||
rise/erode) saturating at `rootScale` (2500 m above `continentBase`). These three
|
rise/erode) saturating at `rootScale` (2500 m above `continentBase`). These three
|
||||||
boosts are **drift-only** (gated on `Planet::drifting`); in Phase-2 drift the
|
boosts are **drift-only** (gated on `Planet::drifting`); in Phase-2 drift the
|
||||||
per-tick `erode()` is what limits their height, so they don't rail the clamp.
|
per-tick `erode()` is what limits their height, so they don't rail the clamp. The
|
||||||
- **Soft peak cap (drift-only) — no 9000 m plateau:** previously the hard
|
hard clamp `[-11000, 9000]` in `step()` is the Everest-class cap; a few peak
|
||||||
`[-11000, 9000]` clamp (in `step()`, `erode()` *and* `hydrology()`) flattened many
|
cells may sit there during drift (<2% — fine).
|
||||||
drift-time peaks into a 9000 m plateau. Now growth is **probabilistic** above
|
|
||||||
`peakSoftCapStart` (7000 m): the chance a tick's positive uplift "takes" falls
|
|
||||||
linearly to 0 at `peakSoftCapEnd` (12000 m), so peaks spread smoothly across a
|
|
||||||
height band instead of railing at one ceiling. A **lost** grow roll forfeits that
|
|
||||||
tick's uplift *and* shaves a random `0..peakFailDrop` (200 m) off, so a peak hovers
|
|
||||||
near its own height (height now tracks orogeny strength: strong seeds reach
|
|
||||||
10–11 km, most cluster lower). The hard clamp's **upper bound now tracks
|
|
||||||
`peakSoftCapEnd`** in all three places (just a safety rail; lower −11000 m
|
|
||||||
unchanged). The roll is a **pure hash of `(cellIndex, erodeIter, seed)`** — never
|
|
||||||
touches `rngState`, stays bit-identical across OpenMP thread counts, and since
|
|
||||||
`erodeIter` is saved (step/erode run 1:1 in drift) F5/F9 resumes **bit-identical**
|
|
||||||
(no save-version bump). **Drift-only** (gated on `drifting`) so Phase-1 forming
|
|
||||||
still auto-settles. Tune `peakSoftCapStart` / `peakSoftCapEnd` / `peakFailDrop` in
|
|
||||||
`planet.cfg`. Verified headless: smooth 8.5→10.5 km taper, 0 cells pinned at the
|
|
||||||
ceiling, determinism + exact resume intact.
|
|
||||||
- **Seafloor aging->depth (PlanetConfig, Phase 2 inc. 4):** `oceanBase` is now the
|
- **Seafloor aging->depth (PlanetConfig, Phase 2 inc. 4):** `oceanBase` is now the
|
||||||
**deep abyssal floor** (-6000 m, not a flat ocean base) and `ridgeDepth` the
|
**deep abyssal floor** (-6000 m, not a flat ocean base) and `ridgeDepth` the
|
||||||
shallow young value (-2500 m); `seafloorSubsidence` (280 m per sqrt(My)) sets how
|
shallow young value (-2500 m); `seafloorSubsidence` (280 m per sqrt(My)) sets how
|
||||||
@ -690,7 +548,7 @@ triangles (plates are fixed in phase 1).
|
|||||||
before the Phase-3 prompt; `phase3DtScale` (0.2) — Phase-3 timestep =
|
before the Phase-3 prompt; `phase3DtScale` (0.2) — Phase-3 timestep =
|
||||||
`cflDtMy()*this` (smaller = finer carving, slower drift per step); `rainfall`
|
`cflDtMy()*this` (smaller = finer carving, slower drift per step); `rainfall`
|
||||||
(1.0) — uniform precip per cell (drainage-area unit; orographic precip is a later
|
(1.0) — uniform precip per cell (drainage-area unit; orographic precip is a later
|
||||||
climate add-on); `riverThreshold` (25) — discharge above which a cell is a river
|
climate add-on); `riverThreshold` (50) — discharge above which a cell is a river
|
||||||
(also the river-render threshold); `riverIncision` K (0.02) + `riverDischargeExp`
|
(also the river-render threshold); `riverIncision` K (0.02) + `riverDischargeExp`
|
||||||
m (0.5) + `riverSlopeExp` n (1.0) — stream-power incision `K*Q^m*S^n*dt` (raise K
|
m (0.5) + `riverSlopeExp` n (1.0) — stream-power incision `K*Q^m*S^n*dt` (raise K
|
||||||
for faster valley carving); `riverTransport` (0.1) — transport capacity
|
for faster valley carving); `riverTransport` (0.1) — transport capacity
|
||||||
|
|||||||
@ -26,8 +26,6 @@ add_executable(planetsim
|
|||||||
src/sim/PlanetHydrology.cpp
|
src/sim/PlanetHydrology.cpp
|
||||||
src/sim/PlanetBiomes.cpp
|
src/sim/PlanetBiomes.cpp
|
||||||
src/sim/PlanetClimate.cpp
|
src/sim/PlanetClimate.cpp
|
||||||
src/sim/PlanetLive.cpp
|
|
||||||
src/sim/PlanetOcean.cpp
|
|
||||||
src/sim/PlanetBiota.cpp
|
src/sim/PlanetBiota.cpp
|
||||||
src/sim/PlanetFloraGen.cpp
|
src/sim/PlanetFloraGen.cpp
|
||||||
src/sim/PlanetFaunaGen.cpp
|
src/sim/PlanetFaunaGen.cpp
|
||||||
|
|||||||
@ -31,10 +31,6 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
|||||||
- `PlanetHydrology.cpp` — `routeFlow`/`computeHydrology`/`hydrology` (depression-fill→lakes,
|
- `PlanetHydrology.cpp` — `routeFlow`/`computeHydrology`/`hydrology` (depression-fill→lakes,
|
||||||
steepest-descent→rivers, mass-conserving stream-power incision).
|
steepest-descent→rivers, mass-conserving stream-power incision).
|
||||||
- `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation).
|
- `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).
|
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
||||||
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
||||||
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
||||||
@ -96,15 +92,7 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede
|
|||||||
|
|
||||||
`computeClimate()` builds two derived per-cell fields:
|
`computeClimate()` builds two derived per-cell fields:
|
||||||
- **Temperature** (°C) = latitude curve (`biomeEquatorTemp/PoleDrop/LatExp`, super-linear so
|
- **Temperature** (°C) = latitude curve (`biomeEquatorTemp/PoleDrop/LatExp`, super-linear so
|
||||||
cold concentrates at poles) − `biomeElevLapse` × elevation. This is the **annual mean**; the
|
cold concentrates at poles) − `biomeElevLapse` × elevation.
|
||||||
**Seasons** pass adds derived `sTempSummer`/`sTempWinter` = mean ± `A`, where the seasonal
|
|
||||||
half-amplitude `A = seasonAmpMax · sin(axialTilt)/sin(23.44°) · latShape · continentality`.
|
|
||||||
Continentality is a multi-source-BFS ring distance from ocean cells (oceans/coasts muted by
|
|
||||||
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
|
- **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
|
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
|
out more on windward upslopes (orographic), loses a multiplicative fraction per cell
|
||||||
@ -116,61 +104,6 @@ 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 &
|
deserts emerge; 13 biomes incl. polar Ice; wetlands require water adjacency. All biome &
|
||||||
climate thresholds are tunable `biome*` / `climate*` keys in `planet.cfg`.
|
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
|
## Headless testing
|
||||||
|
|
||||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||||
@ -178,7 +111,6 @@ 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 \
|
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/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp \
|
||||||
src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||||
src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
# test_biota.cpp uses the same source list (Biota suite).
|
# test_biota.cpp uses the same source list (Biota suite).
|
||||||
|
|||||||
@ -91,9 +91,6 @@ const char* colorModeName(ColorMode m) {
|
|||||||
case ColorMode::FloraDensity: return "Flora density";
|
case ColorMode::FloraDensity: return "Flora density";
|
||||||
case ColorMode::FaunaDensity: return "Fauna density";
|
case ColorMode::FaunaDensity: return "Fauna density";
|
||||||
case ColorMode::FungaDensity: return "Funga density";
|
case ColorMode::FungaDensity: return "Funga density";
|
||||||
case ColorMode::TempSummer: return "Temperature (summer)";
|
|
||||||
case ColorMode::TempWinter: return "Temperature (winter)";
|
|
||||||
case ColorMode::Seasonality: return "Seasonality (summer-winter)";
|
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
@ -141,27 +138,6 @@ Color tempColor(double celsius) {
|
|||||||
return Color{ L(0), L(1), L(2), 255 };
|
return Color{ L(0), L(1), L(2), 255 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seasonality ramp: summer-winter range in deg C, ~[0, 45]: calm grey -> warm orange.
|
|
||||||
Color seasonColor(double rangeC) {
|
|
||||||
static const unsigned char lo[3] = { 95, 100, 110 }, hi[3] = { 235, 130, 40 };
|
|
||||||
double t = std::clamp(rangeC / 45.0, 0.0, 1.0);
|
|
||||||
auto L = [&](int c){ return (unsigned char)(lo[c] + (hi[c] - lo[c]) * t); };
|
|
||||||
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).
|
// Precipitation ramp over normalized [0,1]: tan (dry) -> green -> teal/blue (wet).
|
||||||
Color precipColor(double moist01) {
|
Color precipColor(double moist01) {
|
||||||
double t = std::clamp(moist01, 0.0, 1.0);
|
double t = std::clamp(moist01, 0.0, 1.0);
|
||||||
|
|||||||
@ -5,8 +5,7 @@
|
|||||||
// Cell color mapping for the viewer. Pure functions of cell properties.
|
// Cell color mapping for the viewer. Pure functions of cell properties.
|
||||||
|
|
||||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
||||||
FloraDensity, FaunaDensity, FungaDensity,
|
FloraDensity, FaunaDensity, FungaDensity };
|
||||||
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
|
|
||||||
|
|
||||||
Color elevationColor(double e, double seaLevel);
|
Color elevationColor(double e, double seaLevel);
|
||||||
Color plateColor(int id);
|
Color plateColor(int id);
|
||||||
@ -23,11 +22,6 @@ const char* colorModeName(ColorMode m);
|
|||||||
// (tan dry -> green -> blue wet).
|
// (tan dry -> green -> blue wet).
|
||||||
Color tempColor(double celsius);
|
Color tempColor(double celsius);
|
||||||
Color precipColor(double moist01);
|
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,
|
// Biota density ramps (0..1): flora barren->lush green, fauna pale->amber/red,
|
||||||
// funga pale->violet/brown.
|
// funga pale->violet/brown.
|
||||||
Color floraColor(double d01);
|
Color floraColor(double d01);
|
||||||
|
|||||||
@ -97,71 +97,6 @@ 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>> buildGraticule() {
|
||||||
std::vector<std::vector<Vector2>> g;
|
std::vector<std::vector<Vector2>> g;
|
||||||
const double D = M_PI / 180.0;
|
const double D = M_PI / 180.0;
|
||||||
@ -236,23 +171,6 @@ void drawSegments2D(const std::vector<Vector3>& segs, Color col, float width,
|
|||||||
rlEnd(); rlSetLineWidth(1.0f);
|
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,
|
void drawSubgrids(const std::vector<std::shared_ptr<SubGrid>>& sgs,
|
||||||
float visBase, float elevExagg, double seaLevel, float eps) {
|
float visBase, float elevExagg, double seaLevel, float eps) {
|
||||||
for (const auto& sg : sgs) {
|
for (const auto& sg : sgs) {
|
||||||
|
|||||||
@ -25,20 +25,6 @@ void buildDriftArrows(const Planet& p, float radius,
|
|||||||
void buildRivers(const Planet& p, float radius,
|
void buildRivers(const Planet& p, float radius,
|
||||||
std::vector<Vector3>& rivers, std::vector<Vector3>& bigRivers);
|
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 ------------------------------------------------------
|
// ---- Lat/lon graticule ------------------------------------------------------
|
||||||
// Polylines of (lon,lat) radians (Vector2.x=lon, .y=lat).
|
// Polylines of (lon,lat) radians (Vector2.x=lon, .y=lat).
|
||||||
std::vector<std::vector<Vector2>> buildGraticule();
|
std::vector<std::vector<Vector2>> buildGraticule();
|
||||||
@ -52,11 +38,6 @@ void drawGraticuleLabels2D(Rectangle r, double lonOffset);
|
|||||||
void drawSegments2D(const std::vector<Vector3>& segs, Color col, float width,
|
void drawSegments2D(const std::vector<Vector3>& segs, Color col, float width,
|
||||||
Rectangle r, double lonOffset);
|
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) ------------------
|
// ---- Subgrid overlay (high-res patch over a selected cell) ------------------
|
||||||
void drawSubgrids(const std::vector<std::shared_ptr<SubGrid>>& sgs,
|
void drawSubgrids(const std::vector<std::shared_ptr<SubGrid>>& sgs,
|
||||||
float visBase, float elevExagg, double seaLevel, float eps);
|
float visBase, float elevExagg, double seaLevel, float eps);
|
||||||
|
|||||||
@ -50,20 +50,6 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
|||||||
if (sized(p.temperature()) && sized(p.moisture()))
|
if (sized(p.temperature()) && sized(p.moisture()))
|
||||||
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
|
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
|
||||||
p.temperature()[i], p.moisture()[i] * 100.0)));
|
p.temperature()[i], p.moisture()[i] * 100.0)));
|
||||||
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())));
|
L.push_back(std::string(TextFormat("geoAge %.0f My neighbors %d", age, (int)c.neighbors.size())));
|
||||||
// Hydrology (derived; present once routeFlow()/hydrology() has run).
|
// Hydrology (derived; present once routeFlow()/hydrology() has run).
|
||||||
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)
|
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)
|
||||||
@ -177,8 +163,7 @@ 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});
|
DrawRectangleRec(r, Color{12, 14, 22, 235});
|
||||||
DrawRectangleLinesEx(r, 1, Color{120, 120, 150, 255});
|
DrawRectangleLinesEx(r, 1, Color{120, 120, 150, 255});
|
||||||
int x = (int)r.x + 16, y = (int)r.y + 12;
|
int x = (int)r.x + 16, y = (int)r.y + 12;
|
||||||
@ -210,7 +195,6 @@ void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
|||||||
L(TextFormat("Surface area: %.0f M km2", surfKm2 / 1.0e6));
|
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("Plates: %d active %d continental / %d oceanic", used, contPlates, used - contPlates));
|
||||||
L(TextFormat("Young ridges: %d strips %d cells", babyPlates, babyCells));
|
L(TextFormat("Young ridges: %d strips %d cells", babyPlates, babyCells));
|
||||||
L(TextFormat("Moons: %d", (int)p.getMoons().size()));
|
|
||||||
int water = N - landGeo;
|
int water = N - landGeo;
|
||||||
double wlRatio = landGeo > 0 ? (double)water / landGeo : 0.0;
|
double wlRatio = landGeo > 0 ? (double)water / landGeo : 0.0;
|
||||||
L(TextFormat("Land %.0f%% Ocean %.0f%% (water:land %.2f:1)",
|
L(TextFormat("Land %.0f%% Ocean %.0f%% (water:land %.2f:1)",
|
||||||
@ -218,10 +202,7 @@ void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
|||||||
L(TextFormat("Sea level: %+.0f m", seaLvl));
|
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("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));
|
L(TextFormat("Elevation: %.0f .. %.0f m mean %.0f m", mn, mx, sum / N));
|
||||||
if (live) L(TextFormat("Live World: year %ld, day %.1f",
|
if (drifting) L(TextFormat("Sim time: %.0f My", elapsedMy));
|
||||||
(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;
|
y += 8;
|
||||||
DrawText("plate cells size area speed land", x, y, 15, Color{150, 155, 170, 255}); y += 21;
|
DrawText("plate cells size area speed land", x, y, 15, Color{150, 155, 170, 255}); y += 21;
|
||||||
|
|
||||||
|
|||||||
@ -17,5 +17,4 @@ void drawDetailPanel(const Planet& p, const std::shared_ptr<SubGrid>& sg,
|
|||||||
void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected);
|
void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected);
|
||||||
|
|
||||||
// World statistics panel (shown in the subareas quadrant when no tile 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,13 +35,9 @@ bool Viewer::init(int argc, char** argv) {
|
|||||||
int mapH = mapAreaH - 30;
|
int mapH = mapAreaH - 30;
|
||||||
int mapW = (int)(mapH * (EqualEarth::halfWidth() / EqualEarth::halfHeight()));
|
int mapW = (int)(mapH * (EqualEarth::halfWidth() / EqualEarth::halfHeight()));
|
||||||
if (mapW > leftW - 30) { mapW = leftW - 30; mapH = (int)(mapW / (EqualEarth::halfWidth() / EqualEarth::halfHeight())); }
|
if (mapW > leftW - 30) { mapW = leftW - 30; mapH = (int)(mapW / (EqualEarth::halfWidth() / EqualEarth::halfHeight())); }
|
||||||
// Left-align the 2D map (was centered) so the freed space at right holds the live sky panel.
|
mapRect = Rectangle{ (float)((leftW - mapW) / 2),
|
||||||
const float mapMargin = 16.0f;
|
|
||||||
mapRect = Rectangle{ mapMargin,
|
|
||||||
(float)(mapAreaY + (mapAreaH - mapH) / 2),
|
(float)(mapAreaY + (mapAreaH - mapH) / 2),
|
||||||
(float)mapW, (float)mapH };
|
(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.
|
// Right column.
|
||||||
hoverRect = Rectangle{ (float)rightX + 8, 8.0f, (float)rightW - 16, (float)rightH - 16 };
|
hoverRect = Rectangle{ (float)rightX + 8, 8.0f, (float)rightW - 16, (float)rightH - 16 };
|
||||||
@ -100,8 +96,6 @@ void Viewer::selectCell(int idx) {
|
|||||||
void Viewer::recolor() {
|
void Viewer::recolor() {
|
||||||
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
|
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
|
||||||
const std::vector<double>& temp = planet.temperature();
|
const std::vector<double>& temp = planet.temperature();
|
||||||
const std::vector<double>& summer = planet.summerTemp();
|
|
||||||
const std::vector<double>& winter = planet.winterTemp();
|
|
||||||
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
|
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
|
||||||
const std::vector<double>& flora = planet.floraDensity();
|
const std::vector<double>& flora = planet.floraDensity();
|
||||||
const std::vector<double>& fauna = planet.faunaDensity();
|
const std::vector<double>& fauna = planet.faunaDensity();
|
||||||
@ -120,10 +114,6 @@ void Viewer::recolor() {
|
|||||||
case ColorMode::Crust: vcolors[i] = crustColor(planet.cells[i].oceanic); break;
|
case ColorMode::Crust: vcolors[i] = crustColor(planet.cells[i].oceanic); break;
|
||||||
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
|
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
|
||||||
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[i]); break;
|
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[i]); break;
|
||||||
case ColorMode::TempSummer: vcolors[i] = summer.empty()? Color{90,90,90,255} : tempColor(summer[i]); break;
|
|
||||||
case ColorMode::TempWinter: vcolors[i] = winter.empty()? Color{90,90,90,255} : tempColor(winter[i]); break;
|
|
||||||
case ColorMode::Seasonality: vcolors[i] = (summer.empty()||winter.empty()) ? Color{90,90,90,255}
|
|
||||||
: seasonColor(summer[i] - winter[i]); break;
|
|
||||||
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
|
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
|
||||||
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255} : floraColor(flora[i]); break;
|
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255} : floraColor(flora[i]); break;
|
||||||
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255} : faunaColor(fauna[i]); break;
|
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255} : faunaColor(fauna[i]); break;
|
||||||
@ -140,71 +130,6 @@ void Viewer::recolor() {
|
|||||||
minE = planet.minElevation(); maxE = planet.maxElevation();
|
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() {
|
void Viewer::refreshView() {
|
||||||
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
||||||
planet.computeClimate(); // temperature + precipitation fields
|
planet.computeClimate(); // temperature + precipitation fields
|
||||||
@ -216,8 +141,6 @@ void Viewer::refreshView() {
|
|||||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||||
}
|
}
|
||||||
if (phase3) buildRivers(planet, riverR, rivers, bigRivers);
|
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();
|
if (selectedCell >= 0) rebuildSub();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,15 +174,12 @@ void Viewer::saveGame(const char* path) {
|
|||||||
std::ofstream os(path, std::ios::binary);
|
std::ofstream os(path, std::ios::binary);
|
||||||
if (!os) { setStatus("Save failed"); return; }
|
if (!os) { setStatus("Save failed"); return; }
|
||||||
uint32_t ver = SAVE_VERSION; uint8_t st = settled ? 1 : 0; uint8_t p3 = phase3 ? 1 : 0;
|
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("PLSV", 4);
|
||||||
os.write(reinterpret_cast<const char*>(&ver), sizeof ver);
|
os.write(reinterpret_cast<const char*>(&ver), sizeof ver);
|
||||||
os.write(reinterpret_cast<const char*>(&elapsedMy), sizeof elapsedMy);
|
os.write(reinterpret_cast<const char*>(&elapsedMy), sizeof elapsedMy);
|
||||||
os.write(reinterpret_cast<const char*>(&st), sizeof st);
|
os.write(reinterpret_cast<const char*>(&st), sizeof st);
|
||||||
os.write(reinterpret_cast<const char*>(&driftRate), sizeof driftRate);
|
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*>(&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);
|
planet.writeState(os);
|
||||||
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
||||||
}
|
}
|
||||||
@ -268,24 +188,20 @@ void Viewer::loadGame(const char* path) {
|
|||||||
std::ifstream is(path, std::ios::binary);
|
std::ifstream is(path, std::ios::binary);
|
||||||
if (!is) { setStatus(std::string("No ") + path); return; }
|
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;
|
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(magic, 4);
|
||||||
is.read(reinterpret_cast<char*>(&ver), sizeof ver);
|
is.read(reinterpret_cast<char*>(&ver), sizeof ver);
|
||||||
is.read(reinterpret_cast<char*>(&em), sizeof em);
|
is.read(reinterpret_cast<char*>(&em), sizeof em);
|
||||||
is.read(reinterpret_cast<char*>(&st), sizeof st);
|
is.read(reinterpret_cast<char*>(&st), sizeof st);
|
||||||
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
||||||
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
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 (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota, v9: moons
|
if (!planet.readState(is, ver >= 4, ver >= 7)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota
|
||||||
cfg = planet.cfg; // adopt the loaded config
|
cfg = planet.cfg; // adopt the loaded config
|
||||||
elapsedMy = em; settled = (st != 0);
|
elapsedMy = em; settled = (st != 0);
|
||||||
planet.drifting = settled; // resume drift boosts iff mid-drift
|
planet.drifting = settled; // resume drift boosts iff mid-drift
|
||||||
phase3 = (p3 != 0); phase3Prompt = false;
|
phase3 = (p3 != 0); phase3Prompt = false;
|
||||||
phase3PromptAt = phase3 ? elapsedMy : (elapsedMy + planet.cfg.phase3AfterMy);
|
phase3PromptAt = phase3 ? elapsedMy : (elapsedMy + planet.cfg.phase3AfterMy);
|
||||||
driftRate = dr;
|
driftRate = dr;
|
||||||
liveWorld = (lw != 0); liveTime = lh; // v8: resume the Live World clock
|
|
||||||
settleRun = settleNeed; // keep the settled latch consistent
|
settleRun = settleNeed; // keep the settled latch consistent
|
||||||
dtMy = settled ? planet.cflDtMy() : 0.0;
|
dtMy = settled ? planet.cflDtMy() : 0.0;
|
||||||
driftAccum = 0.0; formAccum = 0.0;
|
driftAccum = 0.0; formAccum = 0.0;
|
||||||
@ -300,28 +216,6 @@ void Viewer::loadGame(const char* path) {
|
|||||||
// Advance the simulation this frame: Phase-1 forming (paced ticks toward
|
// Advance the simulation this frame: Phase-1 forming (paced ticks toward
|
||||||
// equilibrium), or Phase-2 drift / Phase-3 drift+hydrology at a finer dt.
|
// equilibrium), or Phase-2 drift / Phase-3 drift+hydrology at a finer dt.
|
||||||
void Viewer::stepSim() {
|
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) {
|
if (!paused && !settled) {
|
||||||
// --- Phase 1: forming, paced ticks toward equilibrium -------------
|
// --- Phase 1: forming, paced ticks toward equilibrium -------------
|
||||||
formAccum += GetFrameTime() * formRate;
|
formAccum += GetFrameTime() * formRate;
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||||
struct Viewer {
|
struct Viewer {
|
||||||
// ---- Files / save format ------------------------------------------------
|
// ---- Files / save format ------------------------------------------------
|
||||||
static constexpr uint32_t SAVE_VERSION = 9; // v9: +moons; v8: +Live World clock; v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 7; // v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
const char* SAVE_PATH = "planet.save";
|
const char* SAVE_PATH = "planet.save";
|
||||||
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
||||||
@ -26,7 +26,6 @@ struct Viewer {
|
|||||||
int view3DW = 0, view3DH = 0;
|
int view3DW = 0, view3DH = 0;
|
||||||
RenderTexture2D rt3d{};
|
RenderTexture2D rt3d{};
|
||||||
Rectangle mapRect{}, hoverRect{}, panelRect{}, gridRect{};
|
Rectangle mapRect{}, hoverRect{}, panelRect{}, gridRect{};
|
||||||
Rectangle liveInfoRect{}; // free space right of the (left-aligned) 2D map: moon/tide phase
|
|
||||||
Rectangle pauseBtn{}, p3ContinueBtn{}, p3StartBtn{};
|
Rectangle pauseBtn{}, p3ContinueBtn{}, p3StartBtn{};
|
||||||
float pbCx = 0.0f, pbCy = 0.0f;
|
float pbCx = 0.0f, pbCy = 0.0f;
|
||||||
|
|
||||||
@ -73,24 +72,6 @@ struct Viewer {
|
|||||||
bool phase3 = false, phase3Prompt = false;
|
bool phase3 = false, phase3Prompt = false;
|
||||||
double phase3PromptAt = 0.0;
|
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).
|
// Selection + subgrid (phase 4/5 preview).
|
||||||
int selectedCell = -1;
|
int selectedCell = -1;
|
||||||
double selectedThresh = 0.06;
|
double selectedThresh = 0.06;
|
||||||
@ -121,10 +102,6 @@ struct Viewer {
|
|||||||
void selectCell(int idx);
|
void selectCell(int idx);
|
||||||
void recolor();
|
void recolor();
|
||||||
void refreshView();
|
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 regenWorld(); // after generate(): geometry changed
|
||||||
void regen(); // generate(cfg) + regenWorld()
|
void regen(); // generate(cfg) + regenWorld()
|
||||||
void stepOnce(); // one tick + settle bookkeeping
|
void stepOnce(); // one tick + settle bookkeeping
|
||||||
@ -141,7 +118,6 @@ struct Viewer {
|
|||||||
void renderFrame();
|
void renderFrame();
|
||||||
void renderGlobe3D();
|
void renderGlobe3D();
|
||||||
void renderMap2D();
|
void renderMap2D();
|
||||||
void renderLiveInfo(); // Live World: moon phases + (coastal) tidal phase, beside the 2D map
|
|
||||||
void renderPanels();
|
void renderPanels();
|
||||||
void renderHUD();
|
void renderHUD();
|
||||||
void renderPrompt();
|
void renderPrompt();
|
||||||
|
|||||||
@ -102,13 +102,7 @@ void Viewer::handleInput() {
|
|||||||
if (IsKeyPressed(KEY_THREE)) { mode = ColorMode::Age; recolor(); }
|
if (IsKeyPressed(KEY_THREE)) { mode = ColorMode::Age; recolor(); }
|
||||||
if (IsKeyPressed(KEY_FOUR)) { mode = ColorMode::Crust; recolor(); }
|
if (IsKeyPressed(KEY_FOUR)) { mode = ColorMode::Crust; recolor(); }
|
||||||
if (IsKeyPressed(KEY_FIVE)) { mode = ColorMode::Biome; recolor(); }
|
if (IsKeyPressed(KEY_FIVE)) { mode = ColorMode::Biome; recolor(); }
|
||||||
if (IsKeyPressed(KEY_SIX)) { // cycle temperature sub-views: mean->summer->winter->seasonality
|
if (IsKeyPressed(KEY_SIX)) { mode = ColorMode::Temperature; recolor(); }
|
||||||
mode = (mode == ColorMode::Temperature) ? ColorMode::TempSummer
|
|
||||||
: (mode == ColorMode::TempSummer) ? ColorMode::TempWinter
|
|
||||||
: (mode == ColorMode::TempWinter) ? ColorMode::Seasonality
|
|
||||||
: ColorMode::Temperature;
|
|
||||||
recolor();
|
|
||||||
}
|
|
||||||
if (IsKeyPressed(KEY_SEVEN)) { mode = ColorMode::Precip; recolor(); }
|
if (IsKeyPressed(KEY_SEVEN)) { mode = ColorMode::Precip; recolor(); }
|
||||||
if (IsKeyPressed(KEY_EIGHT)) { mode = ColorMode::FloraDensity; recolor(); }
|
if (IsKeyPressed(KEY_EIGHT)) { mode = ColorMode::FloraDensity; recolor(); }
|
||||||
if (IsKeyPressed(KEY_NINE)) { mode = ColorMode::FaunaDensity; recolor(); }
|
if (IsKeyPressed(KEY_NINE)) { mode = ColorMode::FaunaDensity; recolor(); }
|
||||||
@ -131,20 +125,6 @@ void Viewer::handleInput() {
|
|||||||
{ mode = ColorMode::FloraDensity; recolor(); }
|
{ mode = ColorMode::FloraDensity; recolor(); }
|
||||||
setStatus("Biota generated (flora/fauna/funga)");
|
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_C)) { selectedCell = -1; subgrids.clear(); }
|
||||||
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
||||||
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
||||||
@ -166,14 +146,7 @@ void Viewer::handleInput() {
|
|||||||
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
||||||
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
||||||
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
||||||
// Speed control: Live World ramps the live clock (sim hours/s, ~hour -> month);
|
// Drift speed (Phase 2): My simulated per real second.
|
||||||
// otherwise it sets the drift rate (My simulated per real second).
|
if (IsKeyPressed(KEY_RIGHT_BRACKET)) driftRate = std::min(driftRate * 1.5, 80.0);
|
||||||
if (IsKeyPressed(KEY_RIGHT_BRACKET)) {
|
if (IsKeyPressed(KEY_LEFT_BRACKET)) driftRate = std::max(driftRate / 1.5, 0.5);
|
||||||
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,7 +20,6 @@ void Viewer::renderGlobe3D() {
|
|||||||
rlPushMatrix();
|
rlPushMatrix();
|
||||||
rlRotatef((float)planet.cfg.axialTilt, 0.0f, 0.0f, 1.0f);
|
rlRotatef((float)planet.cfg.axialTilt, 0.0f, 0.0f, 1.0f);
|
||||||
const std::vector<int>& tri = planet.triIndices();
|
const std::vector<int>& tri = planet.triIndices();
|
||||||
const std::vector<Color>& dc = displayColors(); // live overlay (day/night + snow) or plain
|
|
||||||
rlBegin(RL_TRIANGLES);
|
rlBegin(RL_TRIANGLES);
|
||||||
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
||||||
int idx[3] = { tri[k], tri[k + 1], tri[k + 2] };
|
int idx[3] = { tri[k], tri[k + 1], tri[k + 2] };
|
||||||
@ -28,16 +27,15 @@ void Viewer::renderGlobe3D() {
|
|||||||
const Cell& cc = planet.cells[idx[j]];
|
const Cell& cc = planet.cells[idx[j]];
|
||||||
const Vec3& u = cc.unit;
|
const Vec3& u = cc.unit;
|
||||||
float r = visBase + (float)cc.elevation * elevExagg;
|
float r = visBase + (float)cc.elevation * elevExagg;
|
||||||
const Color& col = dc[idx[j]];
|
const Color& col = vcolors[idx[j]];
|
||||||
rlColor4ub(col.r, col.g, col.b, 255);
|
rlColor4ub(col.r, col.g, col.b, 255);
|
||||||
rlVertex3f((float)(u.x * r), (float)(u.y * r), (float)(u.z * r));
|
rlVertex3f((float)(u.x * r), (float)(u.y * r), (float)(u.z * r));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rlEnd();
|
rlEnd();
|
||||||
|
|
||||||
// (The clicked tile's high-res subgrid is shown in the right-side detail panel,
|
if (selectedCell >= 0) // detail patch
|
||||||
// not overlaid on the globe -- the overlay was a low-res, always-elevation-coloured
|
drawSubgrids(subgrids, visBase, elevExagg, planet.cfg.seaLevel, 0.0016f);
|
||||||
// patch that clashed with the active colour mode and read as a "strange pattern".)
|
|
||||||
|
|
||||||
if (showBorders && (!borders.empty() || !ridgeBorders.empty())) {
|
if (showBorders && (!borders.empty() || !ridgeBorders.empty())) {
|
||||||
rlSetLineWidth(2.0f); rlBegin(RL_LINES);
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES);
|
||||||
@ -73,36 +71,6 @@ void Viewer::renderGlobe3D() {
|
|||||||
};
|
};
|
||||||
drawRiv(rivers, 1.5f); drawRiv(bigRivers, 3.0f);
|
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);
|
if (showGrat) drawGraticule3D(graticule, gratR);
|
||||||
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
||||||
if (selectedCell >= 0) {
|
if (selectedCell >= 0) {
|
||||||
@ -128,57 +96,6 @@ 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{230, 90, 80, 255}); // north
|
||||||
DrawSphere(Vector3{0.0f, -ax, 0.0f}, 0.05f, Color{80, 140, 230, 255}); // south
|
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();
|
rlPopMatrix();
|
||||||
EndMode3D();
|
EndMode3D();
|
||||||
EndTextureMode();
|
EndTextureMode();
|
||||||
@ -188,13 +105,11 @@ void Viewer::renderGlobe3D() {
|
|||||||
void Viewer::renderMap2D() {
|
void Viewer::renderMap2D() {
|
||||||
DrawRectangleRec(mapRect, Color{6, 8, 14, 255});
|
DrawRectangleRec(mapRect, Color{6, 8, 14, 255});
|
||||||
BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height);
|
BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height);
|
||||||
drawMap2D(planet, displayColors(), map2D, mapRect, mapLon);
|
drawMap2D(planet, vcolors, map2D, mapRect, mapLon);
|
||||||
if (showGrat) { drawGraticule2D(graticule, mapRect, mapLon); drawGraticuleLabels2D(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 && !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 (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 (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) {
|
if (phase3 && showRivers) {
|
||||||
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon);
|
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon);
|
||||||
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon);
|
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon);
|
||||||
@ -215,95 +130,6 @@ 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});
|
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).
|
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
||||||
void Viewer::renderPanels() {
|
void Viewer::renderPanels() {
|
||||||
drawHoverPanel(planet, hoverRect, hovered, selectedCell);
|
drawHoverPanel(planet, hoverRect, hovered, selectedCell);
|
||||||
@ -312,7 +138,7 @@ void Viewer::renderPanels() {
|
|||||||
planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge,
|
planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge,
|
||||||
panelRect, gridRect, hoveredSubIdx);
|
panelRect, gridRect, hoveredSubIdx);
|
||||||
else
|
else
|
||||||
drawStats(planet, panelRect, elapsedMy, settled, liveWorld, liveTime);
|
drawStats(planet, panelRect, elapsedMy, settled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top-left HUD text + the clickable pause button.
|
// Top-left HUD text + the clickable pause button.
|
||||||
@ -327,8 +153,7 @@ void Viewer::renderHUD() {
|
|||||||
int y = 10;
|
int y = 10;
|
||||||
auto line = [&](const std::string& s){ DrawText(s.c_str(), 12, y, 18, RAYWHITE); y += 22; };
|
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);
|
double fastest = 0.0; for (const auto& pl : planet.plates) fastest = std::max(fastest, pl.speedCmYr);
|
||||||
line(liveWorld ? "Planet Sim - Live World"
|
line(!settled ? "Planet Sim - World Creation: forming"
|
||||||
: !settled ? "Planet Sim - World Creation: forming"
|
|
||||||
: phase3 ? "Planet Sim - World Creation: hydrology"
|
: phase3 ? "Planet Sim - World Creation: hydrology"
|
||||||
: "Planet Sim - World Creation: drift & erosion");
|
: "Planet Sim - World Creation: drift & erosion");
|
||||||
line(TextFormat("Cells: %d Subdiv: %d CellWidth: %.0f km",
|
line(TextFormat("Cells: %d Subdiv: %d CellWidth: %.0f km",
|
||||||
@ -337,24 +162,6 @@ void Viewer::renderHUD() {
|
|||||||
if (!settled)
|
if (!settled)
|
||||||
line(TextFormat("Forming terrain tick %lld max change %.1f m/tick%s",
|
line(TextFormat("Forming terrain tick %lld max change %.1f m/tick%s",
|
||||||
stepCount, maxChange, paused ? " [PAUSED]" : ""));
|
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 {
|
else {
|
||||||
line(TextFormat("%s %.1f My elapsed %.1f My/s%s",
|
line(TextFormat("%s %.1f My elapsed %.1f My/s%s",
|
||||||
phase3 ? "Hydrology - drift, rivers & erosion" : "Drift & erosion",
|
phase3 ? "Hydrology - drift, rivers & erosion" : "Drift & erosion",
|
||||||
@ -371,11 +178,11 @@ void Viewer::renderHUD() {
|
|||||||
}
|
}
|
||||||
y += 8;
|
y += 8;
|
||||||
line("hover: cell info | click tile: open detail panel | C close");
|
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("1 elev 2 plates 3 age 4 crust 5 biome 6 temp 7 precip 8 flora 9 fauna 0 funga");
|
||||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
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", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
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] | W live [%s] | R reseed | +/-",
|
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", liveWorld ? "on" : "off"));
|
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off"));
|
||||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||||
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
||||||
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
||||||
@ -447,7 +254,6 @@ void Viewer::renderFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderMap2D();
|
renderMap2D();
|
||||||
renderLiveInfo();
|
|
||||||
renderPanels();
|
renderPanels();
|
||||||
renderHUD();
|
renderHUD();
|
||||||
renderPrompt();
|
renderPrompt();
|
||||||
|
|||||||
@ -33,7 +33,6 @@ void Planet::generate(const PlanetConfig& c) {
|
|||||||
|
|
||||||
assignPlates();
|
assignPlates();
|
||||||
seedInitialRelief();
|
seedInitialRelief();
|
||||||
generateMoons(); // Live World satellites (separate RNG; does not perturb tectonics)
|
|
||||||
computeClimate(); // temperature + precipitation fields (biomes read these)
|
computeClimate(); // temperature + precipitation fields (biomes read these)
|
||||||
classifyBiomes(); // give the fresh world an initial biome per cell
|
classifyBiomes(); // give the fresh world an initial biome per cell
|
||||||
computeBiotaDensity(); // derived flora/fauna/funga density (population is on-demand)
|
computeBiotaDensity(); // derived flora/fauna/funga density (population is on-demand)
|
||||||
|
|||||||
@ -14,7 +14,6 @@ public:
|
|||||||
PlanetConfig cfg;
|
PlanetConfig cfg;
|
||||||
std::vector<Cell> cells;
|
std::vector<Cell> cells;
|
||||||
std::vector<Plate> plates;
|
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
|
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
||||||
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
||||||
@ -52,42 +51,9 @@ public:
|
|||||||
// rain, leeward rain shadow, dry continental interiors). Derived (not saved);
|
// rain, leeward rain shadow, dry continental interiors). Derived (not saved);
|
||||||
// call before classifyBiomes(), which consumes these fields.
|
// call before classifyBiomes(), which consumes these fields.
|
||||||
void computeClimate();
|
void computeClimate();
|
||||||
const std::vector<double>& temperature() const { return sTemp; } // deg C, annual mean
|
const std::vector<double>& temperature() const { return sTemp; } // deg C
|
||||||
const std::vector<double>& precipitation() const { return sPrecip; } // relative units
|
const std::vector<double>& precipitation() const { return sPrecip; } // relative units
|
||||||
const std::vector<double>& moisture() const { return sMoist; } // 0..1 (median land -> 0.5)
|
const std::vector<double>& moisture() const { return sMoist; } // 0..1 (median land -> 0.5)
|
||||||
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
|
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
|
||||||
// fields (temperature + normalized precipitation). Derived + written back into
|
// fields (temperature + normalized precipitation). Derived + written back into
|
||||||
@ -123,9 +89,7 @@ public:
|
|||||||
// hasBiome: whether the stream carries the per-cell biome byte (save v4+). For
|
// 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.
|
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
||||||
// hasBiota: whether the stream carries the biota population block (save v7+).
|
// hasBiota: whether the stream carries the biota population block (save v7+).
|
||||||
// hasMoons: whether the stream carries the moons block (save v9+); older saves
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true);
|
||||||
// synthesize moons from the seed instead.
|
|
||||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool hasMoons = true);
|
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -188,18 +152,11 @@ private:
|
|||||||
std::vector<int> sFlowTo, sHydroOrder;
|
std::vector<int> sFlowTo, sHydroOrder;
|
||||||
|
|
||||||
// Phase-3 climate scratch (derived each computeClimate(); not saved). sMoist is the
|
// Phase-3 climate scratch (derived each computeClimate(); not saved). sMoist is the
|
||||||
// 0..1-normalized precipitation the biome classifier reads. sTempSummer/sTempWinter are
|
// 0..1-normalized precipitation the biome classifier reads.
|
||||||
// the obliquity-driven seasonal extremes around the annual mean sTemp (see Seasons).
|
std::vector<double> sTemp, sPrecip, sMoist;
|
||||||
std::vector<double> sTemp, sPrecip, sMoist, sTempSummer, sTempWinter;
|
|
||||||
std::vector<Vec3> sWind;
|
std::vector<Vec3> sWind;
|
||||||
std::vector<int> sUpwind;
|
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
|
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||||
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||||
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
||||||
|
|||||||
@ -22,18 +22,11 @@ void Planet::classifyBiomes() {
|
|||||||
const double GRASS_MOIST = cfg.biomeGrassMoist, TAIGA_MOIST = cfg.biomeTaigaMoist;
|
const double GRASS_MOIST = cfg.biomeGrassMoist, TAIGA_MOIST = cfg.biomeTaigaMoist;
|
||||||
const double LAKE_MIN_DEPTH = cfg.biomeLakeMinDepth;
|
const double LAKE_MIN_DEPTH = cfg.biomeLakeMinDepth;
|
||||||
const bool haveLake = !sLakeDepth.empty();
|
const bool haveLake = !sLakeDepth.empty();
|
||||||
// Seasons: blend winter temperature into the cold (Tundra/Taiga) cutoffs so cold-winter
|
|
||||||
// continental interiors turn boreal/tundra. The seasonal amplitude is itself geographically
|
|
||||||
// shaped (large only at high-latitude interiors), so this expands cold biomes where seasons
|
|
||||||
// bite, not uniformly. biomeSeasonWeight = 0 -> coldT == annual mean -> biomes unchanged.
|
|
||||||
const double SEASON_W = cfg.biomeSeasonWeight;
|
|
||||||
const bool haveSeason = ((int)sTempWinter.size() == n);
|
|
||||||
|
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
const Cell& c = cells[i];
|
const Cell& c = cells[i];
|
||||||
double elevAbove = std::max(0.0, c.elevation - sea);
|
double elevAbove = std::max(0.0, c.elevation - sea);
|
||||||
double temp = sTemp[i]; // climate temperature (deg C, annual mean)
|
double temp = sTemp[i]; // climate temperature (deg C)
|
||||||
double coldT = haveSeason ? temp + SEASON_W * (sTempWinter[i] - temp) : temp; // winter-blended
|
|
||||||
double moist = sMoist[i]; // climate precipitation, normalized 0..1
|
double moist = sMoist[i]; // climate precipitation, normalized 0..1
|
||||||
double lakeD = haveLake ? sLakeDepth[i] : 0.0;
|
double lakeD = haveLake ? sLakeDepth[i] : 0.0;
|
||||||
bool adjOcean = false, adjWater = false;
|
bool adjOcean = false, adjWater = false;
|
||||||
@ -50,9 +43,9 @@ void Planet::classifyBiomes() {
|
|||||||
else if (elevAbove > MOUNTAIN_ELEV) b = Biome::Mountains;
|
else if (elevAbove > MOUNTAIN_ELEV) b = Biome::Mountains;
|
||||||
else if (elevAbove > HILLS_ELEV) b = Biome::Hills;
|
else if (elevAbove > HILLS_ELEV) b = Biome::Hills;
|
||||||
else { // lowland / plains
|
else { // lowland / plains
|
||||||
if (coldT < TUNDRA_TEMP) b = Biome::Tundra;
|
if (temp < TUNDRA_TEMP) b = Biome::Tundra;
|
||||||
else if (elevAbove < LOWLAND_ELEV && moist > WETLAND_MOIST && adjWater) b = Biome::Wetland; // swamps hug water
|
else if (elevAbove < LOWLAND_ELEV && moist > WETLAND_MOIST && adjWater) b = Biome::Wetland; // swamps hug water
|
||||||
else if (coldT < TAIGA_TEMP) b = (moist > TAIGA_MOIST) ? Biome::Taiga : Biome::Tundra;
|
else if (temp < TAIGA_TEMP) b = (moist > TAIGA_MOIST) ? Biome::Taiga : Biome::Tundra;
|
||||||
else if (moist < DESERT_MOIST) b = Biome::Desert;
|
else if (moist < DESERT_MOIST) b = Biome::Desert;
|
||||||
else if (moist < GRASS_MOIST) b = (temp > SAVANNA_TEMP) ? Biome::Savanna : Biome::Grassland;
|
else if (moist < GRASS_MOIST) b = (temp > SAVANNA_TEMP) ? Biome::Savanna : Biome::Grassland;
|
||||||
else b = Biome::Forest;
|
else b = Biome::Forest;
|
||||||
|
|||||||
@ -26,8 +26,6 @@ void Planet::computeClimate() {
|
|||||||
sTemp.assign(n, 0.0);
|
sTemp.assign(n, 0.0);
|
||||||
sPrecip.assign(n, 0.0);
|
sPrecip.assign(n, 0.0);
|
||||||
sMoist.assign(n, 0.0);
|
sMoist.assign(n, 0.0);
|
||||||
sTempSummer.assign(n, 0.0);
|
|
||||||
sTempWinter.assign(n, 0.0);
|
|
||||||
sWind.assign(n, Vec3{0, 0, 0});
|
sWind.assign(n, Vec3{0, 0, 0});
|
||||||
sUpwind.assign(n, -1);
|
sUpwind.assign(n, -1);
|
||||||
|
|
||||||
@ -63,38 +61,6 @@ void Planet::computeClimate() {
|
|||||||
sUpwind[i] = best;
|
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,
|
// 2. Steady-state moisture advection along the wind (iterative upwind differencing,
|
||||||
// double-buffered -> deterministic). Ocean cells are a moisture source; land
|
// double-buffered -> deterministic). Ocean cells are a moisture source; land
|
||||||
// cells take their upwind moisture, rain part of it out (more on windward upslopes)
|
// cells take their upwind moisture, rain part of it out (more on windward upslopes)
|
||||||
@ -165,42 +131,4 @@ void Planet::computeClimate() {
|
|||||||
ref = std::max(1e-6, landP[mid] / 0.5); // median -> 0.5
|
ref = std::max(1e-6, landP[mid] / 0.5); // median -> 0.5
|
||||||
}
|
}
|
||||||
for (int i = 0; i < n; ++i) sMoist[i] = std::clamp(sPrecip[i] / ref, 0.0, 1.0);
|
for (int i = 0; i < n; ++i) sMoist[i] = std::clamp(sPrecip[i] / ref, 0.0, 1.0);
|
||||||
|
|
||||||
// --- Seasons (obliquity) ------------------------------------------------------
|
|
||||||
// Per-cell summer (warmest-month) and winter (coldest-month) temperatures around
|
|
||||||
// the annual mean sTemp. The seasonal half-amplitude grows with axial tilt, with
|
|
||||||
// latitude (poles swing most), and with continentality -- distance from the ocean,
|
|
||||||
// which moderates coastal climates via its thermal inertia. These are STATIC fields
|
|
||||||
// (the extremes), not an animated year, so hemisphere phase is irrelevant.
|
|
||||||
{
|
|
||||||
// Continentality: multi-source BFS ring distance from ocean cells over the fixed
|
|
||||||
// neighbour graph. Ring distance is order-independent -> deterministic, O(n).
|
|
||||||
const int rings = std::max(1, cfg.seasonContinentRings);
|
|
||||||
std::vector<int> dist(n, -1), frontier, next;
|
|
||||||
for (int i = 0; i < n; ++i)
|
|
||||||
if (cells[i].elevation <= sea) { dist[i] = 0; frontier.push_back(i); }
|
|
||||||
for (int r = 1; r <= rings && !frontier.empty(); ++r) {
|
|
||||||
next.clear();
|
|
||||||
for (int i : frontier)
|
|
||||||
for (int nb : cells[i].neighbors)
|
|
||||||
if (dist[nb] < 0) { dist[nb] = r; next.push_back(nb); }
|
|
||||||
frontier.swap(next);
|
|
||||||
}
|
|
||||||
const double tiltFactor = std::sin(cfg.axialTilt * M_PI / 180.0) /
|
|
||||||
std::sin(23.44 * M_PI / 180.0); // 0 tilt -> 0, Earth -> 1
|
|
||||||
const double ampMax = cfg.seasonAmpMax, latExp = cfg.seasonLatExp;
|
|
||||||
const double oceanF = cfg.seasonOceanFactor;
|
|
||||||
for (int i = 0; i < n; ++i) {
|
|
||||||
double cont;
|
|
||||||
if (cells[i].elevation <= sea) cont = oceanF; // ocean: muted swing
|
|
||||||
else if (dist[i] < 0) cont = 1.0; // deep interior (unreached)
|
|
||||||
else cont = (double)dist[i] / rings;// coast 0 -> interior 1
|
|
||||||
cont = std::clamp(std::max(oceanF, cont), 0.0, 1.0);
|
|
||||||
double lat = std::asin(std::clamp(cells[i].unit.y, -1.0, 1.0));
|
|
||||||
double latShape = std::pow(std::fabs(lat) / (M_PI / 2.0), latExp);
|
|
||||||
double A = ampMax * std::max(0.0, tiltFactor) * latShape * cont;
|
|
||||||
sTempSummer[i] = sTemp[i] + A;
|
|
||||||
sTempWinter[i] = sTemp[i] - A;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,11 +30,9 @@ void Planet::erode(double dtMy) {
|
|||||||
}
|
}
|
||||||
sErode[i] = delta;
|
sErode[i] = delta;
|
||||||
}
|
}
|
||||||
// Upper clamp tracks peakSoftCapEnd (matches step()); the soft peak cap, not a
|
|
||||||
// fixed 9000 m wall here, governs how high mountains stand.
|
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd);
|
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, 9000.0);
|
||||||
|
|
||||||
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,8 +98,7 @@ void Planet::hydrology(double dtMy) {
|
|||||||
}
|
}
|
||||||
load[d] += carried; // pass remaining sediment downstream
|
load[d] += carried; // pass remaining sediment downstream
|
||||||
}
|
}
|
||||||
// Upper clamp tracks peakSoftCapEnd (matches step()/erode()).
|
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
cells[i].elevation = std::clamp(cells[i].elevation, -11000.0, cfg.peakSoftCapEnd);
|
cells[i].elevation = std::clamp(cells[i].elevation, -11000.0, 9000.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
#define CONFIG_FIELDS(D, I, U) \
|
#define CONFIG_FIELDS(D, I, U) \
|
||||||
D(radius) D(seaLevel) D(axialTilt) D(continentBase) D(oceanBase) D(upliftGain) D(relax) \
|
D(radius) D(seaLevel) D(axialTilt) D(continentBase) D(oceanBase) D(upliftGain) D(relax) \
|
||||||
D(collisionFactor) D(arcFactor) D(isostaticPersist) D(rootScale) \
|
D(collisionFactor) D(arcFactor) D(isostaticPersist) D(rootScale) \
|
||||||
D(peakSoftCapStart) D(peakSoftCapEnd) D(peakFailDrop) \
|
|
||||||
D(seafloorSubsidence) D(seafloorSeedAge) \
|
D(seafloorSubsidence) D(seafloorSeedAge) \
|
||||||
D(maxDriftSpeed) D(ridgeDepth) D(splitFraction) D(splitProbBase) D(splitProbSlope) \
|
D(maxDriftSpeed) D(ridgeDepth) D(splitFraction) D(splitProbBase) D(splitProbSlope) \
|
||||||
D(stalemateEps) D(stalemateBoost) D(babyPromoteFrac) D(volcanicLandFrac) \
|
D(stalemateEps) D(stalemateBoost) D(babyPromoteFrac) D(volcanicLandFrac) \
|
||||||
@ -27,18 +26,15 @@
|
|||||||
D(biomeIceTemp) D(biomeTundraTemp) D(biomeTaigaTemp) D(biomeSavannaTemp) \
|
D(biomeIceTemp) D(biomeTundraTemp) D(biomeTaigaTemp) D(biomeSavannaTemp) \
|
||||||
D(biomeMountainElev) D(biomeHillsElev) D(biomeBeachBand) D(biomeLowlandElev) \
|
D(biomeMountainElev) D(biomeHillsElev) D(biomeBeachBand) D(biomeLowlandElev) \
|
||||||
D(biomeWetlandMoist) D(biomeDesertMoist) D(biomeGrassMoist) D(biomeTaigaMoist) \
|
D(biomeWetlandMoist) D(biomeDesertMoist) D(biomeGrassMoist) D(biomeTaigaMoist) \
|
||||||
D(biomeLakeMinDepth) D(biomeSeasonWeight) \
|
D(biomeLakeMinDepth) \
|
||||||
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
||||||
D(climateOroRefHeight) D(climateContinentality) D(climateCurrentFactor) \
|
D(climateOroRefHeight) D(climateContinentality) \
|
||||||
D(seasonAmpMax) D(seasonLatExp) D(seasonOceanFactor) \
|
|
||||||
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
|
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
|
||||||
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
|
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
|
||||||
D(bioFungaTempMin) D(bioRegionBonus) \
|
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(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \
|
I(climateWindPasses) I(climateMoistureSmooth) \
|
||||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
|
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
|
||||||
U(seed)
|
U(seed)
|
||||||
@ -118,9 +114,6 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.arcFactor, 0.0, 20.0, "arcFactor"));
|
E(rng(cfg.arcFactor, 0.0, 20.0, "arcFactor"));
|
||||||
E(rng(cfg.isostaticPersist, 0.0, 0.95, "isostaticPersist"));
|
E(rng(cfg.isostaticPersist, 0.0, 0.95, "isostaticPersist"));
|
||||||
E(rng(cfg.rootScale, 100.0, 20000.0, "rootScale"));
|
E(rng(cfg.rootScale, 100.0, 20000.0, "rootScale"));
|
||||||
E(rng(cfg.peakSoftCapStart, 0.0, 20000.0, "peakSoftCapStart"));
|
|
||||||
E(rng(cfg.peakSoftCapEnd, 0.0, 20000.0, "peakSoftCapEnd"));
|
|
||||||
E(rng(cfg.peakFailDrop, 0.0, 5000.0, "peakFailDrop"));
|
|
||||||
E(rng(cfg.seafloorSubsidence, 0.0, 2000.0, "seafloorSubsidence"));
|
E(rng(cfg.seafloorSubsidence, 0.0, 2000.0, "seafloorSubsidence"));
|
||||||
E(rng(cfg.seafloorSeedAge, 0.0, 1000.0, "seafloorSeedAge"));
|
E(rng(cfg.seafloorSeedAge, 0.0, 1000.0, "seafloorSeedAge"));
|
||||||
E(rng(cfg.maxDriftSpeed, 0.1, 100.0, "maxDriftSpeed"));
|
E(rng(cfg.maxDriftSpeed, 0.1, 100.0, "maxDriftSpeed"));
|
||||||
@ -165,16 +158,11 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.biomeGrassMoist, 0.0, 1.0, "biomeGrassMoist"));
|
E(rng(cfg.biomeGrassMoist, 0.0, 1.0, "biomeGrassMoist"));
|
||||||
E(rng(cfg.biomeTaigaMoist, 0.0, 1.0, "biomeTaigaMoist"));
|
E(rng(cfg.biomeTaigaMoist, 0.0, 1.0, "biomeTaigaMoist"));
|
||||||
E(rng(cfg.biomeLakeMinDepth, 0.0, 5000.0, "biomeLakeMinDepth"));
|
E(rng(cfg.biomeLakeMinDepth, 0.0, 5000.0, "biomeLakeMinDepth"));
|
||||||
E(rng(cfg.biomeSeasonWeight, 0.0, 1.0, "biomeSeasonWeight"));
|
|
||||||
E(rng(cfg.climateOceanMoisture, 0.0, 1.0e3, "climateOceanMoisture"));
|
E(rng(cfg.climateOceanMoisture, 0.0, 1.0e3, "climateOceanMoisture"));
|
||||||
E(rng(cfg.climateRainEfficiency, 0.0, 1.0, "climateRainEfficiency"));
|
E(rng(cfg.climateRainEfficiency, 0.0, 1.0, "climateRainEfficiency"));
|
||||||
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
|
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
|
||||||
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
|
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
|
||||||
E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality"));
|
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"));
|
|
||||||
E(rng(cfg.bioVegTempMin, -40.0, 30.0, "bioVegTempMin"));
|
E(rng(cfg.bioVegTempMin, -40.0, 30.0, "bioVegTempMin"));
|
||||||
E(rng(cfg.bioVegTempOpt, -20.0, 50.0, "bioVegTempOpt"));
|
E(rng(cfg.bioVegTempOpt, -20.0, 50.0, "bioVegTempOpt"));
|
||||||
E(rng(cfg.bioVegMoistRef, 0.01, 1.0, "bioVegMoistRef"));
|
E(rng(cfg.bioVegMoistRef, 0.01, 1.0, "bioVegMoistRef"));
|
||||||
@ -185,12 +173,6 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight"));
|
E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight"));
|
||||||
E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin"));
|
E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin"));
|
||||||
E(rng(cfg.bioRegionBonus, 0.0, 10.0, "bioRegionBonus"));
|
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.subdivisions, 0, 7, "subdivisions"));
|
||||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||||
@ -202,7 +184,6 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery"));
|
E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery"));
|
||||||
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
||||||
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
||||||
E(irng(cfg.seasonContinentRings, 1, 100, "seasonContinentRings"));
|
|
||||||
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
||||||
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
||||||
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
||||||
@ -212,8 +193,6 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
|
|
||||||
if (cfg.oceanBase >= cfg.continentBase)
|
if (cfg.oceanBase >= cfg.continentBase)
|
||||||
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
||||||
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)
|
|
||||||
bad.push_back("peakSoftCapStart >= peakSoftCapEnd (grow probability must span a band)");
|
|
||||||
|
|
||||||
if (bad.empty()) return {};
|
if (bad.empty()) return {};
|
||||||
std::string msg = "Bad config:";
|
std::string msg = "Bad config:";
|
||||||
@ -269,7 +248,6 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writeVec(os, sPrevCount);
|
writeVec(os, sPrevCount);
|
||||||
writeVec(os, sStaleStreak);
|
writeVec(os, sStaleStreak);
|
||||||
writeVec(os, sFreePlateIds);
|
writeVec(os, sFreePlateIds);
|
||||||
writeVec(os, moons); // v9: natural satellites (Live World)
|
|
||||||
// v7: discrete biota population (sBiota). A flag byte gates the block so a
|
// 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.
|
// not-yet-populated world stays compact; otherwise three Organism lists per cell.
|
||||||
uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio);
|
uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio);
|
||||||
@ -281,7 +259,7 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons) {
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) {
|
||||||
// Read the length-prefixed key=value config block (see writeState). A default
|
// 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
|
// 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.
|
// current defaults. The length guard rejects pre-v6 (raw-POD-config) saves.
|
||||||
@ -310,8 +288,6 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
readVec(is, sPrevCount);
|
readVec(is, sPrevCount);
|
||||||
readVec(is, sStaleStreak);
|
readVec(is, sStaleStreak);
|
||||||
readVec(is, sFreePlateIds);
|
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
|
if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state
|
||||||
// v7: discrete biota population. buildGeometry() already sized sBiota empty;
|
// v7: discrete biota population. buildGeometry() already sized sBiota empty;
|
||||||
// older saves (hasBiota=false) just keep the empty population (press L to fill).
|
// older saves (hasBiota=false) just keep the empty population (press L to fill).
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +1,6 @@
|
|||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
// Deterministic, thread-independent value in [0,1). Pure function of (cell, tick,
|
|
||||||
// seed) -- so the parallel step() loops stay bit-identical for any thread count
|
|
||||||
// and this never touches Planet::rngState. `salt` selects an independent stream
|
|
||||||
// (one for the grow roll, one for the drop magnitude). splitmix64 finalizer.
|
|
||||||
static inline double peakHash(int i, uint64_t tick, uint32_t seed, uint32_t salt) {
|
|
||||||
uint64_t x = (uint64_t)(uint32_t)i * 0x9E3779B97F4A7C15ull
|
|
||||||
+ (tick + 1) * 0xD1B54A32D192ED03ull
|
|
||||||
+ (uint64_t)seed * 0xBF58476D1CE4E5B9ull
|
|
||||||
+ (uint64_t)salt + 0x123456789ull;
|
|
||||||
x ^= x >> 30; x *= 0xBF58476D1CE4E5B9ull;
|
|
||||||
x ^= x >> 27; x *= 0x94D049BB133111EBull;
|
|
||||||
x ^= x >> 31;
|
|
||||||
return (double)(x >> 11) * (1.0 / 9007199254740992.0); // 53-bit -> [0,1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Phase 1/2 tectonics: one stress->uplift->relax tick ---------------------
|
// --- Phase 1/2 tectonics: one stress->uplift->relax tick ---------------------
|
||||||
// Data-parallel: every pass writes only its own cell index (double-buffered
|
// Data-parallel: every pass writes only its own cell index (double-buffered
|
||||||
@ -154,23 +138,7 @@ double Planet::step() {
|
|||||||
relaxEff = cfg.relax;
|
relaxEff = cfg.relax;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
double d = delta[i];
|
cells[i].elevation += delta[i];
|
||||||
// Soft probabilistic peak cap (drift-only): above peakSoftCapStart the
|
|
||||||
// chance that uplift takes falls linearly to 0 at peakSoftCapEnd, so peaks
|
|
||||||
// spread across a height band instead of railing at the old hard ceiling.
|
|
||||||
// A lost roll forfeits this tick's uplift and shaves a random 0..peakFailDrop
|
|
||||||
// metres off (so a peak hovers near its own height). erodeIter is the tick
|
|
||||||
// counter (step/erode run 1:1 in drift; it's saved -> exact resume).
|
|
||||||
if (drifting && d > 0.0 && cells[i].elevation > cfg.peakSoftCapStart) {
|
|
||||||
double span = cfg.peakSoftCapEnd - cfg.peakSoftCapStart;
|
|
||||||
double p = span > 0.0 ? (cfg.peakSoftCapEnd - cells[i].elevation) / span : 0.0;
|
|
||||||
p = std::clamp(p, 0.0, 1.0);
|
|
||||||
if (peakHash(i, (uint64_t)erodeIter, cfg.seed, 0u) >= p) { // grow roll lost
|
|
||||||
d = 0.0;
|
|
||||||
cells[i].elevation -= peakHash(i, (uint64_t)erodeIter, cfg.seed, 1u) * cfg.peakFailDrop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cells[i].elevation += d;
|
|
||||||
cells[i].elevation += (base - cells[i].elevation) * relaxEff;
|
cells[i].elevation += (base - cells[i].elevation) * relaxEff;
|
||||||
// (geoAge is crust age in My, advanced by advect() in Phase 2.)
|
// (geoAge is crust age in My, advanced by advect() in Phase 2.)
|
||||||
}
|
}
|
||||||
@ -183,13 +151,9 @@ double Planet::step() {
|
|||||||
for (int nb : cells[i].neighbors) { sum += cells[nb].elevation; ++cnt; }
|
for (int nb : cells[i].neighbors) { sum += cells[nb].elevation; ++cnt; }
|
||||||
smoothed[i] = cells[i].elevation * 0.96 + (sum / cnt) * 0.04;
|
smoothed[i] = cells[i].elevation * 0.96 + (sum / cnt) * 0.04;
|
||||||
}
|
}
|
||||||
// Upper clamp tracks peakSoftCapEnd so the soft cap governs peak heights
|
|
||||||
// (a fixed 9000 m ceiling would re-flatten everything the soft cap allows);
|
|
||||||
// it's just a safety rail now. Lower clamp (-11000 m, trenches) is unchanged.
|
|
||||||
const double elevCeil = cfg.peakSoftCapEnd;
|
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
cells[i].elevation = std::clamp(smoothed[i], -11000.0, elevCeil);
|
cells[i].elevation = std::clamp(smoothed[i], -11000.0, 9000.0);
|
||||||
|
|
||||||
// Largest elevation change this tick -> 0 as the world reaches equilibrium.
|
// Largest elevation change this tick -> 0 as the world reaches equilibrium.
|
||||||
double maxChange = 0.0;
|
double maxChange = 0.0;
|
||||||
|
|||||||
@ -35,20 +35,6 @@ enum class Biome : uint8_t {
|
|||||||
Desert, Forest, Taiga, Tundra, Hills, Mountains
|
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 {
|
struct Plate {
|
||||||
int id = 0;
|
int id = 0;
|
||||||
PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells
|
PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells
|
||||||
@ -108,16 +94,6 @@ struct PlanetConfig {
|
|||||||
double isostaticPersist = 0.85; // how much high crust resists relax (0..<1)
|
double isostaticPersist = 0.85; // how much high crust resists relax (0..<1)
|
||||||
double rootScale = 2500.0;// m above continentBase where persistence saturates
|
double rootScale = 2500.0;// m above continentBase where persistence saturates
|
||||||
|
|
||||||
// --- Soft peak cap (drift-only): spread mountain heights, no hard plateau --
|
|
||||||
// Above peakSoftCapStart the chance that a tick's uplift "takes" falls linearly
|
|
||||||
// to 0 at peakSoftCapEnd, so peaks settle across a height band instead of all
|
|
||||||
// railing at one ceiling. A lost grow roll forfeits that tick's uplift and
|
|
||||||
// shaves a random 0..peakFailDrop metres off. The hard elevation clamp's upper
|
|
||||||
// bound tracks peakSoftCapEnd. Active only during drift (forming still settles).
|
|
||||||
double peakSoftCapStart = 7000.0; // m: below this, uplift always takes (P=1)
|
|
||||||
double peakSoftCapEnd = 12000.0; // m: at/above this, uplift never takes (P=0)
|
|
||||||
double peakFailDrop = 200.0; // m: max random drop when the grow roll loses
|
|
||||||
|
|
||||||
// --- Plate drift (Phase 2) ----------------------------------------------
|
// --- Plate drift (Phase 2) ----------------------------------------------
|
||||||
double maxDriftSpeed = 20.0; // cm/year; fastest plate (Earth is 1-10)
|
double maxDriftSpeed = 20.0; // cm/year; fastest plate (Earth is 1-10)
|
||||||
double ridgeDepth = -2500.0; // m, elevation of brand-new crust at a ridge
|
double ridgeDepth = -2500.0; // m, elevation of brand-new crust at a ridge
|
||||||
@ -172,8 +148,7 @@ struct PlanetConfig {
|
|||||||
double phase3AfterMy = 300.0; // My of drift before the Phase-3 prompt
|
double phase3AfterMy = 300.0; // My of drift before the Phase-3 prompt
|
||||||
double phase3DtScale = 0.2; // Phase-3 timestep = cflDtMy() * this (finer)
|
double phase3DtScale = 0.2; // Phase-3 timestep = cflDtMy() * this (finer)
|
||||||
double rainfall = 1.0; // uniform precip per cell (drainage-area unit)
|
double rainfall = 1.0; // uniform precip per cell (drainage-area unit)
|
||||||
double riverThreshold = 25.0; // discharge above which a cell counts as a river
|
double riverThreshold = 50.0; // discharge above which a cell counts as a river
|
||||||
// (lower = richer network incl. tributaries shown)
|
|
||||||
double riverIncision = 0.02; // K in stream-power incision K*Q^m*S^n*dt
|
double riverIncision = 0.02; // K in stream-power incision K*Q^m*S^n*dt
|
||||||
double riverDischargeExp = 0.5; // m: discharge exponent in stream power
|
double riverDischargeExp = 0.5; // m: discharge exponent in stream power
|
||||||
double riverSlopeExp = 1.0; // n: slope exponent in stream power
|
double riverSlopeExp = 1.0; // n: slope exponent in stream power
|
||||||
@ -200,8 +175,6 @@ struct PlanetConfig {
|
|||||||
double biomeGrassMoist = 0.50; // moisture below this -> Grassland/Savanna, else Forest
|
double biomeGrassMoist = 0.50; // moisture below this -> Grassland/Savanna, else Forest
|
||||||
double biomeTaigaMoist = 0.40; // cool + above this -> Taiga (else Tundra)
|
double biomeTaigaMoist = 0.40; // cool + above this -> Taiga (else Tundra)
|
||||||
double biomeLakeMinDepth= 20.0; // filled-basin depth above sea level counting as a Lake
|
double biomeLakeMinDepth= 20.0; // filled-basin depth above sea level counting as a Lake
|
||||||
double biomeSeasonWeight= 0.6; // how much winter temp (vs annual mean) sets the cold
|
|
||||||
// Tundra/Taiga cutoffs (0 = mean only/old behaviour, 1 = winter)
|
|
||||||
|
|
||||||
// --- Phase 3: climate (orographic precipitation) -- see PlanetClimate.cpp --
|
// --- Phase 3: climate (orographic precipitation) -- see PlanetClimate.cpp --
|
||||||
// Temperature reuses the biome* temperature fields above. Precipitation advects
|
// Temperature reuses the biome* temperature fields above. Precipitation advects
|
||||||
@ -214,18 +187,6 @@ struct PlanetConfig {
|
|||||||
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
||||||
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
||||||
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
|
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
|
|
||||||
// mean sTemp: summer/winter = mean +/- A, with A = seasonAmpMax * tiltFactor *
|
|
||||||
// latShape * continentality. Big swings at high-latitude continental interiors,
|
|
||||||
// small near coasts/equator. Static fields (warmest/coldest month), not animated.
|
|
||||||
double seasonAmpMax = 18.0; // max seasonal half-amplitude (C) at full tilt/lat/interior
|
|
||||||
double seasonLatExp = 1.2; // latitude shape exponent (>1 concentrates swing toward poles)
|
|
||||||
double seasonOceanFactor = 0.15; // continentality floor: ocean/coast seasonal swing fraction
|
|
||||||
int seasonContinentRings = 6; // ocean-distance rings to reach full continentality (1 = ~223 km)
|
|
||||||
|
|
||||||
// --- Biota: flora / fauna / funga (see PlanetBiota.cpp + *Gen.cpp) -------
|
// --- Biota: flora / fauna / funga (see PlanetBiota.cpp + *Gen.cpp) -------
|
||||||
// Density scalars (derived each tick) drive the colour views; the discrete
|
// Density scalars (derived each tick) drive the colour views; the discrete
|
||||||
@ -246,15 +207,4 @@ struct PlanetConfig {
|
|||||||
int bioFloraPoints = 20; // flora point budget at full density (scaled by density)
|
int bioFloraPoints = 20; // flora point budget at full density (scaled by density)
|
||||||
int bioFaunaPoints = 16; // fauna point budget at full density
|
int bioFaunaPoints = 16; // fauna point budget at full density
|
||||||
int bioFungaPoints = 14; // funga 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
137
test_live.cpp
@ -1,137 +0,0 @@
|
|||||||
// 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
144
test_ocean.cpp
@ -1,144 +0,0 @@
|
|||||||
// 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