Wie kann ich die Rendergeschwindigkeit eines Spiels vom Typ Voxel / Minecraft verbessern?

35

Ich schreibe meinen eigenen Klon von Minecraft (auch in Java geschrieben). Es funktioniert gerade großartig. Mit einem Betrachtungsabstand von 40 Metern kann ich mit meinem MacBook Pro 8,1 problemlos 60 FPS erreichen. (Intel i5 + Intel HD Graphics 3000). Wenn ich den Betrachtungsabstand auf 70 Meter stelle, erreiche ich nur 15-25 FPS. In der realen Minecraft kann ich die Sichtweite (= 256m) problemlos einstellen. Meine Frage ist also, was ich tun soll, um mein Spiel zu verbessern?

Die Optimierungen, die ich implementiert habe:

  • Lasse nur lokale Chunks im Speicher (abhängig vom Betrachtungsabstand des Players)
  • Frustum Culling (Erst auf den Brocken, dann auf den Blöcken)
  • Zeichne nur wirklich sichtbare Flächen der Blöcke
  • Verwenden von Listen pro Block, die die sichtbaren Blöcke enthalten. Blöcke, die sichtbar werden, fügen sich dieser Liste hinzu. Wenn sie unsichtbar werden, werden sie automatisch aus dieser Liste entfernt. Blöcke werden (in) sichtbar, indem ein Nachbarblock gebaut oder zerstört wird.
  • Verwenden von Listen pro Block, die die Aktualisierungsblöcke enthalten. Gleicher Mechanismus wie die sichtbaren Sperrlisten.
  • Verwenden Sie fast keine newAnweisungen innerhalb der Spielschleife. (Mein Spiel dauert ungefähr 20 Sekunden, bis der Garbage Collector aufgerufen wird.)
  • Ich verwende derzeit OpenGL-Anruflisten. ( glNewList(), glEndList(), glCallList()) Für jede Seite einer Art von Block.

Momentan benutze ich überhaupt kein Beleuchtungssystem. Ich habe schon von VBO's gehört. Aber ich weiß nicht genau was es ist. Ich werde jedoch einige Nachforschungen anstellen. Verbessern sie die Leistung? Bevor ich VBOs implementiere, möchte ich versuchen, glCallLists()eine Liste von Anruflisten zu verwenden und zu übergeben. Stattdessen tausendmal verwenden glCallList(). (Ich möchte dies versuchen, weil ich denke, dass das echte MineCraft keine VBOs verwendet. Richtig?)

Gibt es andere Tricks, um die Leistung zu verbessern?

Die VisualVM-Profilerstellung hat mir dies gezeigt (Profilerstellung für nur 33 Frames mit einem Betrachtungsabstand von 70 Metern):

Bildbeschreibung hier eingeben

Profilierung mit 40 Metern (246 Frames):

Bildbeschreibung hier eingeben

Hinweis: Ich synchronisiere viele Methoden und Codeblöcke, da ich Blöcke in einem anderen Thread generiere. Ich denke, dass das Erlangen einer Sperre für ein Objekt ein Leistungsproblem ist, wenn man so viel in einer Spieleschleife macht (ich spreche natürlich von der Zeit, in der es nur eine Spieleschleife gibt und keine neuen Blöcke erzeugt werden). Ist das richtig?

Bearbeiten: Nach dem Entfernen einiger synchronisedBlöcke und einiger anderer kleiner Verbesserungen. Die Leistung ist schon viel besser. Hier sind meine neuen Profilierungsergebnisse mit 70 Metern:

Bildbeschreibung hier eingeben

Ich denke, es ist ziemlich klar, dass selectVisibleBlockshier das Problem ist.

Danke im Voraus!
Martijn

Update : Nach einigen zusätzlichen Verbesserungen (z. B. Verwenden von for-Schleifen anstelle von for-Schleifen, Puffern von Variablen außerhalb von Schleifen usw.) kann ich jetzt die Anzeigedistanz 60 ziemlich gut ausführen.

Ich denke, ich werde VBOs so schnell wie möglich implementieren.

PS: Der gesamte Quellcode ist auf GitHub verfügbar:
https://github.com/mcourteaux/CraftMania

