Ruído coerente em jogos
Aula 1 de geração procedural · TAMJ 2026.2 · 1h30
Sumário
- O problema: o jogo precisa de um mundo
- O que é ruído coerente
- Primeiro uso: a altura do terreno
- Os parâmetros: escala, amplitude, offset e semente
- Oitavas: somando camadas de detalhe
- Dando forma ao terreno: curva e faixas de altura
- Onde nascem as coisas: ruído como densidade
- Outros usos no jogo
- Em cada motor
- Por dentro do Perlin (opcional)
- Tarefa até a aula 2
- 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 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:
- 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.
- 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.
- 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.
Não coerente, em 1D: cada x sorteia sem olhar o vizinho.
Coerente, 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.
| Nome | O que é |
|---|---|
x, z | posição do ponto no chão, em metros (um vértice por metro) |
0.05 | a 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. |
n | o que o ruído devolveu, entre −1 e 1 |
n * 0.5 + 0.5 | o mesmo número, agora entre 0 e 1 |
20 | altura máxima do terreno, em metros |
Pintando n em cinza sai um mapa; usando como altura sai o 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ão | Onde entra | No jogo |
|---|---|---|
| escala | multiplica a posição | tamanho 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. |
| amplitude | multiplica a saída | altura máxima. É o "20 metros" da seção 3. |
| offset | soma à posição | desloca 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). |
| semente | reembaralha o ruído | mundo 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. |
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:
- Camada 1 é o ruído como na seção 3: ondas largas, amplitude 1. Sozinha, é o terreno liso.
- 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.
- Soma 1+2: as ondas largas continuam, agora com ondulações menores por cima. Já parece mais terreno.
- 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.
- 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
| Nome | O que é | No jogo |
|---|---|---|
N | quantas camadas somar | 3 a 6. Acima disso o detalhe fica menor que um vértice. |
amp | peso da camada atual; começa em 1 e cai | |
freq | tamanho das colinas da camada atual; começa em 1 e sobe | |
persistencia | quanto amp cai por camada | alta (0,7): terreno áspero, rochoso. Baixa (0,3): liso, suave. |
lacunaridade | quanto freq sobe por camada | quase sempre 2. |
soma, maxAmp | o acumulado e o maior valor que ele pode atingir | dividir 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.
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.
| Nome | O que é |
|---|---|
5 em 5 metros | o passo: não precisa testar cada metro |
+ 1000 | offset grande para o mapa de densidade não ser o mesmo mapa da altura |
0.3 | o limiar: quanto maior, menos itens |
é campo | a 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.
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
}
| Nome | O que é |
|---|---|
offsetX, offsetZ | sorteados uma vez no início; fazem as vezes de semente |
escala, oitavas, persistencia, lacunaridade | os botões das seções 4 e 5, públicos para aparecer no Inspector |
nx, nz | a posição já com offset, escala e frequência da camada: o que entra no ruído |
* 2 - 1 | leva o 0..1 do Unity para −1..1, para somar camadas sem viés |
curvaAltura | um AnimationCurve público: a curva da seção 6 |
alturaMax | a 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
| Nome | O que é |
|---|---|
noise | o objeto FastNoiseLite; guarda semente, frequência e camadas |
fractal_octaves, fractal_lacunarity, fractal_gain | oitavas, lacunaridade e persistência (o Godot chama de gain) |
get_noise_2d(x, z) | já devolve a soma das camadas, em −1..1 |
curva | um recurso Curve exportado: a curva da seção 6 |
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 é nó, 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.
- Uma cena com um plano (quad) exibindo uma textura gerada por código.
- Cada pixel da textura é fBm com pelo menos 3 oitavas.
- Escala, oitavas, persistência, offset e semente (ou offset aleatório, no Unity) ajustáveis no Inspector, com a textura se regenerando quando mudam.
- 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
- libnoise, What is coherent noise? e os tutoriais, que seguem a mesma ordem desta aula: gerar, ver, mexer nos parâmetros, complicar.
- Red Blob Games, Making maps with noise functions. Interativo; cobre oitavas, curva e biomas por dois ruídos.
- Brackeys, PERLIN NOISE in Unity e GENERATING TERRAIN in Unity.
- Sebastian Lague, Procedural Landmass Generation.
- Unity, Mathf.PerlinNoise. Godot, FastNoiseLite.
- Para o "por dentro": Adrian Biagioli, Understanding Perlin Noise, e Ken Perlin, Improving Noise (2002).