Ich fand Minecrafts wunderbare große Welten extrem langsam zu navigieren, selbst mit einem Quad-Core und einer fleischigen Grafikkarte.
Ich nehme an, Minecrafts Langsamkeit kommt von:
- Java, da räumliche Partitionierung und Speicherverwaltung in nativem C ++ schneller sind.
- Schwache Weltaufteilung.
Ich könnte in beiden Annahmen falsch liegen. Dies brachte mich jedoch dazu, darüber nachzudenken, wie ich große Voxelwelten am besten verwalten kann. Da es dich um eine echte 3D - Welt, wo ein Block in jedem Teil der Welt existieren kann, ist es im Grunde ein großer 3D - Array [x][y][z]
, wobei jeder Block in der Welt hat einen Typen (dh BlockType.Empty = 0
, BlockType.Dirt = 1
usw.)
Ich gehe davon aus, dass Sie für eine gute Leistung dieser Art von Welt Folgendes benötigen:
- Verwenden Sie einen Baum einer Sorte ( oct / kd / bsp ), um alle Würfel aufzuteilen. Es sieht so aus, als wäre ein oct / kd die bessere Option, da Sie nur auf einer Ebene pro Würfel und nicht auf einer Ebene pro Dreieck partitionieren können.
- Verwenden Sie einen Algorithmus, um herauszufinden, welche Blöcke derzeit sichtbar sind, da Blöcke, die sich näher am Benutzer befinden, die dahinter liegenden Blöcke verschleiern und das Rendern sinnlos machen können.
- Halten Sie das Blockobjekt selbst leicht, damit Sie es schnell zu den Bäumen hinzufügen und daraus entfernen können.
Ich denke, es gibt keine richtige Antwort darauf, aber ich wäre interessiert, die Meinungen der Menschen zu diesem Thema zu sehen. Wie würden Sie die Leistung in einer großen voxelbasierten Welt verbessern?
quelle
Antworten:
In Bezug auf Java vs C ++ habe ich in beiden Versionen eine Voxel-Engine geschrieben (C ++ - Version siehe oben). Ich schreibe auch Voxel-Motoren seit 2004 (als sie nicht Mode waren). :) Ich kann mit ein wenig Zögern sagen, dass die C ++ - Leistung weit überlegen ist (aber es ist auch schwieriger zu programmieren). Es geht weniger um die Rechengeschwindigkeit als vielmehr um die Speicherverwaltung. Zweifellos ist C (++) die zu übertreffende Sprache, wenn Sie so viele Daten wie in einer Voxel-Welt zuweisen / freigeben. jedochsollten Sie über Ihr Ziel nachdenken. Wenn Leistung Ihre höchste Priorität ist, fahren Sie mit C ++ fort. Wenn Sie nur ein Spiel schreiben möchten, bei dem die Leistung auf dem neuesten Stand ist, ist Java definitiv akzeptabel (wie Minecraft beweist). Es gibt viele Trivial- / Edge-Fälle, aber im Allgemeinen können Sie davon ausgehen, dass Java etwa 1,75-2,0-mal langsamer läuft als (gut geschriebenes) C ++. Sie können hier eine schlecht optimierte, ältere Version meiner Engine in Aktion sehen (EDIT: neuere Version hier ). Während die Chunk-Generierung langsam erscheint, sollten Sie bedenken, dass 3D-Voronoi-Diagramme volumetrisch generiert werden und Oberflächennormalen, Beleuchtung, AO und Schatten auf der CPU mit Brute-Force-Methoden berechnet werden. Ich habe verschiedene Techniken ausprobiert und kann mit verschiedenen Caching- und Instanzentechniken ca. 100x schnellere Chunk-Generierung erzielen.
Um den Rest Ihrer Frage zu beantworten, gibt es viele Möglichkeiten, die Leistung zu verbessern.
Übergeben Sie so wenig Daten wie möglich an die Grafikkarte. Eine Sache, die die Leute gerne vergessen, ist, dass je mehr Daten Sie an die GPU übergeben, desto mehr Zeit wird benötigt. Ich übergebe in einer einzelnen Farbe und einer Scheitelpunktposition. Wenn ich Tag / Nacht-Zyklen machen möchte, kann ich einfach eine Farbkorrektur durchführen oder die Szene neu berechnen, wenn sich die Sonne allmählich ändert.
Da die Weitergabe von Daten an die GPU so teuer ist, ist es möglich, eine Engine in Software zu schreiben, die in mancher Hinsicht schneller ist. Der Vorteil von Software ist, dass sie alle Arten von Datenmanipulationen / Speicherzugriffen ausführen kann, die auf einer GPU einfach nicht möglich sind.
Spielen Sie mit der Losgröße. Wenn Sie eine GPU verwenden, kann die Leistung dramatisch variieren, je nachdem, wie groß jedes Vertex-Array ist, das Sie übergeben. Spielen Sie entsprechend mit der Größe der Chunks (wenn Sie Chunks verwenden). Ich habe festgestellt, dass 64x64x64 Chunks ziemlich gut funktionieren. Egal was passiert, halten Sie Ihre Stücke kubisch (keine rechteckigen Prismen). Dadurch werden die Codierung und verschiedene Vorgänge (z. B. Transformationen) einfacher und in einigen Fällen leistungsfähiger. Wenn Sie nur einen Wert für die Länge jeder Dimension speichern, beachten Sie, dass dies zwei Register weniger sind, die während der Berechnung vertauscht werden.
Betrachten Sie Anzeigelisten (für OpenGL). Obwohl sie der "alte" Weg sind, können sie schneller sein. Sie müssen eine Anzeigeliste in eine Variable backen ... Wenn Sie Anzeigelisten-Erstellungsvorgänge in Echtzeit aufrufen, ist dies gottlos langsam. Wie ist eine Anzeigeliste schneller? Es wird nur der Status im Vergleich zu Attributen pro Scheitelpunkt aktualisiert. Dies bedeutet, dass ich bis zu sechs Gesichter und dann eine Farbe (gegenüber einer Farbe für jeden Scheitelpunkt des Voxels) übergeben kann. Wenn Sie GL_QUADS und kubische Voxel verwenden, können Sie bis zu 20 Byte (160 Bit) pro Voxel einsparen! (15 Bytes ohne Alpha, obwohl normalerweise 4 Bytes ausgerichtet bleiben sollen.)
Ich verwende eine Brute-Force-Methode zum Rendern von "Chunks" oder Datenseiten, was eine übliche Technik ist. Im Gegensatz zu Octrees ist es viel einfacher / schneller, die Daten zu lesen / zu verarbeiten, obwohl es viel weniger speicherfreundlich ist (heutzutage kann man jedoch 64 Gigabyte Speicher für 200-300 US-Dollar erhalten) ... nicht, dass der durchschnittliche Benutzer das hat. Offensichtlich können Sie nicht ein einziges großes Array für die ganze Welt zuweisen (ein Satz von 1024 x 1024 x 1024 Voxeln entspricht 4 Gigabyte Arbeitsspeicher, vorausgesetzt, ein 32-Bit-Int pro Voxel wird verwendet). Sie ordnen also viele kleine Arrays zu, basierend auf ihrer Nähe zum Betrachter. Sie können die Daten auch zuordnen, die erforderliche Anzeigeliste abrufen und dann die Daten sichern, um Speicherplatz zu sparen. Ich denke, die ideale Kombination könnte darin bestehen, einen hybriden Ansatz aus Octrees und Arrays zu verwenden - speichern Sie die Daten in einem Array, wenn Sie die prozedurale Generierung der Welt, der Beleuchtung usw. durchführen.
Nah / Fern rendern ... ein Pixelausschnitt spart Zeit. Die GPU wirft ein Pixel, wenn sie den Tiefenpuffertest nicht besteht.
Rendern Sie nur Teile / Seiten im Ansichtsfenster (selbsterklärend). Auch wenn die GPU weiß, wie Polgyons außerhalb des Ansichtsfensters abgeschnitten werden, dauert das Übergeben dieser Daten noch einige Zeit. Ich weiß nicht, was die effizienteste Struktur dafür wäre ("schade", ich habe noch nie einen BSP-Baum geschrieben), aber selbst ein einfacher Raycast auf Blockbasis könnte die Leistung verbessern, und Tests gegen den Betrachtungskegel würden dies offensichtlich tun Zeit sparen.
Offensichtliche Informationen, aber für Anfänger: Entfernen Sie jedes einzelne Polygon, das sich nicht auf der Oberfläche befindet - dh wenn ein Voxel aus sechs Flächen besteht, entfernen Sie die Flächen, die niemals gerendert werden (berühren ein anderes Voxel).
Grundsätzlich gilt: CACHE LOCALITY! Wenn Sie die Dinge lokal im Cache halten können (auch für eine kurze Zeit), wird dies einen enormen Unterschied bedeuten. Dies bedeutet, dass Sie Ihre Daten kongruent halten (in derselben Speicherregion) und nicht zu oft zwischen Speicherbereichen wechseln, um sie zu verarbeiten Bearbeiten Sie im Idealfall einen Block pro Thread und behalten Sie diesen Speicher ausschließlich für den Thread bei. Dies gilt nicht nur für den CPU-Cache. Stellen Sie sich die Cache-Hierarchie wie folgt vor (am langsamsten bis am schnellsten): Netzwerk (Cloud / Datenbank / usw.) -> Festplatte (besorgen Sie sich eine SSD, falls Sie noch keine haben), RAM (besorgen Sie sich einen Dreifachkanal oder mehr RAM, falls Sie noch keine haben), CPU-Cache (s), Register. Versuchen Sie, Ihre Daten zu behalten das letztere Ende, und tauschen Sie es nicht mehr als Sie müssen.
Einfädeln. Tu es. Voxel-Welten eignen sich gut zum Threading, da jeder Teil (meistens) unabhängig von anderen berechnet werden kann ... Ich sah buchstäblich eine fast 4-fache Verbesserung (gegenüber einem 4-Kern-, 8-Thread-Core i7) bei der Erstellung der prozeduralen Welt Routinen zum Einfädeln.
Verwenden Sie keine char / byte-Datentypen. Oder Shorts. Ihr Durchschnittsverbraucher wird (wie Sie wahrscheinlich auch) über einen modernen AMD- oder Intel-Prozessor verfügen. Diese Prozessoren haben keine 8-Bit-Register. Sie berechnen Bytes, indem sie sie in einen 32-Bit-Slot stecken und sie dann (möglicherweise) zurück in den Speicher konvertieren. Ihr Compiler kann alle Arten von Voodoo ausführen, aber wenn Sie eine 32- oder 64-Bit-Zahl verwenden, erhalten Sie die vorhersehbarsten (und schnellsten) Ergebnisse. Ebenso benötigt ein "Bool" -Wert nicht 1 Bit; Der Compiler verwendet häufig volle 32 Bit für einen Bool. Es kann verlockend sein, bestimmte Arten der Komprimierung Ihrer Daten vorzunehmen. Beispielsweise könnten Sie 8 Voxel als einzelne Zahl (2 ^ 8 = 256 Kombinationen) speichern, wenn sie alle vom selben Typ / von derselben Farbe wären. Sie müssen jedoch über die Konsequenzen nachdenken - es könnte eine Menge Speicher sparen, Aber es kann auch die Leistung beeinträchtigen, selbst bei einer kleinen Dekomprimierungszeit, da selbst diese kleine Menge an zusätzlicher Zeit kubisch mit der Größe Ihrer Welt skaliert. Stellen Sie sich vor, Sie berechnen einen Raycast. Für jeden Schritt des Raycasts müssten Sie den Dekomprimierungsalgorithmus ausführen (es sei denn, Sie haben eine clevere Methode gefunden, um die Berechnung für 8 Voxel in einem Strahlschritt zu verallgemeinern).
Wie Jose Chavez erwähnt, kann das Muster des Fliegengewichts nützlich sein. So wie Sie eine Bitmap verwenden würden, um ein Plättchen in einem 2D-Spiel darzustellen, können Sie Ihre Welt aus mehreren 3D-Plättchentypen (oder Blocktypen) erstellen. Der Nachteil dabei ist die Wiederholung von Texturen, aber Sie können dies verbessern, indem Sie Varianztexturen verwenden, die zusammenpassen. Als Faustregel möchten Sie Instanzen verwenden, wo immer Sie können.
Vermeiden Sie die Verarbeitung von Scheitelpunkten und Pixeln im Shader, wenn Sie die Geometrie ausgeben. In einer Voxel-Engine sind zwangsläufig viele Dreiecke vorhanden, sodass selbst ein einfacher Pixel-Shader die Renderzeit erheblich verkürzen kann. Es ist besser, in einen Puffer zu rendern, als Pixel-Shader als Nachbearbeitung. Wenn Sie das nicht können, versuchen Sie, Berechnungen in Ihrem Vertex-Shader durchzuführen. Andere Berechnungen sollten nach Möglichkeit in die Eckendaten eingearbeitet werden. Zusätzliche Durchläufe werden sehr teuer, wenn Sie die gesamte Geometrie neu rendern müssen (z. B. Schatten- oder Umgebungszuordnung). Manchmal ist es besser, eine dynamische Szene zugunsten von detaillierteren Details aufzugeben. Wenn Ihr Spiel veränderbare Szenen enthält (dh zerstörbares Gelände), können Sie die Szene immer neu berechnen, wenn die Dinge zerstört werden. Die Neukompilierung ist nicht teuer und sollte weniger als eine Sekunde dauern.
Wickeln Sie Ihre Loops ab und halten Sie die Arrays flach! Mach das nicht:
EDIT: Durch umfangreichere Tests habe ich festgestellt, dass dies falsch sein kann. Verwenden Sie den Fall, der für Ihr Szenario am besten geeignet ist. Generell sollten Arrays flach sein, aber die Verwendung von Schleifen mit mehreren Indizes kann je nach Fall oft schneller sein
EDIT 2: Wenn Sie Multi-Index-Schleifen verwenden, schleifen Sie am besten in der Reihenfolge z, y, x und nicht umgekehrt. Ihr Compiler könnte dies optimieren, aber ich wäre überrascht, wenn dies der Fall wäre. Dies maximiert die Effizienz des Speicherzugriffs und der Lokalität.
Weitere Informationen zu meinen Implementierungen finden Sie auf meiner Website
quelle
Es gibt eine Menge Dinge, die Minecraft effizienter machen könnte. Minecraft lädt beispielsweise ganze vertikale Pfeiler mit einer Größe von ca. 16 x 16 Kacheln und rendert sie. Ich finde es sehr ineffizient, so viele Kacheln unnötig zu verschicken und zu rendern. Aber ich glaube nicht, dass die Wahl der Sprache wichtig ist.
Java kann recht schnell sein, aber für etwas, das sich an diesen Daten orientiert, hat C ++ einen großen Vorteil mit deutlich geringerem Overhead für den Zugriff auf Arrays und die Arbeit innerhalb von Bytes. Auf der anderen Seite ist es viel einfacher, das Threading auf allen Plattformen in Java durchzuführen. Wenn Sie nicht vorhaben, OpenMP oder OpenCL zu verwenden, werden Sie diese Bequemlichkeit in C ++ nicht finden.
Mein ideales System wäre eine etwas komplexere Hierarchie.
Tile ist eine Einheit, in der wahrscheinlich 4 Byte Informationen wie Materialtyp und Beleuchtung gespeichert sind.
Das Segment wäre ein 32x32x32-Kachelblock.
Sektoren wären 16x16x8 Segmentblöcke.
Die Welt wäre eine unendliche Karte von Sektoren.
quelle
Minecraft ist ziemlich schnell, auch auf meinem 2-Core. Java scheint hier kein einschränkender Faktor zu sein, obwohl es ein wenig Serververzögerung gibt. Lokale Spiele scheinen besser zu laufen, daher gehe ich hier von Ineffizienzen aus.
In Bezug auf Ihre Frage hat Notch (Minecraft-Autor) ausführlich über die Technologie gebloggt. Insbesondere wird die Welt in "Chunks" gespeichert (diese werden manchmal angezeigt, insbesondere wenn einer fehlt, da die Welt noch nicht ausgefüllt ist.). Daher besteht die erste Optimierung darin, zu entscheiden, ob ein Chunk angezeigt werden kann oder nicht .
Wie Sie vermutet haben, muss die App innerhalb eines Blocks entscheiden, ob ein Block sichtbar ist oder nicht, basierend darauf, ob er von anderen Blöcken verdeckt wird oder nicht.
Beachten Sie auch, dass es Block-GESICHTER gibt, von denen angenommen werden kann, dass sie nicht zu sehen sind, da sie entweder verdeckt sind (dh ein anderer Block bedeckt das Gesicht) oder in welche Richtung die Kamera zeigt (wenn die Kamera nach Norden zeigt, können Sie dies tun) sehe die Nordwand von KEINEN Blöcken!)
Gängige Techniken würden auch beinhalten, keine separaten Blockobjekte zu behalten, sondern einen "Block" von Blocktypen mit einem einzelnen Prototypblock für jeden Block zusammen mit einem minimalen Satz von Daten, um zu beschreiben, wie dieser Block benutzerdefiniert sein kann. Zum Beispiel gibt es keine benutzerdefinierten Granitblöcke (die ich kenne), aber Wasser hat Daten, die angeben, wie tief es entlang jeder Seitenfläche ist, aus denen man seine Fließrichtung berechnen kann.
Ihre Frage ist nicht klar, ob Sie die Rendergeschwindigkeit, die Datengröße oder was optimieren möchten. Klarstellung wäre da hilfreich.
quelle
Hier sind nur ein paar allgemeine Informationen und Ratschläge, die ich als, ähm, übererfahrener Minecraft-Modder geben kann (der Ihnen vielleicht zumindest teilweise eine Anleitung gibt).
Der Grund, warum Minecraft langsam ist, hat viel mit fragwürdigen, einfachen Entwurfsentscheidungen zu tun. Wenn beispielsweise ein Block durch Positionieren referenziert wird, validiert das Spiel die Koordinaten mit etwa 7 if-Anweisungen, um sicherzustellen, dass er nicht außerhalb der Grenzen liegt . Darüber hinaus gibt es keine Möglichkeit, einen 'Block' (eine 16x16x256-Einheit, mit der das Spiel arbeitet) zu erfassen und dann direkt auf Blöcke zu verweisen, um Cache-Lookups und ähm, alberne Validierungsprobleme zu umgehen (iow, jede Blockreferenz beinhaltet auch) Unter anderem ein Chunk-Lookup.) In meinem Mod habe ich eine Möglichkeit geschaffen, das Array von Blöcken direkt zu greifen und zu ändern, was die massive Dungeon-Generierung von unspielbar verzögert auf unbemerkt schnell erhöht hat.
BEARBEITEN: Behauptung entfernt, dass das Deklarieren von Variablen in einem anderen Bereich zu Leistungsverbesserungen führte. Dies scheint jedoch nicht der Fall zu sein. Ich glaube damals, dass ich dieses Ergebnis mit etwas anderem in Verbindung gebracht habe, mit dem ich experimentiert habe (insbesondere das Entfernen von Casts zwischen Doubles und Floats in explosionsbezogenem Code durch Konsolidieren zu Doubles ... das hatte verständlicherweise einen enormen Einfluss!)
Auch wenn es nicht der Bereich ist, in dem ich viel Zeit verbringe, ist der Großteil der Leistungsdrosselung in Minecraft ein Problem beim Rendern (ungefähr 75% der Spielzeit ist auf meinem System dafür vorgesehen). Natürlich ist es Ihnen egal, ob das Problem darin besteht, mehr Spieler im Mehrspielermodus zu unterstützen (Server rendert nichts), aber es ist wichtig, inwieweit alle Computer überhaupt spielen können.
Egal für welche Sprache Sie sich entscheiden, versuchen Sie, sich mit den Implementierungs- / Low-Level-Details vertraut zu machen, denn selbst ein kleines Detail in einem solchen Projekt könnte den Unterschied ausmachen (ein Beispiel für mich in C ++ war "Kann der Compiler statisch inline arbeiten?" Zeiger? "Ja, das ist möglich! Hat einen unglaublichen Unterschied in einem der Projekte bewirkt, an denen ich gearbeitet habe, da ich weniger Code und den Vorteil von Inlining hatte.)
Ich mag diese Antwort wirklich nicht, weil sie das High-Level-Design schwierig macht, aber es ist die schmerzhafte Wahrheit, wenn es um Leistung geht. Hoffe, Sie fanden das hilfreich!
Außerdem enthält Gavins Antwort einige Details, die ich nicht wiederholen wollte (und noch viel mehr! Er ist in diesem Thema eindeutig sachkundiger als ich), und ich stimme ihm größtenteils zu. Ich muss mit seinem Kommentar zu Prozessoren und kürzeren variablen Größen experimentieren. Davon habe ich noch nie gehört. Ich möchte mir selbst beweisen, dass es wahr ist!
quelle
Die Sache ist zu überlegen, wie Sie zuerst die Daten laden würden. Wenn Sie Ihre Kartendaten bei Bedarf in den Speicher streamen, gibt es eine natürliche Grenze für das Rendern. Dies ist bereits eine Leistungsverbesserung beim Rendern.
Was Sie mit diesen Daten anfangen, liegt dann bei Ihnen. Für die GFX-Leistung können Sie dann mithilfe von Ausschneiden ausgeblendete Objekte, Objekte, die zu klein sind, um sichtbar zu sein, usw. ausschneiden.
Wenn Sie dann nur nach Grafikleistungstechniken suchen, finden Sie bestimmt eine Menge Dinge im Internet.
quelle
Sehenswert ist das Flyweight- Designmuster. Ich glaube, die meisten Antworten hier beziehen sich auf die eine oder andere Weise auf dieses Entwurfsmuster.
Ich kenne die genaue Methode, mit der Minecraft den Speicher für jeden Blocktyp minimiert, nicht. Dies ist jedoch eine mögliche Methode für Ihr Spiel. Die Idee ist, dass nur ein Objekt wie ein Prototypobjekt Informationen zu allen Blöcken enthält. Der einzige Unterschied wäre die Position jedes Blocks.
Aber auch der Standort kann minimiert werden: Wenn Sie wissen, dass es sich bei einem Landblock um einen Typ handelt, können Sie die Abmessungen dieses Landes als einen riesigen Block mit einem Satz von Standortdaten speichern.
Die einzige Möglichkeit, dies zu wissen, besteht offensichtlich darin, mit der Implementierung Ihrer eigenen Software zu beginnen und einige Speichertests für die Leistung durchzuführen. Lass uns wissen, wie es geht!
quelle