Aleatoriedad, Noise y fBM
Existen varios algoritmos para generar ruido. Vamos a estudiar alguno de ellos junto con algunas de sus aplicaciones.
Aleatoriedad
Empezamos por el rey de los ruidos: la aleatoriedad. En concreto, para materiales, estamos interesados en la pseudo-aleatoriedad.
En este contexto decimos que una función $f(x)$ es pseudo-aleatoria si la función devuelve el mismo valor para dos entradas iguales. Es decir si $x_o = x_1$ entonces $f(x_0) = f(x_1)$.
Una función pseudo-aleatoria debe devolver valores sin relación aparente incluso para entradas muy parecidas.
¿Cómo conseguir aleatoriedad partiendo de funciones matemáticas bien definidas?
La idea es tomar la entrada (no acotada) y pasarla por una función que la acote. Una buena candidata es $sin$.
float random(float x) {
float y = sin(x);
// ...
}
Teniendo la salida acotada ahora podemos "ampliarla" multiplicando por algún factor relativamente grande:
float random(float x) {
float y = 35137.5234 * sin(x);
// ...
}
La salida $y$ aún no es aleatoria, aunque estamos muy cerca.
De hecho, podemos aplicarle otra función que la convierte en pseudo-aleatoria. ¿Cuál? La función $frac$, es decir, tomar sus decimales.
La función anterior cumple con los criterios que necesitamos:
- que para dos valores distintos de $x$ la salida no esté aparentemente relacionada.
- para valores de $x$ parecidos la salida sea suficientemente distinta.
- para valores de $x$ iguales la salida es la misma.
Si necesitamos que la entrada sea un vector puedes utilizar el producto escalar sobre otro vector:
Usando el nodo Custom Node para escribir código hlsl:
Fíjate como se crea ruido blanco,¡pseudo-aleatoriedad!
O usando los nodos habituales sin Custom Node:
Este ruido "puro" puede ser usado en multitud de efectos:
Hash function
Aunque nuestra función ruido funciona relativamente bien no podemos obviar que también es costosa computacionalmente.
Para aliviar el coste podríamos sustituirla por las bien conocidas funciones hash. Hay multitud de funciones hash: para checksum, CRC, para almacenar mapas claves-valor, criptográficas, etc.,
Algunas funciones hash suelen exhibir buenas propiedades como las ya comentadas para ser usadas como función pseudo-aleatoria.
Aquí algunas:
Sustituyendo la función anterior usando $sin$ por alguna $hash$ anterior deberíamos tener el mismo resultado.
Value Noise
En la naturaleza no encontramos un ruido tan puro. Si bien es cierto existe aleatoriedad, pero esta es "contínua".
Es posible conseguir aleatoriedad "contínua" a partir de aleatoriedad pura. Es tan directo como tomar dos valores aleatorios puros e interpolarlos.
En caso de más de una dimensión podemos dividir el espacio en un grid. Para cada esquina (vértice) del grid le asignamos un valor aleatorio puro. El resto de valores dentro del grid será una interpolación de los cuatro vértices (en caso de 2D) u ocho vértices (en caso 3D).
Veamos el caso 2D.
He creado un material llamado M_VNoise.
Divimos el espacio en un grid de NxN celdas:
La parte fraccional ofrece las coordenadas UV de cada celda:
Mientras que con floor obtenemos la columna y fila de cada celda. La celda superior-izquierda tendrá las coordenadas (0,0) mientras que la inferior derecha (N-1, N-1).
Para cada celda tenemos que obtener sus cuatro vértices. Si estuvieramos en un espacio 3D sus ocho vértices. Para cada vértice computar un valor aleatorio puro:
Y ahora toca interpolar cada valor. Primero interpolamos los dos pares de vértices alineados horizontales. Es decir, el vértice superior izquierdo – superior derecho e inferior izquierdo – inferior derecho:
Y ahora interpolamos en vertical.
La salida de la interpolación es precisamente el valor aleatorio contínuo. También llamado Value Noise.
El último nodo antes de conectar con emissive es para remapear el ruido (-1 a 1) al rango 0 – 1.
Podemos suavizar aún más el ruido. En vez de usar tal cuál los valores $u$ y $v$ como alpha para interpolar linearmente, utilizar una función de interpolación cúbica o includo quinta potencia:
Podemos encapsular esta función en un material function llamado M_SmoothAlpha y usarla así:
En GLSL:
Turbulence y fBM
Usando value noise como base podemos crear ruidos más complejos.
En efecto, si sumamos value-noise cambiando su frecuencia y amplitud por cada sumando tenemos el ruido fBM. Normalmente se va doblando la frecuencia y reduciendo a la mitad la amplitud.
Como decimos a esta forma de proceder se suele llamar Fractional Brownian Motion (fBM).
$$ fBM(p) = \frac{1}{2} n(p) + \frac{1}{4} n(2p) + \frac{1}{8} n(3p) + \frac{1}{16} n(4p) + ... $$
Siendo $n(x)$ la función ruido. Por ejemplo, value-noise.
Una implementación en GLSL:
Quizás te sorprenda el uso de nombres pero tiene su sentido.
- Octaves es el número de sumandos. Como normalmente se dobla la frecuencia para cada sumando, esto en música es una octava superior. De ahí el nombre.
- Laccunarity es el multiplicador de frecuencia. Normalmente se dobla en cada sumando, así que normalmente $laccun = 2$. En geometría se suele usar el término laccunarity para describir el "hueco" entre patrones.
- Gain es el multiplicador de la amplitud para cada sumando. Normalmente cada sumando "pesa" o contribuye al ruido total la mitad que el sumando anterior, así que normalmente este parámetro será igual a $\frac{1}{2}$.
El ruido fBM tiene una pinta tal que así:
Usando este ruido podemos conseguir efectos como éste:
Si en vez de sumar tal cuál, antes hacemos valor absoluto de cada sumando, obtenemos el algoritmo Turbulence:
Perlin / Gradient Noise
A veces ocurre con value noise que los valores random obtenidos en cada vértice del grid se "parecen" mucho entre ellos para una misma fila o columna, provocando que al interpolar no parezca "tan" aleatorio.
Para ello se usa gradient noise. En vez de elegir aleatoriamente un valor para cada vértice elegimos un gradiente. Esto es, elegimos aleatoriamente un vector 2D (ó 3D si estamos en un espacio 3D) para cada vértice del grid.
El objetivo es terminar interpolando valores escalares. Es decir, hacer un value noise "normal".
¿Cómo convertimos un gradiente en un escalar? En gradient noise usamos el producto escalar contra el vector diferencia entre el punto a evaluar y el vértice.
En GLSL el algoritmo queda así:
Fíjate como el algoritmo es el mismo que value noise salvo que el valor a interpolar es el resultado del producto escalar entre un gradiente escogido aleatoriamente por vértice y el vector diferencia entre dicho vértice y el punto en concreto:
Tanto fBM como Turbulence se pueden usar con gradient noise.
Composición de fBM
Podemos usar la salida de un fBM como entrada a otro fBM para crear efectos complejos.
Iñigo Quilez tiene en su web un estupendo artículo explicándolo.
Vamos a ilustrarlo usando ShaderToy.
Empezamos creando las funciones para la aleatoriedad, vector noise y fBM tal y como hemos visto en apartados anteriores:
Ahora usamos la composición. Podemos componer fBM de infinidad de modos, puedes probar tu mismo. Aquí un ejemplo:
float noise_fbm(vec2 p)
{
float h = fbm(0.09*iTime + p + fbm(0.065*iTime + 2.0 * p - 5.0 * fbm(4.0 * p)));
return h;
}
Remapeando el ruido a distintos rangos y luego haciendo interpolación a distintos colores podemos conseguir el efecto final:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// remapeamos el ruido para usarlo como interpolador de colores
float a2 = smoothstep(-0.5, 0.5, noise_fbm(p));
// usamos un fbm normal para darle otro toque de color
float a1 = smoothstep(-1.0, 1.0, fbm(p));
// obtemos el color base, interpolar entre tres colores
// usando el ruido anterior como interpolador
vec3 cc = mix(mix(vec3(0.50,0.00,0.10),
vec3(0.50,0.75,0.35), a1),
vec3(0.00,0.00,0.02), a2);
// le damos algunos toques para mayor complejidad
cc += 0.5 * vec3(0.1, 0.0, 0.2) * noise_fbm(p);
cc += 0.25 * vec3(0.3, 0.4, 0.6) * noise_fbm(2.0 * p);
// ¡¡prueba a comentar los bloques anteriores!!
fragColor = vec4( vec3(cc), 1.0 );
}
Podemos usar una convolución sencilla para intentar obtener el ouline de la figura:
Y añadirlo para obtener el efecto final con outline:
Este efecto podemos traducirlo trivialmente a HLSL y, usando algunos Custom Node, portarlo a UE4: