Live World weather: dynamic clouds & rain cycle (save v10)

A per-cell humidity/cloud/rain cycle advanced on the live clock (PlanetWeather.cpp,
raylib-free): evaporate over warm sunlit seas -> advect humidity & cloud along the prevailing
wind (upwind differencing) -> condense into cloud (saturation vs temperature + windward
orographic lift) -> rain out thick cloud -> dissipate. Bounded exp-rate forms keep it stable at
any timestep, so it runs cleanly from hours/sec up to a month/sec. initWeather() spins the
fields up from the moisture climatology; fully deterministic (no RNG).

Render: a translucent cloud shell over the 3D globe (white -> dark slate where it rains,
alpha = cover) plus a matching drawWeather2D layer on the 2D map (shared drawMapTris
rasterizer), toggled with K (default on). stepSim runs stepWeather each live frame at the
sim-hours added to liveTime (held when paused). Cell-info shows cloud/humidity/raining.

Saved as v10 (humidity/cloud/rain, flag-gated; pre-v10 saves spin weather up live). New
weather* config knobs. Reseed/regen now also drops out of Live World. test_weather.cpp:
fields in range, clouds form + rain falls, oceans moister than land, determinism, v10
round-trip; the other four suites still pass; GUI build clean. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonas Reith 2026-06-28 14:01:34 +02:00
parent d4b46afe00
commit f84e06507a
17 changed files with 404 additions and 31 deletions

View File

@ -51,6 +51,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
N toggle the day/night terminator (Live World) N toggle the day/night terminator (Live World)
T toggle the tide-coloured coastline (Live World) T toggle the tide-coloured coastline (Live World)
O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue) O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue)
K toggle weather clouds/rain cover (Live World)
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/s) -- in Live World: live clock rate (hours/s, hour->month)
S single tectonic tick S single tectonic tick
@ -71,9 +72,9 @@ 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 v10: +weather; v9 +moons; v8 +Live
v7 +biota): seed + config + full planet state; F5 writes it, F9 reloads and World clock; v7 +biota): seed + config + full planet state; F5 writes it, F9
resumes deterministically. As of v6 reloads and 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
@ -202,18 +203,31 @@ moving snow line. axialTilt (above) drives the seasonal declination. Press W to
Moons (1-3, randomized in generateMoons() + saved v9): orbit on the live clock, raise the 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. tides with the sun, and render as small lit spheres with phases, orbit rings and eclipses.
Weather (PlanetConfig, Live World): a dynamic clouds/rain cycle on the live clock (key K),
saved v10. Evaporate over warm seas -> advect along the wind -> condense -> rain -> dissipate.
weatherEvapRate 0.4 /h ocean evaporation toward marine saturation
weatherWindKmh 45 km/h wind speed advecting humidity/cloud
weatherSatBase 0.4 air saturation at 0 C (lower = cloudier)
weatherSatTempCoef 0.025 saturation rise per +1 C
weatherCondense 0.6 /h supersaturation -> cloud
weatherOrographic 0.0009 extra condensation per m of windward upslope
weatherRainThresh 0.5 cloud cover above this rains
weatherRainRate 0.5 /h rain rate from excess cloud
weatherCloudDissip 0.12 /h cloud clearing
## 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/PlanetLive.cpp \
src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetBiota.cpp \
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \ src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
-o /tmp/t && /tmp/t src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
# Biota / Live World / Ocean suites: same source list, swap test_logic.cpp -> # Biota / Live World / Ocean / Weather suites: same source list, swap test_logic.cpp ->
# test_biota.cpp, test_live.cpp or test_ocean.cpp # test_biota.cpp, test_live.cpp, test_ocean.cpp or test_weather.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().

View File

