Skip to content

Construindo um mundo aberto no navegador, parte 21: um renderizador mais rápido que não era mais rápido

Por Oleg Sidorkin, CTO e cofundador da Cinevva

Chegou agora? Consulte o guia da série. Ele explica o que é um experimento e traz links para todas as partes.

A Parte 20 simulou profundidade de superfície em um quad plano. Esta parte trata de uma decisão de arquitetura de renderização e é o experimento em que a resposta dos livros acabou se mostrando errada para o nosso hardware. A pergunta: para grama com teste de alfa em densidade cinematográfica, vista por uma câmera em terceira pessoa, precisamos de um buffer de visibilidade antes de avançar para a densidade de 200 jogadores? A recomendação predominante é um enfático sim. Nós o implementamos, medimos e a resposta foi não.

A técnica que todos recomendam

Abrir o experimento 40 em uma nova aba ↗ · Ver código-fonte

Um buffer de visibilidade divide a renderização em duas passagens. A passagem 1 rasteriza a geometria e grava apenas os IDs dos triângulos e das instâncias em um alvo inteiro compacto, junto com a profundidade, sem fazer nenhum sombreamento. A passagem 2 é uma etapa em tela cheia que lê os IDs em cada pixel coberto, busca novamente os vértices daquele triângulo, reconstrói os atributos interpolados e sombreia cada pixel visível exatamente uma vez. A proposta é uma rejeição perfeita de overdraw: o teste de profundidade é executado em fragmentos nos quais nenhum trabalho de sombreamento foi realizado, portanto o material custoso só é processado no que realmente aparece na tela.

O experimento executa dois caminhos no mesmo canvas e no mesmo dispositivo, para que a única variável seja onde ocorre o sombreamento. O caminho forward usa um MeshStandardNodeMaterial normal por meio do three.js. O caminho com vis-buffer é um pipeline WebGPU bruto de duas passagens, executado fora do three.js, que lê a textura de grama do three.js diretamente do backend, grava (instanceId, triId) em um alvo RG32Uint na passagem 1 e resolve a iluminação na passagem 2. Ambos compartilham uma única configuração de iluminação de referência.

Vale a pena preservar duas observações de implementação. O WebGPU ainda não tem um builtin primitive_index portátil em shaders de fragmento, então o truque é incorporar um ID de triângulo por vértice à geometria não indexada e lê-lo com interpolação flat. Isso triplica a contagem de vértices, mas é insignificante em um cartão de grama com 12 vértices. Além disso, compartilhar o canvas com o renderizador do three.js praticamente não causa problemas, desde que você nunca reconfigure o contexto nem altere as dimensões do canvas, pois ambos pertencem ao three.js. Medir o tempo do caminho forward foi a parte trabalhosa, porque o three.js não oferece nenhum hook para inserir consultas de timestamp da GPU dentro da sua passagem de renderização. A solução alternativa delimita o trabalho dele com duas passagens de timestamp sem operação, enviadas antes e depois, que a GPU executa na ordem de envio.

Os números seguem na direção errada

Em um Mac da série M, a aproximadamente 1080p, com lâminas de grama em cartões cruzados sobre um campo de 80 m:

Com 50.000 instâncias, o caminho com vis-buffer venceu por 25%: 4,13 ms contra 5,51 ms do forward. Com 100.000, houve empate. Com 200.000 instâncias, o forward venceu por 44%: 5,44 ms contra 7,80 ms do vis-buffer. O caminho com vis-buffer fica relativamente pior conforme a densidade aumenta, exatamente o oposto da crença popular de que ele vence justamente quando o overdraw é intenso.

Por que o forward se mantém eficiente

As GPUs Apple Silicon são renderizadores diferidos baseados em tiles, e isso muda todo o cálculo. O sombreamento forward em um TBDR tem uma etapa de remoção de superfícies ocultas executada antes do shader de fragmento: o rasterizador coleta todos os fragmentos mapeados para um tile, ordena-os por profundidade e apenas os sobreviventes, após o teste de alfa, chegam ao shader de fragmento. Portanto, o caminho forward já cumpre gratuitamente, dentro do hardware, a maior parte da promessa do buffer de visibilidade de “sombrear uma vez por pixel”. Conforme as lâminas ocupam mais espaço na tela, mais fragmentos são rejeitados pelo HSR antes de qualquer sombreamento ser disparado, e o custo efetivo por pixel do forward permanece praticamente constante em vez de crescer com o overdraw.

A passagem 1 do caminho com vis-buffer obtém o mesmo benefício do TBDR. O problema está todo na passagem 2. A passagem 2 lê a matriz da instância de cada pixel a partir de um buffer que, com 200.000 instâncias, ocupa 12,8 MB, muito mais do que qualquer cache da GPU. Pixels vizinhos na tela geralmente pertencem a instâncias de grama diferentes — a distribuição usa uma grade com jitter, então lâminas vizinhas têm IDs de instância arbitrários —, de modo que cada wave que acessa esse buffer sofre misses de cache divergentes. Sozinho, esse acesso aleatório e incoerente consome cerca de 4 ms por quadro. O forward evita isso por completo porque a matriz da instância chega junto com o vértice pelo caminho de atributos por instância. Assim, quando o shader de fragmento é executado, os dados transformados dos vértices já estão em registradores locais do tile, sem exigir leituras aleatórias na escala de megabytes.

