Skip to content

Como engines modernas vencem o overdraw da vegetação

Por Oleg Sidorkin, CTO e cofundador da Cinevva

Dossel de floresta densa com cartões de folhas sobrepostos e empilhados diante de um sol poente, sugerindo um alto nível de overdraw

Uma floresta é uma das piores coisas que você pode pedir para uma GPU renderizar. Cada folha é um quad texturizado recortado por uma máscara alfa. Dezenas desses quads se empilham uns sobre os outros ao longo de cada raio de visão. O rasterizador não tem como saber antecipadamente quais fragmentos passarão no teste alfa, então a otimização padrão de early-Z que beneficia cenas opacas fica praticamente desativada. Como resultado, um único pixel da tela pode executar o shader completo das folhas 10 ou 15 vezes antes de o quadro ficar pronto. Isso é o overdraw da vegetação, e ele é a parte mais cara de um quadro de mundo aberto em qualquer jogo lançado na última década.

A boa notícia é que esse é um problema resolvido. Não no sentido de que alguém o corrigiu com um único truque, mas no sentido de que existe um conjunto de sete ou oito técnicas que, combinadas, reduzem o overdraw efetivo de 8-15x em uma floresta densa para 1-2x. Toda engine moderna inclui alguma versão desse conjunto. Veja quais técnicas fazem parte dele, por que cada uma existe e quais são as referências canônicas de cada uma.

1. Por que o overdraw da vegetação é tão problemático

Em uma cena opaca típica, a GPU faz a rejeição antecipada por profundidade: antes mesmo de executar o pixel shader, o hardware verifica o buffer de profundidade existente e ignora fragmentos que já estejam atrás de alguma coisa. Isso é praticamente gratuito e é o que mantém razoável o custo de geometrias densas.

A vegetação com teste alfa quebra esse mecanismo. O fragment shader precisa realmente ser executado para avaliar a textura alfa e chamar discard (ou clip) nos pixels removidos pela máscara. O hardware não consegue saber se um fragmento será descartado até que o shader seja executado, portanto, na maioria das GPUs, usar discard em qualquer ponto de um shader desativa totalmente o early-Z para aquela draw call. Em GPUs baseadas em tiles (dispositivos móveis, chips da série M e alguns consoles), isso pode desativar o Hi-Z e a compactação de profundidade do quadro inteiro. O pixel shader é executado uma vez para cada triângulo que cobre um pixel, e a maioria dessas avaliações termina em descarte.

Empilhe 10 quads de folhas diante de um único pixel da tela e o shader das folhas será executado 10 vezes. Multiplique isso por 4 milhões de pixels e o custo será brutal. O artigo de Marco Salvi, Usar early-Z ou não usar early-Z, é a introdução mais acessível aos motivos pelos quais isso acontece no nível do hardware e é o lugar certo para começar caso você nunca tenha pensado no assunto.

Vista lateral do raio de um único pixel da tela atravessando doze cartões de folhas sobrepostos, com cada cartão destacado para mostrar os fragmentos empilhados que passam por teste alfa

2. O pré-passe de profundidade para geometria com máscara

O maior ganho isolado — e a técnica incluída em toda engine moderna — é dividir a renderização da vegetação em dois passes. O primeiro passe grava apenas a profundidade, usando um shader mínimo que faz o teste alfa, descarta os pixels removidos pela máscara e não grava mais nada. O segundo passe renderiza o material completo com o teste de profundidade definido como "igual" e a gravação de profundidade desativada. Agora, cada pixel visível calcula a BRDF completa exatamente uma vez, independentemente de quantos quads de folhas estejam empilhados atrás dele.

Isso parece o dobro do trabalho porque os mesmos triângulos são processados duas vezes, mas o shader do pré-passe é tão barato (uma amostra de textura, um descarte e uma gravação de profundidade) que a economia no passe principal supera em muito o custo adicional. Em uma floresta densa, o passe principal deixa de executar o shader completo das folhas 10-15 vezes por pixel e passa a executá-lo exatamente uma vez.

Unreal, Frostbite, Decima e as ramificações modernas de Rage e Dunia fazem isso. Esse também é o principal motivo pelo qual materiais com máscara continuam sendo mais baratos do que materiais translúcidos em qualquer uma dessas engines.