@ -83,6 +83,12 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it.
**warm (poleward) / cold (equatorward)** currents back into `sTemp` as a bounded coastal anomaly **warm (poleward) / cold (equatorward)** currents back into `sTemp` as a bounded coastal anomaly
(`climateCurrentFactor`), so biomes shift naturally. Rendered as warm/cold **current arrows** (`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. over the sea (key `O`, 3D + 2D). This completes the Live World ocean/sky pass.
- **Weather — dynamic clouds & rain** *(done — see `PlanetWeather.cpp`)* — a per-cell
humidity/cloud/rain cycle advanced on the live clock: **evaporate** over warm sunlit seas →
**advect** humidity & cloud along the prevailing wind → **condense** into cloud (extra on
windward upslopes) → **rain** out → **dissipate**. Rendered as a translucent moving cloud
shell (white → dark storm where it rains) over the globe + 2D map (key `K`). Saved (v10).
**Next:** tropical cyclones (hurricanes/typhoons) as moving vortex agents on top of this field.
## Current state ## Current state
@ -373,6 +379,19 @@ Working and verified (logic tested headless):
in `refreshView`. New knob `climateCurrentFactor` (4 °C). `test_ocean.cpp` adds: currents 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 tangent + zero on land + widespread, feedback bounded by the knob and produces both warming and
cooling, deterministic. Live-World ocean/sky pass complete. cooling, deterministic. Live-World ocean/sky pass complete.
- **Live World — dynamic weather (clouds & rain):** `Planet::stepWeather(dtHours)`
(PlanetWeather.cpp) advances a per-cell humidity/cloud/rain cycle on the live clock:
**evaporate** over warm sunlit ocean (uses `sInsolation`+`sTemp`), **advect** humidity & cloud
downwind (upwind differencing along `sWind`/`sUpwind`, `weatherWindKmh`), **condense** the
supersaturated air into cloud — saturation `weatherSatBase + weatherSatTempCoef·T`, plus
windward **orographic** lift — **rain** out cloud above `weatherRainThresh`, then **dissipate**.
`initWeather()` spins the fields up from the moisture climatology; bounded exponential rate
forms keep it stable at any timestep. Runs each live frame in `stepSim` (dt = the same sim-hours
added to `liveTime`; held when paused). Render: a translucent **cloud shell** (white → dark
storm where it rains, alpha = cover) over the 3D globe + a `drawWeather2D` layer on the 2D map,
key `K` (default on); cell-info adds cloud/humidity/raining. **Saved v10** (humidity/cloud/rain,
flag-gated; older saves spin weather up live). Deterministic (no RNG). `test_weather.cpp`:
fields in range, clouds form + rain falls, oceans moister than land, determinism, v10 round-trip.
- 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
@ -405,7 +424,8 @@ src/
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) PlanetLive.cpp computeInsolation/computeLiveSeason (Live World: day/night + live seasons)
PlanetOcean.cpp moons (generate/orbit) + computeTides (Live World sky & tides) PlanetOcean.cpp moons (generate/orbit) + computeTides + computeOceanCurrents
PlanetWeather.cpp stepWeather (Live World dynamic clouds & rain cycle)
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
@ -471,12 +491,12 @@ 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/PlanetLive.cpp \
src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetBiota.cpp \
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \ src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
-o /tmp/t && /tmp/t src/sim/PlanetIO.cpp -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`, `test_live.cpp`, `test_ocean.cpp` or
Biota / Live World / Ocean suites — same source list.) `test_weather.cpp` to run the Biota / Live World / Ocean / Weather 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
@ -507,7 +527,8 @@ elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
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) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) ·
`O` ocean-current arrows (warm/cold) · `SPACE` or on-screen button pause · `O` ocean-current arrows (warm/cold) · `K` weather clouds/rain (Live World) ·
`SPACE` or on-screen button pause ·
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) · `[`/`]` 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,
@ -541,9 +562,11 @@ save header is versioned (currently **9**; v2 adds the `[`/`]` drift rate, v3 a
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing `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, v8 appends the **Live World**
clock — a flag byte + `liveTime`, v9 appends the **moons** block); newer-than-supported is clock — a flag byte + `liveTime`, v9 appends the **moons** block, v10 appends the **weather** block —
humidity/cloud/rain, flag-gated); 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. pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
saves spin weather up live.
**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 —
@ -605,6 +628,13 @@ triangles (plates are fixed in phase 1).
`climateCurrentFactor` (4 °C) is the max coastal warming/cooling from ocean currents (0 = off; `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 ocean-current arrows toggle with `O`). Current deflection angle + smoothing passes are
constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config. constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config.
- **Weather (`weather*` in PlanetConfig / `planet.cfg`):** the Live World clouds/rain cycle —
`weatherEvapRate` (ocean evaporation speed), `weatherWindKmh` (advection speed of humidity/cloud),
`weatherSatBase`/`weatherSatTempCoef` (how much moisture the air holds vs temperature — lower
base = cloudier), `weatherCondense` (supersaturation→cloud rate), `weatherOrographic` (windward
rain on mountains), `weatherRainThresh`/`weatherRainRate` (when/how fast thick cloud rains),
`weatherCloudDissip` (cloud clearing). Toggle the overlay with `K`. Cloud render colours
(white→storm, alpha) are constants in ViewerRender/Map2D.
- Seasons (`season*` + `axialTilt` + `biomeSeasonWeight`, `planet.cfg`) — `axialTilt` is the - Seasons (`season*` + `axialTilt` + `biomeSeasonWeight`, `planet.cfg`) — `axialTilt` is the
master driver (0 = no seasons); `seasonAmpMax` (18 °C max seasonal half-range at full 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` tilt/lat/interior), `seasonLatExp` (1.2, push swing toward poles), `seasonContinentRings`

View File

@ -28,6 +28,7 @@ add_executable(planetsim
src/sim/PlanetClimate.cpp src/sim/PlanetClimate.cpp
src/sim/PlanetLive.cpp src/sim/PlanetLive.cpp
src/sim/PlanetOcean.cpp src/sim/PlanetOcean.cpp
src/sim/PlanetWeather.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

View File

@ -34,7 +34,10 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
- `PlanetLive.cpp``computeInsolation()`/`computeLiveSeason()` (Live World: day/night + live - `PlanetLive.cpp``computeInsolation()`/`computeLiveSeason()` (Live World: day/night + live
seasonal temperature; derived, not saved). seasonal temperature; derived, not saved).
- `PlanetOcean.cpp` — moons (`generateMoons`, `moonDirection`/`sunDirection`/`moonOrbitNormal`) + - `PlanetOcean.cpp` — moons (`generateMoons`, `moonDirection`/`sunDirection`/`moonOrbitNormal`) +
`computeTides()` (Live World sky & equilibrium tides). Moons are saved (v9); tides derived. `computeTides()` + `computeOceanCurrents()` (Live World sky, tides & currents). Moons saved
(v9); tides/currents derived.
- `PlanetWeather.cpp``initWeather`/`stepWeather` (Live World dynamic humidity/cloud/rain cycle;
saved v10).
- `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).
@ -171,6 +174,25 @@ back into `sTemp` as a bounded coastal anomaly (`climateCurrentFactor`, smoothed
applied before seasons → biomes shift with it). Rendered as warm/cold arrows over the sea applied before seasons → biomes shift with it). Rendered as warm/cold arrows over the sea
(`buildCurrents`, key `O`). Currents/feedback are derived (not saved). (`buildCurrents`, key `O`). Currents/feedback are derived (not saved).
## Weather (Live World dynamic clouds & rain)
`PlanetWeather.cpp` advances a per-cell **humidity / cloud / rain** cycle on the live clock
(`stepWeather(dtHours)`), time-varying unlike the static climate. One step: **evaporate** over
warm sunlit ocean (relax humidity toward a marine target scaled by `sTemp` warmth + `sInsolation`
daytime), **advect** humidity & cloud downwind (upwind differencing along `sWind`/`sUpwind`, speed
`weatherWindKmh`), **condense** the supersaturated air into cloud (saturation
`weatherSatBase + weatherSatTempCoef·T`, plus windward orographic lift), **rain** out cloud above
`weatherRainThresh`, then **dissipate** (half returns to humidity). All rate terms use bounded
`1exp(rate·dt)` forms so it's stable at any timestep (the clock can run hours→months/sec).
`initWeather()` seeds it from the moisture climatology. Deterministic (no RNG). Driven each live
frame from `Viewer::stepSim` with dt = the sim-hours added to `liveTime` (0 when paused).
Render: a translucent **cloud shell** over the 3D globe (white → dark slate where it rains, alpha
= cover, a second triangle layer at `visBase+0.03`) and a matching `drawWeather2D` layer on the
2D map (shared `drawMapTris` rasterizer), toggled with `K`. Saved as **v10** (humidity/cloud/rain,
flag-gated; pre-v10 saves spin weather up on entering Live World). Future: tropical cyclones
(moving vortex agents) layered on this field.
## 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 +200,7 @@ Engine is raylib-free, so logic is tested without a display. Build/run:
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \ 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/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.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).

