Como posso melhorar a velocidade de renderização de um jogo do tipo Voxel / Minecraft?

35

Estou escrevendo meu próprio clone do Minecraft (também escrito em Java). Funciona muito bem agora. Com uma distância de visualização de 40 metros, posso facilmente atingir 60 FPS no meu MacBook Pro 8,1. (Intel i5 + Intel HD Graphics 3000). Mas se eu colocar a distância de visualização em 70 metros, alcanço apenas 15-25 FPS. No Minecraft real, posso colocar a disnância de visualização longe (= 256m) sem nenhum problema. Então, minha pergunta é o que devo fazer para melhorar meu jogo?

As otimizações que implementei:

  • Mantenha apenas partes locais na memória (dependendo da distância de visualização do player)
  • Seleção de Frustum (primeiro nos pedaços, depois nos blocos)
  • Desenhando apenas faces realmente visíveis dos blocos
  • Usando listas por pedaço que contêm os blocos visíveis. Os pedaços que ficarem visíveis serão adicionados a esta lista. Se ficarem invisíveis, serão automaticamente removidos desta lista. Os blocos se tornam (in) visíveis construindo ou destruindo um bloco vizinho.
  • Usando listas por bloco que contêm os blocos de atualização. Mesmo mecanismo que as listas de bloqueio visíveis.
  • Não use quase nenhuma newinstrução dentro do loop do jogo. (Meu jogo dura cerca de 20 segundos até que o Garbage Collector seja chamado)
  • Estou usando as listas de chamadas do OpenGL no momento. ( glNewList(), glEndList(), glCallList()) Para cada lado de um tipo de bloco.

Atualmente, nem estou usando nenhum tipo de sistema de iluminação. Já ouvi falar de VBO's. Mas não sei exatamente o que é. No entanto, vou fazer algumas pesquisas sobre eles. Eles melhorarão o desempenho? Antes de implementar as VBOs, quero tentar usar glCallLists()e passar uma lista de listas de chamadas. Em vez disso, use milhares de vezes glCallList(). (Eu quero tentar isso, porque acho que o MineCraft real não usa VBOs. Correto?)

Existem outros truques para melhorar o desempenho?

A criação de perfil do VisualVM me mostrou isso (criação de perfil para apenas 33 quadros, com uma distância de visualização de 70 metros):

insira a descrição da imagem aqui

Criação de perfil com 40 metros (246 quadros):

insira a descrição da imagem aqui

Nota: Estou sincronizando muitos métodos e blocos de código, porque estou gerando pedaços em outro encadeamento. Eu acho que a aquisição de um bloqueio para um objeto é um problema de desempenho ao fazer isso em um loop do jogo (é claro, estou falando do momento em que existe apenas o loop do jogo e nenhum novo bloco é gerado). Isto está certo?

Edit: Depois de remover alguns synchronisedblocos e outras pequenas melhorias. O desempenho já é muito melhor. Aqui estão meus novos resultados de criação de perfil com 70 metros:

insira a descrição da imagem aqui

Eu acho que é bem claro que selectVisibleBlocksé o problema aqui.

Desde já, obrigado!
Martijn

Atualização : Após algumas melhorias extras (como usar loops no lugar de cada um, armazenar em buffer variáveis ​​fora dos loops, etc ...), agora posso executar a distância de visualização 60 muito bem.

Acho que vou implementar o VBO o mais rápido possível.

PS: Todo o código-fonte está disponível no GitHub:
https://github.com/mcourteaux/CraftMania

Martijn Courteaux
fonte
2
Você pode nos dar uma foto de perfil a 40m para que possamos ver o que pode estar aumentando mais rapidamente do que outro?
James
Talvez muito especificado, mas se você considerar, está apenas perguntando técnicas de como acelerar um jogo em 3D, parece interessante. Mas o título pode assustar pessoas.
Gustavo Maciel
@Gtoknu: O que você sugere como título?
Martijn Courteaux 19/01/12
5
Dependendo de quem você pergunta, algumas pessoas diriam que o Minecraft também não é tão rápido assim.
Thedaian
Eu acho que algo como "Quais técnicas podem acelerar um jogo em 3D" deve ser muito melhor. Pense em algo, mas tente não usar a palavra "melhor" ou tente comparar com outro jogo. Não podemos dizer exatamente o que eles usam em alguns jogos.
Gustavo Maciel

Respostas:

15

Você menciona fazer frustum selecionando blocos individuais - tente jogar isso fora. A maioria dos pedaços de renderização deve ser totalmente visível ou totalmente invisível.

O Minecraft reconstrói apenas um buffer de lista / vértice de exibição (não sei qual ele usa) quando um bloco é modificado em um determinado pedaço, e eu também . Se você estiver modificando a lista de exibição sempre que a exibição for alterada, não terá o benefício das listas de exibição.

