Ruído coerente em jogos

Aula 1 de geração procedural · TAMJ 2026.2 · 1h30

O que você leva desta aula. Uma função que recebe uma posição e devolve um número suave, e cinco jeitos de usar esse número num jogo: altura de terreno, tipo de terreno, onde nasce um item, textura e movimento. Na aula 2 o terreno vira um mundo em pedaços, com cavernas e itens.

Sumário

  1. O problema: o jogo precisa de um mundo
  2. O que é ruído coerente
  3. Primeiro uso: a altura do terreno
  4. Os parâmetros: escala, amplitude, offset e semente
  5. Oitavas: somando camadas de detalhe
  6. Dando forma ao terreno: curva e faixas de altura
  7. Onde nascem as coisas: ruído como densidade
  8. Outros usos no jogo
  9. Em cada motor
  10. Por dentro do Perlin (opcional)
  11. Tarefa até a aula 2
  12. Referências

1. O problema: o jogo precisa de um mundo

Você quer um campo de airsoft grande, com colinas, e não quer modelar cada colina à mão. Minecraft, Valheim, No Man's Sky e Deep Rock Galactic têm mundos que ninguém desenhou: o jogo cria o terreno a partir de regras. Isso é geração procedural.

Paisagem gerada proceduralmente com Terragen

Paisagem gerada com o Terragen. Nenhum artista posicionou essas montanhas.

A primeira ideia de todo mundo: para cada ponto do terreno, sortear uma altura com Random.Range. Fica assim:

À esquerda não tem colina nem vale: tem um espinho por ponto. Não dá para andar, não dá para ver forma. O problema não é que os números sejam aleatórios. O problema é que cada número ignora os vizinhos. À direita, cada ponto é parecido com o vizinho, e aparecem formas: morros, lagos, planícies.

2. O que é ruído coerente

Ruído coerente é uma função de ruído em que o resultado varia suavemente com a entrada. Você dá uma posição, ela devolve um número. Se você pede a posição do lado, o número que volta é parecido com o anterior. Se você pede uma posição bem longe, o número que volta não tem relação nenhuma com o primeiro, parece sorteado. E se você pede a mesma posição duas vezes, recebe o mesmo número duas vezes.

Compare com Random.Range: ele também devolve um número, mas não recebe posição nenhuma, e por isso não tem como "saber" que dois pontos são vizinhos. Cada chamada é independente da anterior. O ruído coerente troca essa independência por continuidade: os valores continuam imprevisíveis, mas vizinhos se parecem.

O libnoise resume isso em três propriedades, e vale decorar as três porque cada uma vai ser usada em algum momento do curso:

  1. Mesma entrada, mesma saída. A função é determinística: não há sorteio na hora da chamada. Toda a aleatoriedade vem da semente, escolhida uma vez. Por isso, com a semente e a coordenada, você reconstrói qualquer ponto do mundo a qualquer momento, sem guardar nada. É o que permite, na aula 2, jogar fora um pedaço do mundo e recriá-lo idêntico.
  2. Pequena mudança na entrada, pequena mudança na saída. É a continuidade. É o que transforma espinhos em colinas e o que faz uma textura parecer nuvem em vez de estática.
  3. Mudança grande na entrada, saída sem relação aparente. É a variedade. Dois lugares distantes do mapa não se parecem, então o mundo não fica repetitivo.
ruído branco 1DNão coerente, em 1D: cada x sorteia sem olhar o vizinho.
ruído coerente 1DCoerente, em 1D: a curva é contínua.

Gráficos do libnoise.

Existem várias funções com essas propriedades. A que usamos nesta aula se chama Perlin noise, e é a que todo motor traz pronta. Até a seção 10 você pode tratá-la como uma caixa preta: perlin(x, y) recebe dois números fracionários e devolve um número entre −1 e 1. O que importa agora não é como ela funciona por dentro, e sim o que dá para fazer com o número que ela devolve. A faixa abaixo mostra quatro usos, todos a partir da mesma função e da mesma semente:

