FBM: sumar octavas para obtener detalle
Cómo se construye el movimiento browniano fraccionario, qué controlan lacunaridad y ganancia, y cómo evitar que las octavas altas produzcan aliasing.
Una sola octava de ruido produce manchas suaves de un tamaño único, que no se parece a nada natural. Lo que sí se parece es la suma de copias del mismo ruido a frecuencias crecientes y amplitudes decrecientes, porque casi todo lo que hay en el mundo físico —montañas, nubes, vetas de madera, turbulencia— tiene un espectro de potencia que decae con la frecuencia. Esa suma se llama FBM, cuesta lo que cuesta, y tiene dos parámetros que lo controlan todo.
- Escribir un FBM correcto con normalización y límite de bucle constante.
- Explicar qué controlan la lacunaridad y la ganancia y por qué los valores por defecto son 2 y 0.5.
- Implementar las variantes de turbulencia y de ruido de cresta.
- Atenuar las octavas cuya frecuencia supera la resolución de pantalla para evitar aliasing.
La construcción
flowchart TB P[coordenada p] --> O1[octava 1 frecuencia 1 amplitud 0.5] P --> O2[octava 2 frecuencia 2 amplitud 0.25] P --> O3[octava 3 frecuencia 4 amplitud 0.125] P --> O4[octava 4 frecuencia 8 amplitud 0.0625] O1 --> S[suma de las contribuciones] O2 --> S O3 --> S O4 --> S A[suma de las amplitudes] --> N[division para normalizar] S --> N N --> R[fbm en un rango acotado y estable] style P fill:#89b4fa,color:#11111b style O1 fill:#fab387,color:#11111b style O2 fill:#fab387,color:#11111b style O3 fill:#fab387,color:#11111b style O4 fill:#fab387,color:#11111b style A fill:#94e2d5,color:#11111b style S fill:#f9e2af,color:#11111b style N fill:#f9e2af,color:#11111b style R fill:#a6e3a1,color:#11111b
Cada octava evalúa el mismo ruido con la coordenada multiplicada por una frecuencia mayor y la contribución multiplicada por una amplitud menor. El nombre viene de la música: doblar la frecuencia es subir una octava.
#define OCTAVAS 6
float fbm( vec2 p ) {
float valor = 0.0;
float amplitud = 0.5;
float frecuencia = 1.0;
float suma = 0.0;
for ( int i = 0; i < OCTAVAS; i ++ ) {
valor += amplitud * snoise( p * frecuencia );
suma += amplitud;
frecuencia *= 2.0;
amplitud *= 0.5;
}
return valor / suma;
}
El límite del bucle es una macro y no un uniform. En GLSL ES 3.00 un bucle con límite variable compila, pero un límite constante permite al compilador desenrollarlo y calcular las direcciones de forma estática, y en un bucle de seis iteraciones con una función de ruido dentro eso importa.
La división final por la suma de amplitudes es lo que mantiene el rango estable al cambiar el número de octavas. Sin ella, añadir una octava sube el rango y hay que reajustar todos los umbrales de aguas abajo. La suma de amplitudes con ganancia g y n octavas vale ( 1 - gⁿ ) / ( 1 - g ) multiplicado por la amplitud inicial, así que se puede calcular fuera del bucle, pero acumularla dentro es más claro y el compilador la resuelve igual cuando el bucle está desenrollado.
Lacunaridad y ganancia
La lacunaridad es el factor por el que se multiplica la frecuencia en cada octava. La ganancia, también llamada persistencia, es el factor de la amplitud.
Con lacunaridad 2 y ganancia 0.5, la amplitud es inversamente proporcional a la frecuencia. Ese es el espectro que se llama ruido rosa o ruido uno partido por f, y es empíricamente el que producen la mayoría de los procesos naturales de erosión y agregación. No es una elección estética: es la razón por la que una montaña generada así parece una montaña.
Subir la ganancia por encima de 0.5 da un resultado más áspero, con más energía en las frecuencias altas, apropiado para roca fracturada. Bajarla da un resultado más suave, casi de una sola escala, apropiado para dunas o para nubes altas.
Bajar la lacunaridad por debajo de 2 acerca las octavas entre sí y produce una textura más densa y menos jerárquica, a costa de necesitar más octavas para cubrir el mismo rango de escalas.
float fbmParametrico( vec2 p, float lacunaridad, float ganancia ) {
float valor = 0.0;
float amplitud = 0.5;
float suma = 0.0;
for ( int i = 0; i < OCTAVAS; i ++ ) {
valor += amplitud * snoise( p );
suma += amplitud;
p *= lacunaridad;
amplitud *= ganancia;
}
return valor / suma;
}
Turbulencia y cresta
Dos variantes que cuestan lo mismo y cambian por completo el aspecto.
La turbulencia suma valores absolutos. Como el valor absoluto tiene un pico agudo en el cero, y el ruido cruza el cero constantemente, el resultado está lleno de pliegues estrechos. Es la base del mármol y del humo.
float turbulencia( vec2 p ) {
float valor = 0.0;
float amplitud = 0.5;
float suma = 0.0;
for ( int i = 0; i < OCTAVAS; i ++ ) {
valor += amplitud * abs( snoise( p ) );
suma += amplitud;
p *= 2.0;
amplitud *= 0.5;
}
return valor / suma;
}
El ruido de cresta invierte el valor absoluto y lo eleva al cuadrado, con lo que los pliegues se convierten en crestas afiladas y los valles se ensanchan. Es lo que produce las cordilleras de cualquier terreno procedural decente.
float cresta( vec2 p ) {
float valor = 0.0;
float amplitud = 0.5;
float suma = 0.0;
float peso = 1.0;
for ( int i = 0; i < OCTAVAS; i ++ ) {
float n = 1.0 - abs( snoise( p ) );
n = n * n;
n *= peso; // las octavas altas solo
peso = clamp( n * 2.0, 0.0, 1.0 ); // aparecen donde ya hay cresta
valor += amplitud * n;
suma += amplitud;
p *= 2.0;
amplitud *= 0.5;
}
return valor / suma;
}
El factor peso es lo que hace que el detalle fino aparezca en las crestas y no en los valles, que es como funciona la erosión de verdad.
El aliasing de las octavas altas
Éste es el problema que separa un FBM que se ve bien de uno que hierve al mover la cámara. Cuando la frecuencia de una octava supera la resolución de muestreo —es decir, cuando su periodo mide menos de un píxel— esa octava ya no aporta detalle: aporta ruido aleatorio distinto en cada cuadro. El resultado es un centelleo permanente.
La solución correcta es atenuar cada octava en función de cuántos píxeles mide su periodo, y fwidth da esa medida directamente:
float fbmAntialias( vec2 p ) {
float w = max( fwidth( p.x ), fwidth( p.y ) ); // tamano de un pixel en unidades de p
float valor = 0.0;
float amplitud = 0.5;
float frecuencia = 1.0;
float suma = 0.0;
for ( int i = 0; i < OCTAVAS; i ++ ) {
// Si el periodo de esta octava se acerca al tamano del pixel, la apagamos
float atenuacion = 1.0 - smoothstep( 0.3, 0.9, w * frecuencia );
valor += amplitud * snoise( p * frecuencia ) * atenuacion;
suma += amplitud * atenuacion;
frecuencia *= 2.0;
amplitud *= 0.5;
}
return valor / max( suma, 1e-4 );
}
Fíjate en que la atenuación también entra en la suma de normalización: si no, apagar octavas oscurecería el resultado en la distancia en vez de dejarlo simplemente más liso.
Ese max( suma, 1e-4 ) protege el caso en que todas las octavas se apagan, que ocurre en superficies vistas muy de lejos o muy de canto.
La misma idea se aplica al número de octavas por distancia: un objeto lejano no necesita seis, y bajarlo a dos ahorra dos tercios del coste. Como el límite del bucle tiene que ser constante, se hace con la atenuación, no con el contador.
Con lacunaridad 2, la retícula de la segunda octava tiene sus nodos exactamente sobre uno de cada dos nodos de la primera, la de la tercera sobre uno de cada cuatro, y así sucesivamente. Todas las octavas comparten líneas de retícula. Con ruido de valor o Perlin clásico —que están definidos sobre una retícula cuadrada y valen algo especial en sus nodos— eso produce una interferencia constructiva a lo largo de esas líneas compartidas, y el resultado es un enrejado tenue alineado con los ejes que se nota sobre todo en las derivadas, es decir en el sombreado de un terreno más que en su color. Simplex lo disimula bastante porque su retícula es triangular, pero no lo elimina del todo. La solución que usa toda la producción de terrenos son dos líneas. La primera: no usar exactamente 2, sino 1.97 o 2.03, con lo que las retículas se desincronizan y el patrón de interferencia se dispersa. La segunda, mejor todavía: rotar el dominio entre octavas, aplicando una matriz de rotación fija a la coordenada en cada iteración. const mat2 ROT = mat2( 0.80, 0.60, -0.60, 0.80 ); es una rotación de unos treinta y siete grados, sus componentes suman uno en cuadrados así que no escala nada, y basta con escribir p = ROT * p * 2.0; dentro del bucle en vez de p *= 2.0;. El coste son cuatro multiplicaciones y dos sumas por octava, y el resultado es que ninguna de las seis retículas comparte una sola línea con otra. Es la diferencia entre un terreno que parece generado y uno que parece medido.