Comment puis-je améliorer les vitesses de rendu d'un jeu de type Voxel / Minecraft?


35

J'écris mon propre clone de Minecraft (également écrit en Java). Cela fonctionne très bien en ce moment. Avec une distance de visualisation de 40 mètres, je peux facilement atteindre 60 ips sur mon MacBook Pro 8,1. (Intel i5 + Intel HD Graphics 3000). Mais si je mets la distance de vision sur 70 mètres, je n’atteins que 15-25 FPS. Dans le vrai Minecraft, je peux mettre le désordre de visionnage loin (= 256m) sans problème. Ma question est donc: que dois-je faire pour améliorer mon jeu?

Les optimisations que j'ai mises en place:

  • Ne conservez que des morceaux locaux en mémoire (en fonction de la distance de visualisation du lecteur)
  • Frustum culling (d'abord sur les morceaux, puis sur les blocs)
  • Dessiner seulement les faces vraiment visibles des blocs
  • Utilisation de listes par morceau contenant les blocs visibles. Les morceaux qui deviennent visibles s'ajouteront à cette liste. S'ils deviennent invisibles, ils sont automatiquement supprimés de cette liste. Les blocs deviennent (in) visibles en construisant ou en détruisant un bloc voisin.
  • Utilisation de listes par morceau contenant les blocs de mise à jour. Même mécanisme que les listes de blocs visibles.
  • N'utilisez presque pas d' newénoncés dans la boucle de jeu. (Mon jeu dure environ 20 secondes jusqu'à ce que le récupérateur de place soit appelé)
  • J'utilise les listes d'appels OpenGL pour le moment. ( glNewList(), glEndList(), glCallList()) Pour chaque côté d'un type de bloc.

Actuellement, je n'utilise même pas de système d'éclairage. J'ai déjà entendu parler de VBO. Mais je ne sais pas exactement ce que c'est. Cependant, je vais faire des recherches à leur sujet. Est-ce qu'ils vont améliorer les performances? Avant d'implémenter les VBO, je veux essayer d'utiliser glCallLists()et de transmettre une liste de listes d'appels. Au lieu d'utiliser mille fois glCallList(). (Je veux essayer ceci, parce que je pense que le vrai MineCraft n'utilise pas de VBO. Correct?)

Existe-t-il d'autres astuces pour améliorer les performances?

Le profilage VisualVM m'a montré ceci (profilage pour seulement 33 images, avec une distance de visualisation de 70 mètres):

entrez la description de l'image ici

Profiler avec 40 mètres (246 images):

entrez la description de l'image ici

Remarque: Je synchronise beaucoup de méthodes et de blocs de code, car je génère des fragments dans un autre thread. Je pense qu'acquérir un verrou pour un objet est un problème de performance lorsqu'il effectue autant de choses dans une boucle de jeu (bien sûr, je parle du moment où il n'y a que la boucle de jeu et aucun nouveau fragment n'est généré). Est-ce correct?

Edit: Après avoir supprimé quelques synchronisedblocs et quelques autres petites améliorations. La performance est déjà bien meilleure. Voici mes nouveaux résultats de profilage avec 70 mètres:

entrez la description de l'image ici

Je pense qu'il est assez clair que selectVisibleBlocksc'est le problème ici.

Merci d'avance!
Martijn

Mise à jour : après quelques améliorations supplémentaires (utilisation de boucles for à la place de chacune, mise en mémoire tampon des variables en dehors des boucles, etc.), je peux maintenant parcourir la distance de visualisation 60 plutôt bien.

Je pense que je vais mettre en œuvre les VBO dès que possible.

PS: tout le code source est disponible sur GitHub:
https://github.com/mcourteaux/CraftMania


2
Pouvez-vous nous donner une photo de profil à 40m afin que nous puissions voir ce qui pourrait être mis à l'échelle plus rapidement qu'un autre?
James

Peut-être que trop spécifié, mais si vous considérez, demande juste des techniques pour accélérer un jeu en 3D, son intéressant. Mais le titre peut faire peur à ppl.
Gustavo Maciel

@Gtoknu: Que proposez-vous comme titre?
Martijn Courteaux le

5
Selon qui vous demandez, certaines personnes diront que Minecraft n’est vraiment pas aussi rapide.
jeudi

Je pense que quelque chose comme "Quelles techniques peuvent accélérer un jeu en 3D" devrait être beaucoup mieux. Pensez à quelque chose, mais essayez de ne pas utiliser le mot "meilleur" ou d'essayer de vous comparer à un autre jeu. Nous ne pouvons pas dire exactement ce qu'ils utilisent sur certains jeux.
Gustavo Maciel

Réponses:


15

Vous mentionnez la suppression sélective de blocs individuels - essayez de le jeter. La plupart des blocs de rendu doivent être entièrement visibles ou totalement invisibles.

Minecraft ne reconstruit qu'un tampon liste / sommets d'affichage (je ne sais pas lequel il utilise) lorsqu'un bloc est modifié dans un bloc donné, et moi aussi . Si vous modifiez la liste d'affichage chaque fois que l'affichage change, vous ne bénéficiez pas des listes d'affichage.

En outre, vous semblez utiliser des morceaux de taille mondiale. Notez que Minecraft utilise des blocs cubiques 16 × 16 × 16 pour ses listes d’affichage, contrairement au chargement et à la sauvegarde. Si vous faites cela, il y a encore moins de raisons de supprimer des morceaux individuels.

(Remarque: je n'ai pas examiné le code de Minecraft. Toute cette information est du ouï-dire ou bien ma propre conclusion de l'observation du rendu de Minecraft pendant que je joue.)


Conseil plus général:

N'oubliez pas que votre rendu s'exécute sur deux processeurs: le processeur et le processeur graphique. Lorsque votre fréquence d'images est insuffisante, l' une ou l'autre des ressources est la ressource limitante - votre programme est lié au processeur ou au processeur graphique (en supposant qu'il ne permute pas ou n'ait pas de problèmes de planification).

Si votre programme fonctionne à 100% du processeur (et qu'aucune autre tâche non liée ne se termine), votre processeur effectue trop de travail. Vous devriez essayer de simplifier sa tâche (par exemple, faire moins de cueillage) en échange de faire en sorte que le GPU en fasse plus. Je soupçonne fortement que c'est votre problème, compte tenu de votre description.

D'un autre côté, si le GPU est la limite (malheureusement, il n'y a généralement pas de moniteur de charge pratique à 0% -100%), vous devriez réfléchir à la manière de l'envoyer moins de données ou de l'exiger pour remplir moins de pixels.


2
Excellente référence, vos recherches à ce sujet mentionnées sur votre wiki m'ont été très utiles! +1
Gustavo Maciel

@OP: rend uniquement les faces visibles (pas les blocs ). Un bloc pathologique mais monotone de 16 x 16 x 16 aura près de 800 faces visibles, tandis que les blocs contenus contiendront 24 000 faces visibles. Une fois que vous avez fait cela, la réponse de Kevin contient les améliorations les plus importantes suivantes.
AndrewS

@KevinReid Il existe certains programmes pour aider au débogage des performances. AMD GPU PerfStudio, par exemple, vous indique si son processeur ou son processeur graphique est lié et quel est le composant lié à ce processeur (texture vs fragment vs sommet, etc.). Et je suis sûr que Nvidia a aussi quelque chose de similaire.
akaltar

3

Comment appelle-t-on tellement Vec3f.set? Si vous construisez ce que vous voulez rendre à partir de zéro à chaque image, vous voudrez certainement commencer à l’accélérer. Je ne suis pas un grand utilisateur OpenGL et je ne connais pas beaucoup le rendu de Minecraft, mais il semble que les fonctions mathématiques que vous utilisez vous tuent en ce moment (regardez le temps que vous y passez et le nombre de fois que vous y passez). on les appelle - la mort par mille coupures les appelant).

