Como resolvemos grandes requisitos de memória de vídeo em um jogo 2D?
Estamos desenvolvendo um jogo 2D (Factorio) em allegro C / C ++, e estamos enfrentando um problema com o aumento dos requisitos de memória de vídeo à medida que o conteúdo do jogo aumenta.
No momento, reunimos todas as informações sobre as imagens que serão usadas primeiro, recortamos todas essas imagens o máximo possível e as organizamos em grandes atlas o máximo possível. Esses atlas são armazenados na memória de vídeo, cujo tamanho depende das limitações do sistema; atualmente, geralmente são 2 imagens de até 8192x8192, portanto, elas requerem memória de vídeo de 256Mb a 512Mb.
Esse sistema funciona muito bem para nós, pois, com algumas otimizações personalizadas e dividindo o segmento de renderização e atualização, podemos desenhar dezenas de milhares de imagens na tela em 60 fps; temos muitos objetos na tela e permitir um grande zoom é um requisito crítico. Como gostaríamos de acrescentar mais, haverá alguns problemas com os requisitos de memória de vídeo, portanto esse sistema não pode suportar.
Uma das coisas que queríamos tentar é ter um atlas com as imagens mais comuns e o segundo como um cache. As imagens seriam movidas para lá a partir do bitmap de memória, sob demanda. Há dois problemas com esta abordagem:
- O desenho do bitmap de memória para o bitmap de vídeo é dolorosamente lento, no allegro.
- Não é possível trabalhar com bitmap de vídeo que não seja o thread principal, no allegro, portanto é praticamente inutilizável.
Aqui estão alguns requisitos adicionais que temos:
- O jogo deve ser determinista, para que os problemas de desempenho / tempo de carregamento nunca possam alterar o estado do jogo.
- O jogo é em tempo real, e em breve será multiplayer também. Precisamos evitar até a menor gagueira a todo custo.
- A maior parte do jogo é um mundo aberto contínuo.
O teste consistiu em desenhar 10.000 sprites em um lote para tamanhos de 1x1 a 300x300, várias vezes para cada configuração. Eu fiz os testes na Nvidia Geforce GTX 760.
- O bitmap de vídeo para o desenho de bitmap de vídeo levou 0,1 us por sprite, quando o bitmap de origem não estava mudando entre bitmaps individuais (a variante atlas); o tamanho não importava
- O bitmap de vídeo para o desenho de bitmap de vídeo, enquanto o bitmap de origem foi alternado entre desenhos (variante não atlas), levou 0,56us por sprite; o tamanho também não importava.
- O bitmap de memória para o desenho de bitmap de vídeo era realmente suspeito. Os tamanhos de 1x1 a 200x200 levaram 0,3us por bitmap, portanto, não é tão terrivelmente lento. Para tamanhos maiores, o tempo começou a aumentar muito, de 9us para 201x201 a 3116us para 291x291.
O uso de atlas aumenta o desempenho em um fator maior que 5. Se eu tivesse 10ms para a renderização, com um atlas estou limitado a 100.000 sprites por quadro e, sem ele, um limite de 20.000 sprites. Isso seria problemático.
Eu também estava tentando encontrar uma maneira de testar a compactação de bitmap e o formato de bitmap de 1bpp para sombras, mas não consegui encontrar uma maneira de fazer isso no allegro.
fonte
Respostas:
Temos um caso semelhante com o nosso RTS (KaM Remake). Todas as unidades e casas são sprites. Temos 18.000 sprites para unidades e casas e terrenos, além de outros ~ 6.000 para cores de equipe (aplicadas como máscaras). Além disso, também temos cerca de 30.000 caracteres usados em fontes.
Portanto, existem algumas otimizações nos atlas RGBA32 que você está usando:
Divida seu pool de sprites em muitos atlas menores primeiro e use-os sob demanda, conforme coberto em outras respostas. Isso também permite usar diferentes técnicas de otimização para cada atlas individualmente . Eu suspeito que você terá um pouco menos de memória RAM desperdiçada, porque ao embalar com texturas tão grandes, geralmente há áreas não utilizadas na parte inferior;
Tente usar texturas paletizadas . Se você usa shaders, pode "aplicar" a paleta no código shaders;
Você pode adicionar uma opção para usar RGB5_A1 em vez de RGBA8 (se, por exemplo, sombras quadriculado estiverem boas para o seu jogo). Evite Alpha de 8 bits quando possível e use RGB5_A1 ou formatos equivalentes com menor precisão (semelhante RGBA4), eles ocupam metade do espaço;
Certifique-se de colocar sprites firmemente nos atlas (consulte Algoritmos de embalagem de bin), gire os sprites quando necessário e verifique se é possível sobrepor cantos transparentes para sprites de losango;
Você pode tentar formatos de compactação de hardware (DXT, S3TC etc.) - eles podem reduzir drasticamente o uso de RAM, mas verificar artefatos de compactação - em algumas imagens a diferença pode ser imperceptível (você pode usá-lo seletivamente, conforme descrito no primeiro ponto). mas em alguns - muito pronunciado. Diferentes formatos de compactação causam artefatos diferentes; portanto, você pode escolher um que seja melhor para o seu estilo de arte.
Observe a divisão de sprites grandes (é claro que não manualmente, mas dentro do seu atlas pack de textura) em sprites de fundo estáticos e sprites menores para peças animadas.
fonte
Antes de tudo, você precisa usar mais atlas de textura menores. Quanto menos texturas você tiver, mais difícil e rígido será o gerenciamento de memória. Eu sugeriria um tamanho de atlas de 1024; nesse caso, você teria 128 texturas em vez de 2 ou 2048; nesse caso, você teria 32 texturas, que você poderia carregar e descarregar conforme necessário.
A maioria dos jogos faz esse gerenciamento de recursos tendo limites de nível, enquanto uma tela de carregamento é exibida, todos os recursos que não são mais necessários no próximo nível são descarregados e os recursos necessários são carregados.
Outra opção é o carregamento sob demanda, que se torna necessário se os limites do nível não forem desejados ou se um único nível for grande demais para caber na memória. Nesse caso, o jogo tentará prever o que o jogador verá no futuro e carregá-lo em segundo plano. (Por exemplo: coisas que estão atualmente a 2 telas do player.) Ao mesmo tempo, as coisas que não foram mais usadas por mais tempo serão descarregadas.
Há um problema, no entanto, o que acontece quando algo inesperado acontece que o jogo não foi capaz de prever?
fonte
Uau, isso é uma quantidade considerável de sprites de animação, gerados a partir de modelos 3D que eu presumo?
Você realmente não deveria estar fazendo este jogo em 2D bruto. Quando você tem uma perspectiva fixa, algo engraçado acontece, você pode misturar sprites e fundos pré-renderizados com perfeição com modelos 3D renderizados ao vivo, que tem sido muito utilizado por alguns jogos. Se você deseja animações tão finas, parece a maneira mais natural de fazê-lo. Obtenha um mecanismo 3D, configure-o para usar perspectiva isométrica e renderize os objetos para os quais você continua usando sprites como superfícies planas simples com uma imagem neles. E você pode usar a compactação de textura com um mecanismo 3D, que por si só é um grande passo à frente.
Não acho que carregar e descarregar fará muito para você, pois você pode ter praticamente tudo na tela ao mesmo tempo.
fonte
Em primeiro lugar, encontre o formato de textura mais eficiente possível, enquanto ainda está satisfeito com o visual do jogo, seja a compressão RGBA4444 ou DXT, etc. tornar as imagens não transparentes usando a compressão DXT1 para a cor combinada com uma textura de máscara de escala de cinza de 4 ou 8 bits para o alfa? Eu imagino que você ficaria no RGBA8888 para a GUI.
Defendo a divisão de texturas menores usando o formato que você escolher. Determine os itens que estão sempre na tela e, portanto, sempre carregados, pode ser o atlas do terreno e da GUI. Em seguida, dividiria os itens restantes que normalmente são renderizados juntos o máximo possível. Eu não imagino que você perderia muito desempenho, mesmo indo de 50 a 100 chamadas no PC, mas me corrija se eu estiver errado.
O próximo passo será gerar as versões mipmap dessas texturas como alguém apontado acima. Eu não os armazenaria em um único arquivo, mas separadamente. Assim, você terminaria com as versões 1024x1024, 512x512, 256x256 etc de cada arquivo, e eu faria isso até atingir o nível mais baixo de detalhes que gostaria de ser exibido.
Agora que você possui as texturas separadas, é possível criar um sistema de nível de detalhe (LOD) que carrega texturas para o nível de zoom atual e descarrega texturas se não for usado. Uma textura não está sendo usada se o item que está sendo renderizado não estiver na tela ou não for exigido pelo nível de zoom atual. Tente carregar as texturas na RAM de vídeo em um thread separado dos threads de atualização / renderização. Você pode exibir a textura LOD mais baixa até que a necessária seja carregada. Às vezes, isso pode resultar em uma alternância visível entre uma textura de baixo detalhe / alto detalhe, mas imagino que isso aconteceria apenas quando você executasse um zoom extremamente rápido para fora e para dentro enquanto se movia pelo mapa. Você pode tornar o sistema inteligente tentando pré-carregar onde você acha que a pessoa se moverá ou fará o zoom e carregará o máximo possível dentro das atuais restrições de memória.
Esse é o tipo de coisa que eu testaria para ver se isso ajuda. Eu imagino que para obter níveis extremos de zoom, você inevitavelmente precisará de um sistema LOD.
fonte
Acredito que a melhor abordagem é dividir a textura em muitos arquivos e carregá-los sob demanda. Provavelmente, seu problema é que você está tentando carregar texturas maiores, necessárias para uma cena 3D completa e está usando o Allegro para isso.
Para o grande zoom que você deseja aplicar, é necessário usar mipmaps. Mipmaps são versões de resolução mais baixa de suas texturas, usadas quando os objetos estão longe o suficiente da câmera. Isso significa que você pode salvar o seu 8192x8192 como 4096x4096 e depois outro com 2048x2048 e assim por diante, e alternar para as resoluções mais baixas quanto menor o sprite na tela. Você pode salvá-los como texturas separadas ou redimensioná-los ao carregar (mas gerar mipmaps durante o tempo de execução aumentará o tempo de carregamento do seu jogo).
Um sistema de gerenciamento adequado carregaria os arquivos necessários sob demanda e liberaria os recursos quando ninguém os estiver usando, além de outras coisas. O gerenciamento de recursos é um tópico importante no desenvolvimento de jogos e você está reduzindo seu gerenciamento a um mapeamento de coordenadas simples para uma única textura, o que está quase perto de não ter nenhum gerenciamento.
fonte
Eu recomendo a criação de mais arquivos atlas que possam ser compactados com o zlib e transmitidos para fora da compactação de cada atlas; por ter mais arquivos atlas e arquivos de tamanho menor, você poderá restringir a quantidade de dados de imagem ativos na memória de vídeo. Além disso, implemente o mecanismo de buffer triplo para que você prepare cada quadro de desenho mais cedo e tenha a chance de concluir mais rapidamente, para que as gagueiras não apareçam na tela.
fonte