Análises detalhadas:

3. LODs agressivos e impostores octaédricos

O segundo maior ganho é não renderizar as folhas quando isso não for necessário. Os assets de vegetação incluem vários níveis de LOD. O mais próximo usa a malha completa, com cartões individuais para as folhas. A uma distância média, as folhas são combinadas em cartões compostos mais densos (um grupo de 30 folhas se transforma em 1 cartão texturizado com a mesma silhueta). Além de um limite de distância, a árvore inteira se torna um impostor: uma pequena geometria texturizada com vistas pré-renderizadas da árvore a partir de vários ângulos.

O formato moderno é o impostor octaédrico: uma geometria de 8 faces texturizada com um atlas de vistas capturadas a partir de pontos em uma esfera por meio de mapeamento octaédrico. Em tempo de execução, o shader escolhe as duas ou três vistas pré-renderizadas mais próximas com base na direção da câmera e faz a transição entre elas. O resultado é um substituto composto por poucos triângulos, que parece tridimensional de qualquer ângulo e pode ter iluminação adequada, mapas de normais e até animação pelo vento. A implementação de Ryan Brucks, originalmente um plugin da comunidade e agora parte da Unreal, é a referência. As florestas com bilhões de árvores do Microsoft Flight Simulator são compostas essencialmente por impostores octaédricos em todos os lugares, exceto nas proximidades da câmera.

O maior ganho estrutural é que os impostores são opacos ou quase opacos à distância. O cartão composto de 30 folhas é um único quad com máscara em vez de 30 quads. O impostor da árvore inteira tem poucas faces, não milhares. O overdraw ao longe cai para quase zero.

Análises detalhadas:

4. Descarte de clusters controlado pela GPU

Mesmo com um pré-passe de profundidade, o próprio pré-passe tem um custo: ele ainda precisa processar cada triângulo de cada árvore visível (ou potencialmente visível). Engines modernas reduzem esse custo com o descarte de clusters controlado pela GPU, que elimina grupos inteiros de triângulos antes mesmo que o rasterizador os veja.

O pipeline funciona assim: cada malha é previamente dividida em clusters de 64 ou 128 triângulos, com uma caixa delimitadora justa e um cone de normais. Durante a renderização, um compute shader percorre a lista de instâncias, testa cada instância contra o frustum, depois testa cada cluster de cada instância visível contra o frustum e então faz um teste de oclusão Hi-Z de cada cluster restante contra a pirâmide de profundidade do quadro anterior. Galhos inteiros ocultos atrás de uma colina ou de outra árvore são descartados antes da execução de qualquer vertex shader. A saída é uma lista compacta de argumentos indicando "renderize estes clusters", enviada diretamente para uma única chamada DrawIndirect.

É isso que permite renderizar uma floresta de 10.000 árvores em milissegundos, em vez de segundos. A palestra da Ubisoft sobre Assassin's Creed Unity apresentou esse pipeline em uma implementação de produção (20-40% dos triângulos descartados, 30-80% dos triângulos de sombras descartados e 10x mais instâncias na tela do que na geração anterior), e a palestra de Wihlidal sobre a Frostbite levou a técnica ainda mais longe. O Nanite da UE5 é o estágio final visível dessa trajetória: descarte de clusters até o nível dos pixels.

Análises detalhadas:

5. Ordenação de instâncias e clusters da frente para trás

Quando o pré-passe começa a cumprir seu papel, a ordem passa a importar. O pré-passe grava a profundidade, mas apenas para os fragmentos que passam no teste alfa. Se você renderizar primeiro o fundo da floresta e deixar a frente por último, cada fragmento da frente sobrescreverá um fragmento de trás, e o shader do pré-passe ainda será executado para o fragmento de trás. Se você renderizar da frente para trás, cada draw call sucessiva preencherá uma parte maior do buffer de profundidade com valores menores, e o Hi-Z rejeitará cada vez mais fragmentos de trás antes da execução do shader.

É por isso que quase toda engine moderna ordena as instâncias de vegetação pela distância até a câmera antes de executar o pré-passe. A ordenação é barata (algumas centenas de milhares de instâncias na GPU usando radix sort) e transforma o próprio pré-passe em uma operação que reduz progressivamente seu trabalho. O descarte no nível dos clusters faz a ordenação com granularidade de meshlets pelo mesmo motivo. O pré-passe de profundidade e a ordenação da frente para trás formam o tipo de dupla em que cada técnica é boa sozinha, mas a combinação é excelente.

