Pular para o conteúdo
Renderização · Cadeia de pós-processamentoIntermediário

Cadeia de pós-processamento

Rode passadas de tela cheia sobre o quadro pronto, em ordem, cada uma lendo a imagem da anterior.

Demo ao vivo em breve

Como funciona

O pós-processamento trabalha sobre a imagem, não sobre a cena. Primeiro a cena é desenhada num render target. Depois um shader de fragmento passa por cada pixel dessa imagem e escreve uma cópia alterada: borrada, mais clara, com outras cores, com linhas nas bordas. O shader já não sabe nada de malhas nem de luzes. Ele vê uma textura e uma coordenada UV, como um filtro de foto.

Uma cadeia de pós-processamento é uma sequência dessas passadas. Cada uma lê a imagem que a anterior escreveu e escreve uma nova. A última escreve na tela.

Uma passada: um desenho de tela cheia

Uma passada de pós precisa de um fragmento para cada pixel da saída, então ela desenha uma forma que cobre o destino inteiro. O truque de costume é um único triângulo maior que a tela, montado no shader de vértice a partir de gl_VertexID, sem nenhum vertex buffer:

#version 300 es
// shader de vértice: desenhe com gl.drawArrays(gl.TRIANGLES, 0, 3)
out vec2 vUv;

void main() {
    // vértices em (0,0), (2,0), (0,2) em UV: o triângulo cobre o quadrado 0..1 e sobra para fora
    vec2 uv = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
    vUv = uv;
    gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
}

O rasterizador recorta o que fica fora da tela. Um triângulo só também evita a costura diagonal de um quad feito de dois triângulos: a GPU sombreia os pixels em blocos de 2×2, e os blocos ao longo dessa costura são sombreados uma vez para cada triângulo.

O shader de fragmento então lê a imagem anterior em vUv e escreve a nova cor:

#version 300 es
precision highp float;

uniform sampler2D uPrevious;
in vec2 vUv;
out vec4 fragColor;

void main() {
    vec3 color = texture(uPrevious, vUv).rgb;
    float gray = dot(color, vec3(0.2126, 0.7152, 0.0722));
    fragColor = vec4(vec3(gray), 1.0); // uma passada em tons de cinza
}

A cadeia: dois destinos se revezando

Uma passada não pode ler o destino em que está escrevendo, então uma cadeia precisa de pelo menos dois. A passada 1 lê A e escreve B, a passada 2 lê B e escreve A, e assim por diante. A última escreve no canvas. Passadas que também precisam de uma imagem mais antiga, como o bloom somando o blur de volta à cena original, mantêm esse destino vivo até terminar de usá-lo.

A ordem importa

Cada passada supõe algo sobre a imagem que recebe, então a ordem não é livre. Um quadro típico de jogo segue assim:

  1. Cena num destino float, em luz linear.
  2. Anti-aliasing temporal (TAA), que mistura cada quadro com os últimos.
  3. Bloom: separa os pixels acima de 1, borra e soma de volta. Ele precisa de valores acima de 1, então roda antes do tone mapping.
  4. Tone mapping: espreme a faixa HDR para 0–1.
  5. Correção de cor: contraste, tons, uma tabela de consulta (LUT). Ela é ajustada sobre a imagem já mapeada.
  6. Granulação e vinheta.
  7. Codificação sRGB.
  8. Dithering. Ele vem depois da codificação, porque a codificação muda o tamanho do degrau. Os dois rodam no mesmo shader final, logo antes de ele escrever no canvas de 8 bits.
  9. Interface por cima, para os menus não borrarem nem brilharem.

Troque tone mapping e bloom de lugar e a passada de brilho não encontra nada acima de 1. Corrija a cor antes do tone mapping e o visual muda junto com a exposição.

Menor sai mais barato

Toda passada toca cada pixel do seu destino. Um blur largo em resolução cheia é caro, então as cadeias costumam encolher primeiro: renderizam uma cópia com metade do tamanho, depois um quarto, depois um oitavo, borrando a cada etapa, e voltam a ampliar somando as camadas. Um blur de poucos pixels num oitavo do tamanho cobre tanto da tela quanto um blur oito vezes mais largo em tamanho cheio, sobre um sexagésimo quarto dos pixels. O bloom costuma ser feito assim.

No Godot

A engine roda a própria cadeia (glow, tone mapping, ajustes) a partir do recurso Environment. Para uma passada sua sobre o quadro inteiro, a montagem documentada é um ColorRect ocupando uma CanvasLayer, com um shader que lê o quadro por um uniform hint_screen_texture em SCREEN_UV. Várias passadas empilham várias camadas, ou encadeiam SubViewports para cada um renderizar a textura do anterior.

Neste site

Os cards de pós desenham primeiro uma pequena cena de teste e depois rodam suas passadas sobre ela. A cor da cena chega ao shader como t_screen, em HDR linear; passadas extras do render.json leem umas às outras como t_<passada>, e effect é a última.

Leitura complementar