wandres.dev
SHADERS IV · Ruido y patrones

Perlin y simplex: en qué se diferencian de verdad

La historia de las dos funciones, qué problema resolvió cada una, y una implementación completa y correcta de ruido simplex 2D lista para pegar en tu shader.

⏱ 21 min

Perlin y simplex son dos funciones del mismo autor separadas por dieciséis años, y la segunda existe porque la primera tenía dos defectos concretos: el coste crece exponencialmente con la dimensión y la retícula cuadrada deja marcas alineadas con los ejes. Simplex arregla los dos cambiando la forma de la celda. Esta lección explica el mecanismo y entrega la implementación completa, porque una referencia a “usa una librería” no sirve de nada cuando estás dentro de un ShaderMaterial.

🎯 Al terminar esta lección sabrás
  • Situar las tres funciones de Perlin y qué cambió cada una.
  • Explicar por qué una celda simplex reduce el número de esquinas y elimina el sesgo direccional.
  • Pegar una implementación de simplex 2D correcta y entender qué hace cada bloque.
  • Conocer el estado real de la patente y qué implica hoy.

Tres funciones, dieciséis años

Perlin clásico, 1985. Ruido de gradiente sobre retícula cúbica, con gradientes tomados de una tabla de permutación. Ganó un Óscar técnico en 1997 por su impacto en efectos visuales. Es lo que la lección anterior implementa.

Simplex, 2001. Perlin cambia la celda de hipercubo a simplex —el politopo más simple de cada dimensión: triángulo en 2D, tetraedro en 3D— y con ello reduce las esquinas de dos elevado a N a N más uno. También sustituye la interpolación por una suma de contribuciones con caída radial, que elimina la necesidad de interpolar y da una derivada continua analítica.

Perlin mejorado, 2002. No es simplex: es el clásico con dos correcciones. La interpolación pasa de cúbica a quíntica, y los gradientes dejan de venir de una tabla arbitraria para ser los doce vectores que apuntan a los puntos medios de las aristas de un cubo, elegidos así porque el producto escalar con ellos se calcula sumando y restando componentes, sin multiplicar.

Cuando alguien dice “ruido Perlin” casi siempre se refiere a alguna de las dos versiones de retícula cúbica; simplex es lo suficientemente distinto como para merecer su nombre.

Qué cambia una celda simplex

En dos dimensiones, la retícula cuadrada divide el plano en cuadrados de cuatro esquinas. La retícula simplex lo divide en triángulos de tres. En tres dimensiones, ocho esquinas contra cuatro. En cuatro, dieciséis contra cinco. La diferencia crece rápido y es la razón principal de que simplex escale a dimensiones altas.

El segundo cambio es geométrico. Un cuadrado tiene direcciones privilegiadas: sus lados están alineados con los ejes, y por eso el ruido Perlin clásico muestra artefactos leves alineados con X y con Y, visibles sobre todo en las derivadas. Una teselación por triángulos equiláteros es mucho más isótropa y no tiene esas direcciones.

El tercer cambio es cómo se combinan las esquinas. Perlin interpola; simplex suma contribuciones con una caída radial. Cada esquina aporta el producto escalar de su gradiente por el desplazamiento, multiplicado por una función que vale uno en la esquina y cae a cero antes de llegar a las esquinas vecinas. Como la caída se anula fuera del radio de influencia, no hace falta interpolar nada: basta sumar. Y como la función de caída es un polinomio de grado alto, la suma es continua y derivable.

El mecanismo para localizar la celda es un cambio de coordenadas. La retícula simplex 2D se obtiene sesgando una retícula cuadrada, así que el algoritmo sesga el punto de entrada, encuentra el cuadrado que le corresponde, decide en cuál de los dos triángulos de ese cuadrado cae comparando dos componentes, y desesga las tres esquinas de vuelta.

Simplex 2D, completo

Ésta es la implementación de Ian McEwan y Ashima Arts, publicada bajo licencia MIT y usada por prácticamente todo el ecosistema. Funciona tal cual dentro de un ShaderMaterial de Three.js.

vec3 mod289( vec3 x ) { return x - floor( x * ( 1.0 / 289.0 ) ) * 289.0; }
vec2 mod289( vec2 x ) { return x - floor( x * ( 1.0 / 289.0 ) ) * 289.0; }
vec3 permute( vec3 x ) { return mod289( ( ( x * 34.0 ) + 1.0 ) * x ); }

