Desenhando muitos blocos com o OpenGL, a maneira moderna


35

Estou trabalhando em um pequeno jogo de PC baseado em blocos / sprites com uma equipe de pessoas e estamos enfrentando problemas de desempenho. A última vez que usei o OpenGL foi por volta de 2004, por isso tenho me ensinado a usar o perfil principal e estou um pouco confuso.

Preciso desenhar 250-750 telhas 48x48 na tela a cada quadro, bem como talvez cerca de 50 sprites. As peças só mudam quando um novo nível é carregado e os sprites mudam o tempo todo. Algumas das peças são compostas por quatro peças 24x24, e a maioria (mas não todas) dos sprites é do mesmo tamanho das peças. Muitas peças e sprites usam mistura alfa.

No momento, estou fazendo tudo isso no modo imediato, o que sei ser uma má ideia. Mesmo assim, quando um dos membros de nossa equipe tenta executá-lo, ele obtém taxas de quadros muito ruins (~ 20-30 fps), e é muito pior quando há mais peças, especialmente quando muitas delas são do tipo que são cortados em pedaços. Isso tudo me faz pensar que o problema é o número de chamadas efetuadas.

Pensei em algumas soluções possíveis para isso, mas queria executá-las por algumas pessoas que sabem do que estão falando, para não perder meu tempo com algo estúpido:

AZULEJOS:

  1. Quando um nível é carregado, desenhe todas as peças uma vez em um buffer de quadro anexado a uma grande textura de buzina e apenas desenhe um grande retângulo com essa textura em cada quadro.
  2. Coloque todos os blocos em um buffer de vértice estático quando o nível for carregado e desenhe-os dessa maneira. Não sei se há uma maneira de desenhar objetos com diferentes texturas com uma única chamada para glDrawElements, ou se isso é algo que eu gostaria de fazer. Talvez apenas coloque todas as peças em uma grande textura gigante e use coordenadas de textura engraçadas na VBO?

SPRITES:

  1. Desenhe cada sprite com uma chamada separada para glDrawElements. Isso parece envolver muita troca de textura, o que me disseram que é ruim. As matrizes de textura podem ser úteis aqui?
  2. Use um VBO dinâmico de alguma forma. Mesma pergunta de textura que o número 2 acima.
  3. Sprites de ponto? Provavelmente isso é bobagem.

Alguma dessas idéias é sensata? Existe uma boa implementação em algum lugar em que eu possa examinar?


Se os ladrilhos não estiverem se movendo nem mudando e eles tiverem a mesma aparência de todo o nível, use o buffer da primeira idéia - quadro. Será mais eficiente.
Zacharmarz

Tente usar um atlas de textura para não precisar trocar as texturas, mas mantendo tudo o mesmo. Agora, como está a taxa de quadros?
precisa saber é o seguinte

Respostas:


25

A maneira mais rápida de renderizar os blocos é empacotar os dados do vértice em um VBO estático com índices (como glDrawElements indica). Gravá-lo em outra imagem é totalmente desnecessário e exigirá apenas muito mais memória. A alternância de texturas é MUITO dispendiosa, então você provavelmente desejará agrupar todos os blocos no chamado Atlas de Texturas e fornecer a cada triângulo no VBO as coordenadas de textura corretas. Com base nisso, não deve ser um problema renderizar 1000, até 100000 blocos, dependendo do seu hardware.

A única diferença entre a renderização de lado a lado e a de sprite é provavelmente que os sprites são dinâmicos. Portanto, para obter o melhor desempenho, porém fácil de realizar, basta colocar as coordenadas dos vértices do sprite em um fluxo, desenhar VBO cada quadro e desenhar com glDrawElements. Também embale todas as texturas em um Atlas de Textura. Se seus sprites raramente se moverem, você também pode tentar criar um VBO dinâmico e atualizá-lo quando um sprite se mover, mas isso é um exagero total aqui, pois você só deseja renderizar alguns sprites.

Você pode ver um pequeno protótipo que criei em C ++ com o OpenGL: Particulate

Eu renderizo aproximadamente 10000 sprites de ponto, eu acho, com uma média de fps de 400 em uma máquina comum (Quad Core @ 2,66 GHz). É limitado pela CPU, o que significa que a placa gráfica pode renderizar ainda mais. Observe que eu não uso o Texture Atlases aqui, pois só tenho uma textura para as partículas. As partículas são renderizadas com GL_POINTS e os shaders calculam o tamanho real do quad, mas acho que também existe um Quad Renderer.

Ah, e sim, a menos que você tenha um quadrado e use shaders para o mapeamento de texturas, GL_POINTS é bem bobo. ;)


Os sprites mudam de posição e qual textura estão usando, e a maioria deles faz isso a cada quadro. Além disso, sprites e sendo criados e destruídos com muita frequência. São essas coisas que um fluxo de VBO draw pode suportar?
Nic