Análises detalhadas:

6. Transições de LOD com dithering e alfa com hash

A outra grande armadilha são as transições graduais. A maneira ingênua de fazer a transição entre dois LODs (ou de fazer uma instância aparecer ou desaparecer gradualmente conforme a câmera se aproxima) é usar mesclagem alfa. Mas geometrias com mesclagem não podem gravar no buffer de profundidade, o que joga toda árvore em transição para o lento caminho translúcido e quebra o pré-passe. A solução é manter a geometria no caminho com máscara e fazer a transição dentro do teste alfa.

Duas técnicas principais:

  • Transições de LOD com dithering amostram um padrão Bayer 4x4 ou 8x8 (ou uma textura de ruído azul no espaço da tela) e o usam como modificador do limiar por pixel. Uma árvore com 50% de transição mantém um padrão quadriculado de pixels; os pixels ausentes são preenchidos pelo padrão complementar do LOD seguinte. O TAA resolve o padrão quadriculado e produz uma transição suave ao longo de dois ou três quadros. É barato, estável e combina com todo o restante da engine.
  • Teste alfa com hash (Wyman e McGuire, I3D 2017) substitui o limiar alfa fixo de 0,5 por um limiar com hash por pixel no intervalo [0,1). Geometrias alfa distantes que normalmente desapareceriam por completo (porque o alfa com mipmap cai abaixo de 0,5) mantêm uma dispersão estável de pixels sobreviventes. Mais uma vez, o TAA faz o acabamento.

As duas técnicas mantêm a vegetação no caminho opaco/com máscara em que o pré-passe de profundidade funciona, portanto você não paga o custo total da renderização translúcida apenas para fazer algo aparecer ou desaparecer gradualmente. Alfa para cobertura é a técnica equivalente da era do MSAA: ela converte o alfa em uma máscara de cobertura de subpixels, oferecendo transparência parcial sem sair do caminho com máscara. O problema é que o A2C só funciona realmente bem com MSAA, que a maioria dos renderizadores diferidos modernos já não usa.

Análises detalhadas:

7. Redução do custo de sombreamento em pixels com máscara

Mesmo com um pré-passe perfeito e um descarte perfeito, você ainda precisa sombrear uma vez cada pixel visível da vegetação. As engines também reduzem esse custo:

  • BRDF mais barata. A folhagem é fosca e não precisa realmente de um caminho especular Cook-Torrance completo. Uma difusa Lambertiana envolvente mais uma aproximação especular de uma linha é mais do que suficiente.
  • Mapas de normais com frequência mais baixa. As folhas já têm bastante ruído visual. Um mapa de normais de 256x256 parece igual a um de 1024x1024 nas distâncias de visualização típicas e economiza largura de banda.
  • Sem paralaxe, anisotropia ou clearcoat. O conjunto de recursos PBR é desativado para as folhas.
  • Transmissão fina de dois lados em vez de espalhamento subsuperficial completo. As folhas transmitem luz por trás, mas é possível simular isso com um único produto escalar com a luz traseira.
  • Sombreamento em meia resolução em alguns motores. A folhagem é sombreada a 1/4 ou 1/2 da taxa de pixels e ampliada. O ruído estocástico do TAA oculta a reamostragem.
  • Omitir texturas de detalhes e decalques em materiais mascarados por padrão.

Cada uma dessas técnicas gera um pequeno ganho isoladamente. Combinadas, elas permitem que shaders de folhagem mascarada sejam executados de 2 a 3 vezes mais rápido que o material opaco equivalente.

Análises aprofundadas:

8. Buffers de visibilidade e Nanite para materiais mascarados

A solução mais limpa para o overdraw é desacoplar completamente o sombreamento da rasterização. Um buffer de visibilidade rasteriza a geometria em um buffer compacto (apenas o ID do triângulo e o ID da instância por pixel) e depois executa o material completo como um passe diferido que lê o buffer de visibilidade e sombreia cada pixel exatamente uma vez. Por definição, não há overdraw na etapa de sombreamento. O artigo de Burns e Hunt de 2013 apresentou essa técnica; o Nanite da UE5 é sua implementação com qualidade de produção, inclusive para folhagem mascarada desde a UE 5.5.