Além disso, você parece estar usando pedaços de altura mundial. Observe que o Minecraft usa blocos cúbicos 16 × 16 × 16 para suas listas de exibição, ao contrário de carregar e salvar. Se você fizer isso, há ainda menos razões para selecionar pedaços individuais.

(Nota: eu não examinei o código do Minecraft. Todas essas informações são boatos ou minhas próprias conclusões de observar a renderização do Minecraft enquanto eu jogo.)


Mais conselhos gerais:

Lembre-se de que sua renderização é executada em dois processadores: CPU e GPU. Quando sua taxa de quadros é insuficiente, um ou outro é o recurso limitador - seu programa é vinculado à CPU ou à GPU (supondo que não esteja trocando ou tendo problemas de agendamento).

Se o seu programa estiver sendo executado com 100% da CPU (e não tiver uma outra tarefa ilimitada para concluir), sua CPU estará fazendo muito trabalho. Você deve tentar simplificar sua tarefa (por exemplo, fazer menos descarte) em troca de fazer com que a GPU faça mais. Suspeito fortemente que esse seja o seu problema, dada sua descrição.

Por outro lado, se a GPU é o limite (infelizmente, geralmente não há monitores de carga convenientes de 0% a 100%), você deve pensar em como enviar menos dados ou exigir que preencha menos pixels.

Kevin Reid
fonte
2
Ótima referência, sua pesquisa mencionada em seu wiki foi muito útil para mim! 1
Gustavo Maciel
@OP: apenas renderiza faces visíveis (não blocos ). Um pedaço patológico, mas monotônico, de 16x16x16 terá quase 800 faces visíveis, enquanto os blocos contidos terão 24.000 faces visíveis. Depois de fazer isso, a resposta de Kevin contém as próximas melhorias mais importantes.
Andrews
@KevinReid Existem alguns programas para ajudar na depuração de desempenho. O AMD GPU PerfStudio, por exemplo, informa se sua CPU ou GPU está ligada e na GPU qual componente é o limite (textura versus fragmento versus vértice, etc.) E tenho certeza que a Nvidia também tem algo semelhante.
Akaltar 07/07/2015
3

O que chama tanto o Vec3f.set? Se você está construindo o que deseja renderizar do zero a cada quadro, é definitivamente aí que você gostaria de começar a acelerar. Eu não sou muito usuário de OpenGL e não sei muito sobre como o Minecraft é processado, mas parece que as funções matemáticas que você está usando estão acabando com você agora (veja quanto tempo você gasta nelas e o número de vezes eles são chamados - morte por mil cortes chamando-os).

Idealmente, seu mundo seria segmentado para que você possa agrupar coisas para renderizar, construindo Objetos de buffer do vértice e reutilizando-os em vários quadros. Você só precisará modificar um VBO se o mundo que ele representa mudar de alguma forma (como o usuário o edita). Em seguida, você pode criar / destruir VBOs para o que está representando, visto que ele é visível para manter o consumo de memória baixo; você seria atingido apenas quando o VBO foi criado, em vez de todos os quadros.

Se a contagem de "invocação" estiver correta no seu perfil, você estará chamando muitas coisas muitas vezes. (10 milhões de chamadas para Vec3f.set ... ai!)

Roger Perkins
fonte
Eu uso esse método para toneladas de coisas. Simplesmente define os três valores para o vetor. Isso é muito melhor do que alocar cada vez que um novo objeto.
Martijn Courteaux
2

Minha descrição (de minha própria experimentação) aqui é aplicável:

Para renderização de voxel, o que é mais eficiente: VBO pré-fabricado ou um sombreador de geometria?

O Minecraft e seu código provavelmente usam o pipeline de função fixo; meus próprios esforços foram com GLSL, mas a essência é geralmente aplicável, eu sinto:

(De memória) criei um frustum meio bloco maior que o frustum da tela. Depois testei os pontos centrais de cada bloco (o minecraft possui 16 * 16 * 128 blocos ).

As faces de cada uma possuem extensões em uma VBO de matriz de elementos (muitas faces de partes compartilham a mesma VBO até ficar 'cheia'; pense como malloc; aquelas com a mesma textura na mesma VBO sempre que possível) e os índices de vértices para o norte faces, faces sul e assim por diante são adjacentes ao invés de misturadas. Quando desenho, faço um glDrawRangeElementspara as faces norte, com o normal já projetado e normalizado, em um uniforme. Então eu faço as faces sul e assim por diante, para que os normais não estejam em nenhum VBO. Para cada pedaço, eu só tenho que emitir as faces que serão visíveis - somente as que estão no centro da tela precisam desenhar os lados esquerdo e direito, por exemplo; isso é simples GL_CULL_FACEno nível do aplicativo.

A maior aceleração, o iirc, foi selecionar faces internas ao poligonizar cada pedaço.

Também é importante o gerenciamento do atlas de textura e a classificação de faces por textura e a colocação das faces com a mesma textura no mesmo vbo que as de outros blocos. Você deseja evitar muitas alterações de textura e classificar as faces por textura e assim por diante minimiza o número de extensões na glDrawRangeElements. Mesclar faces adjacentes do mesmo mosaico em retângulos maiores também foi importante. Eu falo sobre a fusão na outra resposta citada acima.