View File

@ -29,8 +29,9 @@ Vector2 mapScreen(const Map2D& m, int idx, Rectangle r, double lonOffset) {
return projLonLat(m.lon[idx], m.lat[idx], lonOffset, r); return projLonLat(m.lon[idx], m.lat[idx], lonOffset, r);
} }
void drawMap2D(const Planet& p, const std::vector<Color>& vc, // Shared triangle rasterizer for the 2D map: calls colorAt(cellIndex) -> Color per vertex.
const Map2D& m, Rectangle r, double lonOffset) { template <typename ColorFn>
static void drawMapTris(const Planet& p, const Map2D& m, Rectangle r, double lonOffset, ColorFn colorAt) {
double hw = EqualEarth::halfWidth(); double hw = EqualEarth::halfWidth();
auto px = [&](double lon, double lat) -> float { auto px = [&](double lon, double lat) -> float {
double x, y; EqualEarth::forward(lon, lat, x, y); double x, y; EqualEarth::forward(lon, lat, x, y);
@ -47,7 +48,8 @@ void drawMap2D(const Planet& p, const std::vector<Color>& vc,
double mx = std::max({lo[0], lo[1], lo[2]}); double mx = std::max({lo[0], lo[1], lo[2]});
if (mx - mn <= M_PI) { // fast path (no wrap) if (mx - mn <= M_PI) { // fast path (no wrap)
for (int t = 0; t < 3; ++t) { for (int t = 0; t < 3; ++t) {
rlColor4ub(vc[v[t]].r, vc[v[t]].g, vc[v[t]].b, 255); Color c = colorAt(v[t]);
rlColor4ub(c.r, c.g, c.b, c.a);
rlVertex2f(px(lo[t], m.lat[v[t]]), m.pos[v[t]].y); rlVertex2f(px(lo[t], m.lat[v[t]]), m.pos[v[t]].y);
} }
} else { // antimeridian seam } else { // antimeridian seam
@ -60,10 +62,28 @@ void drawMap2D(const Planet& p, const std::vector<Color>& vc,
const double shift[3] = { 0.0, 2 * M_PI, -2 * M_PI }; // both edges; scissor clips const double shift[3] = { 0.0, 2 * M_PI, -2 * M_PI }; // both edges; scissor clips
for (double sh : shift) for (double sh : shift)
for (int t = 0; t < 3; ++t) { for (int t = 0; t < 3; ++t) {
rlColor4ub(vc[v[t]].r, vc[v[t]].g, vc[v[t]].b, 255); Color c = colorAt(v[t]);
rlColor4ub(c.r, c.g, c.b, c.a);
rlVertex2f(px(ul[t] + sh, m.lat[v[t]]), m.pos[v[t]].y); rlVertex2f(px(ul[t] + sh, m.lat[v[t]]), m.pos[v[t]].y);
} }
} }
} }
rlEnd(); rlEnd();
} }
void drawMap2D(const Planet& p, const std::vector<Color>& vc,
const Map2D& m, Rectangle r, double lonOffset) {
drawMapTris(p, m, r, lonOffset, [&](int i) { return Color{ vc[i].r, vc[i].g, vc[i].b, 255 }; });
}
void drawWeather2D(const Planet& p, const std::vector<double>& cloud, const std::vector<double>& rain,
const Map2D& m, Rectangle r, double lonOffset) {
if (cloud.empty()) return;
double maxR = 1e-6; for (double v : rain) maxR = std::max(maxR, v);
drawMapTris(p, m, r, lonOffset, [&](int i) -> Color {
double c = std::clamp(cloud[i], 0.0, 1.0);
double rain01 = std::clamp(rain[i] / maxR, 0.0, 1.0);
return Color{ (unsigned char)(245 - 150 * rain01), (unsigned char)(245 - 130 * rain01),
(unsigned char)(250 - 95 * rain01), (unsigned char)(c * 205.0) };
});
}

View File

@ -21,3 +21,8 @@ Vector2 mapScreen(const Map2D& m, int idx, Rectangle r, double lonOffset);
// lonOffset pans the map east/west (radians); y is unchanged by the pan. // lonOffset pans the map east/west (radians); y is unchanged by the pan.
void drawMap2D(const Planet& p, const std::vector<Color>& vc, void drawMap2D(const Planet& p, const std::vector<Color>& vc,
const Map2D& m, Rectangle r, double lonOffset); const Map2D& m, Rectangle r, double lonOffset);
// Translucent Live World cloud/rain layer over the 2D map (white -> dark storm where it rains;
// alpha = cloud cover). Same triangle iteration as drawMap2D but blended on top.
void drawWeather2D(const Planet& p, const std::vector<double>& cloud, const std::vector<double>& rain,
const Map2D& m, Rectangle r, double lonOffset);

View File

@ -64,6 +64,11 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
if (sized(p.tide())) if (sized(p.tide()))
L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i], L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i],
p.tide()[i] >= 0.0 ? "high" : "low"))); p.tide()[i] >= 0.0 ? "high" : "low")));
if (sized(p.cloud()))
L.push_back(std::string(TextFormat("weather: cloud %.0f%% humidity %.0f%%%s",
p.cloud()[i] * 100.0,
sized(p.humidity()) ? p.humidity()[i] * 100.0 : 0.0,
(sized(p.rain()) && p.rain()[i] > 0.02) ? " raining" : "")));
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)