A diferença do Nanite é fazer isso com geometria virtualizada no nível de clusters, de modo que o próprio rasterizador use um caminho por software para triângulos menores que um pixel e mantenha o overdraw limitado. Materiais mascarados no Nanite precisam do recurso de "rasterização programável": o teste alfa é executado durante o passe do buffer de visibilidade, mas o sombreamento do material ainda acontece uma vez por pixel visível na resolução diferida. O resultado é que uma folhagem Nanite muito densa, antes notoriamente lenta com materiais mascarados, agora é competitiva com a geometria opaca ou até mais barata, pois o overdraw durante o sombreamento é zero. Há uma contrapartida: muitas vezes é melhor processar geometria de árvores opaca e com muitos polígonos pelo Nanite do que manter árvores de poucos polígonos feitas com cartões mascarados, pois o caminho mascarado adiciona o custo da rasterização programável.

Análises aprofundadas:

9. Representações de sombras separadas para a folhagem

Mapas de sombras para folhagem com teste alfa são o segundo problema de sombras mais caro em qualquer frame de mundo aberto, atrás apenas das cascatas grandes. Cada cascata precisa do próprio pré-passe de profundidade, cada pré-passe executa o teste alfa e o custo se acumula rapidamente em 4 cascatas e dezenas de frustos de luz. Por isso, a maioria dos motores não renderiza as sombras da folhagem da mesma forma que renderiza sua cor.

Substituições comuns:

  • Sombras com campos de distância da malha (UE Lumen, motores personalizados). Cada malha tem um campo de distância com sinal pré-calculado. Um curto traçado de cone pelo SDF produz uma sombra suave sem jamais tocar na malha com teste alfa. Isso funciona particularmente bem para árvores porque o SDF captura a silhueta da copa como um único volume sólido e ignora os detalhes de cada folha.
  • Cascatas de resolução mais baixa para a folhagem. As sombras da folhagem são gravadas em uma camada com metade da resolução e ampliadas com filtragem sensível às bordas. O olho não percebe a redução da resolução porque as sombras já são suaves.
  • Mapas de sombras com alpha-to-coverage e MSAA. Em motores que ainda usam MSAA no caminho de sombras, o A2C produz sombras de folhagem com bordas suaves sem o custo completo do teste alfa.
  • Sombras de cápsula para o tronco e os galhos grandes, campo de distância para a copa e teste alfa completo apenas na cascata mais próxima. Representações diferentes para distâncias diferentes, combinadas no passe de iluminação.
  • Distância de desativação do WPO. O World Position Offset controlado pelo vento é desativado além de um limite para que os dados de sombra armazenados em cache continuem válidos entre frames. Os Virtual Shadow Maps da UE dependem muito disso.

Análises aprofundadas:

10. Vento, animação e cache de sombras

Um problema sutil relacionado: a maior parte da folhagem se move. A animação de vértices controlada pelo vento (World Position Offset na UE e equivalentes na Frostbite e na Decima) faz com que a geometria da folhagem não permaneça estável entre frames, o que invalida o cache de sombras e a reprojeção. Os motores modernos combatem isso de duas maneiras:

  • Limitar o WPO à distância. Além de um limite, a amplitude da animação do vento é reduzida suavemente até zero. De qualquer forma, o olho não consegue perceber o balanço a uma distância tão grande, e os caches de sombras continuam válidos.
  • Incorporar o vento aos limites dos clusters. As caixas delimitadoras dos clusters são expandidas pelo deslocamento máximo do WPO, de modo que o culling permaneça conservador sem precisar reenviar dados a cada frame.
  • Deslocamentos de fase por instância. Árvores idênticas usam uma semente aleatória por instância para deslocar a fase do vento, evitando que uma floresta inteira balance em sincronia sem pagar por uma animação exclusiva para cada árvore.

Esse é o tipo de detalhe que não aparece em listas de técnicas, mas que, na prática, representa a diferença entre uma floresta de 3 ms e outra de 9 ms.

Análises aprofundadas:

11. A matemática combinada