Esse é exatamente o custo que a passagem de classificação de materiais do Nanite existe para amortizar: agrupar pixels por instância e despachar waves de computação ordenadas, para que as leituras de cada wave sejam coerentes. Nós não temos isso. Uma estimativa rápida indica que ordenar os pixels por instância reduziria esses 4 ms para talvez 1,5 a 2 ms e deslocaria o ponto de equilíbrio para 400.000 a 500.000 instâncias. Mas isso seria empilhar otimizações sobre uma arquitetura que, para começo de conversa, não está vencendo neste caso.

A conclusão honesta e a auditoria que a sustentou

Para folhagem em cartões cruzados com teste de alfa no WebGPU do Apple Silicon, o caminho forward com o pipeline TSL do three.js já tem custo igual ou inferior ao do vis-buffer. Toda a infraestrutura do vis-buffer não oferece nenhum benefício visível antes de ultrapassarmos bastante as 200.000 instâncias, e isso somente se também adicionarmos uma passagem de ordenação ou agrupamento. A decisão prática para o motor de produção é manter a combinação de forward, LOD e impostores dos experimentos anteriores e não investir em infraestrutura de vis-buffer até que GPUs discretas da NVIDIA ou AMD sejam o principal alvo de implantação — onde o custo do overdraw é mais linear — ou até migrarmos para uma arquitetura de meshlets, na qual o vis-buffer já é a saída natural.

Como esse resultado contraria a intuição, a conclusão só tem valor se a comparação for justa. Por isso, o experimento passou por uma auditoria completa. Vários bugs reais apareceram e foram corrigidos: um controle de escala das lâminas que dessincronizava silenciosamente os dois caminhos, metade das lâminas do forward sendo renderizada quase preta por causa de normais antiparalelas — corrigido com o truque canônico de folhagem com normais voltadas para cima — e o vis-buffer aparecendo cerca de duas vezes mais claro por usar um fator de Lambert escolhido manualmente em vez do valor 1/π, que conserva energia, além de um termo de luz ambiente fixo e da ausência de mapeamento de tons. A correção copiou para WGSL a curva fílmica ACES exata do three.js e passou a ler, a cada quadro, as cores e intensidades das luzes diretamente das luzes reais da cena. A lacuna conhecida restante, a ausência de componente especular direta na passagem 2, favorece o vis-buffer na comparação. Isso significa que o forward realiza estritamente mais trabalho por pixel e, ainda assim, vence em alta densidade. Portanto, a conclusão principal é conservadora, não otimista. A única ressalva permanece: tudo isso é específico da série M, e o ponto de equilíbrio pode muito bem se inverter em uma placa discreta. Assim, vale repetir o teste antes de adotar definitivamente essa arquitetura para alvos que não sejam da Apple.

Tecnologias abordadas neste capítulo

Renderização com buffer de visibilidade. A passagem 1 rasteriza a geometria e grava apenas os IDs dos triângulos e das instâncias, junto com a profundidade, sem realizar nenhum sombreamento. A passagem 2 é uma resolução em tela cheia que lê os IDs de cada pixel coberto, busca novamente o triângulo de origem, reconstrói atributos baricêntricos com correção de perspectiva e sombreia cada pixel visível uma vez. Como o WebGPU não oferece um primitive_index portátil para fragmentos, o ID do triângulo é incorporado como um atributo por vértice com interpolação flat em uma geometria não indexada.

Remoção de superfícies ocultas em TBDR versus resolução diferida. Em uma GPU diferida baseada em tiles, como o Apple Silicon, o sombreamento forward já rejeita fragmentos ocluídos antes da execução do shader de fragmento. Assim, ele obtém gratuitamente a maior parte do benefício do buffer de visibilidade de sombrear uma única vez, e seu custo por pixel permanece praticamente constante conforme o overdraw aumenta. Já uma passagem de resolução com vis-buffer paga pelo acesso aleatório e incoerente a um grande buffer por instância — 12,8 MB com 200 mil instâncias —, que domina o custo em alta densidade, a menos que os pixels sejam primeiro ordenados ou agrupados por instância, como faz a classificação de materiais do Nanite.

Compartilhamento de um canvas com o WebGPURenderer do three.js. Buffers de comandos WebGPU brutos podem ser intercalados corretamente com os envios do three.js na fila compartilhada, desde que você nunca volte a chamar context.configure() nem altere canvas.width/height, pois ambos pertencem ao renderizador. A medição do tempo de GPU no caminho forward, para a qual o three.js não oferece um hook, pode ser delimitada por duas passagens de renderização de timestamp sem operação enviadas ao redor da chamada de renderização, já que a GPU executa os buffers de comandos na ordem de envio.

Validação de um benchmark contraintuitivo. Um resultado de desempenho surpreendente só é tão confiável quanto a imparcialidade da comparação. Auditar os dois caminhos para garantir conteúdo de cena e sombreamento idênticos — mapeamento de tons ACES correspondente, Lambert com conservação de energia, luzes lidas dos mesmos objetos e escala idêntica das lâminas — foi o que transformou “o vis-buffer é mais lento” de um provável artefato de medição em uma conclusão defensável, com a única assimetria restante favorecendo o lado conservador da comparação.


Parte 21 de 29. Anterior: Parte 20 — Simulando profundidade em um plano Próxima: Parte 22 — Nuvens pelas quais você pode voar e um culling que compensa Guia da série: /pt-BR/blog/2026-02-25-open-world-browser-series-guide