View File

@ -227,6 +227,7 @@ void Viewer::regenWorld() { // after generate(): geometry change
buildMap2D(planet, mapRect, map2D); buildMap2D(planet, mapRect, map2D);
selectedCell = -1; subgrids.clear(); selectedCell = -1; subgrids.clear();
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false; settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
liveWorld = false; // reseed/regen drops back to World Creation
planet.drifting = false; // Phase 1: original forming behavior planet.drifting = false; // Phase 1: original forming behavior
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy; phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
rivers.clear(); bigRivers.clear(); rivers.clear(); bigRivers.clear();
@ -278,7 +279,7 @@ void Viewer::loadGame(const char* path) {
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw); if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock 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, ver >= 9, ver >= 10)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather
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
@ -302,7 +303,8 @@ void Viewer::loadGame(const char* path) {
void Viewer::stepSim() { void Viewer::stepSim() {
if (liveWorld) { if (liveWorld) {
// --- Live World: advance the slow clock; geology is frozen -------- // --- Live World: advance the slow clock; geology is frozen --------
if (!paused) liveTime += liveRate * GetFrameTime(); // hours double dtH = (!paused) ? liveRate * GetFrameTime() : 0.0; // simulated hours this frame
liveTime += dtH;
double days = liveTime / planet.cfg.dayLengthHours; double days = liveTime / planet.cfg.dayLengthHours;
double dayOfYear01 = days / planet.cfg.yearLengthDays; double dayOfYear01 = days / planet.cfg.yearLengthDays;
dayOfYear01 -= std::floor(dayOfYear01); dayOfYear01 -= std::floor(dayOfYear01);
@ -319,6 +321,7 @@ void Viewer::stepSim() {
moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z }); 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 }); moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z });
} }
planet.stepWeather(dtH); // dynamic clouds & rain on the live clock
rebuildLiveOverlay(); rebuildLiveOverlay();
return; return;
} }

