Cadeia de pós-processamento
Rode passadas de tela cheia sobre o quadro pronto, em ordem, cada uma lendo a imagem da anterior.
Como funciona
O pós-processamento trabalha sobre a imagem, não sobre a cena. Primeiro a cena é desenhada num
render targetUma textura em que um desenho escreve no lugar da tela, para uma passada seguinte ler o resultado. Blur, bloom e todo efeito com várias passadas usam isso. Render target Uma textura em que um desenho escreve no lugar da tela, para uma passada seguinte ler o resultado. Blur, bloom e todo efeito com várias passadas usam isso. Coordenadas UV Uma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.uv = gl_FragCoord.xy / resolution
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:
- Cena num destino float, em luz linear.
- Anti-aliasing temporal (TAA), que mistura cada quadro com os últimos.
- 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.
- Tone mappingA renderização mantém valores de luz acima de 1 em buffers float (HDR), e depois uma curva de tone mapping os comprime nos 0–1 que uma tela consegue mostrar.Renderização
HDR e tone mapping
A renderização mantém valores de luz acima de 1 em buffers float (HDR), e depois uma curva de tone mapping os comprime nos 0–1 que uma tela consegue mostrar.
Reinhard = c / (1 + c)Fontes: espreme a faixa HDR para 0–1. - Correção de cor: contraste, tons, uma tabela de consulta (LUT). Ela é ajustada sobre a imagem já mapeada.
- Granulação e vinheta.
- Codificação sRGBA curva que telas e arquivos de imagem usam para guardar cor, com mais degraus nos tons escuros. Shaders fazem as contas em luz linear e codificam para sRGB uma vez, no fim.Renderização
Codificação sRGB
A curva que telas e arquivos de imagem usam para guardar cor, com mais degraus nos tons escuros. Shaders fazem as contas em luz linear e codificam para sRGB uma vez, no fim.
c ≤ 0.0031308 ? 12.92c : 1.055c^(1/2.4) − 0.055Fontes. - DitheringBanding são as faixas em que um degradê suave se parte quando guardado com poucos níveis. O dithering soma um ruído minúsculo por pixel antes do arredondamento, e as faixas viram uma granulação fina.Renderização
Banding e dithering
Banding são as faixas em que um degradê suave se parte quando guardado com poucos níveis. O dithering soma um ruído minúsculo por pixel antes do arredondamento, e as faixas viram uma granulação fina.
Fontes. 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.- Colour banding (abre em uma nova aba)Referência
- Ordered dithering (abre em uma nova aba)Referência
- 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.