Temperature field: In IslandsCampo de temperatura: En IslandsMoisture field: In IslandsCampo de humedad: En IslandsWhittaker biome classification: In IslandsClasificación de biomas de Whittaker: En IslandsRain shadow: PartialSombra de lluvia: Parcial

The idea in plain words

A biome (rainforest, steppe, tundra) depends mostly on two numbers: how warm and how wet. The ecologist Robert Whittaker is credited with a diagram that puts temperature on one axis and rainfall on the other, with each biome in its own region. Map generators borrow the idea: they work out a temperature and a moisture at each point and look them up in a table.

Here temperature falls with altitude and towards the poles, and rises a little by the sea. Moisture comes from a noise, from the nearby sea and, if you turn it on, from the wind: wet air that crosses the map and leaves its water on the slopes it climbs. Behind a hill the air arrives dry. That is the rain shadow.

Switch the layer under the map to Temperature, Moisture or Wind air to see each number on its own, and tap a cell of the table to see where it falls on the map. ✦ IA

TODO: Claudio: why this matters to you (the climate of the game's islands, a place you know).

The 4 × 4 table

The table cuts temperature into four rows (cold, cool, warm, hot) and moisture into four columns. It is Islands' table, not one from a book: Amit Patel, in his 2010 map generator, uses four elevation zones and up to six moisture zones; the version of Whittaker's diagram in Ricklefs (2008) has nine biomes with curved borders. Under each cell is the share of the land that falls in it.

Henrik Kniberg says Minecraft's tables are tuned so that incompatible biomes, such as a desert and snowy peaks, do not touch. Here we measured it: in the nine recipes, and with the temperature noise at its maximum, no cold cell touches a hot one (0 neighbouring pairs). The reason is simple: two rows lie between the cold row and the hot one, a jump of 0.5 in temperature, and here temperature changes slowly from one cell to the next.

One quirk of this table: cold and wet gives snow, not tundra. Tundra is the cold, dry corner. ✦ IA

Wind and the rain shadow

A single wind blows over the whole map. The air enters saturated at the upwind edge and moves cell by cell. Over the sea it picks up moisture; over flat land it loses a little; and every time it climbs, it loses a lot, because rising air cools and rains. The slope facing the wind keeps the water, and the one behind gets dry air.

The lever that matters most is Rain when the air climbs. With NoiseLab's wind, dry land falls from 55 % to 28 % and the west half ends up wetter than the east. With that slider at 0, almost nothing is dry (1.6 %) and the halves even out (measured on this page).

A NoiseLab detail worth knowing: it counts every climb, a small hill's too. On rugged ground the air dries quickly, not only behind the range. And since this noise has no direction, there are no long ranges along the coast: the shadows come out in patches. ✦ IA

Places: how they look alike and how they don't

Each recipe tries a real place with these few numbers. None has millimetres or degrees: the model gives numbers from 0 to 1, so a place looks alike in its shape (where it rains, which side stays dry), not in its figures. Each recipe was measured before its note was written, and its code opens the same map in NoiseLab. ✦ IA

  • No wind: moisture comes only from the noise and the coast. Over half the land is dry (temperate desert 38 %), and both halves of the map are just as dry.
  • NoiseLab's wind, from the west. Dry land falls from 55 % to 28 %, and the west half is wetter than the east (0.44 against 0.36): every slope facing the wind takes rain from the air.
  • The same wind with “Rain when the air climbs” at 0: climbing no longer costs the air anything. Almost nothing is dry (1.6 %) and the two halves even out. That rain is what makes the rain shadow.
  • A warm island with the trade wind from the northeast. Rainforest on the slopes facing the wind (19 % of the land), dry grass and desert behind them; the windward half is twice as wet (0.59 against 0.31). Like Kauai in that; not in its numbers: Waiʻaleʻale gets about 9,300 mm a year, and here there are no millimetres.
  • A mild coast soaked by the sea and a strong west wind: half the land is temperate rainforest (50 %), and the high, cold land turns boreal. Like the Valdivian forest in its wet, mild coast; not in the Andes: this noise makes no range along the coast, so here the coast does the work and there is no clear lee.
  • Cool land, a strong west wind and little moisture from the coast. Forest and shrubland on the west, steppe (temperate desert, 37 %) on the east: the west half is twice as wet (0.47 against 0.24). Like Patagonia in its direction; not in its scale: there the drop is from up to 6,000 mm to under 100 mm within about 100 km.
  • A trick, said as one: hot land, the coast's moisture turned off and no wind. 83 % subtropical desert. The real Atacama is dry mainly for other reasons (sinking air under the Pacific high, the cold current and the inversion over it); this model has none of them, so it turns off the sea instead.
  • A high plain (the plateau terrain from the previous page) with a steep cold lapse and the wind from the east. Half the land is tundra, the cold-and-dry cell (50 %), with snow on top and drier ground on the west side. Like the Altiplano in being cold and high; the table has no cell for a high, cold grassland like the puna, so tundra is the nearest.
  • The view moved halfway to the pole (y = 4.25) on a wet land: boreal forest (51 %) with tundra and snow as it gets colder. One difference from the real thing: in this table cold and wet gives snow (27 %), not tundra, which is why snow covers so much low land.

Recipe notes written with AI. ✦ IA

A place that does not come out: central Chile. Its climate is Mediterranean-type: 100 to 1,000 mm a year, almost all in winter (Garreaud et al., 2017). What defines it is when it rains, and this model has no seasons; the table has no cell for Mediterranean scrub either. ✦ IA

Is there a theorem?

Two short results. The first: over land, the air never gains moisture. Each step multiplies it by numbers between 0 and 1, so along a line of wind it can only fall until it reaches the sea again. That is why the rain shadow cannot be avoided in this model: all you choose is how fast it comes.

The second is about the distance to the coast. Finding, for each cell, the distance to the nearest water cell looks expensive: every cell against every other. Felzenszwalb and Huttenlocher (2012) showed that the exact Euclidean distance can be computed in linear time: first along columns, then along rows, and on each line the lower envelope of a few parabolas is enough. This page compares it with brute force: the largest error is 2 · 10−7. ✦ IA

The equations

T = sat( base − calt · r − clat · sat(|y| / 8) + warmth / (1 + d) + noiseT · (nT − 0.5) )

M = sat( noiseM · nM + msea / (1 + d · fade) + weight · air )

over the sea: air ← air + (1 − air)(1 − e−evap · s)   over land: air ← air · e−drying · s · e−4 · rain · max(0, Δr)

biome = TABLE[ ⌊4T⌋ ][ ⌊4M⌋ ]   (beach if d < 1.5 and T ≥ 0.25)

The names are the sliders': calt and clat, the cooling with altitude and with latitude; msea, the moisture from the sea; weight, rain, drying and evap, the wind's. r is the land's rank: 0 at the coast, 1 at the top, counted as a percentile (how much of the land lies below), not as a height. d is the distance to the coast in cells of a 256 map. nT and nM are noises from 0 to 1. s is the length of one wind step, longer where the ground climbs. sat clips to 0..1. ✦ IA

What the picture says and what it doesn't ✦ IA

  • There are no seasons: one temperature and one moisture per point, for the whole year. No monsoons, no winter rain.
  • There are no units: T and M go from 0 to 1. The table knows nothing of degrees or millimetres.
  • The wind is one, straight across the map. No global circulation, no breezes, no fronts; and the wind does not bend around hills, it only crosses them.
  • No ocean currents, and no sinking air that warms up: that is why the Atacama needs a trick.
  • Altitude cools by rank, not by metres: the top of a small island is as cold as the top of a continent.
  • The distance to the coast does not see water outside the view: near the edges the coast can look farther away.
  • The map is computed at 256² by default (about 25–70 ms per slider move on a PC), and at 64² while you drag.

How it works ✦ IA

This part walks through the climate code, step by step. It covers:

  • The exact distance to the coast, in linear time.
  • The land's rank, as a percentile.
  • Temperature.
  • The wind, a sweep line by line.
  • Moisture.
  • The biome table.

The terrain is the one from Reshaping height. The climate (src/scripts/islands/climate.js) is a port of NoiseLab v3's: it does not touch the DOM and uses flat arrays, so it goes back to C# line by line. The excerpts are cut from that file when the site is built.

1. Distance to the coast

Two passes: along columns and along rows. Each pass solves one line with edt1d, which keeps only the parabolas that make up the lower envelope. Distance is measured in cells of a 256 map, so the climate does not change with the resolution.

// Distance from each land cell to the nearest water cell of the grid, in cells (0 on
// water). Exact Euclidean distance transform (Felzenszwalb & Huttenlocher, "Distance
// Transforms of Sampled Functions", Theory of Computing 8, 2012): squared distances along
// each column, then along each row, each pass a lower envelope of parabolas, O(n²) in all.
// Water outside the grid is not seen: near the edges the coast can look farther.
export function coastDistance(out, n, height, threshold) {
  const INF = 1e20;
  const f = new Float64Array(n), d = new Float64Array(n), z = new Float64Array(n + 1), v = new Int32Array(n);
  for (let x = 0; x < n; x++) {
    for (let y = 0; y < n; y++) f[y] = height[y * n + x] >= threshold ? INF : 0;
    edt1d(f, d, z, v, n);
    for (let y = 0; y < n; y++) out[y * n + x] = d[y];
  }
  for (let y = 0; y < n; y++) {
    const row = y * n;
    for (let x = 0; x < n; x++) f[x] = out[row + x];
    edt1d(f, d, z, v, n);
    for (let x = 0; x < n; x++) out[row + x] = Math.sqrt(d[x]);
  }
  return out;
}

// d[q] = min over p of (q − p)² + f[p], in O(n).
function edt1d(f, d, z, v, n) {
  const cross = (q, p) => ((f[q] + q * q) - (f[p] + p * p)) / (2 * q - 2 * p);
  let k = 0;
  v[0] = 0; z[0] = -Infinity; z[1] = Infinity;
  for (let q = 1; q < n; q++) {
    let s = cross(q, v[k]);
    while (s <= z[k]) { k--; s = cross(q, v[k]); }
    k++;
    v[k] = q; z[k] = s; z[k + 1] = Infinity;
  }
  k = 0;
  for (let q = 0; q < n; q++) {
    while (z[k + 1] < q) k++;
    const dq = q - v[k];
    d[q] = dq * dq + f[v[k]];
  }
}

2. The land's rank

NoiseLab's height has no units, so the cooling with altitude uses each cell's place among all the land cells: a binary search in the heights already sorted for the sea level.

// Land rank: 0 at the coast, 1 at the highest land of the cut source (the sorted heights
// the sea level was read from: the world region or the view). 0 on water. It is the
// altitude NoiseLab's lapse uses: a percentile, not a height.
export function landRank(out, height, sortedSource, threshold) {
  const below = lowerBound(sortedSource, threshold), land = sortedSource.length - below;
  for (let i = 0; i < height.length; i++)
    out[i] = land > 0 ? sat((lowerBound(sortedSource, height[i]) - below) / land) : 0;
  return out;
}

3. Temperature

One sum per cell. Latitude comes from the world y position: the equator is at y = 0 and the poles 8 views away. Move North–south position in the Terrain tab and everything cools.

// T = base − lapse · rank − latitude · |y| / 8 + coastMod / (1 + d) + tempNoise · (noise01 − 0.5),
// saturated to 0..1. y0 is the world y of the first row's cell centres, step the cell size
// (1 / n for a view); d is the coast distance in cells of a 256 map.
export function temperature(out, n, y0, step, rank, coast, tempNoise, c) {
  const toUnits = COAST_UNITS / n;
  for (let y = 0; y < n; y++) {
    const lat = sat(Math.abs(y0 + y * step) / LATITUDE_SPAN);
    for (let x = 0; x < n; x++) {
      const i = y * n + x;
      const t = c.base
        - c.lapse * rank[i]
        - c.latitude * lat
        + c.coastMod / (1 + coast[i] * toUnits)
        + c.tempNoise * 0.5 * tempNoise[i];   // amplitude · (noise01 − 0.5)
      out[i] = sat(t);
    }
  }
  return out;
}

4. The wind

The sweep moves along the axis the wind moves most on. Each cell's air comes from the previous line, interpolated at the point one step upwind, so it works for any angle and costs the same as one pass over the map.

// One direction for the whole map. Air enters at the upwind edge, saturated, and moves cell
// by cell along the wind. Over water it picks up moisture; over land it slowly dries, and
// much faster when it climbs (orographic rain): the windward slope gets the rain, the lee
// gets dry air (rain shadow). out[i] is the humidity of the air passing cell i, 0..1.
// The sweep goes line by line along the axis the wind moves most on; the air of a cell comes
// from the previous line, interpolated at the point one step upwind. O(n²), any angle.
export function windField(out, n, height, threshold, rank, w) {
  const a = (w.angle * Math.PI) / 180;
  const wx = Math.cos(a), wy = Math.sin(a);         // direction the wind blows toward (y up)
  const xMajor = Math.abs(wx) >= Math.abs(wy);
  const major = xMajor ? wx : wy, minor = xMajor ? wy : wx;
  const dir = major >= 0 ? 1 : -1;
  const slope = minor / Math.abs(major);            // minor-axis shift per major step
  const dist = Math.sqrt(1 + slope * slope) * COAST_UNITS / n; // step length, in cells of a 256 map
  const keepFlat = Math.exp(-w.dry * dist);
  const pickUp = 1 - Math.exp(-w.evap * dist);
  const cell = xMajor ? (u, j) => j * n + u : (u, j) => u * n + j;

  const first = dir > 0 ? 0 : n - 1;
  for (let k = 0; k < n; k++) {
    const u = first + dir * k;
    for (let j = 0; j < n; j++) {
      const i = cell(u, j);
      let air, rankBefore;
      const pj = j - slope;                         // where this air was one step ago
      if (k === 0 || pj < 0 || pj > n - 1) {
        air = 1;                                    // enters the view: sea air
        rankBefore = rank[i];
      } else {
        const j0 = Math.min(Math.floor(pj), n - 2), f = pj - j0, prev = u - dir;
        const a0 = cell(prev, j0), a1 = cell(prev, j0 + 1);
        air = out[a0] + (out[a1] - out[a0]) * f;
        rankBefore = rank[a0] + (rank[a1] - rank[a0]) * f;
      }
      if (height[i] < threshold) air += (1 - air) * pickUp;   // over the sea
      else air *= keepFlat * Math.exp(-w.lift * LIFT_SCALE * Math.max(0, rank[i] - rankBefore));
      out[i] = air;
    }
  }
  return out;
}

5. Moisture

// M = moistNoise · noise01 + coastBonus / (1 + d · decay) + (wind ? strength · air : 0), saturated.
export function moisture(out, n, moistNoise, coast, air, c, w) {
  const toUnits = COAST_UNITS / n;
  for (let i = 0; i < out.length; i++) {
    const m = c.moistNoise * sat(moistNoise[i] * 0.5 + 0.5)
      + c.coastBonus / (1 + coast[i] * toUnits * c.decay)
      + (w.on ? w.strength * air[i] : 0);
    out[i] = sat(m);
  }
  return out;
}

6. The table

Four bands per axis, a lookup in a flat table of 16 numbers, and the beach before anything else.

// Row = temperature band (cold → hot), column = moisture band (dry → saturated).
export const WHITTAKER = Uint8Array.from([
  2, 2, 1, 1,     // cold: tundra, tundra, snow, snow
  4, 5, 3, 3,     // cool: temperate desert, shrubland, boreal, boreal
  9, 6, 6, 7,     // warm: grassland, temperate forest ×2, temperate rainforest
  8, 9, 10, 11,   // hot: subtropical desert, grassland, tropical seasonal, tropical rainforest
]);

// Index 0..15 of the table cell for temperature t and moisture m (both 0..1).
export function tableCell(t, m) {
  const ti = Math.min(T_BANDS - 1, Math.max(0, Math.floor(t * T_BANDS)));
  const mi = Math.min(M_BANDS - 1, Math.max(0, Math.floor(m * M_BANDS)));
  return ti * M_BANDS + mi;
}

// Beach override, as in the stage: warm land right next to water.
export function biomeAt(t, m, coastCells) {
  return coastCells < 1.5 && t >= BEACH_MIN_T ? BEACH : WHITTAKER[tableCell(t, m)];
}

Going further

Give the relief a direction, for ranges along the coast and long rain shadows like the Andes'. Seasons, so that central Chile gets its winter rain. And a table with soft borders, blending the colours of neighbouring cells instead of straight cuts. ✦ IA

La idea en palabras sencillas

Un bioma (selva, estepa, tundra) depende sobre todo de dos números: cuánto calor y cuánta agua. Al ecólogo Robert Whittaker se le atribuye un diagrama con la temperatura en un eje y la lluvia en el otro, donde cada bioma ocupa una región. Los generadores de mapas copian la idea: calculan una temperatura y una humedad en cada punto y las buscan en una tabla.

Aquí la temperatura baja con la altura y hacia los polos, y sube un poco junto al mar. La humedad sale de un ruido, del mar cercano y, si lo enciendes, del viento: un aire húmedo que cruza el mapa y deja su agua en las laderas que suben. Detrás de un cerro el aire llega seco. Eso es la sombra de lluvia.

Cambia la capa bajo el mapa a Temperatura, Humedad o Aire para ver cada número por separado, y toca una casilla de la tabla para ver en qué parte del mapa cae. ✦ IA

TODO: Claudio: por qué te importa esto (el clima de las islas del juego, un lugar que conoces).

La tabla 4 × 4

La tabla corta la temperatura en cuatro filas (frío, fresco, templado, cálido) y la humedad en cuatro columnas. Es la tabla de Islands, no la de un libro: Amit Patel, en su generador de mapas de 2010, usa cuatro zonas de altura y hasta seis de humedad; la versión del diagrama de Whittaker en Ricklefs (2008) tiene nueve biomas con fronteras curvas. Bajo cada casilla va la parte de la tierra que cae en ella.

Henrik Kniberg cuenta que en Minecraft las tablas se ajustan para que no se toquen biomas incompatibles, como un desierto y picos nevados. Aquí lo medimos: en las nueve recetas, y con el ruido de temperatura al máximo, ninguna celda fría toca una cálida (0 pares vecinos). La razón es sencilla: entre la fila fría y la cálida hay dos filas, un salto de 0,5 en la temperatura, y aquí la temperatura cambia despacio de una celda a la siguiente.

Una rareza de esta tabla: frío y húmedo da nieve, no tundra. La tundra es la esquina fría y seca. ✦ IA

El viento y la sombra de lluvia

Un solo viento sopla sobre todo el mapa. El aire entra saturado por el borde de barlovento y avanza celda a celda. Sobre el mar recoge humedad; sobre tierra llana pierde un poco; y cada vez que sube, pierde mucho, porque el aire que sube se enfría y llueve. La ladera que mira al viento se queda con el agua, y la de detrás recibe aire seco.

La palanca que más pesa es Lluvia al subir. Con el viento de NoiseLab, la tierra seca baja del 55 % al 28 % y la mitad oeste queda más húmeda que la este. Con Lluvia al subir en 0, casi nada queda seco (1,6 %) y las mitades se igualan (medido en esta página).

Un detalle de NoiseLab que conviene saber: cuenta cada subida, también la de una colina pequeña. En un terreno accidentado el aire se seca enseguida, no solo detrás de la cordillera. Y como este ruido no tiene dirección, no hay cordilleras largas junto a la costa: las sombras salen en manchas. ✦ IA

Lugares: en qué se parecen y en qué no

Cada receta intenta un lugar real con estos pocos números. Ninguno tiene milímetros ni grados: el modelo da números de 0 a 1, así que un lugar se parece en la forma (dónde llueve, de qué lado queda seco), no en las cifras. Cada receta se midió antes de escribir su nota, y su código abre el mismo mapa en NoiseLab. ✦ IA

  • Sin viento: la humedad sale solo del ruido y de la costa. Más de la mitad de la tierra es seca (desierto templado, 38 %), y las dos mitades del mapa lo son por igual.
  • El viento de NoiseLab, del oeste. La tierra seca baja del 55 % al 28 %, y la mitad oeste queda más húmeda que la este (0,44 frente a 0,36): cada ladera que mira al viento le quita lluvia al aire.
  • El mismo viento con «Lluvia al subir» en 0: subir ya no le cuesta nada al aire. Casi nada queda seco (1,6 %) y las dos mitades se igualan. Esa lluvia es lo que hace la sombra de lluvia.
  • Una isla cálida con el alisio del noreste. Selva en las laderas que miran al viento (19 % de la tierra), pasto seco y desierto detrás; la mitad de barlovento es el doble de húmeda (0,59 frente a 0,31). Se parece a Kauai en eso; no en sus cifras: el Waiʻaleʻale recibe unos 9.300 mm al año, y aquí no hay milímetros.
  • Una costa templada empapada por el mar y un viento fuerte del oeste: la mitad de la tierra es selva templada (50 %) y la tierra alta y fría pasa a boreal. Se parece a la selva valdiviana en su costa húmeda y templada; no en los Andes: este ruido no hace una cordillera junto a la costa, así que aquí el trabajo lo hace la costa y no hay un sotavento claro.
  • Tierra fresca, viento fuerte del oeste y poca humedad de la costa. Bosque y matorral al oeste, estepa (desierto templado, 37 %) al este: la mitad oeste es el doble de húmeda (0,47 frente a 0,24). Se parece a la Patagonia en la dirección; no en la escala: allí se pasa de hasta 6.000 mm a menos de 100 mm en unos 100 km.
  • Un truco, dicho como tal: tierra caliente, la humedad de la costa apagada y sin viento. 83 % de desierto subtropical. El Atacama real es seco sobre todo por otras razones (el aire que baja bajo el anticiclón del Pacífico, la corriente fría y la inversión sobre ella); este modelo no tiene ninguna, así que apaga el mar.
  • Una llanura alta (el terreno de meseta de la página anterior) con mucho frío por altura y el viento del este. La mitad de la tierra es tundra, la casilla fría y seca (50 %), con nieve arriba y suelo más seco en la cara oeste. Se parece al Altiplano en ser frío y alto; la tabla no tiene casilla para un pastizal alto y frío como la puna, así que la tundra es lo más cercano.
  • El encuadre llevado a medio camino del polo (y = 4,25) sobre una tierra húmeda: bosque boreal (51 %), con tundra y nieve a medida que hace más frío. Una diferencia con lo real: en esta tabla frío y húmedo da nieve (27 %), no tundra, y por eso la nieve cubre tanta tierra baja.

Notas de las recetas escritas con IA. ✦ IA

Un lugar que no sale: Chile central. Su clima es de tipo mediterráneo: entre 100 y 1.000 mm al año, casi todos en invierno (Garreaud et al., 2017). Lo que lo define es cuándo llueve, y este modelo no tiene estaciones; la tabla tampoco tiene una casilla para el matorral mediterráneo. ✦ IA

¿Hay un teorema?

Dos resultados cortos. El primero: sobre tierra, el aire nunca gana humedad. Cada paso lo multiplica por números entre 0 y 1, así que a lo largo de una línea de viento solo puede bajar hasta volver al mar. Por eso la sombra de lluvia es inevitable en este modelo: lo único que puedes elegir es cuán rápido llega.

El segundo es de la distancia a la costa. Calcular para cada celda la distancia a la celda de agua más cercana parece costar mucho: cada celda contra todas. Felzenszwalb y Huttenlocher (2012) demostraron que la distancia euclídea exacta se puede calcular en tiempo lineal: primero por columnas, después por filas, y en cada línea basta con la envolvente inferior de unas parábolas. Esta página la compara con la fuerza bruta: el error máximo es de 2 · 10−7. ✦ IA

Las ecuaciones

T = sat( base − ealt · r − elat · sat(|y| / 8) + templanza / (1 + d) + ruidoT · (nT − 0,5) )

H = sat( ruidoH · nH + hmar / (1 + d · pérdida) + peso · aire )

sobre el mar: aire ← aire + (1 − aire)(1 − e−evap · s)   sobre tierra: aire ← aire · e−secado · s · e−4 · lluvia · max(0, Δr)

bioma = TABLA[ ⌊4T⌋ ][ ⌊4H⌋ ]   (playa si d < 1,5 y T ≥ 0,25)

r es el rango de la tierra: 0 en la costa, 1 en lo más alto, contado como percentil (qué parte de la tierra queda por debajo), no como altura. d es la distancia a la costa en celdas de un mapa de 256. Los nombres son los de los controles: ealt y elat, el enfriamiento con la altura y con la latitud; hmar, la humedad del mar; peso, lluvia, secado y evap, los del viento. nT y nH son ruidos de 0 a 1. s es el largo de un paso del viento, más largo cuando el terreno sube. sat recorta a 0..1. ✦ IA

Lo que la imagen dice y lo que no ✦ IA

  • No hay estaciones: una temperatura y una humedad por punto, para todo el año. Ni monzones, ni lluvia de invierno.
  • No hay unidades: T y H van de 0 a 1. La tabla no sabe de grados ni de milímetros.
  • El viento es uno y recto en todo el mapa. No hay circulación global, ni brisas, ni frentes; y el viento no se desvía alrededor de los cerros, solo los cruza.
  • Nada de corrientes marinas, ni de aire que baja y se calienta: por eso el Atacama sale con un truco.
  • La altura enfría por rango, no por metros: la cima de una isla pequeña es tan fría como la de un continente.
  • La distancia a la costa no ve el agua fuera del encuadre: cerca de los bordes la costa puede parecer más lejos.
  • El mapa se calcula a 256² por defecto (unos 25–70 ms al mover un control en un PC), y a 64² mientras arrastras.

Cómo funciona ✦ IA

Esta parte recorre el código del clima, paso a paso. Cubre:

  • La distancia exacta a la costa, en tiempo lineal.
  • El rango de la tierra, como percentil.
  • La temperatura.
  • El viento, un barrido línea a línea.
  • La humedad.
  • La tabla de biomas.

El terreno es el de Remodelar la altura. El clima (src/scripts/islands/climate.js) es un port del de NoiseLab v3: no toca el DOM y usa arrays planos, así que vuelve a C# línea a línea. Los fragmentos se cortan de ese archivo al construir el sitio. Los comentarios del código quedan en inglés.

1. La distancia a la costa

Dos pasadas: por columnas y por filas. Cada pasada resuelve una línea con edt1d, que guarda solo las parábolas que forman la envolvente inferior. La distancia se mide en celdas de un mapa de 256, para que el clima no cambie con la resolución.

// Distance from each land cell to the nearest water cell of the grid, in cells (0 on
// water). Exact Euclidean distance transform (Felzenszwalb & Huttenlocher, "Distance
// Transforms of Sampled Functions", Theory of Computing 8, 2012): squared distances along
// each column, then along each row, each pass a lower envelope of parabolas, O(n²) in all.
// Water outside the grid is not seen: near the edges the coast can look farther.
export function coastDistance(out, n, height, threshold) {
  const INF = 1e20;
  const f = new Float64Array(n), d = new Float64Array(n), z = new Float64Array(n + 1), v = new Int32Array(n);
  for (let x = 0; x < n; x++) {
    for (let y = 0; y < n; y++) f[y] = height[y * n + x] >= threshold ? INF : 0;
    edt1d(f, d, z, v, n);
    for (let y = 0; y < n; y++) out[y * n + x] = d[y];
  }
  for (let y = 0; y < n; y++) {
    const row = y * n;
    for (let x = 0; x < n; x++) f[x] = out[row + x];
    edt1d(f, d, z, v, n);
    for (let x = 0; x < n; x++) out[row + x] = Math.sqrt(d[x]);
  }
  return out;
}

// d[q] = min over p of (q − p)² + f[p], in O(n).
function edt1d(f, d, z, v, n) {
  const cross = (q, p) => ((f[q] + q * q) - (f[p] + p * p)) / (2 * q - 2 * p);
  let k = 0;
  v[0] = 0; z[0] = -Infinity; z[1] = Infinity;
  for (let q = 1; q < n; q++) {
    let s = cross(q, v[k]);
    while (s <= z[k]) { k--; s = cross(q, v[k]); }
    k++;
    v[k] = q; z[k] = s; z[k + 1] = Infinity;
  }
  k = 0;
  for (let q = 0; q < n; q++) {
    while (z[k + 1] < q) k++;
    const dq = q - v[k];
    d[q] = dq * dq + f[v[k]];
  }
}

2. El rango de la tierra

La altura de NoiseLab no tiene unidades, así que el frío por altura usa la posición de cada celda entre todas las de tierra: una búsqueda binaria en las alturas ya ordenadas para el nivel del mar.

// Land rank: 0 at the coast, 1 at the highest land of the cut source (the sorted heights
// the sea level was read from: the world region or the view). 0 on water. It is the
// altitude NoiseLab's lapse uses: a percentile, not a height.
export function landRank(out, height, sortedSource, threshold) {
  const below = lowerBound(sortedSource, threshold), land = sortedSource.length - below;
  for (let i = 0; i < height.length; i++)
    out[i] = land > 0 ? sat((lowerBound(sortedSource, height[i]) - below) / land) : 0;
  return out;
}

3. La temperatura

Una suma por celda. La latitud sale de la posición y del mundo: el ecuador está en y = 0 y los polos a 8 encuadres. Mueve Posición norte-sur en la pestaña Terreno y todo se enfría.

// T = base − lapse · rank − latitude · |y| / 8 + coastMod / (1 + d) + tempNoise · (noise01 − 0.5),
// saturated to 0..1. y0 is the world y of the first row's cell centres, step the cell size
// (1 / n for a view); d is the coast distance in cells of a 256 map.
export function temperature(out, n, y0, step, rank, coast, tempNoise, c) {
  const toUnits = COAST_UNITS / n;
  for (let y = 0; y < n; y++) {
    const lat = sat(Math.abs(y0 + y * step) / LATITUDE_SPAN);
    for (let x = 0; x < n; x++) {
      const i = y * n + x;
      const t = c.base
        - c.lapse * rank[i]
        - c.latitude * lat
        + c.coastMod / (1 + coast[i] * toUnits)
        + c.tempNoise * 0.5 * tempNoise[i];   // amplitude · (noise01 − 0.5)
      out[i] = sat(t);
    }
  }
  return out;
}

4. El viento

El barrido avanza por el eje en que más se mueve el viento. El aire de cada celda viene de la línea anterior, interpolado en el punto que queda un paso a barlovento, así que sirve para cualquier ángulo y cuesta lo mismo que recorrer el mapa una vez.

// One direction for the whole map. Air enters at the upwind edge, saturated, and moves cell
// by cell along the wind. Over water it picks up moisture; over land it slowly dries, and
// much faster when it climbs (orographic rain): the windward slope gets the rain, the lee
// gets dry air (rain shadow). out[i] is the humidity of the air passing cell i, 0..1.
// The sweep goes line by line along the axis the wind moves most on; the air of a cell comes
// from the previous line, interpolated at the point one step upwind. O(n²), any angle.
export function windField(out, n, height, threshold, rank, w) {
  const a = (w.angle * Math.PI) / 180;
  const wx = Math.cos(a), wy = Math.sin(a);         // direction the wind blows toward (y up)
  const xMajor = Math.abs(wx) >= Math.abs(wy);
  const major = xMajor ? wx : wy, minor = xMajor ? wy : wx;
  const dir = major >= 0 ? 1 : -1;
  const slope = minor / Math.abs(major);            // minor-axis shift per major step
  const dist = Math.sqrt(1 + slope * slope) * COAST_UNITS / n; // step length, in cells of a 256 map
  const keepFlat = Math.exp(-w.dry * dist);
  const pickUp = 1 - Math.exp(-w.evap * dist);
  const cell = xMajor ? (u, j) => j * n + u : (u, j) => u * n + j;

  const first = dir > 0 ? 0 : n - 1;
  for (let k = 0; k < n; k++) {
    const u = first + dir * k;
    for (let j = 0; j < n; j++) {
      const i = cell(u, j);
      let air, rankBefore;
      const pj = j - slope;                         // where this air was one step ago
      if (k === 0 || pj < 0 || pj > n - 1) {
        air = 1;                                    // enters the view: sea air
        rankBefore = rank[i];
      } else {
        const j0 = Math.min(Math.floor(pj), n - 2), f = pj - j0, prev = u - dir;
        const a0 = cell(prev, j0), a1 = cell(prev, j0 + 1);
        air = out[a0] + (out[a1] - out[a0]) * f;
        rankBefore = rank[a0] + (rank[a1] - rank[a0]) * f;
      }
      if (height[i] < threshold) air += (1 - air) * pickUp;   // over the sea
      else air *= keepFlat * Math.exp(-w.lift * LIFT_SCALE * Math.max(0, rank[i] - rankBefore));
      out[i] = air;
    }
  }
  return out;
}

5. La humedad

// M = moistNoise · noise01 + coastBonus / (1 + d · decay) + (wind ? strength · air : 0), saturated.
export function moisture(out, n, moistNoise, coast, air, c, w) {
  const toUnits = COAST_UNITS / n;
  for (let i = 0; i < out.length; i++) {
    const m = c.moistNoise * sat(moistNoise[i] * 0.5 + 0.5)
      + c.coastBonus / (1 + coast[i] * toUnits * c.decay)
      + (w.on ? w.strength * air[i] : 0);
    out[i] = sat(m);
  }
  return out;
}

6. La tabla

Cuatro bandas por eje, una búsqueda en una tabla plana de 16 números, y la playa antes que nada.

// Row = temperature band (cold → hot), column = moisture band (dry → saturated).
export const WHITTAKER = Uint8Array.from([
  2, 2, 1, 1,     // cold: tundra, tundra, snow, snow
  4, 5, 3, 3,     // cool: temperate desert, shrubland, boreal, boreal
  9, 6, 6, 7,     // warm: grassland, temperate forest ×2, temperate rainforest
  8, 9, 10, 11,   // hot: subtropical desert, grassland, tropical seasonal, tropical rainforest
]);

// Index 0..15 of the table cell for temperature t and moisture m (both 0..1).
export function tableCell(t, m) {
  const ti = Math.min(T_BANDS - 1, Math.max(0, Math.floor(t * T_BANDS)));
  const mi = Math.min(M_BANDS - 1, Math.max(0, Math.floor(m * M_BANDS)));
  return ti * M_BANDS + mi;
}

// Beach override, as in the stage: warm land right next to water.
export function biomeAt(t, m, coastCells) {
  return coastCells < 1.5 && t >= BEACH_MIN_T ? BEACH : WHITTAKER[tableCell(t, m)];
}

Para ir más lejos

Darle dirección al relieve, para tener cordilleras junto a la costa y sombras de lluvia largas como la de los Andes. Estaciones, para que Chile central tenga su lluvia de invierno. Y una tabla con fronteras suaves, mezclando los colores de casillas vecinas, en vez de cortes rectos. ✦ IA

SourcesFuentes

Part of Islands Lab. The terrain is the one of Reshaping height; the climate is a port of NoiseLab v3, the didactic scene in Islands, which follows the Stage_Biome2D stage and its biome table, and the codes open the same map there. Texts marked ✦ IA were written with AI (on a heading: the whole section; ✓: reviewed by Claudio); the code, the measures and the recipes were checked on this page.Parte de Islands Lab. El terreno es el de Remodelar la altura; el clima es un port de NoiseLab v3, la escena didáctica de Islands, que sigue la etapa Stage_Biome2D y su tabla de biomas, y los códigos abren allí el mismo mapa. Los textos marcados con ✦ IA están escritos con IA (en un título: toda la sección; ✓: revisados por Claudio); el código, las medidas y las recetas se comprobaron en esta página.