View File

@ -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 = 10; // v10: +weather; v9: +moons; v8: +Live World clock; v7: +biota; 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)
@ -90,6 +90,7 @@ struct Viewer {
bool showTides = false; // colour the coastline by the live tide level (key T) bool showTides = false; // colour the coastline by the live tide level (key T)
std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows
bool showCurrents = false; // ocean current arrows, warm/cold (key O) bool showCurrents = false; // ocean current arrows, warm/cold (key O)
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
// Selection + subgrid (phase 4/5 preview). // Selection + subgrid (phase 4/5 preview).
int selectedCell = -1; int selectedCell = -1;

View File

@ -145,6 +145,7 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator
if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover
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)

View File

@ -103,6 +103,31 @@ void Viewer::renderGlobe3D() {
} }
rlEnd(); rlSetLineWidth(1.0f); rlEnd(); rlSetLineWidth(1.0f);
} }
// Live World weather: a translucent cloud shell over the globe (white -> dark storm where it
// rains), alpha = cloud cover. Drawn as a second triangle layer just above the terrain.
if (liveWorld && showClouds && !planet.cloud().empty()) {
const std::vector<double>& cl = planet.cloud();
const std::vector<double>& rn = planet.rain();
double maxR = 1e-6; for (double r : rn) maxR = std::max(maxR, r);
const std::vector<int>& ctri = planet.triIndices();
const float cr = visBase + 0.03f;
rlBegin(RL_TRIANGLES);
for (size_t k = 0; k + 2 < ctri.size(); k += 3) {
for (int j = 0; j < 3; ++j) {
int idx = ctri[k + j];
double c = std::clamp(cl[idx], 0.0, 1.0);
double rain01 = std::clamp(rn[idx] / maxR, 0.0, 1.0);
unsigned char R = (unsigned char)(245 - 150 * rain01); // white -> slate
unsigned char G = (unsigned char)(245 - 130 * rain01);
unsigned char B = (unsigned char)(250 - 95 * rain01);
unsigned char A = (unsigned char)(std::clamp(c, 0.0, 1.0) * 205.0);
const Vec3& u = planet.cells[idx].unit;
rlColor4ub(R, G, B, A);
rlVertex3f((float)(u.x * cr), (float)(u.y * cr), (float)(u.z * cr));
}
}
rlEnd();
}
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) {
@ -195,6 +220,7 @@ void Viewer::renderMap2D() {
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 (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, mapRect, mapLon);
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon); if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon);
if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, 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);
@ -374,8 +400,8 @@ 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 (*6 cycles mean/summer/winter/season)");
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] | N day/night [%s] | T tides [%s] | O currents [%s] | K clouds [%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", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off", showClouds ? "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] | W live [%s] | R reseed | +/-",
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off")); phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg"); line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");

View File