Idéalement, votre monde serait segmenté de manière à ce que vous puissiez regrouper les éléments à rendre ensemble, en créant des objets Vertex Buffer et en les réutilisant dans plusieurs cadres. Vous n'auriez besoin de modifier un VBO que si le monde qu'il représente change d'une manière ou d'une autre (comme l'utilisateur le modifie). Vous pouvez ensuite créer / détruire des VBO correspondant à ce que vous représentez, car cela permet de réduire la consommation de mémoire. Vous ne feriez que prendre la mesure, car le VBO a été créé plutôt que chaque image.

Si le nombre d'invocations est correct dans votre profil, vous appelez un nombre incalculable de fois. (10 millions d'appels vers Vec3f.set ... ouch!)


J'utilise cette méthode pour des tonnes de choses. Il définit simplement les trois valeurs pour le vecteur. C'est beaucoup mieux que d'allouer à chaque fois un nouvel objet.
Martijn Courteaux le

2

Ma description (de ma propre expérimentation) ici est applicable:

Pour le rendu en voxels, quoi de plus efficace: un VBO préfabriqué ou un shader de géométrie?

Minecraft et votre code utilisent probablement le pipeline de fonctions fixes; mes propres efforts ont été avec GLSL mais l'essentiel est généralement applicable, je sens:

(De mémoire) J'ai créé un tronc d'un demi-bloc plus grand que celui de l'écran. J'ai ensuite testé les points centraux de chaque morceau ( minecraft a 16 * 16 * 128 blocs ).

Les faces dans chacune ont des étendues dans un VBO d’éléments (de nombreuses faces de morceaux partagent le même VBO jusqu’à ce qu’il soit «plein»; pensez comme malloccelles ayant la même texture dans le même VBO si possible) et les indices de sommet pour le nord. les faces, les faces sud, etc., sont adjacentes plutôt que mélangées. Quand je dessine, je fais un glDrawRangeElementspour les faces nord, avec la normale déjà projetée et normalisée, dans un uniforme. Ensuite, je fais les faces sud et ainsi de suite, donc les normales ne figurent dans aucun VBO. Pour chaque morceau, je n'ai qu'à émettre les visages qui seront visibles - seuls ceux au centre de l'écran doivent dessiner les côtés gauche et droit, par exemple; c'est simple GL_CULL_FACEau niveau de l'application.

La plus grande accélération, iirc, consistait à éliminer les faces intérieures lors de la polygonisation de chaque bloc.

Il est également important de gérer les atlas de texture , de trier les faces par texture et de placer les faces de même texture dans le même vbo que celles des autres morceaux. Vous souhaitez éviter de trop modifier la texture et de trier les faces par texture, etc. afin de réduire le nombre d'étendues dans le fichier glDrawRangeElements. La fusion de faces adjacentes de même mosaïque en de plus grands rectangles était également un problème important. Je parle de la fusion dans l'autre réponse citée ci-dessus.

Évidemment, vous ne polygonisez que les morceaux qui ont déjà été visibles, vous pouvez supprimer les morceaux qui ne sont pas visibles depuis longtemps, et vous polygamiser de nouveau les morceaux qui ont été édités (car il s'agit d'un événement rare par rapport à leur rendu).


J'aime l'idée de l'optimisation de votre frustum. Mais ne confondez-vous pas les termes "bloc" et "bloc" dans votre explication?
Martijn Courteaux

Probablement oui. Un bloc de blocs est un bloc de blocs en anglais.
Will

1

D'où proviennent toutes vos comparaisons ( BlockDistanceComparator)? S'il s'agit d'une fonction de tri, pourrait-elle être remplacée par une sorte de base (qui est asymptotiquement plus rapide et non basée sur la comparaison)?

Si vous examinez vos horaires, même si le tri en soi n’est pas si mauvais, votre relativeToOriginfonction est appelée deux fois pour chaque comparefonction; toutes ces données doivent être calculées une fois. Il devrait être plus rapide de trier une structure auxiliaire, par exemple

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

puis en 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 ]

Désolé si ce n'est pas une structure Java valide (je n'ai pas touché Java depuis le premier cycle), mais j'espère que vous aurez l'idée.


