240 lines
11 KiB
JavaScript
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;
|
|
}
|