Tooling
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user