float snoise( vec2 v ) {
  const vec4 C = vec4(
     0.211324865405187,   // (3 - raiz(3)) / 6
     0.366025403784439,   // (raiz(3) - 1) / 2
    -0.577350269189626,   // -1 + 2 * C.x
     0.024390243902439    // 1 / 41
  );

  // 1. Sesgar al espacio simplex y quedarse con la celda
  vec2 i  = floor( v + dot( v, C.yy ) );
  vec2 x0 = v - i + dot( i, C.xx );

  // 2. Decidir en cual de los dos triangulos cae
  vec2 i1 = ( x0.x > x0.y ) ? vec2( 1.0, 0.0 ) : vec2( 0.0, 1.0 );
  vec4 x12 = x0.xyxy + C.xxzz;
  x12.xy -= i1;

  // 3. Hashear las tres esquinas con la permutacion modulo 289
  i = mod289( i );
  vec3 p = permute(
    permute( i.y + vec3( 0.0, i1.y, 1.0 ) ) + i.x + vec3( 0.0, i1.x, 1.0 )
  );

  // 4. Caida radial: max(0.5 - r2, 0) elevado a la cuarta
  vec3 m = max( 0.5 - vec3(
    dot( x0, x0 ), dot( x12.xy, x12.xy ), dot( x12.zw, x12.zw )
  ), 0.0 );
  m = m * m;
  m = m * m;

  // 5. Gradientes: 41 direcciones repartidas, sin tabla ni textura
  vec3 x = 2.0 * fract( p * C.www ) - 1.0;
  vec3 h = abs( x ) - 0.5;
  vec3 ox = floor( x + 0.5 );
  vec3 a0 = x - ox;

  // 6. Normalizar los gradientes escalando la caida en vez de los vectores
  m *= 1.79284291400159 - 0.85373472095314 * ( a0 * a0 + h * h );

  // 7. Sumar las tres contribuciones
  vec3 g;
  g.x  = a0.x  * x0.x  + h.x  * x0.y;
  g.yz = a0.yz * x12.xz + h.yz * x12.yw;

  return 130.0 * dot( m, g );
}

Vale la pena entender dos decisiones de esa implementación, porque son lo que la hace rápida.

La permutación módulo 289 sustituye a la tabla de permutación de Perlin. permute( x ) calcula ( 34x + 1 ) x módulo 289, que es una permutación de los residuos porque 289 es 17 al cuadrado y el polinomio es biyectivo en ese anillo. Eso evita tener que subir una textura de permutación o declarar un array de 256 enteros, que en GLSL sería un desastre de registros.

El truco de los gradientes de los pasos 5 y 6: en vez de generar direcciones unitarias con senos y cosenos, se generan 41 vectores no unitarios repartidos y se compensa su longitud desigual escalando la función de caída. La expresión 1.79284291400159 - 0.85373472095314 * ( a0 * a0 + h * h ) es una aproximación lineal de la inversa de la longitud del gradiente, válida porque las longitudes que produce el generador caen todas en un intervalo estrecho. Equivale a normalizar y cuesta una multiplicación y una resta en lugar de una raíz inversa y tres multiplicaciones.

Cómo usarla

uniform float uTiempo;
varying vec2 vUv;

void main() {
  // El factor de escala controla el tamano de los rasgos.
  // El desplazamiento evita evaluar en coordenadas redondas.
  float n = snoise( vUv * 6.0 + vec2( 13.7, 7.3 ) + uTiempo * 0.15 );

  // snoise devuelve aproximadamente [-1, 1]. A [0, 1]:
  float t = n * 0.5 + 0.5;

  gl_FragColor = vec4( vec3( t ), 1.0 );
}

La versión de tres dimensiones sigue exactamente la misma estructura con cuatro esquinas en vez de tres y un sesgo de un tercio en vez del que usa la 2D; está en el mismo paquete de Ashima y ocupa unas cincuenta líneas. Si necesitas ruido 2D animado que evolucione en vez de desplazarse, la 3D con el tiempo en la tercera componente es el camino.

La patente

Perlin patentó el método de ruido simplex en implementaciones de tres dimensiones o más. La solicitud es de enero de 2001 y la patente estadounidense 6.867.776 se concedió en 2005. Con el plazo de veinte años desde la solicitud, expiró en enero de 2022.

Eso importa porque explica el ecosistema. Durante casi dos décadas, la incertidumbre legal empujó a muchos proyectos hacia OpenSimplex, una alternativa creada explícitamente para esquivar la patente, y hacia el Perlin clásico. Hoy esa razón ya no existe: simplex se puede usar sin reservas. La implementación de Ashima además nunca estuvo afectada en su versión 2D, porque la patente cubría tres dimensiones en adelante.

snoise no reparte uniformemente, y por eso tus umbrales no hacen lo que crees

Se dice que el ruido simplex devuelve valores en el intervalo de menos uno a uno, y es una media verdad que causa problemas concretos. El factor 130.0 del final es una constante empírica elegida para que el rango observado se acerque a menos uno y uno; los extremos reales rondan más menos 0.98 y se alcanzan en muy pocos puntos. Pero el problema serio no es el rango sino la distribución: como cada valor es una suma de tres contribuciones aproximadamente independientes, el histograma es una campana centrada en cero, no una uniforme. La inmensa mayoría de los valores se apiña cerca del cero y los que rozan los extremos son rarísimos. La consecuencia práctica aparece en cuanto pones un umbral. Si conviertes con n * 0.5 + 0.5 y haces step( 0.5, t ), obtienes aproximadamente la mitad del área, que es lo que esperabas. Pero si haces step( 0.8, t ) esperando un 20 % del área, obtienes bastante menos del 5 %, y si haces step( 0.95, t ) no obtienes prácticamente nada y crees que el ruido está roto. Lo mismo ocurre con smoothstep para nubes: los umbrales que funcionan están todos apiñados alrededor del centro, y mover uno de ellos 0.05 cambia la cobertura muchísimo más de lo que la intuición dice. Hay dos maneras de recuperar el control. La primera es aplicar una función de reparto: t = 0.5 + 0.5 * sign( n ) * pow( abs( n ), 0.6 ) ensancha las colas y acerca el histograma a una uniforme. La segunda, y la que usa la producción, es no adivinar: pinta el histograma una vez leyendo el render target a un array con readRenderTargetPixels y elige los umbrales mirándolo.