Reshaping Height with Curves
Raw noise makes blobs, not landscapes. Here three noises pass through three curves you can edit, and the height is C + E · PV. Drag the points of a curve, drag the map to explore, switch the layer to see each part on its own, and load a recipe to see how a kind of landscape is made.✦ IA How it works.
El ruido en crudo hace manchas, no paisajes. Aquí tres ruidos pasan por tres curvas que puedes editar, y la altura es C + E · PV. Arrastra los puntos de una curva, arrastra el mapa para explorar, cambia de capa para ver cada parte por separado y carga una receta para ver cómo se hace un tipo de paisaje.✦ IA Cómo funciona.
Histogram of the height in the view, coloured by band. Left of the dashed line is under water.Histograma de la altura en el encuadre, coloreado por banda. A la izquierda de la línea discontinua, bajo el agua.
The idea in plain words
Noise used as height gives soft blobs: every hill looks like every other. The trick, as Henrik Kniberg tells it for Minecraft, is to not use the noise as height, but to pass it through a curve you draw. A flat stretch of the curve makes a plateau; a steep one, a cliff.
Here there are three noises and three curves. C (continentalness) says how far inland a place is: ocean, coast or interior. E (erosion) says how much relief is allowed there. PV (peaks and valleys) adds the bumps. The height is C + E · PV: the base level, plus the relief, scaled by how much is allowed.
The analogy is a sculptor's rule of thumb, "first the big masses, then the detail". It has limits: here nothing is carved, every point just reads three numbers and adds them. Switch the layer under the map to C, E or E·PV to see each part on its own. ✦ IA
TODO: Claudio: why this matters to you (the game's islands, a place you want to make).
The grey bars: where the noise really lives
Under each curve, the grey bars show where that noise's values fall in the view. They are not spread from −1 to +1: with the default octaves, 97 to 99 % of the values lie between −0.5 and +0.5, and none went past ±0.73 on a region of 16 × 16 views (measured on this page). Summing octaves averages them towards 0.
So the ends of a curve are almost never used. If a change does nothing, look at the bars: the point you moved is probably outside them. The steps that shape a landscape go where the bars are tall. ✦ IA
The catch: water by percentage
NoiseLab can fill the sea in two ways. With a raw threshold, everything below a fixed height is water. With a percentage, the scene sorts all the heights and puts the sea level where that share of them lies below.
The second is handy, because the sea stays at 40 % whatever you do. But it hides something: moving a whole curve up or down changes nothing. Press "↑ 0.1" under the C curve: the map stays the same, pixel for pixel (checked on this page). Switch to the raw threshold and press it again: the water drops from 38.6 % to 27.4 % of the view. With water by percentage, only the shape of the curves counts. ✦ IA
Bands by height or by share
NoiseLab paints the land by share: the highest 8 % is always snow, even on a flat plain. That keeps every map colourful but lies about the shape: the recipes below looked alike. This page paints by height by default, like the tints of an atlas: each colour starts at a fixed height above the sea. NoiseLab's way is one button away, under View. ✦ IA
Guide: how to make a landscape
Four levers do most of the work. A flat stretch in a curve gives flat ground at that height. A steep stretch gives a wall: in C at the coast, cliffs; in C inland, the edge of a plateau. E low flattens whatever PV says; E high lets PV build mountains. PV that stays at 0 and then climbs leaves most of the land quiet and raises only its rare high values.
Each recipe below loads into the scene, and its code opens the same terrain in NoiseLab. Each was rendered and measured before its text was written. ✦ IA
- The starting point: ocean, shelf, coast and a gentle rise inland, with some hills.
- The example from the video: C climbs slowly up to 0.3, jumps between 0.3 and 0.4, then stays flat. Where the continent passes 0.3, a wall rises to a high plain.
- C almost flat right at sea level over a long stretch: the small bumps of E · PV decide what pokes out, so the land breaks into many islands and channels. It does not make long, straight drowned valleys: this noise has no direction.
- Kniberg's step moved inland: a coastal lowland, a steep rise where C passes 0.2–0.35, and a flat top because E is low there. Flat is the curve's flat stretch, not the noise.
- A near-vertical stretch of C right at the coast: the sea floor ends and the land starts high, with no beach. A height map cannot lean a cliff out over the sea: each point has one height.
- E low everywhere: whatever PV says, it is multiplied by almost nothing, so the land is flat to the horizon. C still draws the coast.
- E high almost everywhere and PV ending in a steep climb: relief is allowed all over and the high values of PV become sharp peaks. They come in patches, not in a long chain: this noise has no direction, so nothing lines the peaks up.
- A flat C and a PV that stays at 0 until 0.4 and then climbs: only the rare high values of PV rise, so lone hills stand on a plain.
Recipe notes written with AI. ✦ IA
Is there a theorem?
For the catch above, yes, and a short one: a percentile does not change under an increasing transformation. If every height goes through the same increasing function (adding 0.1, doubling, any curve that never goes down), the order of the cells does not change. The cell that was the 40th percentile still is, so the same cells end up under water.
Moving a whole C curve adds the same amount to every height, so it is one of those functions. Changing its shape is not: two cells with different C, E and PV can swap places, and the coast moves. For the curves themselves there is no deep result: they are piecewise-linear interpolation, the simplest kind of spline. ✦ IA
The equations
h(x, y) = C(c(x, y)) + E(e(x, y)) · PV(p(x, y))
c, e, p = fBm noise in [−1, 1]: Σo go · simplex(λo f · x) / Σo go (o = octave, f = frequency, λ = lacunarity, g = gain)
C(t) = yi + (yi+1 − yi) (t − xi) / (xi+1 − xi) for xi ≤ t ≤ xi+1; constant past the ends
sea level = h(k), k = round(share · N) (the k-th smallest of N heights)
Read the first line as: the continent sets the base, erosion sets a volume knob, and peaks and valleys is the signal that knob turns up or down. When E is near 0, PV is silenced and the land is flat; when E is 1, PV passes through whole. ✦ IA
What the picture does and does not say ✦ IA
- It is a height map: one height per point. No overhangs, arches, caves or cliffs leaning over the sea. Minecraft adds a 3D density for those (Kniberg, nutshell, 17:59); this page does not.
- The formula is a simplification. In Minecraft each point of one spline can lead to another spline (Kniberg, Jfokus, 23:13); here the three curves are independent and only added and multiplied.
- The noise has no direction: no chains of mountains, no long valleys, no dunes. Those need ridged noise, domain warping or simulation, later parts of the series.
- Nothing here is erosion as a process: E is a noise named after it, not water carving rock.
- Heights are unitless. A real place named in a recipe is resembled in shape, not in metres.
- The map is sampled at 256² by default, and at 64² while you drag. NoiseLab computes in 32-bit floats and this page in 64-bit: the noise matches to about 5 · 10−6, so a sea level can move by one cell, no more.
How it works ✦ IA
This part walks through the scene's code, step by step. It covers:
- Fractal noise that matches Islands' C# to the last digit that matters.
- Three fields with their own seeds.
- A curve: piecewise-linear interpolation.
- The height, C + E · PV.
- Water by percentage, as a quantile.
- Bands by height, shading and contour lines.
Plain JavaScript and the Canvas 2D API, no libraries. The generator
(src/scripts/islands/terrain-shaping.js) has no DOM and uses flat arrays, so it ports
line by line to C#. The snippets below are cut from the running scripts when the site is built.
1. Fractal noise, the same as Islands
Each field is simplex noise from part 4, with Islands'
PerlinOnSimplex gradient, which reaches ±1, summed in octaves. Each octave multiplies
the frequency by the lacunarity (2 by default), multiplies the amplitude by the gain and gets its own hash (Islands adds the octave
to the seeded hash, which is the same as seeding with seed + octave). The sum is divided by the
sum of the amplitudes, so the range stays that of one octave.
// value at (x, y); `opts`: { seed, frequency, octaves, lacunarity, gain, gradient }.
// gradient is a key of GRADIENTS ("perlinOnSimplex" by default, as in NoiseLab v3).
export function fractalNoise(x, y, opts) {
const { seed = 0, frequency = 1, octaves = 1, lacunarity = 2, gain = 0.5 } = opts;
const grad = GRADIENTS[opts.gradient ?? "perlinOnSimplex"];
let f = frequency, amplitude = 1, amplitudeSum = 0, sum = 0;
for (let o = 0; o < octaves; o++) {
const s = (seed + o) | 0;
sum += amplitude * simplexNoise(x * f, y * f, (i, j) => grad(s, i, j)).v;
amplitudeSum += amplitude;
f *= lacunarity;
amplitude *= gain;
}
return sum / amplitudeSum;
}
To be sure a code opens the same map in Unity, a throwaway dotnet console compiled
Islands' own hash, gradients and simplex and wrote 2,112 values; the port differs by at most
5 · 10−6 (tools/islands/check-fbm.mjs). The scene uses a flat copy of the
same steps, without objects, checked against the same values.
2. Three fields
Continentalness has big, slow shapes; peaks and valleys, small ones. The seeds are spread with a prime, so the three fields do not repeat each other. Move Frequency in the Noises tab and watch the shapes shrink.
// Defaults as in NoiseLab v3: frequency in cycles per view width, octaves, lacunarity, gain.
export const DEFAULT_FIELDS = {
cont: { frequency: 2, octaves: 4, lacunarity: 2, gain: 0.5 }, // big shapes
eros: { frequency: 3, octaves: 3, lacunarity: 2, gain: 0.5 }, // where relief is allowed
peaks: { frequency: 8, octaves: 4, lacunarity: 2, gain: 0.5 }, // detail
};
// Field k uses seed + k · 7919, so the fields are decorrelated from each other.
export const fieldSeed = (seed, k) => (seed + k * 7919) | 0; 3. A curve
A curve is a list of points with x growing. Between two points the value goes in a straight line; past the ends it stays at the last value. A binary search finds the stretch.
// Piecewise linear, clamped to the end values (Islands' ScalarSpline.Evaluate).
export function evalCurve(points, x) {
const n = points.length;
if (x <= points[0][0]) return points[0][1];
if (x >= points[n - 1][0]) return points[n - 1][1];
let lo = 0, hi = n - 1;
while (lo < hi - 1) {
const mid = (lo + hi) >> 1;
if (x < points[mid][0]) hi = mid; else lo = mid;
}
const [x0, y0] = points[lo], [x1, y1] = points[hi];
const span = x1 - x0;
return span <= 0 ? y1 : y0 + (y1 - y0) * ((x - x0) / span);
} 4. The height
One line. Each field goes through its curve, and erosion scales the peaks and valleys.
// height = C(c) + E(e) · PV(p), cell by cell.
export function combineHeight(target, c, e, p, curves) {
for (let i = 0; i < target.length; i++)
target[i] = evalCurve(curves.cont, c[i]) + evalCurve(curves.eros, e[i]) * evalCurve(curves.peaks, p[i]);
return target;
} The noises are sampled only when their settings or the view change. Dragging a curve point only runs this line again, so the map follows the finger.
5. Water by percentage
The heights are sorted and the sea level is read at the chosen share: the quantile. With Cuts: world, they come from a fixed region of 16 × 16 views, so the coast does not move while you drag.
// Raw mode: the threshold is given, the area is measured. Percentage mode: the area is
// given, and the threshold is read from the sorted values (the quantile).
export function waterLevel(source, water) {
const count = source.length;
if (water.mode !== "percent") return water.raw;
const k = Math.round((water.percent / 100) * count);
return k <= 0 ? source[0] : k >= count ? source[count - 1] + 1e-6 : source[k];
} 6. Bands, shading and contours
The bands by height are eight fixed cuts measured from the sea level:
// Bands by height instead of by share (this page's default; NoiseLab only has shares): the
// cuts sit at fixed heights above and below the sea, like the tints of an atlas. With
// shares, even a flat plain shows 8 % of "snow"; with heights, snow means high ground.
export const HEIGHT_CUTS = { water: [-0.2, -0.05], land: [0.02, 0.1, 0.2, 0.3, 0.45] };
export function heightEdges(threshold, cuts = HEIGHT_CUTS) {
return Float64Array.from([...cuts.water.map((d) => threshold + d), threshold, ...cuts.land.map((d) => threshold + d)]);
} The shading lights each cell from the north-west with the slope to its neighbours, as NoiseLab does. The slope is scaled with the resolution, so the look does not change from 128² to 256².
// Brightness factor of cell i for relief `relief` (0 = flat). The slope is exaggerated by
// relief · 0.05 · n / 2 so the look does not change with the resolution.
export function shadeFactor(values, i, n, relief) {
const x = i % n, y = (i / n) | 0;
const hl = values[y * n + Math.max(x - 1, 0)], hr = values[y * n + Math.min(x + 1, n - 1)];
const hd = values[Math.max(y - 1, 0) * n + x], hu = values[Math.min(y + 1, n - 1) * n + x];
const k = relief * 0.05 * n * 0.5;
const nx = -(hr - hl) * k, ny = -(hu - hd) * k, m = Math.hypot(nx, ny, 1);
return Math.max(0, (nx * L[0] + ny * L[1] + L[2]) / m) / L[2];
} A cell is on a contour line when its right or upper neighbour lies on another step of height:
// 0 if cell i is not on a contour line; otherwise how dark the line is (0.55 for every
// fifth line, 0.3 for the rest, halved under water). A cell is on a line when its right or
// upper neighbour lies in another step of height.
export function contourWeight(values, i, n, step, threshold) {
const x = i % n, y = (i / n) | 0;
const level = Math.floor(values[i] / step);
const right = x < n - 1 ? Math.floor(values[i + 1] / step) : level;
const up = y < n - 1 ? Math.floor(values[i + n] / step) : level;
if (right === level && up === level) return 0;
const line = Math.max(level, right, up);
let w = line % 5 === 0 ? 0.55 : 0.3;
if (values[i] < threshold) w *= 0.5;
return w;
} Going further
Chain the curves, as Minecraft does: let the C curve choose which E curve applies. Add the bands by height to NoiseLab in Islands, so the colours match too (noted as a follow-up). And give the noise a direction, with ridged noise and domain warping, to get mountain chains and long valleys. ✦ IA
La idea en palabras sencillas
El ruido usado como altura da manchas suaves: cada cerro se parece a todos los demás. El truco, como lo cuenta Henrik Kniberg para Minecraft, es no usar el ruido como altura, sino pasarlo por una curva que dibujas tú. Un tramo plano de la curva hace una meseta; uno empinado, un acantilado.
Aquí hay tres ruidos y tres curvas. C (continentalidad) dice cuán tierra adentro está un lugar: océano, costa o interior. E (erosión) dice cuánto relieve se permite allí. PV (picos y valles) pone los bultos. La altura es C + E · PV: el nivel de base, más el relieve, escalado por lo que se permite.
La analogía es la regla del escultor, «primero las masas grandes, luego el detalle». Tiene límites: aquí no se talla nada, cada punto solo lee tres números y los suma. Cambia la capa bajo el mapa a C, E o E·PV para ver cada parte por separado. ✦ IA
TODO: Claudio: por qué te importa esto (las islas del juego, un lugar que quieres hacer).
Las barras grises: dónde vive de verdad el ruido
Bajo cada curva, las barras grises muestran dónde caen los valores de ese ruido en el encuadre. No se reparten de −1 a +1: con las octavas por defecto, entre el 97 y el 99 % de los valores queda entre −0,5 y +0,5, y ninguno pasó de ±0,73 en una región de 16 × 16 encuadres (medido en esta página). Sumar octavas los promedia hacia el 0.
Así que los extremos de una curva casi nunca se usan. Si un cambio no hace nada, mira las barras: seguramente el punto que moviste está fuera de ellas. Los escalones que dan forma a un paisaje van donde las barras son altas. ✦ IA
La trampa: el agua por porcentaje
NoiseLab puede llenar el mar de dos maneras. Con un umbral crudo, todo lo que está bajo una altura fija es agua. Con un porcentaje, la escena ordena todas las alturas y pone el nivel del mar donde queda por debajo esa parte de ellas.
La segunda es cómoda, porque el mar se queda en el 40 % hagas lo que hagas. Pero esconde algo: subir o bajar una curva entera no cambia nada. Pulsa «↑ 0.1» bajo la curva C: el mapa queda igual, píxel a píxel (comprobado en esta página). Cambia al umbral crudo y vuelve a pulsarlo: el agua baja del 38,6 % al 27,4 % del encuadre. Con el agua por porcentaje, solo cuenta la forma de las curvas. ✦ IA
Bandas por altura o por porcentaje
NoiseLab pinta la tierra por porcentaje: el 8 % más alto siempre es nieve, aunque sea una llanura. Así todos los mapas salen coloridos, pero mienten sobre la forma: las recetas de abajo se veían iguales. Esta página pinta por altura por defecto, como los colores de un atlas: cada color empieza a una altura fija sobre el mar. La manera de NoiseLab está a un botón, en Vista. ✦ IA
Guía: cómo hacer un paisaje
Cuatro palancas hacen casi todo el trabajo. Un tramo plano en una curva da terreno plano a esa altura. Un tramo empinado da un muro: en C junto a la costa, acantilados; en C tierra adentro, el borde de una meseta. E baja aplana lo que diga PV; E alta deja que PV levante montañas. PV que se queda en 0 y luego sube deja tranquila casi toda la tierra y levanta solo sus valores altos, que son pocos.
Cada receta de abajo se carga en la escena, y su código abre el mismo terreno en NoiseLab. Cada una se dibujó y se midió antes de escribir su texto. ✦ IA
- El punto de partida: océano, plataforma, costa y una subida suave tierra adentro, con algunos cerros.
- El ejemplo del video: C sube despacio hasta 0,3, salta entre 0,3 y 0,4 y luego se queda plana. Donde la continentalidad pasa de 0,3, se levanta un muro hasta una llanura alta.
- C casi plana justo al nivel del mar en un tramo largo: los pequeños bultos de E · PV deciden qué asoma, y la tierra se rompe en muchas islas y canales. No hace valles ahogados largos y rectos: este ruido no tiene dirección.
- El escalón de Kniberg llevado tierra adentro: una franja baja en la costa, una subida brusca donde C pasa de 0,2 a 0,35 y una cima plana porque allí E es baja. Lo plano es el tramo plano de la curva, no el ruido.
- Un tramo casi vertical de C justo en la costa: el fondo marino termina y la tierra empieza alta, sin playa. Un mapa de alturas no puede inclinar un acantilado sobre el mar: cada punto tiene una sola altura.
- E baja en todas partes: diga lo que diga PV, se multiplica por casi nada, y la tierra queda plana hasta el horizonte. C sigue dibujando la costa.
- E alta casi en todas partes y PV que termina en una subida empinada: se permite relieve en todo el mapa y los valores altos de PV se vuelven picos afilados. Salen en manchas, no en una cadena larga: este ruido no tiene dirección, así que nada alinea los picos.
- Una C plana y una PV que se queda en 0 hasta 0,4 y luego sube: solo los valores altos y escasos de PV se levantan, y quedan cerros sueltos sobre una llanura.
Notas de las recetas escritas con IA. ✦ IA
¿Hay un teorema?
Para la trampa de arriba, sí, y corto: un percentil no cambia con una transformación creciente. Si todas las alturas pasan por la misma función creciente (sumar 0,1, duplicar, cualquier curva que nunca baje), el orden de las celdas no cambia. La celda que era el percentil 40 lo sigue siendo, así que bajo el agua quedan las mismas celdas.
Mover una curva C entera suma lo mismo a todas las alturas, así que es una de esas funciones. Cambiar su forma no lo es: dos celdas con C, E y PV distintas pueden cambiar de puesto, y la costa se mueve. Para las curvas en sí no hay un resultado profundo: son interpolación lineal por tramos, la spline más sencilla. ✦ IA
Las ecuaciones
h(x, y) = C(c(x, y)) + E(e(x, y)) · PV(p(x, y))
c, e, p = ruido fBm en [−1, 1]: Σo go · simplex(λo f · x) / Σo go (o = octava, f = frecuencia, λ = lacunaridad, g = ganancia)
C(t) = yi + (yi+1 − yi) (t − xi) / (xi+1 − xi) para xi ≤ t ≤ xi+1; constante pasados los extremos
nivel del mar = h(k), k = round(parte · N) (la k-ésima menor de N alturas)
Léase la primera línea así: el continente pone la base, la erosión es una perilla de volumen, y picos y valles es la señal que esa perilla sube o baja. Con E cerca de 0, PV se calla y la tierra queda plana; con E en 1, PV pasa entera. ✦ IA
Lo que la imagen dice y lo que no ✦ IA
- Es un mapa de alturas: una altura por punto. Nada de voladizos, arcos, cuevas ni acantilados inclinados sobre el mar. Minecraft añade una densidad 3D para eso (Kniberg, nutshell, 17:59); esta página no.
- La fórmula es una simplificación. En Minecraft cada punto de una spline puede llevar a otra spline (Kniberg, Jfokus, 23:13); aquí las tres curvas son independientes y solo se suman y multiplican.
- El ruido no tiene dirección: ni cadenas de montañas, ni valles largos, ni dunas. Para eso hace falta ruido con crestas, deformación del dominio o simulación, partes posteriores de la serie.
- Nada aquí es la erosión como proceso: E es un ruido que lleva ese nombre, no agua que talla la roca.
- Las alturas no tienen unidades. Un lugar real nombrado en una receta se parece en la forma, no en los metros.
- El mapa se calcula a 256² por defecto, y a 64² mientras arrastras. NoiseLab calcula con números de 32 bits y esta página con 64: el ruido coincide hasta unos 5 · 10−6, así que un nivel del mar puede moverse una celda, no más.
Cómo funciona ✦ IA
Esta parte recorre el código de la escena, paso a paso. Cubre:
- Un ruido fractal que coincide con el C# de Islands hasta el último decimal que importa.
- Tres campos con su propia semilla.
- Una curva: interpolación lineal por tramos.
- La altura, C + E · PV.
- El agua por porcentaje, como un cuantil.
- Bandas por altura, sombreado y curvas de nivel.
JavaScript sin más y la API Canvas 2D, sin librerías. El generador
(src/scripts/islands/terrain-shaping.js) no toca el DOM y usa arrays planos, así que se
portea línea a línea a C#. Los fragmentos de abajo se cortan de los scripts que corren cuando se
construye el sitio. Los comentarios del código quedan en inglés.
1. Ruido fractal, el mismo de Islands
Cada campo es el ruido simplex de la parte 4, con el gradiente
PerlinOnSimplex de Islands, que llega a ±1, sumado en octavas. Cada octava multiplica la
frecuencia por la lacunaridad (2 por defecto), multiplica la amplitud por la ganancia y recibe su propio
hash (Islands suma la octava al hash con semilla, que es lo mismo que sembrar con semilla + octava).
La suma se divide por la suma de las amplitudes, así que el rango queda como el de una octava.
// value at (x, y); `opts`: { seed, frequency, octaves, lacunarity, gain, gradient }.
// gradient is a key of GRADIENTS ("perlinOnSimplex" by default, as in NoiseLab v3).
export function fractalNoise(x, y, opts) {
const { seed = 0, frequency = 1, octaves = 1, lacunarity = 2, gain = 0.5 } = opts;
const grad = GRADIENTS[opts.gradient ?? "perlinOnSimplex"];
let f = frequency, amplitude = 1, amplitudeSum = 0, sum = 0;
for (let o = 0; o < octaves; o++) {
const s = (seed + o) | 0;
sum += amplitude * simplexNoise(x * f, y * f, (i, j) => grad(s, i, j)).v;
amplitudeSum += amplitude;
f *= lacunarity;
amplitude *= gain;
}
return sum / amplitudeSum;
}
Para estar seguros de que un código abre el mismo mapa en Unity, una consola dotnet de
usar y tirar compiló el hash, los gradientes y el simplex de Islands y escribió 2.112 valores; el port
se aparta como mucho 5 · 10−6 (tools/islands/check-fbm.mjs). La escena usa una
copia plana de los mismos pasos, sin objetos, comprobada contra los mismos valores.
2. Tres campos
La continentalidad tiene formas grandes y lentas; picos y valles, pequeñas. Las semillas se separan con un número primo, para que los tres campos no se repitan. Mueve Frecuencia en la pestaña Ruidos y mira cómo se achican las formas.
// Defaults as in NoiseLab v3: frequency in cycles per view width, octaves, lacunarity, gain.
export const DEFAULT_FIELDS = {
cont: { frequency: 2, octaves: 4, lacunarity: 2, gain: 0.5 }, // big shapes
eros: { frequency: 3, octaves: 3, lacunarity: 2, gain: 0.5 }, // where relief is allowed
peaks: { frequency: 8, octaves: 4, lacunarity: 2, gain: 0.5 }, // detail
};
// Field k uses seed + k · 7919, so the fields are decorrelated from each other.
export const fieldSeed = (seed, k) => (seed + k * 7919) | 0; 3. Una curva
Una curva es una lista de puntos con x creciente. Entre dos puntos el valor va en línea recta; pasados los extremos se queda en el último valor. Una búsqueda binaria encuentra el tramo.
// Piecewise linear, clamped to the end values (Islands' ScalarSpline.Evaluate).
export function evalCurve(points, x) {
const n = points.length;
if (x <= points[0][0]) return points[0][1];
if (x >= points[n - 1][0]) return points[n - 1][1];
let lo = 0, hi = n - 1;
while (lo < hi - 1) {
const mid = (lo + hi) >> 1;
if (x < points[mid][0]) hi = mid; else lo = mid;
}
const [x0, y0] = points[lo], [x1, y1] = points[hi];
const span = x1 - x0;
return span <= 0 ? y1 : y0 + (y1 - y0) * ((x - x0) / span);
} 4. La altura
Una línea. Cada campo pasa por su curva, y la erosión escala los picos y valles.
// height = C(c) + E(e) · PV(p), cell by cell.
export function combineHeight(target, c, e, p, curves) {
for (let i = 0; i < target.length; i++)
target[i] = evalCurve(curves.cont, c[i]) + evalCurve(curves.eros, e[i]) * evalCurve(curves.peaks, p[i]);
return target;
} Los ruidos solo se recalculan cuando cambian sus ajustes o el encuadre. Arrastrar un punto de una curva solo vuelve a correr esta línea, y por eso el mapa sigue al dedo.
5. El agua por porcentaje
Las alturas se ordenan y el nivel del mar se lee en la parte elegida: el cuantil. Con Cortes: mundo, salen de una región fija de 16 × 16 encuadres, así que la costa no se mueve mientras arrastras.
// Raw mode: the threshold is given, the area is measured. Percentage mode: the area is
// given, and the threshold is read from the sorted values (the quantile).
export function waterLevel(source, water) {
const count = source.length;
if (water.mode !== "percent") return water.raw;
const k = Math.round((water.percent / 100) * count);
return k <= 0 ? source[0] : k >= count ? source[count - 1] + 1e-6 : source[k];
} 6. Bandas, sombreado y curvas de nivel
Las bandas por altura son ocho cortes fijos medidos desde el nivel del mar:
// Bands by height instead of by share (this page's default; NoiseLab only has shares): the
// cuts sit at fixed heights above and below the sea, like the tints of an atlas. With
// shares, even a flat plain shows 8 % of "snow"; with heights, snow means high ground.
export const HEIGHT_CUTS = { water: [-0.2, -0.05], land: [0.02, 0.1, 0.2, 0.3, 0.45] };
export function heightEdges(threshold, cuts = HEIGHT_CUTS) {
return Float64Array.from([...cuts.water.map((d) => threshold + d), threshold, ...cuts.land.map((d) => threshold + d)]);
} El sombreado ilumina cada celda desde el noroeste con la pendiente hacia sus vecinas, como NoiseLab. La pendiente se escala con la resolución, así que el aspecto no cambia de 128² a 256².
// Brightness factor of cell i for relief `relief` (0 = flat). The slope is exaggerated by
// relief · 0.05 · n / 2 so the look does not change with the resolution.
export function shadeFactor(values, i, n, relief) {
const x = i % n, y = (i / n) | 0;
const hl = values[y * n + Math.max(x - 1, 0)], hr = values[y * n + Math.min(x + 1, n - 1)];
const hd = values[Math.max(y - 1, 0) * n + x], hu = values[Math.min(y + 1, n - 1) * n + x];
const k = relief * 0.05 * n * 0.5;
const nx = -(hr - hl) * k, ny = -(hu - hd) * k, m = Math.hypot(nx, ny, 1);
return Math.max(0, (nx * L[0] + ny * L[1] + L[2]) / m) / L[2];
} Una celda está en una curva de nivel cuando su vecina de la derecha o de arriba cae en otro escalón de altura:
// 0 if cell i is not on a contour line; otherwise how dark the line is (0.55 for every
// fifth line, 0.3 for the rest, halved under water). A cell is on a line when its right or
// upper neighbour lies in another step of height.
export function contourWeight(values, i, n, step, threshold) {
const x = i % n, y = (i / n) | 0;
const level = Math.floor(values[i] / step);
const right = x < n - 1 ? Math.floor(values[i + 1] / step) : level;
const up = y < n - 1 ? Math.floor(values[i + n] / step) : level;
if (right === level && up === level) return 0;
const line = Math.max(level, right, up);
let w = line % 5 === 0 ? 0.55 : 0.3;
if (values[i] < threshold) w *= 0.5;
return w;
} Para ir más lejos
Encadenar las curvas, como Minecraft: que la curva C elija qué curva E se aplica. Llevar las bandas por altura a NoiseLab en Islands, para que también coincidan los colores (anotado como seguimiento). Y darle dirección al ruido, con crestas y deformación del dominio, para tener cordilleras y valles largos. ✦ IA
SourcesFuentes
- H. Kniberg, "Minecraft terrain generation in a nutshell" (2022), 13:55–15:50: spline points over continentalness; two points give flat ground; −1 → 50, 0.3 → 100, 0.4 → 150 makes a cliff. youtu.be/CSa5O6knuwI?t=835
- H. Kniberg, "Reinventing Minecraft world generation", Jfokus 2022, 19:49–24:35: the three noises (continentalness, erosion, peaks and valleys) and the splines that lead to other splines (23:13). youtu.be/ob3VwY4JyzE?t=1189
- J. Flick, "Pseudorandom Noise: Simplex Noise", Catlike Coding (2021). The simplex design Islands.Noise follows; code under MIT-0. catlikecoding.com/unity/tutorials/pseudorandom-noise/simplex-noise/
- J. Flick, "Pseudorandom Noise: Noise Variants" (fractal noise: octaves, lacunarity, persistence), Catlike Coding (2021). catlikecoding.com/unity/tutorials/pseudorandom-noise/noise-variants/
Part of Islands Lab. The noise is the simplex of part 4 with Islands' PerlinOnSimplex gradient, summed in octaves; the scene is a port of NoiseLab v3, the didactic scene in Islands, and its 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 ruido es el simplex de la parte 4 con el gradiente PerlinOnSimplex de Islands, sumado en octavas; la escena es un port de NoiseLab v3, la escena didáctica de Islands, y sus 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.