Files
2026-09-25 17:02:24 +03:00

240 lines
11 KiB
JavaScript

// Temperature simulation: computes per-region surface temperature for summer
// and winter seasons based on ITCZ position, continentality, moisture-dependent
// elevation lapse rate (dry adiabatic 9.3 C/km to moist adiabatic 4.5 C/km),
// ocean current warmth, and precipitation/cloud cover moderation.
// Returns normalized 0-1 values mapped to a fixed -45 to +45 C range.
import { smoothstep } from './wind.js';
import { elevToHeightKm } from './color-map.js';
import { smoothField, makeItczLookup } from './climate-util.js';
const DEG = Math.PI / 180;
// ── Diffuse ocean warmth onto nearby coastal land ───────────────────────────
// Uses plate-based continentality so that warmth spreads freely across
// shallow continental-shelf ocean and penetrates further inland. Ocean cells
// on continental plates (shallow seas) inherit warmth from nearby oceanic-
// plate cells first, then the warmth diffuses onto land.
function diffuseOceanWarmth(mesh, r_oceanWarmth, r_isLand, r_plateContinentality, passes) {
const { adjOffset, adjList, numRegions } = mesh;
const coastal = new Float32Array(numRegions);
// Seed: all ocean cells contribute their warmth directly.
// Continental-shelf ocean cells may have weak/no current warmth;
// they'll pick up values from nearby oceanic-plate neighbors via diffusion.
for (let r = 0; r < numRegions; r++) {
if (!r_isLand[r]) {
coastal[r] = r_oceanWarmth ? r_oceanWarmth[r] : 0;
}
}
const tmp = new Float32Array(numRegions);
for (let pass = 0; pass < passes; pass++) {
tmp.set(coastal);
for (let r = 0; r < numRegions; r++) {
// Skip deep-interior continental cells (plate-based)
if (r_plateContinentality && r_plateContinentality[r] >= 0.95) continue;
// Ocean cells also participate in diffusion so continental-shelf
// cells inherit warmth from nearby open-ocean neighbors
let sum = coastal[r];
let count = 1;
const end = adjOffset[r + 1];
for (let ni = adjOffset[r]; ni < end; ni++) {
sum += coastal[adjList[ni]];
count++;
}
tmp[r] = sum / count;
}
coastal.set(tmp);
}
return coastal;
}
// ── Main entry point ────────────────────────────────────────────────────────
/**
* Compute seasonal temperature fields.
*
* @param {SphereMesh} mesh
* @param {Float32Array} r_xyz - per-region 3D positions
* @param {Float32Array} r_elevation - per-region elevation
* @param {object} windResult - output from computeWind()
* @param {object} oceanResult - output from computeOceanCurrents()
* @param {object} precipResult - output from computePrecipitation()
* @returns {{ r_temperature_summer, r_temperature_winter, _tempTiming }}
*/
export function computeTemperature(mesh, r_xyz, r_elevation, windResult, oceanResult, precipResult, temperatureOffset = 0) {
const numRegions = mesh.numRegions;
const timing = [];
const { r_lat, r_lon, r_isLand, r_continentality, r_plateContinentality } = windResult;
// Minimal smoothing: 1 pass just to blend cell-to-cell noise
const smoothPasses = 1;
const T_MIN = -45;
const T_MAX = 45;
const T_RANGE = T_MAX - T_MIN;
const result = {};
// Pre-compute constants shared across seasons
const avgEdgeKm = (Math.PI * 6371) / Math.sqrt(numRegions);
const oceanWarmthPasses = Math.max(4, Math.round(1400 / avgEdgeKm));
const plateCont = r_plateContinentality || r_continentality;
const seasons = ['summer', 'winter'];
for (const name of seasons) {
const t0 = performance.now();
const r_oceanWarmth = oceanResult[`r_ocean_warmth_${name}`];
const r_oceanSpeed = oceanResult[`r_ocean_speed_${name}`];
const r_precip = precipResult[`r_precip_${name}`];
const itczLookup = makeItczLookup(windResult.itczLons,
name === 'summer' ? windResult.itczLatsSummer : windResult.itczLatsWinter);
// Pre-compute diffused ocean warmth for coastal land influence
// Use plate-based continentality for diffusion so warmth crosses
// continental shelves and reaches further inland
const coastalWarmth = diffuseOceanWarmth(mesh, r_oceanWarmth, r_isLand, plateCont, oceanWarmthPasses);
const temp = new Float32Array(numRegions);
for (let r = 0; r < numRegions; r++) {
const lat = r_lat[r];
const lon = r_lon[r];
const latDeg = lat / DEG;
const isLand = r_isLand[r];
const elev = r_elevation[r];
const cont = r_continentality ? r_continentality[r] : 0;
const pCont = r_plateContinentality ? r_plateContinentality[r] : cont;
// ── 1. Base temperature from thermal equator (ITCZ) ──
// Two curves blended by absolute latitude:
// - T_itcz: based on distance from the actual (land-warped) ITCZ
// - T_flat: based on distance from a fixed ITCZ at ±5° (ocean default)
// Near the tropics the real ITCZ matters; at high latitudes the
// ITCZ position is irrelevant and a stable zonal baseline takes over.
const tropicalHW = 13; // flat plateau half-width (degrees)
const maxDist = 90 - tropicalHW;
// Actual ITCZ curve
const itczLat = itczLookup(lon);
const distItcz = Math.abs(lat - itczLat) / DEG;
const tItcz = Math.max(0, distItcz - tropicalHW) / maxDist;
const T_itcz = 28 - 47 * Math.pow(tItcz, 1.4);
// Flat reference curve (ITCZ at 5° in summer hemisphere)
const flatItczLat = (name === 'summer' ? 5 : -5) * DEG;
const distFlat = Math.abs(lat - flatItczLat) / DEG;
const tFlat = Math.max(0, distFlat - tropicalHW) / maxDist;
const T_flat = 28 - 47 * Math.pow(tFlat, 1.4);
// Blend: ITCZ curve dominates tropics, flat curve dominates poles
const absLatDeg = Math.abs(lat) / DEG;
const blend = smoothstep(45, 90, absLatDeg);
let T = T_itcz * (1 - blend) + T_flat * blend;
// ── 2. Elevation lapse rate ──
// Moisture-dependent: dry air cools at ~9.8 C/km (dry adiabatic),
// saturated air at ~5 C/km (moist adiabatic) due to latent heat
// release. Use precipitation as a moisture proxy to interpolate.
const moisture = r_precip ? r_precip[r] : 0.5;
const lapse = 4.5 + 4.8 * (1 - moisture); // 4.5 C/km (wet) to 9.3 C/km (dry)
if (isLand && elev > 0) {
T -= lapse * elevToHeightKm(elev);
}
// ── 5. Ocean current temperature influence ──
if (!isLand && r_oceanWarmth && r_oceanSpeed) {
// Direct ocean effect: warm/cold currents shift SST
const warmth = r_oceanWarmth[r];
const speed = r_oceanSpeed[r];
T += warmth * Math.min(1, speed * 2) * 16;
} else if (isLand) {
// Coastal land: diffused ocean warmth fades with plate-based
// continentality so the effect reaches further inland and
// crosses continental shelves naturally
const cw = coastalWarmth[r];
if (Math.abs(cw) > 0.001) {
T += cw * (1 - smoothstep(0, 0.95, pCont)) * 20;
}
}
// ── 6. Precipitation / cloud cover moderation ──
if (r_precip) {
const p = r_precip[r];
if (p > 0.5) {
// High precip → clouds → moderate toward latitude baseline
const mod = smoothstep(0.5, 1.0, p) * 0.15;
// Pull toward 0 (moderate extremes)
T *= (1 - mod);
} else if (p < 0.3) {
// Low precip → clear skies → amplify extremes
const amp = smoothstep(0.3, 0.0, p) * 0.15;
T *= (1 + amp);
}
}
// ── 7. Maritime / continental moderation ──
// Ocean has high thermal inertia: coasts and small islands have
// smaller seasonal temperature swings (moderate climate), while
// continental interiors get more extreme summers and winters.
// Compute an annual-mean baseline (ITCZ at equator, no seasonal
// shift) and scale the seasonal deviation by continentality.
{
const distAnn = Math.abs(lat) / DEG; // distance from equator
const tAnn = Math.max(0, distAnn - tropicalHW) / maxDist;
const T_annual = 28 - 47 * Math.pow(tAnn, 1.4); // match new curve
// Apply same moisture-dependent lapse to annual baseline
const T_ann_adj = isLand && elev > 0
? T_annual - lapse * elevToHeightKm(elev)
: T_annual;
const deviation = T - T_ann_adj;
// Latitude-dependent seasonal boost: ITCZ shift alone gives ~5-6°C
// swing; real planets have 15-25°C from direct solar heating.
// Peaks at 55-75° latitude, zero at equator and poles.
const seasonalBoost = 12 * smoothstep(10, 55, distAnn)
* (1 - smoothstep(75, 90, distAnn));
const isLocalSummer = (name === 'summer') ? (lat >= 0) : (lat < 0);
const seasonSign = isLocalSummer ? 1 : -1;
const boostedDeviation = deviation + seasonSign * seasonalBoost;
// Maritime: coast damps swing to 50%, deep interior amplifies to 120%
const maritimeFactor = 0.50 + cont * 0.70;
T = T_ann_adj + boostedDeviation * maritimeFactor;
}
T += temperatureOffset;
temp[r] = T;
}
const tCompute = performance.now() - t0;
// ── 7. Laplacian smoothing ──
const tSmooth0 = performance.now();
smoothField(mesh, temp, smoothPasses);
const tSmooth = performance.now() - tSmooth0;
// ── 8. Normalize to 0-1 using fixed range ──
const tNorm0 = performance.now();
for (let r = 0; r < numRegions; r++) {
temp[r] = Math.max(0, Math.min(1, (temp[r] - T_MIN) / T_RANGE));
}
const tNorm = performance.now() - tNorm0;
timing.push({ stage: `Temp: compute (${name})`, ms: tCompute });
timing.push({ stage: `Temp: smooth (${name})`, ms: tSmooth });
timing.push({ stage: `Temp: normalize (${name})`, ms: tNorm });
result[`r_temperature_${name}`] = temp;
}
result._tempTiming = timing;
return result;
}