Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Otimização da GPU

Introdução

A demanda por novos recursos gráficos e o progresso quase garantem que você encontrará gargalos gráficos. Alguns deles podem estar no lado da CPU, por exemplo, em cálculos dentro da engine do Godot para preparar objetos para renderização. Os gargalos também podem ocorrer na CPU dentro do driver gráfico, que organiza as instruções para passar para a GPU, e na transferência dessas instruções. E, finalmente, os gargalos também ocorrem na própria GPU.

O local onde os gargalos ocorrem na renderização é altamente específico do hardware. As GPUs móveis, em particular, podem ter dificuldades com cenas que rodam facilmente no desktop.

Entender e investigar gargalos de GPU é uma situação um pouco diferente daquela na CPU. Isso ocorre porque, frequentemente, você só pode alterar o desempenho indiretamente, alterando as instruções que envia para a GPU. Além disso, pode ser mais difícil fazer medições. Na maioria dos casos, a única maneira de medir o desempenho é examinando as mudanças no tempo gasto renderizando cada quadro.

Chamadas de desenho (Draw calls), mudanças de estado e APIs

Nota

A seção a seguir não é relevante para os usuários finais, mas é útil para fornecer informações de base que serão relevantes em seções posteriores.

O Godot envia instruções para a GPU por meio de uma API gráfica (Vulkan, OpenGL, OpenGL ES ou WebGL). A comunicação e a atividade do driver envolvidas podem ser bastante caras, especialmente em OpenGL, OpenGL ES e WebGL. Se pudermos fornecer essas instruções de uma forma que seja preferida pelo driver e pela GPU, podemos aumentar muito o desempenho.

Quase todo comando de API no OpenGL requer uma certa quantidade de validação para garantir que a GPU esteja no estado correto. Mesmo comandos aparentemente simples podem levar a uma enxurrada de gerenciamentos internos nos bastidores. Portanto, o objetivo é reduzir essas instruções a um mínimo absoluto e agrupar objetos semelhantes o máximo possível para que possam ser renderizados juntos, ou com o número mínimo dessas caras mudanças de estado.

Processamento por lotes 2D

Em 2D, os custos de tratar cada item individualmente podem ser proibitivamente altos — pode facilmente haver milhares deles na tela. É por isso que o batching 2D é usado. Múltiplos itens semelhantes são agrupados e renderizados em um lote (batch), por meio de uma única draw call, em vez de fazer uma draw call separada para cada item. Além disso, isso significa que as mudanças de estado, mudanças de material e de textura podem ser mantidas no mínimo.

Processamento por lotes 3D

Em 3D, ainda buscamos minimizar chamadas de desenho (draw calls) e mudanças de estado. No entanto, pode ser mais difícil agrupar vários objetos em uma única chamada de desenho. Malhas 3D tendem a compreender centenas ou milhares de triângulos, e combinar malhas grandes em tempo real é proibitivamente caro. Os custos de juntá-las superam rapidamente quaisquer benefícios à medida que o número de triângulos cresce por malha. Uma alternativa muito melhor é juntar as malhas com antecedência (malhas estáticas em relação umas às outras). Isso pode ser feito por artistas ou programaticamente dentro do Godot usando um add-on.

Também há um custo em agrupar objetos em 3D. Vários objetos renderizados como um só não podem ser ocultados (culled) individualmente. Uma cidade inteira que está fora da tela ainda será renderizada se estiver unida a uma única folha de grama que está na tela. Portanto, você deve sempre levar em consideração a localização dos objetos e o culling ao tentar agrupar objetos 3D. Apesar disso, os benefícios de juntar objetos estáticos geralmente superam outras considerações, especialmente para grandes quantidades de objetos distantes ou de baixa contagem de polígonos (low-poly).

Para mais informações sobre otimizações específicas em 3D, veja Otimizando o desempenho 3D.

Reutilizar shaders e materiais

O renderizador do Godot é um pouco diferente do que existe por aí. Ele foi projetado para minimizar as mudanças de estado da GPU o máximo possível. O StandardMaterial3D faz um bom trabalho ao reutilizar materiais que precisam de shaders semelhantes. Se shaders customizados forem usados, certifique-se de reutilizá-los o máximo possível. As prioridades do Godot são:

  • Reutilização de Materiais: Quanto menos materiais diferentes na cena, mais rápida será a renderização. Se uma cena tiver uma quantidade enorme de objetos (sendo centenas ou milhares), tente reutilizar os materiais. No pior dos casos, use atlas para diminuir a quantidade de mudanças de textura.

  • Reutilização de Shaders: Se os materiais não puderem ser reutilizados, pelo menos tente reutilizar os shaders. Nota: os shaders são reutilizados automaticamente entre StandardMaterial3Ds que compartilham a mesma configuração (recursos que são ativados ou desativados com uma caixa de seleção), mesmo que tenham parâmetros diferentes.

Se uma cena tiver, por exemplo, 20.000 objetos com 20.000 materiais diferentes cada, a renderização será lenta. Se a mesma cena tiver 20.000 objetos, mas usar apenas 100 materiais, a renderização será muito mais rápida.