Nenhuma dessas técnicas é uma solução milagrosa. O interessante é o que acontece quando elas são combinadas:

  • Um passe ingênuo de folhagem mascarada em uma cena de floresta densa apresenta de 8 a 15 vezes de overdraw efetivo. Cada pixel visível executa o shader de folha de 8 a 15 vezes.
  • Adicione um pré-passe de profundidade e o passe principal cai para cerca de 1x de overdraw, mas o próprio pré-passe ainda toca em tudo.
  • Adicione ordenação da frente para trás e o pré-passe começa a eliminar trabalho por conta própria.
  • Adicione culling na GPU no nível de clusters e o pré-passe toca apenas no que poderia estar visível.
  • Adicione cadeias de LODs e impostores, e a quantidade de quads visíveis cai em uma ordem de grandeza além dos 30 m.
  • Adicione o caminho de buffer de visibilidade/Nanite e a etapa de sombreamento realmente passa a ser executada uma vez por pixel, mesmo com sobreposição densa.
  • Adicione sombras com campos de distância e o custo das sombras deixa de crescer com o teste alfa.

O principal resultado, repetido nas palestras indicadas acima: o overdraw efetivo cai de 8-15x para 1-2x, e o custo total da folhagem por frame diminui de 4 a 6 vezes em uma cena de floresta densa. Esse é o motivo pelo qual jogos modernos de mundo aberto conseguem renderizar florestas a mais de 60 fps em hardware de consumo.

Diagrama mostrando o pipeline de renderização da folhagem como cinco etapas empilhadas: culling na GPU, pré-passe ordenado, passe de cor mascarado com depth-equal, sombreamento por buffer de visibilidade e caminho separado de sombras com campos de distância

12. O que isso significa para o navegador

A maior parte desse conjunto de técnicas pode ser adaptada diretamente para WebGPU. Já disponibilizamos culling controlado pela GPU, dispatch indireto, oclusão Hi-Z e pré-passes ordenados da frente para trás no motor de mundo aberto para navegadores. O pré-passe de profundidade para geometria mascarada é simples: a WebGPU aceita testes depth-equal e discard em shaders de fragmento, com as mesmas ressalvas sobre early-Z. Impostores octaédricos podem ser portados mecanicamente; a matemática consiste apenas no desdobramento de esfera para octaedro e na indexação do atlas.

As partes mais difíceis são as modernas. Um buffer de visibilidade em WebGPU exige gravar um ID de triângulo de 32 bits em um destino de renderização e resolver os materiais em um passe de computação em tela cheia; os componentes necessários existem, mas sua orquestração é complexa. Sombras com campos de distância da malha exigem uma textura 3D por asset e um curto traçado de cone, ambos ao alcance da WebGPU. Alfa com hash e LODs com dithering exigem apenas uma função de shader cada.

O caminho para a folhagem no navegador é o mesmo de todo o restante desse conjunto: disponibilizar primeiro as partes baratas e robustas (pré-passe, instâncias ordenadas, LODs, impostores, alfa com hash) e adicionar os mecanismos mais pesados (buffer de visibilidade, sombras com SDF de malha) por cima. O patamar mínimo do hardware dos navegadores finalmente está alto o bastante para que não exista nenhum motivo arquitetural pelo qual uma floresta em WebGPU não possa ter a mesma aparência e o mesmo desempenho que uma de console. Restam apenas motivos de engenharia, e motivos de engenharia são do tipo que apreciamos.

Leituras adicionais sobre todo o conjunto

Se você quiser uma única fonte que reúna tudo isso, o arquivo "Advances in Real-Time Rendering in Games" da SIGGRAPH (advances.realtimerendering.com) contém as palestras canônicas sobre folhagem e renderização controlada pela GPU desde 2014. Os artigos sobre análise de desempenho da GPU de Adrian Courrèges incluem decomposições frame a frame de GTA V e Horizon Zero Dawn que mostram, em ordem de produção, cada passe discutido aqui. Especificamente para a matemática do teste alfa, a página de pesquisas de Chris Wyman apresenta os artigos sobre alfa com hash e transparência estocástica, acompanhados de shaders de referência. E os capítulos de Real-Time Rendering, 4ª edição sobre transparência, amostragem e gerenciamento de profundidade continuam sendo o ponto de partida acadêmico.