Je trouve ça amusant. Java n'a pas de structure. Eh bien, il y a quelque chose qui s'appelle comme ça dans le monde Java mais cela a à voir avec les bases de données, pas la même chose du tout. Ils peuvent créer un dernier cours avec des membres du public, je suppose que cela fonctionne.
Théraot

1

Oui, utilisez les VBO et CULL, mais cela vaut pour presque tous les matchs. Ce que vous voulez faire, c'est ne rendre le cube que s'il est visible par le joueur, ET si les blocs se touchent d'une manière spécifique (disons un bloc que vous ne pouvez pas voir parce qu'il est souterrain), vous ajoutez les sommets des blocs et faites c'est presque comme un "plus gros bloc", ou dans votre cas, un morceau. Cela s'appelle un maillage gourmand et augmente considérablement les performances. Je développe un jeu (basé sur voxel) qui utilise un algorithme de maillage gourmand.

Au lieu de tout rendre comme ceci:

rendre

Cela le rend comme ceci:

rendre2

L'inconvénient à ceci est que vous devez faire plus de calculs par bloc lors de la construction initiale du monde, ou si le joueur supprime / ajoute un bloc.

à peu près tout type de moteur voxel en a besoin pour de bonnes performances.

Cela vérifie si la face du bloc touche une autre face du bloc, et si c'est le cas: ne restitue que comme une (ou zéro) face (s) du bloc. C'est une touche coûteuse lorsque vous rendez des morceaux très rapidement.

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])));
                        }
                    }
                }
            }
        }
    }
}

1
Et ça vaut le coup? Il semble qu'un système de LOD serait plus approprié.
MichaelHouse

0

Il semblerait que votre code soit noyé dans des objets et des appels de fonction. À en juger par les chiffres, il ne semble pas qu’il y ait d’intronisation.

Vous pouvez essayer de trouver un environnement Java différent ou simplement jouer avec les paramètres de celui que vous avez, mais un moyen simple et rapide de rendre votre code non pas rapide, mais beaucoup moins lent est au moins en interne dans Vec3f pour arrêter codant OOO *. Faites en sorte que chaque méthode soit autonome, n'appelez aucune des autres méthodes uniquement pour effectuer une tâche quelconque.

Edit: Bien qu’il y ait une surcharge partout, il semblerait que commander les blocs avant le rendu est le pire casse-tête. Est-ce vraiment nécessaire? Si tel est le cas, vous devriez probablement commencer par parcourir une boucle et calculer chaque distance entre les blocs jusqu'à l'origine, puis effectuer un tri.

* Trop orienté objet


Oui, vous économiserez de la mémoire, mais vous perdrez du processeur! So OOO n'est pas très bon dans les jeux en temps réel.
Gustavo Maciel

Dès que vous commencez à profiler (et pas seulement à échantillonner), toute insertion dans la JVM disparaît normalement. C'est un peu comme la théorie quantique, on ne peut pas mesurer quelque chose sans changer le résultat: p
Michael

@Gtoknu Ce n'est pas toujours vrai, à un certain niveau de OOO, les appels de fonction commencent à utiliser plus de mémoire que le code en ligne. Je dirais qu'il y a une bonne partie du code en question qui tourne autour du seuil de rentabilité pour la mémoire.
aaaaaaaaaaaa

0

Vous pouvez également essayer de décomposer les opérations mathématiques en opérateurs au niveau des bits. Si vous avez 128 / 16, essayez de faire un opérateur de bits: 128 << 4. Cela aidera beaucoup avec vos problèmes. N'essayez pas de faire tourner les choses à toute vitesse. Mettez votre jeu à jour à un rythme de 60 ou quelque chose d’autre, et décomposez-le pour d’autres choses, mais vous devrez détruire et / ou placer des voxels ou créer une liste de tâches qui réduirait votre fps. Vous pouvez faire un taux de mise à jour d'environ 20 pour les entités. Et quelque chose comme 10 pour les mises à jour mondiales et / ou la génération.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.