2
O fluxo de desenho significa basicamente: "Envie esses dados para a placa gráfica e descarte-os após o desenho". Portanto, é necessário enviar os dados novamente a cada quadro e isso significa que não importa quantos sprites você renderize, qual posição eles têm, que textura coordena ou que cor. Mas enviar todos os dados de uma só vez e deixar que a GPU processe é MUITO mais rápido que o modo imediato, é claro.
Marco

Tudo isso faz sentido. Vale a pena usar um buffer de índice para isso? Os únicos vértices que serão repetidos são dois cantos de cada retângulo, certo? (O meu entendimento é que os índices são a diferença entre glDrawElements e glDrawArrays é mesmo.?)
Nic

11
Sem índices, você não pode usar GL_TRIANGLES, o que geralmente é ruim, pois esse método de desenho é aquele com melhor desempenho garantido. Além disso, a implementação do GL_QUADS está obsoleta no OpenGL 3.0 (fonte: stackoverflow.com/questions/6644099/… ). Triângulos são a malha nativa de qualquer placa gráfica. Portanto, você "usa" mais 2 * 6 bytes para salvar 2 execuções de shader de vértice e vertex_size * 2 bytes. Então, você geralmente pode dizer que é SEMPRE melhor.
Marco

2
O link para Particulate está morto ... Você poderia fornecer um novo, por favor?
SWdV

4

Mesmo com esse número de chamadas de empate, você não deve ver esse tipo de queda de desempenho - o modo imediato pode ser lento, mas não é tão lento (para referência, até o querido e velho Quake pode gerenciar vários milhares de chamadas de modo imediato por quadro sem cair tão mal).

Suspeito que haja algo mais interessante acontecendo aqui. A primeira coisa que você precisa fazer é investir algum tempo na criação de perfil do seu programa, caso contrário, você corre um risco enorme de re-arquitetar com base em uma suposição que pode resultar em ganho de desempenho zero. Portanto, execute-o em algo tão básico quanto o GLIntercept e veja para onde está indo o seu tempo. Com base nos resultados, você poderá solucionar o problema com algumas informações reais sobre quais são seus principais gargalos.


Eu fiz alguns perfis, embora seja estranho porque os problemas de desempenho não estão acontecendo na mesma máquina que o desenvolvimento. Estou um pouco cético de que o problema esteja em outro lugar, porque os problemas definitivamente aumentam com o número de peças, e as peças literalmente não fazem nada além de serem desenhadas.
Nic

Que tal mudanças de estado então? Você está agrupando suas peças opacas por estado?
Maximus Minimus

Essa é uma possibilidade. Definitivamente, isso merece mais atenção da minha parte.
Nic

2

Tudo bem, já que minha última resposta ficou fora de controle aqui é uma nova que talvez seja mais útil.


Sobre o desempenho 2D

Primeiro, alguns conselhos gerais: o 2D não é exigente para o hardware atual, mesmo o código não otimizado funcionará. No entanto, isso não significa que você deva usar o Modo Intermediário, pelo menos, para não alterar os estados quando desnecessário (por exemplo, não vincule uma nova textura ao glBindTexture quando a mesma textura já estiver vinculada, e se a CPU estiver em toneladas mais rápido que uma chamada glBindTexture) e não usar algo totalmente errado e estúpido como o glVertex (mesmo o glDrawArrays será muito mais rápido e não será mais difícil de usar, embora não seja muito "moderno"). Com essas duas regras muito simples, o tempo de quadro deve ser de pelo menos 10ms (100 fps). Agora, para obter ainda mais velocidade, o próximo passo lógico é fazer o lote, por exemplo, agrupar tantas chamadas de empate em uma, para isso você deve considerar a implementação de atlas de textura, para minimizar a quantidade de vinculações de textura e aumentar a quantidade de retângulos que você pode desenhar com uma chamada em grande quantidade. Se agora você não está com menos de 2ms (500fps), está fazendo algo errado :)


Mapas lado a lado

A implementação do código de desenho para mapas de blocos é encontrar o equilíbrio entre flexibilidade e velocidade. Você pode usar VBOs estáticos, mas isso não funcionará com blocos animados ou você pode apenas gerar os dados de vértice a cada quadro e aplicar as regras que expliquei acima, isso é muito flexível, mas de longe não tão rápido.

Na minha resposta anterior, introduzi um modelo diferente no qual o shader de fragmento cuida de toda a texturização, mas foi apontado que ele requer uma pesquisa de textura dependente e, portanto, pode não ser tão rápido quanto os outros métodos. (A idéia é basicamente que você faça o upload apenas das indicações do bloco e, no shader do fragmento, calcule as coordenadas da textura, o que significa que você pode desenhar o mapa inteiro com apenas um retângulo)


Sprites

Os sprites exigem muita flexibilidade, dificultando a otimização, além dos discutidos na seção "Sobre o desempenho 2D". E a menos que você queira dez milhares de sprites na tela ao mesmo tempo, provavelmente não vale o esforço.


