Render targets
Desenhe numa textura em vez da tela e leia essa textura numa passada seguinte.
Como funciona
Um shader de fragmentoO programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele. Shader de fragmento O programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele.
Um render target muda o destino da saída. Ele é uma textura em que você desenha no lugar da tela. Terminado o desenho, a textura guarda uma imagem como qualquer outra, e um desenho seguinte pode amostrá-la. Essa única mudança, desenhar agora e ler depois, é a base de blur, bloom, contornos, espelhos e de todo efeito com várias passadas.
Framebuffers
O OpenGL chama de framebuffer o lugar onde um desenho escreve. O canvas já vem com um, o framebuffer padrão. Você pode criar os seus e anexar texturas a eles: uma textura de cor para a saída do shader de fragmento e, se quiser, um buffer de profundidade para a geometria 3D continuar se ordenando direito. Enquanto o seu framebuffer está ativo, todo desenho vai parar nas texturas dele.
No WebGL2, fica assim:
// uma textura do tamanho do canvas, ainda sem dados
const target = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, target);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// um framebuffer que escreve nela
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target, 0);
// passada 1: desenha a cena na textura
drawScene();
// passada 2: volta para o canvas e amostra a textura
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, target);
drawFullScreen();
Dois detalhes importam. O viewport (gl.viewport) é definido por destino, então uma textura com metade do tamanho
precisa de um viewport com metade do tamanho. E gl.checkFramebufferStatus diz se os anexos formam um framebuffer em
que a GPU consegue desenhar; um formato em que ela não renderiza falha ali, não mais tarde.
Um destino não pode ser lido e escrito ao mesmo tempo
Uma passada não pode amostrar a textura em que está desenhando. A GPU roda muitos fragmentos em paralelo, então um fragmento que lesse o vizinho não teria como saber se esse vizinho já foi sobrescrito. O OpenGL chama isso de feedback loop e deixa o resultado indefinido; o WebGL2 transforma isso num erro. Efeitos que precisam do resultado anterior leem um destino, escrevem em outro e depois trocam os dois.
O que um destino consegue guardar
O formato da textura decide o que sobrevive à passada:
RGBA8: 8 bits por canal, valores limitados a 0–1. Sempre renderizável. Serve para a imagem final, mas é grosseiro demais para luz linear nos tons escuros (veja bandingBanding 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çãoBanding 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).- Colour banding (abre em uma nova aba)Referência
- Ordered dithering (abre em uma nova aba)Referência
RGBA16F: meio float, até uns 65 000, com valores negativos. É disso que o HDRA 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çãoHDR 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)Fontesprecisa: um brilho forte continua acima de 1 até o fim do quadro.RGBA32F: float completo, para dados como posições. Ocupa quatro vezes a memória doRGBA8.
O WebGL2 amostra meio float com filtro LINEAR em qualquer lugar. Float completo (RGBA32F) pede NEAREST, ou a
extensão OES_texture_float_linear para usar LINEAR; sem ela, o trecho acima amostra preto. Renderizar em qualquer
um dos dois exige a extensão EXT_color_buffer_float.
Peça com gl.getExtension("EXT_color_buffer_float") e tenha um plano B para os aparelhos que disserem não.
Várias saídas numa passada
Um framebuffer pode ter vários anexos de cor, e um único shader de fragmento pode escrever em todos. Isso se chama multiple render targets (MRT). Cada saída ganha uma location:
#version 300 es
precision highp float;
in vec3 vNormal;
in vec4 vColor;
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec4 outNormal;
void main() {
outColor = vColor;
outNormal = vec4(normalize(vNormal) * 0.5 + 0.5, 1.0);
}
No lado do JavaScript, anexe também uma textura em COLOR_ATTACHMENT1 e chame
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]). Renderizadores deferred usam isso para escrever
albedo, normais e rugosidade numa só passada de geometria.
No Godot
O Godot embrulha a mesma ideia num nó. Um SubViewport renderiza os filhos dele numa textura própria, e uma
ViewportTexture deixa qualquer material amostrar essa imagem. O próprio renderizador 3D da engine usa render targets
por dentro (a cena, a profundidade e as normais antes do pós-processamento), e um shader chega ao quadro atual pelos
uniforms hint_screen_texture e hint_depth_texture, sem um framebuffer que você precise gerenciar.
Neste site
Um card com mais de uma passada lista todas no render.json dele. Cada passada renderiza no seu próprio destino de
meio float, e uma passada seguinte o lê pelo nome, como t_<passada>. A passada effect é sempre a última e a única
que chega à tela.