@ -51,6 +51,8 @@ void Planet::buildGeometry() {
} }
sBiota.assign(cells.size(), {}); // empty biota population until generateBiota() sBiota.assign(cells.size(), {}); // empty biota population until generateBiota()
sHasBiota = false; sHasBiota = false;
sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World
sHasWeather = false;
} }
void Planet::assignPlates() { void Planet::assignPlates() {

View File

@ -89,6 +89,17 @@ public:
void computeOceanCurrents(); void computeOceanCurrents();
const std::vector<Vec3>& current() const { return sCurrent; } const std::vector<Vec3>& current() const { return sCurrent; }
// Weather (Live World): dynamic per-cell humidity / cloud cover / rain advanced on the live
// clock. initWeather() spins the fields up from the climatology; stepWeather(dtHours) runs
// one cycle (evaporate over warm seas -> advect along the wind -> condense into cloud, with
// orographic lift -> rain out -> dissipate). Reads sInsolation/sTemp/sWind/sUpwind/sMoist
// (computeClimate + computeInsolation set those). Saved (v10).
void initWeather();
void stepWeather(double dtHours);
const std::vector<double>& humidity() const { return sHumidity; }
const std::vector<double>& cloud() const { return sCloud; }
const std::vector<double>& rain() const { return sRain; }
// 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
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves. // cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
@ -124,8 +135,10 @@ public:
// 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 // hasMoons: whether the stream carries the moons block (save v9+); older saves
// synthesize moons from the seed instead. // synthesize moons from the seed instead. hasWeather: the weather block (save v10+);
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool hasMoons = true); // older saves leave weather to spin up on entering Live World.
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
bool hasMoons = true, bool hasWeather = true);
// Helpers for rendering / info. // Helpers for rendering / info.
double cellWidthMeters() const; // approx lateral cell spacing double cellWidthMeters() const; // approx lateral cell spacing
@ -199,6 +212,9 @@ private:
// sTide is the equilibrium tidal height (m) from the moons + sun. // sTide is the equilibrium tidal height (m) from the moons + sun.
std::vector<double> sInsolation, sLiveTemp, sTide; std::vector<double> sInsolation, sLiveTemp, sTide;
std::vector<Vec3> sCurrent; // ocean surface current velocity (tangent; zero on land) std::vector<Vec3> sCurrent; // ocean surface current velocity (tangent; zero on land)
// Weather (Live World; saved v10). sHasWeather latches once spun up/loaded.
std::vector<double> sHumidity, sCloud, sRain;
bool sHasWeather = false;
// 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.

View File

@ -36,6 +36,9 @@
D(bioFungaTempMin) D(bioRegionBonus) \ D(bioFungaTempMin) D(bioRegionBonus) \
D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \ D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \
D(tideAmplitude) D(tideSunFactor) \ D(tideAmplitude) D(tideSunFactor) \
D(weatherEvapRate) D(weatherWindKmh) D(weatherSatBase) D(weatherSatTempCoef) \
D(weatherCondense) D(weatherOrographic) D(weatherRainThresh) D(weatherRainRate) \
D(weatherCloudDissip) \
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(seasonContinentRings) \
@ -191,6 +194,15 @@ std::string validateConfig(const PlanetConfig& cfg) {
E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp")); E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp"));
E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude")); E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude"));
E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor")); E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor"));
E(rng(cfg.weatherEvapRate, 0.0, 50.0, "weatherEvapRate"));
E(rng(cfg.weatherWindKmh, 0.0, 1000.0, "weatherWindKmh"));
E(rng(cfg.weatherSatBase, 0.01, 5.0, "weatherSatBase"));
E(rng(cfg.weatherSatTempCoef, 0.0, 1.0, "weatherSatTempCoef"));
E(rng(cfg.weatherCondense, 0.0, 50.0, "weatherCondense"));
E(rng(cfg.weatherOrographic, 0.0, 1.0, "weatherOrographic"));
E(rng(cfg.weatherRainThresh, 0.0, 1.5, "weatherRainThresh"));
E(rng(cfg.weatherRainRate, 0.0, 50.0, "weatherRainRate"));
E(rng(cfg.weatherCloudDissip, 0.0, 50.0, "weatherCloudDissip"));
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"));
@ -279,9 +291,12 @@ void Planet::writeState(std::ostream& os) const {
writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga); writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga);
} }
} }
// v10: Live World weather (humidity/cloud/rain). Flag-gated like biota.
uint8_t hasWx = (sHasWeather && sHumidity.size() == cells.size()) ? 1 : 0; writePod(os, hasWx);
if (hasWx) { writeVec(os, sHumidity); writeVec(os, sCloud); writeVec(os, sRain); }
} }
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons) { bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, bool hasWeather) {
// 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.
@ -327,6 +342,16 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
} }
} }
} }
// v10: Live World weather. Older saves leave it to spin up on entering Live World.
sHasWeather = false; sHumidity.clear(); sCloud.clear(); sRain.clear();
if (hasWeather) {
uint8_t hasWx = 0; readPod(is, hasWx);
if (hasWx) {
readVec(is, sHumidity); readVec(is, sCloud); readVec(is, sRain);
if (!is || (int)sHumidity.size() != (int)cells.size()) return false;
sHasWeather = true;
}
}
computeBiotaDensity(); // derived density scalars for the colour views computeBiotaDensity(); // derived density scalars for the colour views
return (bool)is; return (bool)is;
} }

View File