Martijn Courteaux
quelle
2
Können Sie uns eine Profilaufnahme auf 40 m geben, damit wir sehen können, was möglicherweise schneller skaliert als ein anderes?
James
Vielleicht zu genau, aber wenn man bedenkt, dass man nur nach Techniken fragt, wie man ein 3D-Spiel beschleunigt, klingt das interessant. Aber der Titel kann ppl erschrecken.
Gustavo Maciel
@Gtoknu: Was schlägst du als Titel vor?
Martijn Courteaux
5
Abhängig davon, wen Sie fragen, würden einige Leute sagen, dass Minecraft auch wirklich nicht so schnell ist.
thedaian
Ich denke, etwas wie "Welche Techniken können ein 3D-Spiel beschleunigen" sollte viel besser sein. Überlegen Sie sich etwas, aber versuchen Sie nicht, das Wort "am besten" zu verwenden oder es mit einem anderen Spiel zu vergleichen. Wir können nicht genau sagen, was sie für einige Spiele verwenden.
Gustavo Maciel

Antworten:

15

Sie erwähnen das Ausmerzen einzelner Blöcke mit Kegelstumpf - werfen Sie das aus. Die meisten Rendering-Chunks sollten entweder vollständig sichtbar oder vollständig unsichtbar sein.

Minecraft erstellt nur dann eine Anzeigeliste / einen Vertex-Puffer neu (ich weiß nicht, welchen er verwendet), wenn ein Block in einem bestimmten Block geändert wird, und ich auch . Wenn Sie die Anzeigeliste bei jeder Änderung der Ansicht ändern, profitieren Sie nicht von Anzeigelisten.

Außerdem scheinen Sie Brocken von Welthöhe zu verwenden. Beachten Sie, dass Minecraft im Gegensatz zum Laden und Speichern kubische 16 × 16 × 16-Blöcke für seine Anzeigelisten verwendet. Wenn Sie das tun, gibt es noch weniger Gründe, einzelne Stücke zu entfernen.

(Hinweis: Ich habe den Code von Minecraft nicht untersucht. Alle diese Informationen sind entweder Hörensagen oder meine eigenen Schlussfolgerungen aus der Beobachtung von Minecrafts Rendering während des Spiels.)


Allgemeine Hinweise:

Denken Sie daran, dass Ihr Rendering auf zwei Prozessoren ausgeführt wird: CPU und GPU. Wenn Ihre Bildrate nicht ausreicht, ist der eine oder andere die begrenzende Ressource - Ihr Programm ist entweder CPU- oder GPU-gebunden (vorausgesetzt, es wird nicht ausgetauscht oder es treten Planungsprobleme auf).

Wenn Ihr Programm zu 100% mit CPU läuft (und keine andere unbegrenzte Aufgabe zu erledigen hat), dann arbeitet Ihre CPU zu viel. Sie sollten versuchen, die Aufgabe zu vereinfachen (z. B. weniger Culling), damit die GPU mehr leistet. Ich vermute sehr, dass dies Ihr Problem ist, wenn Sie Ihre Beschreibung geben.

Auf der anderen Seite sollten Sie überlegen, wie Sie weniger Daten senden oder weniger Pixel ausfüllen müssen, wenn die GPU das Limit darstellt (leider gibt es normalerweise keine geeigneten 0% -100% -Lastmonitore).

Kevin Reid
quelle
2
Tolle Referenz, Ihre in Ihrem Wiki erwähnte Recherche war für mich sehr hilfreich! +1
Gustavo Maciel
@OP: Nur sichtbare Flächen rendern (keine Blöcke ). Ein pathologischer, aber monotoner 16x16x16-Block hat fast 800 sichtbare Flächen, während die enthaltenen Blöcke 24.000 sichtbare Flächen haben. Danach enthält Kevins Antwort die wichtigsten Verbesserungen.
AndrewS
@ KevinReid Es gibt einige Programme, die beim Debuggen der Leistung helfen. AMD GPU PerfStudio sagt Ihnen zum Beispiel, ob seine CPU oder GPU gebunden ist und welche Komponente die gebundene ist (Textur gegen Fragment gegen Scheitelpunkt usw.). Und ich bin sicher, dass Nvidia auch etwas Ähnliches hat.
Altar
3

Was nennt Vec3f.set so viel? Wenn Sie jedes Bild von Grund auf neu rendern möchten, sollten Sie damit beginnen, es zu beschleunigen. Ich bin kein großer OpenGL-Benutzer und ich weiß nicht viel darüber, wie Minecraft rendert, aber es scheint, dass die von Ihnen verwendeten mathematischen Funktionen Sie gerade töten (sehen Sie sich nur an, wie viel Zeit Sie damit verbringen und wie oft Sie es tun) sie werden gerufen - der Tod durch tausend Schnitte, die sie rufen).

