Buffer de profundidade
Como a GPU esconde o que fica atrás, por que a precisão da profundidade acaba ao longe e como efeitos leem a profundidade de volta.
Como funciona
Os triângulos chegam à GPU na ordem em que as chamadas de desenho os mandam. Se cada um simplesmente pintasse por cima do anterior, uma parede desenhada depois do personagem que está na frente dela cobriria o personagem. Ordenar todos os triângulos pela distância também não funciona: dois triângulos podem se cruzar, cada um em parte na frente do outro. O buffer de profundidade resolve isso um pixel de cada vez.
O teste de profundidade
Ao lado da imagem de cor, o framebuffer guarda uma segunda imagem do mesmo tamanho: um valor de profundidade por
pixel (por amostra, com MSAA), limpo para o valor mais distante no início do quadro. Para cada fragmento, o teste de
profundidade compara a profundidade do fragmento com a guardada. Com o habitual gl.depthFunc(gl.LESS), um fragmento
mais próximo passa: a cor dele é escrita e a profundidade dele substitui a guardada. Um mais distante falha e não muda
nada. Seja qual for a ordem de desenho, cada pixel termina mostrando a superfície mais próxima.
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS); // fica com o fragmento mais próximo
gl.clearDepth(1.0); // 1.0 é o plano distante
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
O teste é uma das operações por amostraAs etapas fixas depois do shader de fragmento que decidem se a cor dele chega ao pixel, e como. Testes de tesoura, stencil e profundidade, depois o blending e as máscaras de escrita, configurados pelo host e não no GLSL. Operações por amostra As etapas fixas depois do shader de fragmento que decidem se a cor dele chega ao pixel, e como. Testes de tesoura, stencil e profundidade, depois o blending e as máscaras de escrita, configurados pelo host e não no GLSL.gl.depthMask(false) mantém o teste mas para as escritas, e é assim que superfícies transparentes são
desenhadas. Num canvas WebGL2 o buffer de profundidade existe, a não ser que o contexto seja criado com
depth: false.
De onde vêm os números
A profundidade guardada é o z do fragmento depois da divisão de perspectiva e da transformação de viewport, de 0 no
plano próximo a 1 no plano distante. Dá para lê-la no shader de fragmento como gl_FragCoord.z.
Esse valor não é proporcional à distância. Uma projeção em perspectiva faz ele seguir 1 / distância, então a maior parte do intervalo é gasta perto da câmera. Com o plano próximo em 0,1 e o distante em 1000, cerca de 90% dos valores descrevem o primeiro metro; tudo além de 200 metros divide os últimos 0,04%. Para transformá-lo de volta numa distância, numa projeção no estilo OpenGL:
// profundidade da janela (0 perto, 1 longe) → distância até o plano da câmera, em unidades da cena
float viewDistance(float depth, float near, float far) {
float zNdc = depth * 2.0 - 1.0;
return 2.0 * near * far / (far + near - zNdc * (far - near));
}
Z-fighting
Quando duas superfícies estão mais próximas entre si do que a precisão da profundidade naquela distância, as profundidades delas arredondam para os mesmos valores e o vencedor muda de pixel para pixel e de quadro para quadro: uma mistura listrada e piscante chamada z-fighting. As correções, da mais para a menos eficaz:
- Afaste o plano próximo. A precisão depende principalmente da razão distante / próximo, e o plano próximo manda nela: ir de 0,1 para 1 multiplica por cerca de dez a precisão ao longe.
- Não construa geometria coplanar. Um decalque ou uma estrada sobre um terreno fica melhor levemente erguido do que colado na superfície.
- Desloque a profundidade.
gl.polygonOffset()comgl.POLYGON_OFFSET_FILLempurra a profundidade de um desenho em direção à câmera para que ele vença os empates. - Use mais bits. Um buffer de profundidade em float de 32 bits (
DEPTH_COMPONENT32F) no lugar do comum de 24 bits ajuda, principalmente combinado com um intervalo invertido (1 no plano próximo), que algumas engines usam.
Lendo a profundidade num efeito
Um shader de fragmento não consegue ler o buffer de profundidade em que está desenhando. Efeitos que precisam da profundidade da cena (neblina, partículas suaves, contornos a partir de bordas de profundidade, profundidade de campo) renderizam a cena primeiro, guardam a profundidade numa textura e leem essa textura numa passada de tela cheia seguinte, convertendo cada amostra em distância como acima.
No Godot, essa textura é um uniform declarado com uma dica: uniform sampler2D depth_texture : hint_depth_texture;. Os
valores do Godot seguem a convenção dele, um intervalo invertido, então o shader os converte com a matriz de projeção
inversa antes de tratá-los como distâncias. As opções de render_mode depth_draw_opaque, depth_draw_always,
depth_draw_never e depth_test_disabled controlam a escrita e o teste.
Neste site, uma passada de pós-processamento que lê "depth" chama sceneDepth(uv), que já devolve a distância até
o plano da câmera em unidades do mundo, tanto na web quanto no Godot.
Leitura complementar
- Depth testing (abre em uma nova aba)Artigo
- glDepthFunc (abre em uma nova aba)Referência
- Spatial shaders (abre em uma nova aba)Documentação