@ -257,4 +257,18 @@ struct PlanetConfig {
double seaIceTemp = -2.0; // C: ocean below the live temperature shows sea ice 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 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) double tideSunFactor = 0.46; // sun's tide weight relative to a unit moon (Earth ~0.46)
// --- Weather (Live World dynamic clouds & rain) -- see PlanetWeather.cpp -----
// A per-cell humidity/cloud/rain cycle advanced on the live clock: evaporate over warm
// sunlit seas, advect along the prevailing wind, condense into cloud (more on windward
// upslopes), rain out, and dissipate. Rates are per simulated hour.
double weatherEvapRate = 0.4; // /h: ocean evaporation toward marine saturation
double weatherWindKmh = 45.0; // km/h: prevailing wind speed for advecting humidity/cloud
double weatherSatBase = 0.4; // air saturation humidity at 0 C (warmer air holds more)
double weatherSatTempCoef = 0.025; // saturation rise per +1 C
double weatherCondense = 0.6; // /h: fraction of supersaturation that becomes cloud
double weatherOrographic = 0.0009; // extra condensation per m of windward upslope
double weatherRainThresh = 0.5; // cloud cover above this precipitates
double weatherRainRate = 0.5; // /h: rain rate from excess cloud
double weatherCloudDissip = 0.12; // /h: cloud clearing (half returns to humidity)
}; };

96
src/sim/PlanetWeather.cpp Normal file
View File

@ -0,0 +1,96 @@
#include "Planet.hpp"
#include <algorithm>
#include <cmath>
#include <vector>
// Live World weather: a dynamic per-cell humidity / cloud / rain cycle advanced on the live
// clock (geometry fixed -- these are fields flowed over the grid, like climate, but time-varying).
// One step: evaporate over warm sunlit seas -> advect humidity & cloud along the prevailing wind
// -> condense the supersaturated air into cloud (extra on windward upslopes) -> rain out the
// thick cloud -> dissipate. Reads the static climate scaffolding (sTemp/sWind/sUpwind/sMoist set
// by computeClimate) and the live sInsolation (computeInsolation). Deterministic; saved (v10).
void Planet::initWeather() {
const int n = (int)cells.size();
sHumidity.assign(n, 0.0);
sCloud.assign(n, 0.0);
sRain.assign(n, 0.0);
const double sea = cfg.seaLevel;
const bool haveM = ((int)sMoist.size() == n);
for (int i = 0; i < n; ++i) {
if (cells[i].elevation <= sea) sHumidity[i] = 0.9; // saturated marine air
else sHumidity[i] = haveM ? (0.2 + 0.5 * sMoist[i]) : 0.3; // land: from climatology
}
sHasWeather = true;
}
void Planet::stepWeather(double dtHours) {
const int n = (int)cells.size();
if (!sHasWeather || (int)sHumidity.size() != n || (int)sCloud.size() != n || (int)sRain.size() != n)
initWeather();
if (dtHours <= 0.0) return; // paused: hold the current sky
if ((int)sTemp.size() != n) return; // need the climate fields
const double sea = cfg.seaLevel;
auto isOcean = [&](int i) { return cells[i].elevation <= sea; };
const double cw = std::max(1.0, cellWidthMeters());
double advFrac = std::clamp(cfg.weatherWindKmh * 1000.0 * dtHours / cw, 0.0, 1.0);
const bool haveSun = ((int)sInsolation.size() == n);
const bool haveUp = ((int)sUpwind.size() == n);
// 1. Advect humidity downwind (upwind differencing) + evaporate over warm sunlit ocean.
std::vector<double> nh(n);
for (int i = 0; i < n; ++i) {
double hUp = (haveUp && sUpwind[i] >= 0) ? sHumidity[sUpwind[i]] : sHumidity[i];
double h = sHumidity[i] * (1.0 - advFrac) + hUp * advFrac;
if (isOcean(i)) {
double tf = std::clamp((sTemp[i] + 2.0) / 30.0, 0.0, 1.0); // warm seas evaporate more
double sun = haveSun ? (0.5 + 0.5 * sInsolation[i]) : 0.7; // daytime boost
double target = 0.55 + 0.45 * tf; // marine humidity target
double rate = 1.0 - std::exp(-cfg.weatherEvapRate * sun * dtHours);
if (target > h) h += (target - h) * rate;
}
nh[i] = h;
}
sHumidity.swap(nh);
// 2. Advect cloud (it drifts with the wind too).
std::vector<double> nc(n);
for (int i = 0; i < n; ++i) {
double cUp = (haveUp && sUpwind[i] >= 0) ? sCloud[sUpwind[i]] : sCloud[i];
nc[i] = sCloud[i] * (1.0 - advFrac) + cUp * advFrac;
}
sCloud.swap(nc);
// 3. Condense (saturation + orographic lift) -> rain -> dissipate, per cell.
const double condR = 1.0 - std::exp(-cfg.weatherCondense * dtHours);
const double rainR = 1.0 - std::exp(-cfg.weatherRainRate * dtHours);
const double dissR = 1.0 - std::exp(-cfg.weatherCloudDissip * dtHours);
const double invDt = 1.0 / dtHours;
for (int i = 0; i < n; ++i) {
double sat = std::max(0.05, cfg.weatherSatBase + cfg.weatherSatTempCoef * std::max(0.0, sTemp[i]));
double cond = 0.0;
double excess = sHumidity[i] - sat;
if (excess > 0.0) cond += excess * condR; // convective/thermal
if (haveUp && sUpwind[i] >= 0) { // orographic (windward)
double up = cells[i].elevation - cells[sUpwind[i]].elevation;
if (up > 0.0) cond += sHumidity[i] * std::min(1.0, up * cfg.weatherOrographic) * condR;
}
cond = std::min(cond, sHumidity[i]);
sHumidity[i] -= cond;
sCloud[i] += cond;
double rain = 0.0;
if (sCloud[i] > cfg.weatherRainThresh) {
rain = (sCloud[i] - cfg.weatherRainThresh) * rainR;
sCloud[i] -= rain;
}
double diss = sCloud[i] * dissR;
sCloud[i] -= diss;
sHumidity[i] += diss * 0.5; // half re-evaporates
sRain[i] = rain * invDt; // intensity (per hour)
if (sHumidity[i] < 0.0) sHumidity[i] = 0.0;
sCloud[i] = std::clamp(sCloud[i], 0.0, 1.5);
}
}

