Como resolvemos grandes requisitos de memória de vídeo em um jogo 2D?

40

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:

  1. O desenho do bitmap de memória para o bitmap de vídeo é dolorosamente lento, no allegro.
  2. 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.

Marwin
fonte
11
Grande fã do seu jogo, apoiei a campanha Indiegogo. Eu binge nele a cada poucos meses. Bom trabalho até agora! Eu removi as perguntas "que tecnologia usar" que estão fora do tópico do site. As perguntas restantes ainda são bastante amplas, se você tiver algo mais específico, tente restringir o escopo.
MichaelHouse
Obrigado pelo apoio. Então, onde é o lugar para perguntar qual tecnologia usar então? Não estou procurando resposta com recomendação específica de mecanismo, mas não consegui encontrar uma comparação detalhada dos mecanismos 2D e inspecioná-los manualmente, um por um, incluindo testes de desempenho e usabilidade que levariam séculos.
Marwin
Confira na parte inferior desta página alguns lugares para fazer perguntas como "qual tecnologia usar". Você tem uma pergunta totalmente válida e razoável, mas não é o tipo de pergunta com a qual lidamos neste site. Mesmo que você não esteja procurando um mecanismo específico, essa é realmente a única maneira de responder à pergunta "Existe alguma tecnologia que faça X?". Alguém poderia simplesmente responder "sim" e não dar uma recomendação para uma específica, mas isso não seria muito útil. Boa sorte com isso!
Michaelhouse
2
Você está comprimindo suas texturas?
precisa
3
@Marwin, as texturas compactadas podem ter um desempenho muito melhor do que as texturas não compactadas, porque reduzem a largura de banda da memória necessária (isso é especialmente verdade em plataformas móveis onde a largura de banda é muito menor). Você pode economizar uma quantidade enorme de memória apenas compactando suas texturas. Realmente, a única desvantagem são os artefatos que são inevitavelmente introduzidos.
GuyRT

Respostas:

17

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.

Kromster diz apoio Monica
fonte
2
+1 por usar o DXT, é uma coisa muito boa de se ter. Ótima compactação e usada diretamente pela GPU, portanto a sobrecarga é mínima.
11
Eu concordo com o dxt. Você também pode consultar o suporte ao DXT7 (hardware DX11 +), que é do mesmo tamanho do DXT1, mas (aparentemente) de qualidade superior. No entanto, você precisaria ter o dobro das texturas (uma DXT7 e uma DXT1) ou comprimir / descomprimir durante o carregamento.
Programmdude
5

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?

  • Entrando em pânico e exibindo uma tela de carregamento até que todo o material necessário seja carregado. Isso pode parecer perturbador para a experiência.
  • Tenha sprites de baixa resolução para tudo pré-carregado, continuando o jogo e substituindo-os assim que os sprites de alta resolução terminarem de carregar. Isso pode parecer barato para o jogador.
  • Faça impactar a jogabilidade e adie o evento pelo tempo que for necessário. Por exemplo, não crie esse inimigo até que seus gráficos sejam carregados. Não abra a arca do tesouro antes que todos os gráficos para esse saque sejam carregados, etc.
API-Beast
fonte
Adicionei alguns dos requisitos que omiti. A tela de carregamento ou qualquer tipo de carregamento não é possível. Tudo deve ser feito em segundo plano, ou entre os ticks individuais (menos de 15 ms para cada), enquanto a maior parte do tempo geralmente já é usada pelos preparativos da renderização e atualização do jogo. De qualquer forma, dividir em partes menores pode adicionar alguma flexibilidade na comutação, seria mais rápido, com certeza. A questão é quanto prejudica o desempenho ao renderizar, pois alternar o bitmap de origem enquanto desenha diminui a renderização. Eu teria que fazer uma medição exata para dizer quanto.
Marwin
@ Marwin Impacto no desempenho, sim, mas como você lida com 2D, você ainda deve estar longe de se tornar um problema. Se a renderização atualmente leva 1ms por quadro e, através do uso de texturas menores, de repente demora 2ms, isso ainda é mais do que rápido o suficiente para atingir 60 FPS consistentes. (16ms)
API-Beast
O @Marwin Multiplayer é um negócio complicado, sempre foi, sempre será. Você provavelmente terá que fazer compromissos lá. Você terá gagueira, simplesmente porque precisa transferir dados pela Internet, pacotes serão perdidos, pings poderão repentinamente aparecer, etc. Saber quando esperar e como esperar por outros jogadores.
API-Beast
Olá, a gagueira é quase evitável no modo multiplayer, estamos trabalhando nessa área agora e acredito que temos um bom plano. Eu poderia até postar e responder minha própria pergunta, descrevendo o que pesquisamos em detalhes posteriormente :) Pode ser uma surpresa, mas o tempo de renderização é realmente um problema. Fizemos muitas otimizações para tornar a renderização mais rápida. A renderização principal agora é feita em thread separado e outros pequenos ajustes. Não se esqueça que, com o zoom máximo, o jogador pode ver facilmente dezenas de milhares de sprites ao mesmo tempo. E gostaríamos de permitir níveis de zoom ainda mais altos posteriormente.
Marwin
@ Marwin Hm, 10 mil objetos geralmente não devem ser problema para um PC ou laptop moderno, se você usar lotes apropriados, você definiu o perfil do seu código de renderização?
API-Beast
2

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.

aaaaaaaaaaaa
fonte
2

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.

cristão
fonte
1

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.

Pablo Ariel
fonte
11
Ao dividir em arquivos, você quer dizer arquivos no HDD? Eu suponho que eu poderia armazenar todas as imagens na RAM para iniciantes, e até mesmo copiar do bitmap de memória para o bitmap de vídeo é muito lento atualmente, portanto, o carregamento do HDD seria certamente ainda mais lento. Ter mimpaps não vai me ajudar, pois ainda terei a maior resolução no vram.
Marwin
Sim, você não precisa carregar tudo, apenas o que você usa. Sempre que você deseja alterar um pixel em uma textura carregada na VRAM, o sistema precisa mover a TEXTURA INTEIRA para a RAM, apenas para modificar um único pixel e movê-lo de volta para a VRAM. Se você tiver tudo em uma única textura, isso envolve mover 256 MB para a RAM e voltar para a VRAM novamente, o que bloqueia todo o computador. Separá-lo em diferentes arquivos e texturas é a maneira correta de fazê-lo.
Pablo Ariel #
A modificação da textura que aciona a cópia na memória e volta ao ram aplica-se apenas a bitmaps persistentes; o cache provavelmente não seria definido como persistente; a única desvantagem seria a necessidade de atualizá-lo quando a exibição for perdida / encontrada. Mas no allegro, mesmo uma cópia simples da imagem 640X480 do vram para o bitmap de memória (salvar a visualização do jogo) leva muito tempo.
Marwin
11
Eu preciso ter tudo em uma grande textura para otimizar o desenho em si, sem ele, o efeito de alternar o contexto entre sprites individuais diminui demais a renderização, pelo menos no allegro. Não me entenda mal, mas você é o tipo de capitão óbvio aqui, pois está me sugerindo vagamente que faça algo que peço nesta pergunta.
Marwin
11
Ter essas texturas mapeadas por mip em arquivos diferentes me forçaria a recarregar todos os atlas quando o jogador aumentar o zoom. Como o mecanismo tem apenas poucas unidades de ms, no máximo, não vejo uma maneira de fazê-lo.
Marwin
0

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.

Darxval
fonte