No primeiro mapa o número virou altura. No segundo, virou tipo de terreno, por faixas. No terceiro, decidiu onde nasce uma árvore. No quarto, virou cor. O resto da aula percorre esses usos um por um, começando pelo mais direto.

Demo 1: aleatório vs coerente, em 2D e em 1D →

3. Primeiro uso: a altura do terreno

O uso mais direto: o número vira altura. Imagine o chão como uma grade de pontos, um a cada metro. Para cada ponto (x, z), pergunte ao ruído e levante o chão até a altura que ele devolver. Como pontos vizinhos recebem números parecidos, o chão sobe e desce em colinas, sem degraus.

para z em 0..tamanho-1:
  para x em 0..tamanho-1:
    n = perlin(x * 0.05, z * 0.05)     // x e z são inteiros da grade; × 0.05 vira fracionário
    altura = (n * 0.5 + 0.5) * 20      // leva −1..1 para 0..1 e depois para 0..20 metros
    colocar_vertice(x, altura, z)

Um detalhe do código: a entrada do Perlin precisa ser fracionária. Em coordenada inteira exata a função devolve sempre zero (a seção 10 explica o porquê), então nunca passe o x da grade direto; multiplique por uma escala, como o 0,05 acima.

NomeO que é
x, zposição do ponto no chão, em metros (um vértice por metro)
0.05a escala: multiplica a posição antes de perguntar ao ruído. Além de tornar a entrada fracionária, decide o tamanho das colinas. Vem na seção 4.
no que o ruído devolveu, entre −1 e 1
n * 0.5 + 0.5o mesmo número, agora entre 0 e 1
20altura máxima do terreno, em metros

Pintando n em cinza sai um mapa; usando como altura sai o terreno:

da função ao terreno

A função recebe (x, y) e devolve um número. Claro é alto, escuro é baixo. A malha é o mesmo mapa visto de lado.

Demo 5: o mapa virando terreno 3D, passo a passo →

4. Os parâmetros: escala, amplitude, offset e semente

A função de ruído é sempre a mesma; o que muda o resultado é como você a chama. Há quatro parâmetros. Dois mexem na entrada (escala e offset), um mexe na saída (amplitude) e um mexe na própria função (semente). A linha abaixo é o padrão que aparece em todo código de terreno, em qualquer motor:

n = perlin(x * escala + offsetX,  y * escala + offsetY) * amplitude
BotãoOnde entraNo jogo
escalamultiplica a posiçãotamanho das colinas. Uma colina mede mais ou menos 1 / escala metros: escala 0,02 dá colinas de uns 50 m; 0,2 dá montinhos de 5 m. Alguns motores chamam de frequência.
amplitudemultiplica a saídaaltura máxima. É o "20 metros" da seção 3.
offsetsoma à posiçãodesloca a janela sobre o campo de ruído. Offset aleatório na inicialização = mapa diferente a cada partida. Também é como um chunk vizinho continua o terreno: ele começa onde o anterior parou (aula 2).
sementereembaralha o ruídomundo completamente diferente. É o número que o jogador compartilha com o amigo para jogar o mesmo mapa. O Unity não tem semente no Mathf.PerlinNoise; usa-se offset aleatório. O Godot tem.
Demo 2: os quatro botões, com o código se atualizando →

Faixa de saída

O algoritmo devolve um número em −1..1, mas nem todo motor entrega assim. O Mathf.PerlinNoise do Unity já remapeia para 0..1 (e a documentação avisa que pode passar um pouco dos limites; use Mathf.Clamp01 antes de indexar um array ou uma cor). O FastNoiseLite do Godot e quase todas as bibliotecas entregam −1..1. Antes de usar o número como altura, cor ou probabilidade, confira em que faixa ele está e, se precisar, leve para 0..1 com n * 0.5 + 0.5. Um limiar de bioma em 0,35 não faz sentido se o número vai de −1 a 1.

