Levando o Nanite ao Fortnite Battle Royale no Capítulo 4

26 de janeiro de 2023
Olá, sou Graham Wihlidal, engenheiro-chefe (gráfico) da Epic Games. Estou aqui para mostrar as funcionalidades e melhorias muito interessantes que desenvolvemos este ano para usar o Nanite no Capítulo 4 do Fortnite Battle Royale. Você pode testar essas funcionalidades na Unreal Engine 5.1 (como Beta).

A Unreal Engine 5.0 foi lançada com o Nanite em um estado pronto para produção. A tecnologia já era usável para muitas coisas incríveis, mas a versão inicial não oferecia suporte total às diversas funcionalidades disponíveis para malhas não Nanite. Em vez disso, concentramos nosso tempo para aprimorar as principais funcionalidades do Nanite.

Mas faz parte dos nossos planos estender o suporte a muitas áreas que o Nanite ainda não engloba. Os usuários têm solicitado muito suporte para funcionalidades como World Position Offset, Pixel Depth Offset, UVs personalizados, materiais de duas faces e materiais com máscara. No início do ano, a equipe assumiu o desafio de tentar implementar esse suporte para algumas funcionalidades, o que nos levou a resolver inúmeros problemas não triviais.

No início, as GPUs trabalhavam com um pipeline de função fixa, onde o método como a geometria era transformada, e como a profundidade e a cor eram escritas, era incorporado ao hardware e só podia ser configurado com um conjunto limitado de funções predefinidas. Depois, o hardware passou a ser “programável” por meio do código de shader, oferecendo novas possibilidades de gráficos e permitindo funcionalidades que eram difíceis ou impossíveis de fazer com o pipeline de função fixa.

Desde o início, o Nanite oferece suporte para que shaders de gráfico de material “programáveis” controlem a cor de saída, mas o rasterizador em si, que decide como os vértices são posicionados na tela e quais pixels são cobertos pelos triângulos, era uma “função fixa”: apesar de ser implementada como código de shader, os criadores de conteúdo não controlavam essa lógica.

Para oferecer suporte às funcionalidades mencionadas anteriormente no Nanite, precisávamos tornar o próprio rasterizador programável.

Protótipo inicial

Decidimos prototipar a arquitetura necessária para dar suporte à lógica do gráfico de material no rasterizador, que chamamos de “Rasterizador Programável Nanite”.

As imagens a seguir mostram o protótipo inicial de um material com máscara animado sobre uma malha Nanite de caminhão de lixo.
 
O protótipo conseguiu provar se o rasterizador programável seria ou não possível, mas ainda havia muito trabalho a ser feito para deixá-lo eficiente e pronto para produções.

Definimos alguns objetivos para conduzir o desenvolvimento do trabalho:
  • Manter o perfil de desempenho do caminho rápido de “função fixa” existente: a introdução do rasterizador programável não deve reduzir a velocidade do conteúdo atual.
  • Garantir que os caminhos de função fixa e do rasterizador programável compartilhem amplamente o mesmo caminho de código, por razões de manutenção.
  • Ter em mente que o rasterizador programável será usado intensamente por muitos conteúdos, então os avaliadores simples devem oferecer um desempenho apenas um pouco mais lento do que o rasterizador de função fixa.
  • Executar o trabalho de oclusão de instância e oclusão de agrupamento apenas uma vez.
  • Minimizar o impacto de memória adicional na GPU e no Nanite.
  • Realizar isso como uma funcionalidade de produção na UE 5.1.

O protótipo inicial foi codificado para suportar apenas um único material programável na cena, então o passo seguinte foi uma etapa adequada de “rasterizer binning” para que pudéssemos oferecer suporte a cenas de jogos compostas de centenas de materiais, o que nos permitiu testar conteúdos de verdade.
Tomada de jogo medieval convertida quase totalmente para Nanite!
Visualização de triângulos Nanite
“Bins” rasterizadores únicos (materiais) na cena
Mesmo com a cena de teste funcional do jogo medieval, ainda havia muito trabalho de otimização e funcionalidades a ser feito para que o framework de Rasterizador Programável Nanite pudesse ser transportado para um jogo.

Desde o início, era evidente a vontade de fazer com que o Capítulo 4 do Fortnite Battle Royale aproveitasse o Nanite, mas ainda não oferecíamos suporte às funcionalidades necessárias para uma adesão em grande escala. Mesmo as malhas aparentemente simples, como peças de edifícios opacas, precisavam do World Position Offset para animar o icônico efeito de “rebate” ao sofrerem dano. O Fortnite se tornou o primeiro usuário do rasterizador programável.

Casos de uso do Fortnite

Props animados

O primeiro caso de uso compatível são os props de malha estática opacos simples que usam o World Position Offset para animações secundárias. Embora esses props não exijam que o Nanite seja renderizado com eficiência, o desempenho do Mapa de Sombra Virtual fica muito melhor com malhas Nanite. Por isso, era importante renderizar o máximo possível da cena usando o Nanite.


 

 

Edifícios

