Perlin noise: terreno, cuevas y nubes procedurales
Ruido coherente: la base de cualquier generación procedural. Heightmaps, cuevas, texturas, fog. Lo que Minecraft, No Man's Sky y Terraria usan.
Publicado: · Por Juanjo "Banyo" López
0. Por qué no random a secas
Imagina que generas un heightmap llamando Random.value por celda. Cada celda es un valor aleatorio independiente. Resultado: ruido TV. Saltos abruptos entre vecinos, sin estructura, ningún terreno reconocible.
El terreno real es continuo: la altura en (x, y) y en (x+1, y) se parecen. Las nubes lo mismo. La distribución de minerales lo mismo. Necesitas algo que dé valores aleatorios coherentes — que dos puntos cercanos den valores parecidos.
Eso es Perlin noise: ruido pseudo-aleatorio que varía suave en el espacio. Ken Perlin lo inventó en 1983 para los efectos de Tron y se ganó un Oscar técnico.
Scale controla qué tan anchos son los rasgos. Octaves agrega capas de detalle más fino.Persistence dice qué tanto pesa cada octava extra (0.5 = clásico). Lacunarity es cuánto sube la frecuencia entre octavas (2 = clásico). Cambia uno y observa qué pasa.
Figura 1 — Heatmap 2D de Perlin/value noise. Toca los sliders y mira el patrón cambiar. Cambia paleta a “terrain” para verlo como heightmap (azul=agua, verde=hierba, marrón=montaña, blanco=nieve).
1. Continuidad: lo que distingue a Perlin
Arriba: Perlin 1D scrolleando, con relleno tipo terreno. Abajo: random uniforme con la misma seed por sample. La diferencia visual es exactamente el motivo por el que Perlin reemplazó al random crudo en gamedev: continuidad entre samples vecinos.
Figura 2 — La línea verde es Perlin 1D (suave). La línea roja es random uniforme con la misma seed. Ambas usan el mismo “espacio de muestra”; la diferencia es brutal.
Random uniforme sample por sample: cada valor es independiente del anterior. Perlin: cada valor depende de la celda en la que cae y del gradiente asociado a esa celda — los puntos cercanos comparten gradiente, así los outputs se interpolan suavemente.
2. Cómo funciona Perlin clásico
Tres pasos por cada (x, y) consultado:
2.1 Identificar la celda
Cada par entero (xi, yi) define la esquina inferior izquierda de una celda 1×1. Para (x, y) cualquiera:
xi = floor(x)
yi = floor(y)
xf = x - xi # fracción dentro de la celda, [0, 1)
yf = y - yi
2.2 Pseudo-aleatorio por esquina
Cada esquina de cada celda tiene un gradient vector (un vector unitario pseudo-aleatorio). En implementaciones reales, no se calcula al vuelo — se precomputa una tabla de 256 gradients, y dada una esquina (xi, yi), hashes un índice a esa tabla:
hash(xi, yi) = perm[ perm[xi & 255] + (yi & 255) ] & 255
gradient = GRADIENT_TABLE[hash]
perm es una permutación de 0..255 (la “permutation table”). Así dos esquinas distintas dan gradients distintos pero deterministas.
2.3 Producto punto + interpolación suave
Para cada esquina de la celda, calcula el producto punto entre su gradient y el vector que va de esa esquina al punto consultado. Te da 4 valores; interpola con fade (smoothstep mejorado):
function fade(t) -> float
return t * t * t * (t * (t * 6 - 15) + 10)
u = fade(xf)
v = fade(yf)
x1 = lerp(dot(g00, (xf, yf)), dot(g10, (xf-1, yf)), u)
x2 = lerp(dot(g01, (xf, yf-1)), dot(g11, (xf-1, yf-1)), u)
result = lerp(x1, x2, v)
fade es 6t⁵ - 15t⁴ + 10t³. Tiene derivada y segunda derivada cero en t=0 y t=1 — eso elimina las “líneas de cuadrícula” visibles del fade ingenuo (3t² - 2t³).
3. fBm: el ruido que parece terreno
Una sola octava de Perlin se ve… lavada. Como nubes muy suaves. El terreno real tiene rugosidad multi-escala: montañas grandes, colinas medias, piedras pequeñas. Eso lo das con fractal Brownian motion: suma N octavas, cada una con frecuencia mayor y amplitud menor.
function fbm(x, y, octaves, persistence, lacunarity) -> float
total = 0
amplitude = 1
frequency = 1
maxValue = 0
for i = 1..octaves:
total += perlin(x * frequency, y * frequency) * amplitude
maxValue += amplitude
amplitude *= persistence
frequency *= lacunarity
return total / maxValue
Tres parámetros que dominan el resultado:
- octaves — cuántas capas.
1= noise crudo,8+= mucho detalle, costo lineal. - persistence — qué tanto pesa cada octava extra.
0.5clásico (cada octava es la mitad de la anterior).<0.5= predominan estructuras grandes;>0.5= predominan las chicas. - lacunarity — multiplicador de frecuencia.
2.0clásico (cada octava tiene el doble de frecuencia).
Misma seed (1337), mismo scale (32). La estructura grande del terreno es idéntica — lo único que cambia es la riqueza de detalle. Cada octava añade una capa con frecuencia × 2 y amplitud × 0.5.
Figura 3 — Misma seed, mismo scale, distinto número de octavas. La estructura grande es idéntica; cambia el detalle. Más octavas = más realismo, más costo.
4. Cuevas con threshold
Una de las aplicaciones más simples y poderosas: genera Perlin 2D y marca cada celda como “muro” si supera un umbral.
Genera Perlin 2D, luego marca cada celda como "muro" si su valor > threshold, "aire" si no. Sube el threshold: las cuevas se cierran. Bájalo: el mapa se vuelve casi vacío. Ajusta scale para que las cavidades sean más grandes o más fragmentadas.
Figura 4 — Perlin + threshold. Cambia el slider de threshold y mira cómo el espacio se cierra o abre. A 0.5, distribución equilibrada. A 0.7, casi todo es muro con pocas cavidades. Es lo que hace Terraria con su generación inicial de mundo.
function generateCaves(width, height, threshold) -> Grid
grid = new Grid(width, height)
for y in 0..height:
for x in 0..width:
v = (perlin(x / scale, y / scale) + 1) / 2
grid[x, y] = v > threshold ? WALL : AIR
return grid
Usualmente se combina con cellular automata post-Perlin: el ruido crea las cavidades base, los autómatas las suavizan y eliminan trozos disconexos.
5. Aplicaciones típicas
5.1 Heightmap
height(x, y) = fbm(x, y, octaves: 5, persistence: 0.5, lacunarity: 2.0)
height = (height + 1) * 0.5 // [0, 1]
height = pow(height, 2.5) // exponente para enfatizar valles
Multiplica por la altura máxima del mundo y tienes terreno. Aplica la paleta “terrain” para colorearlo por bandas de altura.
5.2 Distribución de biomas / minerales
Dos passes de Perlin con seeds y scales distintos generan dos campos: temperatura y humedad. Combinados, te dan biomas (Whittaker biome diagram). Para minerales: pass de Perlin a baja frecuencia para concentración general, pass a alta para clusters individuales.
5.3 Texturas animadas
Perlin 3D o 4D donde la coordenada extra es tiempo: nubes que se deforman, agua que ondula, fuego procedural. Costo proporcional a octaves × resolution × frames.
5.4 Foliage placement
Distribuye árboles donde fbm(x, y) > umbral: bosques aparecen en zonas naturalmente, no uniformemente.
6. Variantes que también deberías conocer
| Algoritmo | Pros | Contras | Uso |
|---|---|---|---|
| Value noise | Trivial de implementar | Visualmente más “blocky” | Didáctico; assets retro |
| Perlin clásico | Estándar | Patentes vencidas; visualmente distintivo en altas frecuencias | Default histórico |
| Simplex noise | Mejor distribución, menos artefactos en 3D+ | Más complejo de implementar | 3D, animado, texturas |
| Worley / cellular | Genera patrones tipo “voronoi” suaves | No es ruido tradicional | Piedras, escamas, panales |
Simplex (Perlin 2001) es el reemplazo moderno; corre en O(n²) por dimensión vs O(2ⁿ) del clásico, lo que lo hace mucho más rápido en 3D y 4D.
7. Implementación en Unity / C#
Unity ya trae Mathf.PerlinNoise(x, y) que devuelve [0, 1]. Para fBm:
public static class Noise {
public static float Fbm(float x, float y, int octaves, float persistence = 0.5f, float lacunarity = 2.0f) {
float total = 0f;
float amplitude = 1f;
float frequency = 1f;
float maxValue = 0f;
for (int i = 0; i < octaves; i++) {
total += Mathf.PerlinNoise(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return total / maxValue;
}
public static float[,] HeightMap(int width, int height, float scale, int octaves) {
var map = new float[width, height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
map[x, y] = Fbm(x / scale, y / scale, octaves);
}
}
return map;
}
}
Mathf.PerlinNoise no es exactamente Perlin clásico (es value-like) pero rinde para casi todo. Para Simplex / Perlin estricto, usa Job + noise.snoise del package com.unity.mathematics.
8. En otros engines
- Godot:
FastNoiseLiteviene built-in con Perlin, Simplex, Cellular y más, todo configurable desde el inspector. - Unreal:
UE5 Niagarausa noise para particles. Para gameplay, hay nodes de noise en Blueprints o C++ víaFMath::PerlinNoise2D. - Web/Canvas:
simplex-noise.js(Stefan Gustavson port) es el estándar. Cuesta < 5 KB gzipped.
9. Errores comunes
- Olvidar normalizar el output — Perlin clásico devuelve
[-1, 1]; si lo usas directo como height, las zonas negativas se cortan. - Scale demasiado bajo —
noise(x, y)conx, yenteros consultados directos da el mismo valor quenoise(0, 0). Hay que dividir las coordenadas (o elscaleactúa como tal). - Persistence > 1 — las octavas se hacen cada vez más grandes, el output sale del rango y se ve roto.
- Lacunarity = 1 — todas las octavas tienen la misma frecuencia: estás sumando ruido idéntico × N veces. No agregás detalle.
- Confundir scale con octaves — scale es el “zoom” (qué tan grandes son los rasgos). Octaves es el detalle (cuántas capas). Son dimensiones independientes del control.
10. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Generas un heightmap usando random uniforme por celda. Sale ruido TV. ¿Por qué Perlin lo arregla?
Quieres montañas grandes con detalle fino. Estás en 1 octava. ¿Qué cambias?
Generas cuevas con Perlin + threshold. Las cavidades quedan desconectadas (islas). ¿Solución?
Tienes Perlin 2D para un heightmap pero quieres que el agua se mueva en el tiempo. ¿Mínimo cambio?
11. Siguientes pasos
Perlin te da el campo de densidad. Wave Function Collapse ataca el problema desde otro lado: reglas explícitas de adyacencia entre tiles. Misma meta, filosofía opuesta — y te toca el siguiente.