5. Oitavas: somando camadas de detalhe

O terreno da seção 3 é liso demais: colinas de um tamanho só, todas redondas, porque uma chamada de Perlin com uma escala produz manchas de um tamanho só. Terreno de verdade tem montanha grande com colina média em cima com pedra pequena em cima da colina. A solução é perguntar ao ruído várias vezes, cada vez com colinas menores e mais baixas, e somar as respostas. Cada resposta é uma camada, e o resultado é a soma delas.

É mais fácil ver em 1D, uma curva por vez. Acompanhe de cima para baixo:

  1. Camada 1 é o ruído como na seção 3: ondas largas, amplitude 1. Sozinha, é o terreno liso.
  2. Camada 2 é o mesmo ruído com a entrada multiplicada por 2 (ondas com metade da largura) e a saída multiplicada por 0,5 (metade da altura). Ela não substitui a primeira: é somada a ela.
  3. Soma 1+2: as ondas largas continuam, agora com ondulações menores por cima. Já parece mais terreno.
  4. Camada 3 repete a regra: entrada × 4, saída × 0,25. Camada 4: entrada × 8, saída × 0,125. Cada uma acrescenta detalhe menor e mais fraco que a anterior.
  5. Soma das quatro: a forma grande da camada 1 com o detalhe fino das outras. Repare que a soma passa de 1 em alguns pontos: as amplitudes somam 1 + 0,5 + 0,25 + 0,125 = 1,875. A última linha divide tudo por esse total para voltar a −1..1.

Em código, isso é um laço. Duas variáveis novas controlam a regra: lacunaridade é quanto a entrada multiplica a cada camada (2 no exemplo), e persistência é quanto a saída multiplica (0,5 no exemplo).

amp = 1; freq = 1; soma = 0; maxAmp = 0
repetir N vezes:                         // N oitavas (3 a 6)
  soma   += perlin(x*freq, y*freq) * amp
  maxAmp += amp
  amp    *= persistencia                 // 0,5: cada camada pesa metade
  freq   *= lacunaridade                 // 2: cada camada tem colinas duas vezes menores
resultado = soma / maxAmp                // volta para −1..1
NomeO que éNo jogo
Nquantas camadas somar3 a 6. Acima disso o detalhe fica menor que um vértice.
amppeso da camada atual; começa em 1 e cai
freqtamanho das colinas da camada atual; começa em 1 e sobe
persistenciaquanto amp cai por camadaalta (0,7): terreno áspero, rochoso. Baixa (0,3): liso, suave.
lacunaridadequanto freq sobe por camadaquase sempre 2.
soma, maxAmpo acumulado e o maior valor que ele pode atingirdividir um pelo outro evita que 4 camadas deem um terreno 1,9 vezes mais alto que o esperado.

Cada camada é uma oitava. O nome vem da música: com lacunaridade 2, cada camada tem o dobro da frequência da anterior, como uma nota uma oitava acima. A soma se chama fBm, fractional Brownian motion. No Godot, o FastNoiseLite já soma as oitavas por você (fractal_octaves, fractal_gain para a persistência, fractal_lacunarity); no Unity, você escreve o laço acima. A demo 3 mostra a mesma soma em 2D, com cada camada separada.

Demo 3: cada camada separada, a soma e o perfil →

6. Dando forma ao terreno: curva e faixas de altura

Com oitavas o terreno tem detalhe, mas ainda é um morro contínuo. O motivo é estatístico: somando várias camadas, a maioria dos valores cai perto do meio da faixa, e poucos chegam perto de 0 ou de 1. O resultado é muita meia-altura, quase nenhuma planície e quase nenhum pico. Dois passos dão forma a ele: uma curva que redistribui os valores e uma tabela que decide o tipo de terreno.

6.1 Curva de altura