Um aspecto importante do Fortnite são os vários conjuntos de edifícios, e o Nanite já provou lidar muito bem com grandes cidades. É por isso que escolhemos usar o Nanite em todas as malhas de edifícios, aumentando a fidelidade visual, eliminando alterações a nível de detalhe e melhorando o desempenho.
Construção
Há muitas malhas de edifícios no Fortnite, o que dificulta reconstruir tudo do zero em todas as plataformas. Por isso, criamos um processo off-line que aceita texturas e regras de deslocamento criadas por artistas, produzindo malhas Nanite deslocadas de alta qualidade com uma contagem de triângulos muito maior do que as versões tradicionais.

Observação: não se trata de um deslocamento de “tempo de execução”, mas sim de um fluxo de trabalho off-line específico do Fortnite para criação de malhas deslocadas.

Além das melhorias visuais na renderização da visualização inicial, as malhas Nanite deslocadas também melhoraram muito a qualidade dos Mapas de Sombras Virtuais, já que detalhes geométricos como tijolos, que eram representados como áreas pintadas em 2D na textura, agora são deslocados para o espaço 3D de verdade. Isso resulta em mais detalhes e profundidade nas superfícies, permitindo um sombreamento automático adequado e silhuetas.
 
Os edifícios no Fortnite são todos malhas estáticas opacas, e podem ser totalmente renderizados com o conjunto de funcionalidades do Nanite disponível na Unreal Engine 5.0, exceto o efeito de “tremor”, que ocorre temporariamente enquanto uma parte da construção sofre dano (quando um jogador a golpeia com uma picareta, por exemplo).

O tremor é uma faixa animada simples aplicada ao material com o World Position Offset, e a avaliação é constante, mesmo quando o tremor não ocorre visualmente (um peso zero é usado).

Devido ao grande número de malhas de construções do Fortnite, implementamos uma otimização para direcionar esse padrão: um modo especial pode ser ativado (r.OptimizedWPO) e, independentemente de um material ter ou não lógica para conduzir o World Position Offset, o Nanite avaliará essa lógica apenas se um determinado componente primitivo tiver a opção “Evaluate World Position Offset” ativada (que é a configuração padrão).
Quando o Nanite executa a etapa “rasterizer binning” mencionada anteriormente, quaisquer primitivos que normalmente seguiriam o caminho programável, mas que têm a opção “Evaluate World Position Offset” ativada, passarão a usar o rasterizador de função fixa padrão.

Essa otimização se mostrou útil em vários momentos (incluindo a desativação do World Position Offset à distância), mas foi vital no tremor das construções. Por padrão, desativamos a opção “Evaluate World Position Offset” em todas as malhas de construção e ajustamos o código do jogo no Fortnite para definir programaticamente esse valor com base no fato de a construção estar ou não sendo danificada (e tremendo).
Além dessa nova otimização, adicionamos uma visualização de depuração no Nanite chamada “Evaluate WPO” (r.Nanite.Visualize EvaluateWPO), que indica com a cor verde se uma malha está avaliando o World Position Offset, e vermelha caso contrário.
Com essa otimização, quase todas as malhas de construção seguirão o caminho da função fixa, a não ser por algumas malhas que, ocasionalmente, seguirão o tremor programável quando necessário.
Tomada de exemplo
r.OptimizedWPO desativado vs. ativado

Árvores

A área em que gastamos mais tempo com prototipagem e desenvolvimento foi a das árvores. Queríamos que o Capítulo 4 tivesse áreas florestais exuberantes, então precisávamos de uma solução eficiente e com um desempenho previsível. As árvores dependiam de uma funcionalidade totalmente inédita no Nanite dos títulos lançados antes, então o trabalho de prototipagem e otimização para determinar a abordagem que usaríamos foi bem intenso.
Construção
Nos primeiros experimentos, usamos materiais com máscara e cards para as árvores.
Ao lidar com os conteúdos do Fortnite, descobrimos que costuma ser mais rápido evitar materiais com máscara e, em vez disso, contar com o aumento da contagem de triângulos da malha e manter os materiais opacos, especialmente as árvores e a grama. Isso acontece porque os materiais com máscara no Nanite resultam em um custo significativo durante a etapa básica de sombreamento, pois temos que recalcular as coordenadas baricêntricas dos triângulos por pixel, e o espaço negativo nos mapas alfa adiciona ainda mais custo de overdraw.

Preservação de área

Depois de converter as árvores do Fortnite para Nanite, notamos que elas perderiam suas copas à distância devido ao processo de simplificação. Em alguns casos as folhas afinavam e, em outros, desapareciam completamente. Chegava um ponto em que cada folha não poderia simplificar mais do que um único triângulo, e as folhas precisavam ser removidas para reduzir a contagem de triângulos. Essa remoção de folhas fez com que a copa ficasse visualmente mais fina.

Para corrigir isso, adicionamos uma nova lógica ao construtor do Nanite (uma opção chamada “Preserve Area” que pode ser ativada em “Nanite Settings” da malha) que redistribui a área perdida para os triângulos restantes, dilatando as extremidades do limite aberto. No caso das copas, é como se as folhas restantes ficassem maiores. Embora o efeito fique estranho se realizado de perto, a ilusão de densidade que aquela área deveria mostrar funciona bem quando a preservação de área é ativada à distância.