11
E mesmo se você tem milhares dez de sprites, hardware moderno deve executá-lo a uma velocidade decente :)
Marco

@ API-Besta espera o que? como você calcula a textura UV no shader de fragmento? Você não deveria enviar os UVs para o shader de fragmentos?
HgMerk

0

Se todo o resto falhar...

Configure um método de desenho de flip-flop. Atualize apenas todos os outros sprites de cada vez. Embora, mesmo com o VisualBasic6 e métodos simples, você possa desenhar ativamente milhares de sprites por quadro. Talvez você deva procurar esses métodos, pois seu método direto de desenhar sprites parece estar falhando. (Parece mais que você está usando um "método de renderização", mas tentando usá-lo como um "método de jogo". Renderizar é sobre clareza, não velocidade.)

As chances são de que você esteja constantemente redesenhando a tela inteira repetidamente. Em vez de apenas redesenhar apenas as áreas alteradas. Isso é muita sobrecarga. O conceito é simples, mas não fácil de entender.

Use um buffer para o fundo estático virgem. Isso nunca é renderizado, a menos que não haja sprites na tela. Isso é usado constantemente para "reverter" o local onde um sprite foi desenhado, para redesenhar o sprite na próxima chamada. Você também precisa de um buffer para "desenhar", que não é a tela. Você desenha lá, então, uma vez desenhado, você o joga na tela, uma vez. Essa deve ser uma chamada de tela por todos os seus sprites. (Em vez de desenhar cada sprite na tela, um de cada vez, ou tentar fazer tudo de uma vez, o que fará com que sua mistura alfa falhe.) A gravação na memória é rápida e não requer tempo de tela para "desenhar". " Cada chamada de espera aguardará um sinal de retorno antes de tentar novamente. (Não é um v-sync, um tick de hardware real, que é muito mais lento que o tempo de espera da RAM.)

Imagino que seja parte da razão pela qual você vê esse problema apenas em um computador. Ou está voltando à renderização de software do ALPHA-BLEND, que não suporta todos os cartões. Você verifica se esse recurso é suportado por hardware antes de tentar usá-lo? Você tem um fallback (modo não-alpha-blend), se ele não o tiver? Obviamente, você não possui códigos com limites (número de itens combinados), pois eu presumo que isso prejudicaria o conteúdo do jogo. (Diferentemente, se esses eram apenas efeitos de partículas, todos misturados com alfa e, portanto, por que os programadores os limitam, pois são altamente exigentes na maioria dos sistemas, mesmo com suporte de hardware.)

Por fim, sugiro limitar o que você está misturando com alfa, apenas às coisas que precisam dele. Se tudo precisar ... Você não tem escolha a não ser exigir que seus usuários tenham melhores requisitos de hardware ou degradar o jogo para o desempenho desejado.


-1

Crie uma folha de sprite para objetos e um conjunto de peças para o terreno, como faria em outro jogo 2D, não há necessidade de mudar as texturas.

A renderização de blocos pode ser um problema, pois cada par de triângulos precisa de suas próprias coordenadas de textura. Há uma solução para esse problema, no entanto, é chamado de renderização instanciada .

Contanto que você possa classificar seus dados de maneira que, por exemplo, você possa ter uma lista de blocos de grama e suas posições, você possa renderizar cada bloco de grama com uma única chamada de desenho, tudo o que você precisa fazer é fornecer uma matriz do modelo para matrizes mundiais para cada bloco. Classificar seus dados dessa maneira não deve ser um problema, mesmo com o gráfico de cena mais simples.


-1: Instanciar é uma ideia pior do que a solução de shader puro do Sr. Beast. A instância funciona melhor para o desempenho ao renderizar objetos de complexidade moderada (aproximadamente 100 triângulos). Cada bloco de triângulo que precisa de coordenadas de textura não é um problema. Você acabou de criar uma malha com vários quadriláteros soltos que formam um mapa de blocos.
Nicol Bolas

11
@NicolBolas Tudo bem, eu vou deixar a resposta para uma questão de aprender
dreta

11
Para maior clareza, Nicol Bolas, qual é a sua sugestão de como lidar com tudo isso? O fluxo de Marco desenhar coisa? Existe algum lugar em que eu possa ver uma implementação disso?
Nic

@ Nic: O streaming para objetos de buffer não é um código particularmente complexo. Mas sério, se você está falando apenas de 50 despeitos, isso não é nada . As probabilidades são boas de que o seu desenho de terreno estava causando o problema de desempenho, portanto, mudar para buffers estáticos para isso provavelmente seria bom o suficiente.
Nicol Bolas

Na verdade, se o instanciamento funcionasse como poderíamos pensar, seria a melhor solução - mas como não funciona, agrupar todas as instâncias em um único vbo estático é o caminho a seguir.
Jari Komppa
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.