Custo de pixel versus custo de vértice

Você já deve ter ouvido falar que quanto menor o número de polígonos em um modelo, mais rápido ele será renderizado. Isso é realmente relativo e depende de muitos fatores.

Em um PC e console modernos, o custo de vértice é baixo. Originalmente, as GPUs apenas renderizavam triângulos. Isso significava que a cada quadro:

  1. Todos os vértices tinham que ser transformados pela CPU (incluindo o clipping).

  2. All os vértices tinham que ser enviados para a memória da GPU a partir da RAM principal.

Hoje em dia, tudo isso é tratado dentro da GPU, aumentando muito o desempenho. Os artistas 3D geralmente têm a sensação errada sobre o desempenho da contagem de polígonos (polycount) porque os softwares de modelagem 3D (como Blender, 3ds Max, etc.) precisam manter a geometria na memória da CPU para que ela possa ser editada, reduzindo o desempenho real. As engines de jogos dependem mais da GPU, então elas podem renderizar muitos triângulos de maneira muito mais eficiente.

Em dispositivos móveis, a história é diferente. As GPUs de PC e console são monstros de força bruta que podem extrair quanta eletricidade precisarem da rede elétrica. As GPUs móveis são limitadas a uma bateria minúscula, por isso precisam ser muito mais eficientes no consumo de energia.

Para serem mais eficientes, as GPUs móveis tentam evitar o overdraw. O overdraw ocorre quando o mesmo pixel na tela está sendo renderizado mais de uma vez. Imagine uma cidade com vários edifícios. As GPUs não sabem o que está visível e o que está oculto até desenharem. Por exemplo, uma casa pode ser desenhada e depois outra casa na frente dela (o que significa que a renderização aconteceu duas vezes para o mesmo pixel). As GPUs de PC normalmente não se importam muito com isso e apenas jogam mais processadores de pixel no hardware para aumentar o desempenho (o que também aumenta o consumo de energia).

Usar mais energia não é uma opção no mobile, por isso os dispositivos móveis usam uma técnica chamada tile-based rendering (renderização baseada em blocos), que divide a tela em uma grade. Cada célula mantém a lista de triângulos desenhados nela e os organiza por profundidade para minimizar o overdraw. Essa técnica melhora o desempenho e reduz o consumo de energia, mas cobra um preço no desempenho dos vértices. Como resultado, menos vértices e triângulos podem ser processados para o desenho.

Além disso, a renderização baseada em blocos enfrenta dificuldades quando há pequenos objetos com muita geometria dentro de uma pequena porção da tela. Isso força as GPUs móveis a colocar muita sobrecarga em um único bloco da tela, o que diminui consideravelmente o desempenho, pois todas as outras células devem esperar que ele termine antes de exibir o quadro.

Para resumir, não se preocupe com a contagem de vértices no mobile, mas evite a concentração de vértices em pequenas partes da tela. Se um personagem, NPC, veículo, etc. estiver longe (o que significa que ele parece minúsculo), use um modelo com menor nível de detalhe (LOD). Mesmo em GPUs de desktop, é preferível evitar ter triângulos menores que o tamanho de um pixel na tela.

Preste atenção ao processamento adicional de vértices necessário ao usar:

  • Skinning (animação esquelética)

  • Morphs (chaves de forma)

  • Objetos iluminados por vértice (comum em mobile)

Shaders de pixel/fragmento e fill rate

Em contraste com o processamento de vértices, os custos do sombreamento de fragmentos (por pixel) aumentaram dramaticamente ao longo dos anos. As resoluções de tela aumentaram: a área de uma tela 4K é de 8.294.400 pixels, contra 307.200 de uma antiga tela VGA de 640×480. Isso é 27 vezes a área! Além disso, a complexidade dos shaders de fragmento explodiu. A renderização baseada em física (PBR) requer cálculos complexos para cada fragmento.

Você pode testar se um projeto está limitado pela taxa de preenchimento (fill rate) de forma bastante fácil. Desligue o V-Sync para evitar limitar os quadros por segundo e, em seguida, compare os quadros por segundo ao rodar com uma janela grande versus rodar com uma janela muito pequena. Você também pode se beneficiar reduzindo de forma semelhante o tamanho do seu mapa de sombras (shadow map), se estiver usando sombras. Normalmente, você descobrirá que o FPS aumenta bastante usando uma janela pequena, o que indica que você está, até certo ponto, limitado pela fill rate. Por outro lado, se houver pouco ou nenhum aumento no FPS, o seu gargalo está em outro lugar.

Você pode aumentar o desempenho em um projeto limitado por fill rate reduzindo a quantidade de trabalho que a GPU tem que fazer. Você pode fazer isso simplificando o shader (talvez desativando opções caras se estiver usando um StandardMaterial3D) ou reduzindo o número e o tamanho das texturas utilizadas. Além disso, ao usar partículas sombreadas, considere forçar o sombreamento de vértice (vertex shading) no material delas para diminuir o custo de sombreamento.

Ver também

Em hardwares suportados, o Variable rate shading (sombreamento de taxa variável) pode ser usado para reduzir os custos de processamento de sombreamento sem impactar a nitidez das bordas na imagem final.

