Pular para o conteúdo
Fundamento · RuídoIniciante

Hash e pseudoaleatoriedade

Transforme uma coordenada num valor que parece aleatório mas nunca muda, para ter aleatoriedade por pixel ou por célula.

Como funciona

O GLSL não tem random(). Um shader roda em milhares de pixels ao mesmo tempo, sem nenhum estado compartilhado para avançar, e precisa desenhar a mesma imagem a cada quadro. Por isso, aleatoriedade num shader é uma função: você entrega uma coordenada e ela devolve um valor com cara de aleatório, sempre o mesmo para a mesma entrada. Essa função é um hash.

Um bom hash tem três propriedades. Entradas próximas (o pixel 10 e o pixel 11) dão saídas que parecem não ter relação. As saídas se espalham por igual entre 0 e 1, sem nenhum valor mais comum que outro. E ele é barato, porque muitas vezes roda várias vezes por pixel.

O hash de seno

O hash mais curto tira o seno de um número, multiplica por uma constante grande e fica só com a parte fracionária usando fract:

float hash21(vec2 p) {
    return fract(sin(dot(p, vec2(41.37, 289.13))) * 24634.6345);
}

O dot junta as duas coordenadas num número só, com pesos diferentes, para que (1, 2) e (2, 1) não colidam. O sin leva isso para -1..1, o multiplicador grande espalha diferenças minúsculas do seno por muitos números inteiros e o fract joga a parte inteira fora. O que sobra muda loucamente de uma entrada para a outra. As constantes são arbitrárias; quaisquer números grandes e sem relação entre si funcionam, e trocá-las dá outro campo aleatório. O nome hash21 diz o que entra e o que sai: dois valores entram, um sai.

Mostre o resultado por pixel e você tem chuvisco, como uma TV fora do ar:

float n = hash21(gl_FragCoord.xy);
fragColor = vec4(vec3(n), 1.0);

Esse hash é popular porque cabe numa linha, mas depende da precisão com que a GPU calcula o sin de um argumento grande. Essa precisão não é especificada, então o mesmo shader pode ficar bom numa GPU e mostrar listras ou blocos repetidos em outra, principalmente com entradas grandes, como posições no mundo ou um tempo que não para de crescer. Mantenha as entradas pequenas (abaixo de algumas centenas) e trate-o como ferramenta rápida para protótipos e visuais. O chuvisco acima já quebra essa regra: o gl_FragCoord chega à casa dos milhares numa tela full HD, então pode listrar em algumas GPUs. Para valores por pixel confiáveis, passe o pixel pelo hash inteiro abaixo, hashCell(ivec2(gl_FragCoord.xy)).

Hashes inteiros

O GLSL ES 3.00 tem inteiros sem sinal e operações de bits, o que permite hashes que dão os mesmos bits em qualquer GPU. A multiplicação de inteiros dá a volta em 2³², e deslocamentos com XOR misturam os bits altos de volta nos baixos. Esta função de mistura é o finalizador do MurmurHash3, que o autor colocou em domínio público:

uint mix32(uint x) {
    x ^= x >> 16;
    x *= 0x85ebca6bu;
    x ^= x >> 13;
    x *= 0xc2b2ae35u;
    x ^= x >> 16;
    return x;
}

// Um valor aleatório em 0..1 por célula inteira.
float hashCell(ivec2 cell) {
    uint h = mix32(uint(cell.x) ^ mix32(uint(cell.y)));
    return float(h >> 8) / 16777216.0; // os 24 bits de cima cabem num float sem perda
}

Passar o y pelo hash antes de combiná-lo com o x mantém as células (1, 2) e (2, 1) separadas. Converter um int negativo em uint preserva os bits, então células negativas também funcionam. O estudo nas fontes compara muitos hashes de GPU em velocidade e qualidade: hashes inteiros como este custam mais ou menos o mesmo que o truque do seno e são bem mais uniformes.

Para passar uma posição float direto pelo hash, floatBitsToUint(p) entrega os bits crus. É exato, mas dois valores que diferem só por arredondamento dão resultados sem relação, então em geral você passa pelo hash o índice de uma célula.

Aleatoriedade por célula

Passar cada pixel pelo hash dá chuvisco. Passar a célula em que o pixel está dá um valor aleatório por célula, a base de ladrilhos aleatórios, de ruído e de pontos espalhados. Com o floor do ladrilhamento:

vec2 grid = uv * 8.0;
ivec2 cell = ivec2(floor(grid));
vec3 color = vec3(hashCell(cell),
                  hashCell(cell + ivec2(17, 0)),
                  hashCell(cell + ivec2(0, 31)));

Cada célula ganha uma cor própria, constante dentro dela. Deslocar a entrada para cada canal tira três valores diferentes de uma única função de hash. O mesmo número aleatório pode escolher a rotação de um ladrilho, decidir se uma célula acende ou empurrar um ponto dentro da célula, que é onde o ruído celular começa.

Granulado animado

Um hash do pixel com um contador de quadros dá um granulado que muda a cada quadro. Conte quadros inteiros, para que cada quadro seja um padrão novo e sem relação com o anterior, e use o hash inteiro, porque o contador não para de crescer:

uint frame = uint(time * 24.0);  // 24 padrões novos por segundo
uvec2 q = uvec2(gl_FragCoord.xy);
uint h = mix32(q.x ^ mix32(q.y ^ mix32(frame)));
float grain = float(h >> 8) / 16777216.0;

Hashes são a camada de baixo de tudo o que é procedural. Sozinhos, são ásperos: saídas vizinhas não têm nada a ver umas com as outras. Suavizá-los pelo espaço é o próximo passo, o ruído de valor.

Leitura complementar