Im Idealfall wird Ihre Welt so segmentiert, dass Sie zu rendernde Objekte gruppieren, Vertex-Pufferobjekte erstellen und über mehrere Frames hinweg wiederverwenden können. Sie müssten ein VBO nur ändern, wenn sich die Welt, die es darstellt, irgendwie ändert (wie der Benutzer es bearbeitet). Sie können dann VBOs für das erstellen / zerstören, was Sie darstellen, da sie sichtbar werden, um den Speicherverbrauch gering zu halten. Sie würden nur den Treffer erhalten, da der VBO erstellt wurde, und nicht jeden Frame.

Wenn die Anzahl der "Aufrufe" in Ihrem Profil korrekt ist, rufen Sie sehr viele Dinge sehr oft an. (10 Millionen Anrufe bei Vec3f.set ... autsch!)

Roger Perkins
quelle
Ich benutze diese Methode für Unmengen von Dingen. Es setzt einfach die drei Werte für den Vektor. Dies ist viel besser, als jedes Mal ein neues Objekt zuzuweisen.
Martijn Courteaux
2

Meine Beschreibung (aus meinen eigenen Experimenten) gilt hier:

Was ist für das Voxel-Rendering effizienter: vorgefertigtes VBO oder ein Geometrie-Shader?

Minecraft und Ihr Code verwenden wahrscheinlich die feste Funktionspipeline. Ich habe mich selbst um GLSL bemüht, aber das Wesentliche ist meines Erachtens allgemein gültig:

(Aus dem Gedächtnis) Ich habe einen Kegelstumpf gemacht, der einen halben Block größer war als der Bildschirmkegelstumpf. Ich habe dann die Mittelpunkte jedes Blocks getestet ( Minecraft hat 16 * 16 * 128 Blöcke ).

Die Flächen in jeder haben Spannweiten in einem Elementarray-VBO (viele Flächen aus Blöcken teilen sich die gleiche VBO, bis sie "voll" sind; denken Sie, wie mallocdiejenigen mit der gleichen Textur in der gleichen VBO, wenn möglich) und die Scheitelindizes für den Norden Gesichter, Südwände und so weiter sind eher benachbart als gemischt. Wenn ich zeichne, mache ich eine glDrawRangeElementsfür die Nordwände, wobei die Normalen bereits projiziert und normalisiert sind, in Uniform. Dann mache ich die Südwände und so weiter, damit die Normalen in keinem VBO sind. Für jeden Chunk muss ich nur die sichtbaren Gesichter ausgeben - nur die in der Mitte des Bildschirms müssen zum Beispiel die linke und die rechte Seite zeichnen. Dies ist GL_CULL_FACEauf Anwendungsebene einfach .

Die größte Beschleunigung, iirc, bestand darin, beim Polygonisieren der einzelnen Teile Innenflächen auszusondern.

Wichtig ist auch das Textur-Atlas- Management und das Sortieren von Gesichtern nach Textur und das Einfügen von Gesichtern mit derselben Textur in dasselbe VBO wie die von anderen Stücken. Sie möchten zu viele Texturänderungen vermeiden und die Flächen nach Textur sortieren und so weiter, um die Anzahl der Bereiche in der zu minimieren glDrawRangeElements. Das Zusammenführen benachbarter Flächen gleicher Kacheln zu größeren Rechtecken war ebenfalls eine große Sache. Ich spreche über das Zusammenführen in der anderen oben zitierten Antwort.

Offensichtlich polygonisieren Sie nur die Chunks, die jemals sichtbar waren. Möglicherweise verwerfen Sie die Chunks, die lange Zeit nicht mehr sichtbar waren, und polygonisieren bearbeitete Chunks erneut (da dies im Vergleich zum Rendern selten vorkommt).

Wille
quelle
Ich mag die Idee Ihrer Kegelstumpfoptimierung. Aber verwechseln Sie in Ihrer Erklärung nicht die Begriffe "Block" und "Chunk"?
Martijn Courteaux
Wahrscheinlich ja. Ein Block von Blöcken ist ein Block von Blöcken auf Englisch.
Will
1

Woher kommen all deine Vergleiche ( BlockDistanceComparator)? Wenn es sich um eine Sortierfunktion handelt, könnte diese durch eine Radix-Sortierung ersetzt werden (die asymptotisch schneller und nicht vergleichsbasiert ist)?

Wenn Sie Ihre Zeitabläufe betrachten, wird Ihre relativeToOriginFunktion für jede compareFunktion zweimal aufgerufen , auch wenn die Sortierung selbst nicht so schlecht ist. Alle diese Daten sollten einmal berechnet werden. Es sollte schneller sein, eine Hilfsstruktur zu sortieren, z

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