Obviamente, você poligoniza apenas os pedaços que já foram visíveis, você pode descartar aqueles que não são visíveis há muito tempo e poligonalizar os pedaços que são editados (pois essa é uma ocorrência rara em comparação à renderização).

Vai
fonte
Gosto da ideia da sua otimização de frustum. Mas você não está misturando os termos "bloco" e "parte" em sua explicação?
Martijn Courteaux 20/01/12
provavelmente sim. Um bloco de blocos é um bloco de blocos em inglês.
Will
1

De onde vêm todas as suas comparações ( BlockDistanceComparator)? Se for de uma função de classificação, isso poderia ser substituído por uma classificação radix (que é assintoticamente mais rápida e não baseada em comparação)?

Observando seus horários, mesmo que a classificação em si não seja tão ruim, sua relativeToOriginfunção está sendo chamada duas vezes para cada comparefunção; todos esses dados devem ser calculados uma vez. Deve ser mais rápido classificar uma estrutura auxiliar, por exemplo

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

e depois no pseudoCode

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

Desculpe se essa não é uma estrutura Java válida (eu não toquei em Java desde a graduação), mas espero que você entenda.

celion
fonte
Eu acho isso divertido. Java não possui estruturas. Bem, há algo assim chamado no mundo Java, mas tem a ver com bancos de dados, não é a mesma coisa. Eles podem criar uma aula final com membros do público, acho que funciona.
Theraot
1

Sim, use VBOs e CULL, mas isso vale para praticamente todos os jogos. O que você quer fazer é renderizar o cubo apenas se estiver visível para o jogador, E se os blocos estiverem tocando de uma maneira específica (digamos, um pedaço que você não pode ver porque é subterrâneo), adicione os vértices dos blocos e faça quase como um "bloco maior", ou no seu caso - um pedaço. Isso é chamado de malha gulosa e aumenta drasticamente o desempenho. Estou desenvolvendo um jogo (baseado em voxel) e ele usa um algoritmo de malha ganancioso.

Em vez de renderizar tudo assim:

render

Renderiza assim:

render2

A desvantagem disso é que você precisa fazer mais cálculos por bloco na construção inicial do mundo, ou se o jogador remover / adicionar um bloco.

praticamente qualquer tipo de mecanismo voxel precisa disso para um bom desempenho.

O que faz é verificar se a face do bloco está tocando em outra face do bloco e, se estiver: renderize apenas como uma (ou zero) face (s) do bloco. É um toque caro quando você está processando pedaços muito rápido.

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}
Liam Larsen
fonte
11
E Vale a pena? Parece que um sistema LOD seria mais apropriado.
MichaelHouse
0

Parece que seu código está se afogando em objetos e chamadas de função. Medindo os números, não parece que ocorra algo interno.

Você pode tentar encontrar um ambiente Java diferente ou simplesmente mexer com as configurações que você possui, mas uma maneira simples e simples de criar seu código, não rápida, mas muito menos lenta é pelo menos internamente no Vec3f para parar codificação OOO *. Torne todos os métodos independentes, não chame nenhum dos outros métodos apenas para realizar alguma tarefa servil.

Edit: Embora exista sobrecarga em todo o lugar, parece que ordenar os blocos antes da renderização é o pior comedor de desempenho. Isso é mesmo necessário? Nesse caso, você provavelmente deve começar fazendo um loop e calcular a distância de cada bloco até a origem e, em seguida, classificar por isso.

* Orientado a objetos excessivamente

aaaaaaaaaaaa
fonte
Sim, você economizará memória, mas perderá a CPU! Portanto, o OOO não é muito bom em jogos em tempo real.
Gustavo Maciel
Assim que você inicia a criação de perfil (e não apenas a amostragem), qualquer indicação de que a JVM normalmente desaparece. É como a teoria quântica, não pode medir algo sem perseguir o resultado: p
Michael
@ Gtoknu Isso não é universalmente verdade; em algum nível de OOO, as chamadas de função começam a ocupar mais memória do que o código embutido. Eu diria que há uma boa parte do código em questão que está em torno do ponto de equilíbrio da memória.
Aaaaaaaaaaa
0

Você também pode tentar dividir as operações matemáticas em operadores bit a bit. Se você tem 128 / 16, tentar fazer um operador bit a bit: 128 << 4. Isso ajudará muito com seus problemas. Não tente fazer as coisas correrem a toda velocidade. Faça a atualização do seu jogo a uma taxa de 60 ou algo assim, e até divida isso em outras coisas, mas você teria que destruir e / ou colocar voxels ou teria que fazer uma lista de tarefas, o que reduziria seus fps. Você pode fazer uma taxa de atualização de cerca de 20 para entidades. E algo como 10 para atualizações mundiais e / ou geração.

JBakker
fonte