Seasons (obliquity): per-cell summer/winter temps + richer cold biomes
axialTilt was visual-only ("groundwork for seasons"). It now drives climate.
computeClimate() adds derived sTempSummer/sTempWinter around the annual mean:
summer/winter = sTemp +/- A, A = seasonAmpMax * sin(tilt)/sin(23.44) *
latShape * continentality
- tiltFactor: 0 tilt -> no seasons, Earth tilt -> 1.
- latShape (pow(|lat|/90, seasonLatExp)): poles swing most.
- continentality: a multi-source BFS ring-distance from ocean cells -- oceans
and coasts are muted by thermal inertia, interiors swing most.
Result: ~0 swing at the equatorial coast, large at high-latitude interiors.
classifyBiomes() blends WINTER temp into the Tundra/Taiga cold cutoffs via
biomeSeasonWeight (0 = annual-mean only = unchanged biomes; default 0.6), so
cold-winter continental interiors become boreal/tundra (Siberia effect). The
amplitude is geographically shaped, so cold biomes expand only where seasons
bite. Fields are derived/not-saved -> no save-format change.
Viewer: color key 6 now CYCLES Temperature -> summer -> winter -> seasonality
(new seasonColor ramp + labels); cell-info shows summer/winter. New season* +
biomeSeasonWeight config knobs (planet.cfg, validated). Docs updated.
Headless (test/season): summer >= mean >= winter; 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. test_logic + test_biota pass; full
app builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
708b23d774
commit
8ed9ae4515
9
BUILD.md
9
BUILD.md
@ -140,6 +140,15 @@ 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
|
||||||
|
|||||||
28
CLAUDE.md
28
CLAUDE.md
@ -47,7 +47,10 @@ 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.
|
||||||
Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
|
- **Seasons (obliquity)** *(done)* — `axialTilt` drives per-cell summer/winter temperatures
|
||||||
|
(`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
|
||||||
@ -252,6 +255,20 @@ 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
|
||||||
@ -409,7 +426,8 @@ 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; active mode shown top-center of the globe) ·
|
(`8`/`9`/`0` = biota density; `6` **cycles** temperature → summer → winter → seasonality;
|
||||||
|
active mode shown top-center of the globe) ·
|
||||||
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
||||||
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
||||||
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
||||||
@ -493,6 +511,12 @@ 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.
|
||||||
|
- 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`/
|
||||||
|
|||||||
@ -92,7 +92,13 @@ 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.
|
cold concentrates at poles) − `biomeElevLapse` × elevation. This is the **annual mean**; the
|
||||||
|
**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.
|
||||||
- **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
|
||||||
|
|||||||
@ -91,6 +91,9 @@ 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 "?";
|
||||||
}
|
}
|
||||||
@ -138,6 +141,14 @@ 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 };
|
||||||
|
}
|
||||||
|
|
||||||
// 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,7 +5,8 @@
|
|||||||
// 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);
|
||||||
@ -22,6 +23,8 @@ 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);
|
||||||
// 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);
|
||||||
|
|||||||
@ -50,6 +50,9 @@ 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])));
|
||||||
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)
|
||||||
|
|||||||
@ -96,6 +96,8 @@ 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();
|
||||||
@ -114,6 +116,10 @@ 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;
|
||||||
|
|||||||
@ -102,7 +102,13 @@ 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)) { mode = ColorMode::Temperature; recolor(); }
|
if (IsKeyPressed(KEY_SIX)) { // cycle temperature sub-views: mean->summer->winter->seasonality
|
||||||
|
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(); }
|
||||||
|
|||||||
@ -179,7 +179,7 @@ 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");
|
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]",
|
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"));
|
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off"));
|
||||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | R reseed | +/-",
|
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | R reseed | +/-",
|
||||||
|
|||||||
@ -51,9 +51,11 @@ 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
|
const std::vector<double>& temperature() const { return sTemp; } // deg C, annual mean
|
||||||
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
|
||||||
|
|
||||||
// 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
|
||||||
@ -152,8 +154,9 @@ 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.
|
// 0..1-normalized precipitation the biome classifier reads. sTempSummer/sTempWinter are
|
||||||
std::vector<double> sTemp, sPrecip, sMoist;
|
// the obliquity-driven seasonal extremes around the annual mean sTemp (see Seasons).
|
||||||
|
std::vector<double> sTemp, sPrecip, sMoist, sTempSummer, sTempWinter;
|
||||||
std::vector<Vec3> sWind;
|
std::vector<Vec3> sWind;
|
||||||
std::vector<int> sUpwind;
|
std::vector<int> sUpwind;
|
||||||
|
|
||||||
|
|||||||
@ -22,11 +22,18 @@ 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)
|
double temp = sTemp[i]; // climate temperature (deg C, annual mean)
|
||||||
|
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;
|
||||||
@ -43,9 +50,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 (temp < TUNDRA_TEMP) b = Biome::Tundra;
|
if (coldT < 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 (temp < TAIGA_TEMP) b = (moist > TAIGA_MOIST) ? Biome::Taiga : Biome::Tundra;
|
else if (coldT < 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,6 +26,8 @@ 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);
|
||||||
|
|
||||||
@ -131,4 +133,42 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,15 +27,16 @@
|
|||||||
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(biomeLakeMinDepth) D(biomeSeasonWeight) \
|
||||||
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
||||||
D(climateOroRefHeight) D(climateContinentality) \
|
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) \
|
||||||
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(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \
|
||||||
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)
|
||||||
@ -162,11 +163,15 @@ 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.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"));
|
||||||
@ -188,6 +193,7 @@ 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"));
|
||||||
|
|||||||
@ -186,6 +186,8 @@ 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
|
||||||
@ -199,6 +201,16 @@ struct PlanetConfig {
|
|||||||
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)
|
||||||
|
|
||||||
|
// --- 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
|
||||||
// slot/point population (generated on demand, saved) draws archetypes by size.
|
// slot/point population (generated on demand, saved) draws archetypes by size.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user