Pular para o conteúdo
Fundamento · RuídoIntermediário

Ruído de gradiente (Perlin)

Inclinações aleatórias nos cantos da grade em vez de valores aleatórios, para um ruído que esconde a grade; o ruído simplex como variante.

Como funciona

O ruído de valor guarda uma altura aleatória em cada canto da grade, então os picos e vales ficam nos cantos e a grade aparece. O ruído de gradiente, que Ken Perlin apresentou em 1985, guarda em cada canto uma direção aleatória: a inclinação que a superfície tem ali. O valor dele em cada canto é exatamente zero, e os detalhes caem entre os cantos, o que esconde a grade muito melhor. É o ruído por trás da maioria das texturas procedurais: madeira, mármore, terreno, água.

Gradientes e deslocamentos

A estrutura é a mesma do ruído de valor: achar a célula, olhar os quatro cantos, misturar com uma curva de transição. O que muda é a contribuição de cada canto. Cada canto recebe um vetor unitário aleatório, o seu gradiente. A contribuição do canto é o produto escalar desse gradiente com o deslocamento do canto até o pixel:

vec2 gradient(vec2 cell) {
    float angle = hash21(cell) * 6.2831853; // uma direção aleatória
    return vec2(cos(angle), sin(angle));
}

float gradientNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);

    float a = dot(gradient(i),                   f);
    float b = dot(gradient(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
    float c = dot(gradient(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
    float d = dot(gradient(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));

    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // transição quíntica
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

O termo de cada canto é um plano inclinado: zero no canto, subindo na direção do gradiente e descendo no sentido contrário. A curva de transição mistura os quatro planos. Perto de um canto, o plano dele domina, então o ruído passa por zero ali com a inclinação daquele canto. As ondulações se formam entre os cantos, onde os planos discordam.

O resultado é centrado no zero e, com gradientes unitários em 2D, fica dentro de uns ±0.7. Remapeie antes de mostrar ou de usar como máscara: n * 0.7 + 0.5 dá mais ou menos 0..1. Mas a saída com sinal muitas vezes é o que você quer: somada a uma coordenada, ela empurra para os dois lados por igual.

A transição aqui é a quíntica. A revisão de 2002 que Perlin fez do algoritmo trouxe essa mudança, porque a segunda derivada descontínua da cúbica aparece como vincos quando o ruído recebe luz. A mesma revisão passou a escolher os gradientes num conjunto pequeno e fixo (em 3D, as doze direções que apontam para as arestas de um cubo) em vez de direções totalmente aleatórias, o que é mais barato e evita aglomerados de gradientes parecidos. O ângulo aleatório acima é a versão mais fácil de ler; um conjunto fixo de oito direções escolhidas pelo hash funciona igualmente bem.

Ruído simplex

O ruído de gradiente numa grade quadrada tem dois custos que crescem com a dimensão. Cada pixel mistura 2ⁿ cantos: 4 em 2D, 8 em 3D, 16 em 4D. E a grade quadrada ainda deixa uma leve tendência ao longo dos eixos. O ruído simplex, a continuação que Perlin criou em 2001, usa a forma mais simples que ladrilha o espaço: triângulos em 2D, tetraedros em 3D. São n + 1 cantos por pixel: 3 em 2D, 4 em 3D, 5 em 4D.

Duas ideias fazem isso funcionar. Para achar o triângulo, inclina-se o plano para que a grade de triângulos se alinhe com uma grade quadrada, aplica-se o floor ali e desfaz-se a inclinação; uma comparação então diz qual dos dois triângulos da célula contém o pixel. E em vez de misturar os cantos com uma curva de transição, a contribuição de cada canto some com a distância por conta própria, com um decaimento radial, e as contribuições são somadas:

float simplexNoise(vec2 p) {
    // (sqrt(3) - 1) / 2: inclina o plano sobre uma grade quadrada
    const float F = 0.3660254;
    const float G = 0.2113249; // (3 - sqrt(3)) / 6: desfaz a inclinação

    vec2 i  = floor(p + (p.x + p.y) * F); // a célula inclinada
    vec2 d0 = p - i + (i.x + i.y) * G;    // deslocamento a partir do primeiro canto
    // qual triângulo
    vec2 o  = d0.x > d0.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec2 d1 = d0 - o + G;                 // deslocamento a partir do canto do meio
    vec2 d2 = d0 - 1.0 + 2.0 * G;         // deslocamento a partir do último canto

    vec3 w = max(0.5 - vec3(dot(d0, d0), dot(d1, d1), dot(d2, d2)), 0.0);
    w = w * w * w * w;                    // decaimento: zero à distância sqrt(0.5)
    vec3 n = vec3(dot(gradient(i), d0),
                  dot(gradient(i + o), d1),
                  dot(gradient(i + 1.0), d2));
    return 99.0 * dot(w, n);              // escalado para uns -1..1
}

O 99 só normaliza a saída para gradientes unitários; ele muda com o decaimento e com o conjunto de gradientes. Em 2D, o ruído simplex é mais ou menos tão rápido quanto a versão quadrada, e o ganho é que o padrão não tem eixo visível. Em 3D e 4D (ruído que muda com o tempo, ou volumes) ele é bem mais barato, e é aí que ele mais importa. A aparência é um pouco diferente: manchas mais redondas, um pouco mais de contraste. A patente que cobria o ruído simplex em três ou mais dimensões expirou em 2022, então engines e bibliotecas agora o distribuem livremente.

Qual usar

O ruído de valor é o mais barato e serve para máscaras e qualquer coisa que se mexa rápido. O ruído de gradiente é o padrão para texturas: os detalhes têm tamanho uniforme e não se alinham com a grade. O ruído simplex é a escolha em 3D ou 4D, ou quando o leve alinhamento aos eixos do ruído de grade quadrada aparece. Os três são blocos de construção: empilhados em oitavas viram fBm e, realimentados nas próprias coordenadas, viram distorção de domínio.

Leitura complementar