Ruido de valor y ruido de gradiente
Las dos familias de ruido de retícula, implementadas completas, y por qué la función de interpolación decide si el patrón delata la rejilla.
Ruido de valor: interpolar valores
La idea más directa. Se coloca un valor aleatorio en cada nodo de una retícula entera y se interpola bilinealmente entre los cuatro que rodean al punto.
float hash12( vec2 p ) { vec3 p3 = fract( vec3( p.xyx ) * 0.1031 ); p3 += dot( p3, p3.yzx + 33.33 ); return fract( ( p3.x + p3.y ) * p3.z );}float ruidoValor( vec2 p ) { vec2 i = floor( p ); vec2 f = fract( p ); // Suavizado de la coordenada local: sin esto, la interpolacion es lineal // y se ven las aristas de la reticula. vec2 u = f * f * ( 3.0 - 2.0 * f ); float a = hash12( i + vec2( 0.0, 0.0 ) ); float b = hash12( i + vec2( 1.0, 0.0 ) ); float c = hash12( i + vec2( 0.0, 1.0 ) ); float d = hash12( i + vec2( 1.0, 1.0 ) ); return mix( mix( a, b, u.x ), mix( c, d, u.x ), u.y );}El resultado está en el rango de cero a uno. Cuesta cuatro hashes y tres mix.
Y tiene un defecto estructural: los extremos del ruido caen siempre sobre los nodos de la retícula. Como el valor en cada nodo es exactamente el del hash y entre nodos solo hay interpolación, el máximo local está siempre en un nodo. Con una sola octava eso produce un patrón de manchas alineadas en cuadrícula que se ve a simple vista, y con muchas octavas sigue viéndose como una textura ligeramente ortogonal.
Ruido de gradiente: interpolar productos escalares
La solución de Ken Perlin en 1985 fue cambiar qué se guarda en el nodo. En vez de un valor, un vector de dirección aleatorio. Y el aporte de cada nodo al punto no es su valor sino el producto escalar entre su gradiente y el vector que va del nodo al punto.
vec2 gradiente( vec2 celda ) { float a = hash12( celda ) * 6.283185307179586; return vec2( cos( a ), sin( a ) );}float ruidoGradiente( vec2 p ) { vec2 i = floor( p ); vec2 f = fract( p ); // Quintica de Perlin: derivada Y segunda derivada nulas en los extremos vec2 u = f * f * f * ( f * ( f * 6.0 - 15.0 ) + 10.0 ); float a = dot( gradiente( i + vec2( 0.0, 0.0 ) ), f - vec2( 0.0, 0.0 ) ); float b = dot( gradiente( i + vec2( 1.0, 0.0 ) ), f - vec2( 1.0, 0.0 ) ); float c = dot( gradiente( i + vec2( 0.0, 1.0 ) ), f - vec2( 0.0, 1.0 ) ); float d = dot( gradiente( i + vec2( 1.0, 1.0 ) ), f - vec2( 1.0, 1.0 ) ); return mix( mix( a, b, u.x ), mix( c, d, u.x ), u.y );}La diferencia clave está en el vector f - esquina: en el propio nodo ese vector es cero, así que el producto escalar es cero. El ruido de gradiente vale exactamente cero en todos los nodos de la retícula. Los extremos caen entre nodos, en posiciones que dependen de las direcciones de los gradientes vecinos, y por eso el patrón no delata la rejilla.
El rango es aproximadamente de menos raíz de dos partido por dos a más lo mismo, es decir unos menos 0.707 a 0.707 en dos dimensiones. Para llevarlo a cero-uno, n * 0.7071 * 0.5 + 0.5 o simplemente n * 0.5 + 0.5 aceptando que no llegará a los extremos.
La función de interpolación
Las dos implementaciones usan una curva distinta para suavizar la coordenada local, y esa elección no es cosmética.
La interpolación lineal, es decir usar f directamente sin suavizar, produce discontinuidades en la derivada al cruzar una línea de la retícula. Se ven como una cuadrícula de pliegues, y con ruido de gradiente el efecto es todavía peor porque los nodos valen cero y los pliegues pasan por ahí.
La cúbica, f * f * ( 3.0 - 2.0 * f ), que es exactamente lo que hace smoothstep, tiene derivada cero en los extremos. Elimina los pliegues visibles y es lo que Perlin usó en la versión original.
La quíntica, 6f⁵ - 15f⁴ + 10f³, tiene además la segunda derivada nula en los extremos. Perlin la introdujo en 2002, en el artículo donde revisó su propia función. La razón es concreta: si vas a usar el ruido como campo de alturas y calcular normales a partir de él, las normales dependen de la primera derivada, y un brillo especular sobre esas normales depende de la segunda. Con la cúbica, la segunda derivada salta al cruzar cada línea de la retícula, y ese salto reaparece en la iluminación como un enrejado tenue que solo se ve cuando la luz da rasante.
// Las tres, para compararvec2 lineal = f;vec2 cubica = f * f * ( 3.0 - 2.0 * f );vec2 quintica = f * f * f * ( f * ( f * 6.0 - 15.0 ) + 10.0 );La quíntica cuesta unas cuatro operaciones más por eje. Si el ruido se va a ver solo como color, la cúbica basta. Si se va a derivar, la quíntica no es opcional.
Coste y dimensión
| Valor 2D | Gradiente 2D | Valor 3D | Gradiente 3D | |
|---|---|---|---|---|
| Nodos a evaluar | 4 | 4 | 8 | 8 |
| Hashes | 4 | 4 | 8 | 8 |
| Productos escalares | 0 | 4 | 0 | 8 |
| Interpolaciones | 3 | 3 | 7 | 7 |
El número de nodos es dos elevado a la dimensión. En dos dimensiones son cuatro y en tres son ocho; en cuatro dimensiones —que hacen falta para animar ruido 3D sin que se note el desplazamiento— son dieciséis. Ese crecimiento exponencial es la razón por la que existe el ruido simplex, que reduce los nodos a la dimensión más uno: tres en 2D, cuatro en 3D, cinco en 4D.
Para el caso muy frecuente de ruido 2D que se anima, hay un atajo que evita subir a 3D: desplazar las coordenadas.
// Barato: el patron se desplaza, no evolucionafloat n = ruidoGradiente( p + vec2( uTiempo * 0.2, 0.0 ) );// Caro pero correcto: el patron cambia de formafloat n = ruidoGradiente3D( vec3( p, uTiempo * 0.2 ) );El primero se ve como una textura que se mueve; el segundo como algo que hierve. Para nubes o humo, el segundo. Para agua que fluye en una dirección, el primero es lo que quieres de todas formas.