und dann in 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 ]

Tut mir leid, wenn dies keine gültige Java-Struktur ist (ich habe Java seit Undergrad nicht mehr angerührt), aber Sie haben hoffentlich eine Idee.

celion
quelle
Ich finde das amüsant. Java hat keine Strukturen. Nun, es gibt so etwas in der Java-Welt, aber es hat mit Datenbanken zu tun, und das ist überhaupt nicht dasselbe. Sie können eine Abschlussklasse mit öffentlichen Mitgliedern erstellen, ich denke, das funktioniert.
Theraot
1

Yeah, benutze VBOs und CULL Faces, aber das gilt für so ziemlich jedes Spiel. Was Sie tun möchten, ist, den Würfel nur zu rendern, wenn er für den Spieler sichtbar ist, UND wenn sich die Blöcke auf eine bestimmte Weise berühren (sagen wir einen Block, den Sie nicht sehen können, weil er unterirdisch ist), fügen Sie die Scheitelpunkte der Blöcke hinzu und erstellen es ist fast wie ein "größerer Block" oder in Ihrem Fall ein Stück. Dies wird als "Greedy Meshing" bezeichnet und steigert die Leistung drastisch. Ich entwickle ein Spiel (Voxel-basiert) und es verwendet einen Greedy-Meshing-Algorithmus.

Anstatt alles so zu rendern:

render

Es macht es so:

render2

Der Nachteil dabei ist, dass Sie mehr Berechnungen pro Chunk für den anfänglichen World Build durchführen müssen oder wenn der Spieler einen Block entfernt / hinzufügt.

So ziemlich jede Art von Voxel-Motor benötigt dies für eine gute Leistung.

Dabei wird geprüft, ob die Blockfläche eine andere Blockfläche berührt. In diesem Fall wird nur eine (oder keine) Blockfläche (n) gerendert. Es ist eine teure Sache, wenn Sie Brocken sehr schnell rendern.

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
quelle
1
Und ist es das wert? Es scheint, als wäre ein LOD-System angemessener.
MichaelHouse
0

Es scheint, dass Ihr Code in Objekten und Funktionsaufrufen ertrinkt. Nach den Zahlen zu urteilen, scheint es nicht so zu sein, als ob es irgendwelche Zwischenfälle gibt.

Sie könnten versuchen, eine andere Java-Umgebung zu finden oder einfach mit den Einstellungen der Java-Umgebung zu experimentieren, aber eine einfache Methode, Ihren Code nicht schnell, sondern viel langsamer zu machen, ist zumindest intern in Vec3f zu stoppen Kodierung OOO *. Machen Sie jede Methode eigenständig. Rufen Sie keine der anderen Methoden auf, nur um eine grundlegende Aufgabe auszuführen.

Bearbeiten: Während es überall Overhead gibt, scheint es, dass das Ordnen der Blöcke vor dem Rendern der schlechteste Leistungsfresser ist. Ist das wirklich notwendig? In diesem Fall sollten Sie wahrscheinlich zunächst eine Schleife durchlaufen, die Entfernung jedes Blocks zum Ursprung berechnen und danach sortieren.

* Übermäßig objektorientiert

aaaaaaaaaaa
quelle
Ja, Sie werden Speicher sparen, aber die CPU verlieren! So ist OOO in Echtzeitspielen nicht allzu gut.
Gustavo Maciel
Sobald Sie mit der Profilerstellung beginnen (und nicht nur mit dem Sampling), verschwindet jedes Inlining, das die JVM normalerweise durchführt. Es ist wie in der Quantentheorie, man kann nichts messen, ohne das Ergebnis zu ändern: p
Michael
@Gtoknu Das ist nicht allgemein gültig. Auf einer bestimmten Ebene von OOO beanspruchen die Funktionsaufrufe mehr Speicher als Inline-Code. Ich würde sagen, dass es einen guten Teil des fraglichen Codes gibt, der sich um die Gewinnschwelle für das Gedächtnis dreht.
aaaaaaaaaaa 20.01.12
0

You could also try to break down Math operations down to bitwise operators. If you have 128 / 16, try to make a bitwise operator: 128 << 4. This will help a lot with your problems. Don't try to make things run at full speed. Make your game update at a rate of 60 or something, and even break that down for other things, but you would have to do destroying and or placing voxels or you would have to make a todo-list, which would bring down your fps. You could do an update rate of about 20 for entities. And something like 10 for world updates and or generation.

JBakker
quelle