Ao direcionar para dispositivos móveis, considere usar os shaders mais simples possíveis que você possa realisticamente se dar ao luxo de usar.

Lendo texturas

O outro fator nos shaders de fragmento é o custo de leitura de texturas. Ler texturas é uma operação cara, especialmente ao ler de várias texturas em um único shader de fragmento. Além disso, considere que a filtragem pode torná-la ainda mais lenta (filtragem trilinear entre mipmaps e média). Ler texturas também é caro em termos de uso de energia, o que é um grande problema em celulares.

Se você usa shaders de terceiros ou escreve seus próprios shaders, tente usar algoritmos que exijam o menor número possível de leituras de textura.

Compressão de texturas

Por padrão, o Godot comprime as texturas de modelos 3D quando importadas usando compressão de memória de vídeo (VRAM). A compressão de memória de vídeo não é tão eficiente em tamanho quanto o PNG ou JPG quando armazenada, mas aumenta enormemente o desempenho ao desenhar texturas grandes o suficiente.

Isso ocorre porque o principal objetivo da compressão de textura é a redução de largura de banda entre a memória e a GPU.

No 3D, as formas dos objetos dependem mais da geometria do que da textura, então a compressão geralmente não é perceptível. No 2D, a compressão depende mais das formas dentro das texturas, então os artefatos resultantes da compressão 2D são mais perceptíveis.

Como um aviso, a maioria dos dispositivos Android não suporta compressão de textura para texturas com transparência (apenas opacas), então tenha isso em mente.

Nota

Mesmo em 3D, texturas de "pixel art" devem ter a compressão VRAM desativada, pois isso afetará negativamente sua aparência, sem melhorar o desempenho significativamente devido à sua baixa resolução.

Pós-processamento e sombras

Efeitos de pós-processamento e sombras também podem ser caros em termos de atividade de sombreamento de fragmentos. Sempre teste o impacto destes em hardwares diferentes.

Reduzir o tamanho dos mapas de sombra (shadowmaps) pode aumentar o desempenho, tanto em termos de escrita quanto de leitura dos mapas de sombra. Além disso, a melhor maneira de melhorar o desempenho das sombras é desativar as sombras para o maior número possível de luzes e objetos. OmniLights/SpotLights menores ou distantes frequentemente podem ter suas sombras desativadas com apenas um pequeno impacto visual.

Transparência e mesclagem (blending)

Objetos transparentes apresentam problemas específicos para a eficiência de renderização. Objetos opacos (especialmente em 3D) podem ser renderizados essencialmente em qualquer ordem e o Z-buffer garantirá que apenas os objetos mais à frente sejam sombreados. Objetos transparentes ou misturados são diferentes. Na maioria dos casos, eles não podem contar com o Z-buffer e devem ser renderizados na "ordem do pintor" (ou seja, de trás para frente) para parecerem corretos.

Objetos transparentes também são particularmente ruins para a fill rate, porque cada item precisa ser desenhado mesmo que outros objetos transparentes sejam desenhados por cima mais tarde.

Objetos opacos não precisam fazer isso. Eles geralmente podem tirar vantagem do Z-buffer escrevendo primeiro apenas no Z-buffer, e então realizando o shader de fragmento apenas no fragmento "vencedor", o objeto que está na frente em um determinado pixel.

A transparência é particularmente cara onde múltiplos objetos transparentes se sobrepõem. Geralmente é melhor usar áreas transparentes o menor possível para minimizar esses requisitos de fill rate, especialmente no mobile, onde a fill rate é muito cara. De fato, em muitas situações, renderizar uma geometria opaca mais complexa pode acabar sendo mais rápido do que usar a transparência para "trapacear".

Assessoria multiplataforma

Se o seu objetivo é lançar em múltiplas plataformas, teste cedo e teste frequentemente em todas as suas plataformas, especialmente no mobile. Desenvolver um jogo no desktop, mas tentar portá-lo para o mobile no último minuto, é uma receita para o desastre.

Em geral, você deve projetar seu jogo para o menor denominador comum e, em seguida, adicionar melhorias opcionais para plataformas mais potentes. Por exemplo, você pode querer usar o método de renderização Compatibility tanto para plataformas de desktop quanto móveis quando visar a ambas.

Renderizadores móveis/baseados em blocos (tiled)

Como descrito acima, as GPUs em dispositivos móveis funcionam de maneiras drasticamente diferentes das GPUs em computadores desktop. A maioria dos dispositivos móveis usa renderizadores baseados em blocos (tile renderers). Os tile renderers dividem a tela em blocos de tamanho regular que cabem em uma memória cache super-rápida, o que reduz o número de operações de leitura/escrita na memória principal.

Existem algumas desvantagens, no entanto. A renderização baseada em blocos pode tornar certas técnicas muito mais complicadas e caras de serem executadas. Blocos que dependem dos resultados da renderização em blocos diferentes ou de os resultados de operações anteriores serem preservados podem ser muito lentos. Tenha muito cuidado ao testar o desempenho de shaders, texturas de viewport e pós-processamento.