Passe o número por uma curva antes de usar. O jeito mais simples é uma potência: h = pow(n, 2) com n em 0..1. Isso achata tudo que estava baixo (vira planície) e mantém só o que estava bem alto (vira pico). No Unity um AnimationCurve no Inspector faz isso com controle visual; no Godot, um recurso Curve. Se o mapa é finito, um falloff abaixa as bordas para o mundo terminar em água (ilha).

6.2 Faixas de altura

Agora o número decide o tipo de terreno, com uma tabela:

se h < 0.35: água
senão se h < 0.42: areia
senão se h < 0.60: campo
senão se h < 0.78: rocha
senão: neve

Essa tabela é o bioma mais simples que existe. Ela decide a cor ou a textura de cada ponto, e também o que o jogo permite ali: água não é caminhável, rocha é íngreme, campo é onde nasce árvore. Arraste os limiares na demo e veja a linha da costa e a linha da neve se moverem.

Demo 4: curva, histograma e limiares de bioma →

7. Onde nascem as coisas: ruído como densidade

Segundo uso: o ruído como mapa de densidade. Em vez de sortear a posição de cada árvore, carregador ou inimigo, você percorre o mapa e pergunta ao ruído se aquele lugar é "rico" ou "pobre":

para cada ponto (x, z) de 5 em 5 metros:
  d = perlin(x * 0.03 + 1000, z * 0.03 + 1000)   // outro offset: outro mapa, independente da altura
  se d > 0.3 e terreno[x,z] é campo:
    nascer_item(x, altura[x,z], z)

À esquerda, Random.Range: cada ponto de campo tem 30% de chance de receber um item, então eles ficam espalhados por igual, sem lógica. À direita, ruído: onde o mapa de densidade é alto nascem vários itens juntos, onde é baixo não nasce nenhum. Aparecem bolsões e áreas vazias, que o jogador lê como clareiras, caminhos e "aqui vale a pena explorar", mesmo sem saber por quê. O mesmo esquema serve para árvores, pedras, inimigos e pontos de interesse.

NomeO que é
5 em 5 metroso passo: não precisa testar cada metro
+ 1000offset grande para o mapa de densidade não ser o mesmo mapa da altura
0.3o limiar: quanto maior, menos itens
é campoa tabela de faixas da seção 6 decidindo onde pode nascer

Na aula 2 isso ganha mais uma peça: cada item recebe uma chave feita de (semente, posição) e, coletado, não volta.

8. Outros usos no jogo

O mesmo número, em outros lugares do jogo. Todos podem aparecer no jogo final.

perlin(t)Movimento no tempo. A entrada é o relógio. Balanço de câmera ao andar, tremor da mira ao respirar, vento na folhagem, luz de tocha. Random por frame dá tremedeira; ruído dá respiração. perlin(x, y) → corTextura. Nuvens, mármore, madeira, terra, ferrugem. Sem arquivo de imagem. 1 − |perlin|Cristas. Serras e leitos de rio: o zero do ruído vira uma linha alta e fina. altitude × umidadeBiomas de verdade. Dois ruídos independentes e uma tabela 2D: alto e seco é rocha, baixo e úmido é pântano. perlin(x, y, z) > limiarCavernas e nuvens. Ruído 3D: denso é pedra, ralo é ar. É como o Minecraft faz beirais e cavernas. Volta na aula 2.
Demo 6: uma caixinha interativa para cada uso →

9. Em cada motor

Unity

// Mathf.PerlinNoise devolve ≈ 0..1. Não tem semente: use offset aleatório.
float Altura(int x, int z) {
    float amp = 1, freq = 1, soma = 0, maxAmp = 0;
    for (int i = 0; i < oitavas; i++) {
        float nx = (x + offsetX) * escala * freq;
        float nz = (z + offsetZ) * escala * freq;
        soma   += (Mathf.PerlinNoise(nx, nz) * 2 - 1) * amp;   // leva para −1..1
        maxAmp += amp;
        amp    *= persistencia;
        freq   *= lacunaridade;
    }
    float n = soma / maxAmp * 0.5f + 0.5f;                   // 0..1
    return curvaAltura.Evaluate(n) * alturaMax;              // AnimationCurve
}
NomeO que é
offsetX, offsetZsorteados uma vez no início; fazem as vezes de semente
escala, oitavas, persistencia, lacunaridadeos botões das seções 4 e 5, públicos para aparecer no Inspector
nx, nza posição já com offset, escala e frequência da camada: o que entra no ruído
* 2 - 1leva o 0..1 do Unity para −1..1, para somar camadas sem viés
curvaAlturaum AnimationCurve público: a curva da seção 6
alturaMaxa amplitude, em metros

