Pular para o conteúdo
Renderização · Profundidade e blendingIntermediário

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.

Demo ao vivo em breve

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 amostra, então é configurado pelo host, não no shader. 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() com gl.POLYGON_OFFSET_FILL empurra 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