Essa funcionalidade só deve ser ativada em malhas de folhagem que apresentam esse problema, e em nenhuma outra situação.
 
Sem a opção “Preserve Area”

 
Com a opção “Preserve Area”
Animações de vento
Seria estranho se todas as árvores fossem renderizadas em alta fidelidade, mas não tivessem animações de vento. Normalmente, o Fortnite realizaria a animação de vento com uma lógica complexa para conduzir o World Position Offset. Como as árvores no Nanite têm muito mais vértices (cerca de 300-500 mil) do que suas equivalentes não Nanite (cerca de 10-20 mil), e com a lógica de posição do mundo sendo avaliada no rasterizador do Nanite, exploramos outras maneiras de animar as árvores que reduziram o custo de avaliação por vértice.
Os experimentos nos levaram a incorporar uma simulação de vento complexa em uma textura que poderíamos amostrar ao conduzir o World Position Offset, em vez de avaliar uma infinidade de cálculos complicados para cada vértice.
É possível extrair o pivô de cada ramificação e o nível de hierarquia da geometria das árvores, assim como o galho principal de cada ramificação. Podemos criar um esqueleto a partir dessas informações e inseri-lo em uma simulação Vellum do Houdini
O pivô e a orientação de cada ramificação da simulação podem ser codificados como um valor de pixel em uma imagem, amostrados em um shader de material e listados usando um valor UV personalizado, codificado no recurso de malha usado para escolher a linha de pixels adequada para a ramificação.

Por causa disso, o rasterizador do Nanite só precisa procurar uma única posição e um único quatérnio para calcular o deslocamento, sem precisar de várias leituras de textura dependentes. Atualmente, essa abordagem suporta apenas animações rígidas, pois cada ramificação tem o mesmo valor de UV.
Cull distance
A simulação do vento nas árvores é muito importante perto da câmera, mas não muito perceptível à distância. Para melhorar o desempenho, adicionamos a capacidade do Nanite desabilitar os cálculos do World Position Offset a uma distância definida pelos artistas. Essa melhoria se baseia no modo “Evaluate World Position Offset”.

Grama

Adicionamos suporte para que a grama da Paisagem possa gerar instâncias de malha Nanite, incluindo o suporte para cull distance próprio do sistema. A abordagem de recursos adotada aqui é semelhante à abordagem usada nas árvores. Usamos materiais opacos com geometria real para as lâminas de grama, mas apenas uma matemática simples para conduzir a animação do World Position Offset.
 

 

Paisagem (Nanite)

Devido à natureza do funcionamento dos Mapas de Sombras Virtuais, as malhas não Nanite grandes renderizam sombras muito mais lentamente do que malhas Nanite do mesmo tamanho. Isso foi bem problemático ao lidarmos com a Paisagem, uma enorme malha não Nanite que estava custando muito tempo da GPU. Implementamos uma funcionalidade de renderização experimental para a Paisagem no Nanite, lançada na UE 5.1, que converte o campo de altura da paisagem em uma malha Nanite durante a construção. A conversão não aumenta a qualidade visual, mas fornece todas as características de desempenho de oclusão e rasterização do Nanite ao renderizar na etapa base ou nos Mapas de Sombras Virtuais.
Lumen possui um caminho especializado para traçar no campo de altura da Paisagem, e o Nanite não oferece suporte à renderização em “Runtime Virtual Textures” no momento. Portanto, usamos o campo de altura nesses casos, e não a representação do Nanite.
 

Próximos trabalhos

Talvez o ponto restante mais importante seja o cálculo exato (por quadro) da instância e do volume delimitador do agrupamento que corresponda à animação executada pelo World Position Offset. Isso é vital para garantir que a remoção por oclusão do Nanite remova o maior número de agrupamentos possível antes da rasterização.
 

No momento, usamos os limites da pose de referência (ignorando o World Position Offset), que ou é conservador demais (rasterizando mais agrupamentos do que o necessário) ou causa artefatos visuais se o World Position Offset animar os agrupamentos totalmente fora dos limites da pose de referência (partes da malha desaparecem). Implementamos um suporte inicial para “Bounds Scale” em componentes primitivos (semelhante a como os não Nanite abordam esse problema) para que você possa aumentar os limites em algum valor arbitrário, mas isso torna a oclusão ainda mais conservadora e custa um desempenho desnecessário.

Também planejamos continuar otimizando o sistema de materiais no Nanite para reduzir o custo de materiais com máscara e materiais com Pixel Depth Offset.

Estamos muito felizes em lançar esta nova funcionalidade para o Nanite na Unreal Engine 5.1, e mal podemos esperar para ver todos os conteúdos incríveis que os desenvolvedores criarão!
Para mais informações sobre as funcionalidades mencionadas neste blog, confira a documentação do Nanite.

    Obtenha a Unreal Engine hoje mesmo!

    Obtenha a ferramenta de criação mais aberta e avançada do mundo.
    Com diversas funcionalidades e acesso ao código-fonte, a Unreal Engine vem carregada e pronta para usar.