Godot 4

var noise := FastNoiseLite.new()
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.seed = semente
noise.frequency = escala
noise.fractal_octaves = 4          # oitavas já embutidas
noise.fractal_lacunarity = 2.0
noise.fractal_gain = 0.5           # persistência

func altura(x: float, z: float) -> float:
    var n := noise.get_noise_2d(x, z) * 0.5 + 0.5   # −1..1 → 0..1
    return curva.sample(n) * altura_max              # recurso Curve
NomeO que é
noiseo objeto FastNoiseLite; guarda semente, frequência e camadas
fractal_octaves, fractal_lacunarity, fractal_gainoitavas, lacunaridade e persistência (o Godot chama de gain)
get_noise_2d(x, z)já devolve a soma das camadas, em −1..1
curvaum recurso Curve exportado: a curva da seção 6
ruído no Godot

O FastNoiseLite tem Perlin, Simplex, Value e Cellular no mesmo objeto.

10. Por dentro do Perlin (opcional)

Ken Perlin trabalhou nos efeitos de Tron (1982) e achou o visual "de máquina" demais. Em 1983 criou o algoritmo para dar textura natural a superfícies geradas por computador. Ganhou um Oscar técnico por ele em 1997.

Você não precisa disto para usar o ruído. Precisa só para entender por que o mapa fica cinza nas coordenadas inteiras e por que a grade não aparece. A ideia, sem matemática:

Imagine um campo com estacas fincadas a cada metro, formando uma grade. Ao lado de cada estaca há uma seta pintada no chão dizendo "daqui o terreno sobe para cá". Cada estaca tem a sua seta, sorteada uma vez só (é isso que a semente decide).

Você está em pé entre quatro estacas e pergunta a altura do chão. Cada estaca responde olhando o caminho que você andou saindo dela: se foi na direção da seta, "subiu"; contra, "desceu"; de lado, "ficou no nível". A altura é a média das quatro respostas, com a estaca mais próxima pesando mais. Em cima de uma estaca você não andou nada, então a altura é zero: por isso o mapa cinza nas coordenadas inteiras. E o peso muda de forma suave conforme você anda, por isso não aparece quina nem grade.

No vocabulário do algoritmo: estaca é , seta é gradiente, resposta é produto escalar, média ponderada é interpolação, e a regra do peso suave é a curva S. A demo 7 mostra o cálculo passo a passo, com todos os números, e tem uma tabela com o nome de cada variável.

Demo 7: o cálculo passo a passo, com o mouse sobre a grade →

11. Tarefa até a aula 2

Quad com ruído, no motor que você vai usar nas próximas atividades. Individual, não vale nota, mas quem chegar com isso pronto vai absorver a aula 2 muito melhor.

  1. Uma cena com um plano (quad) exibindo uma textura gerada por código.
  2. Cada pixel da textura é fBm com pelo menos 3 oitavas.
  3. Escala, oitavas, persistência, offset e semente (ou offset aleatório, no Unity) ajustáveis no Inspector, com a textura se regenerando quando mudam.
  4. Prova de determinismo: um botão "regenerar" que produz a mesma imagem quando os parâmetros não mudam.

Referência: o vídeo do Brackeys PERLIN NOISE in Unity faz exatamente isso, sem oitavas. Poste um print no Discord.

12. Referências

Demo 1: aleatório vs coerente →