92
test_weather.cpp Normal file
View File

@ -0,0 +1,92 @@
// Headless test for the Live World weather cycle (humidity / cloud / rain). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_weather.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/PlanetWeather.cpp \
// src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tw && /tmp/tw
//
// Verifies: fields stay in range; oceans (the evaporation source) end up moister than land;
// clouds form and rain falls somewhere; the cycle is deterministic; and save v10 round-trips it.
#include "Planet.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;
}
// Run a fixed weather sequence on a planet (returns whether rain ever fell, max cloud seen).
static void runWeather(Planet& p, bool& everRained, double& maxCloud) {
p.initWeather();
everRained = false; maxCloud = 0.0;
for (int k = 0; k < 200; ++k) {
p.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); // sun advances
p.stepWeather(1.0); // 1-hour steps
const std::vector<double>& rn = p.rain();
const std::vector<double>& cl = p.cloud();
for (size_t i = 0; i < rn.size(); ++i) {
if (rn[i] > 0.0) everRained = true;
maxCloud = std::max(maxCloud, cl[i]);
}
}
}
int main() {
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 1337;
Planet p; p.generate(cfg);
const int n = (int)p.cells.size();
std::printf("Weather: cycle\n");
bool rained = false; double maxCloud = 0.0;
runWeather(p, rained, maxCloud);
bool inRange = true;
for (int i = 0; i < n; ++i) {
if (p.humidity()[i] < -1e-9) inRange = false;
if (p.cloud()[i] < -1e-9 || p.cloud()[i] > 1.5 + 1e-9) inRange = false;
if (p.rain()[i] < -1e-9) inRange = false;
}
check(inRange, "humidity/cloud/rain stay in range");
check(maxCloud > 0.05, "clouds form");
check(rained, "rain falls somewhere");
// Oceans are the moisture source -> moister than land on average.
double oh = 0, lh = 0; int oc = 0, lc = 0;
for (int i = 0; i < n; ++i) {
if (p.cells[i].elevation <= cfg.seaLevel) { oh += p.humidity()[i]; ++oc; }
else { lh += p.humidity()[i]; ++lc; }
}
oh /= std::max(1, oc); lh /= std::max(1, lc);
std::printf(" mean humidity: ocean %.3f, land %.3f\n", oh, lh);
check(oh > lh, "oceans end up moister than land");
std::printf("Weather: determinism\n");
Planet p2; p2.generate(cfg);
bool r2; double mc2; runWeather(p2, r2, mc2);
bool same = true;
for (int i = 0; i < n; ++i)
if (p2.humidity()[i] != p.humidity()[i] || p2.cloud()[i] != p.cloud()[i]
|| p2.rain()[i] != p.rain()[i]) same = false;
check(same, "same seed + sequence -> identical weather");
std::printf("Weather: save v10\n");
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
p.writeState(ss);
Planet q;
bool ok = q.readState(ss, true, true, true, true);
bool rt = ok && (int)q.cloud().size() == n;
for (int i = 0; i < n && rt; ++i)
if (q.humidity()[i] != p.humidity()[i] || q.cloud()[i] != p.cloud()[i] || q.rain()[i] != p.rain()[i])
rt = false;
check(rt, "save v10 round-trips the weather state");
std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures);
return failures ? 1 : 0;
}