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.
- Caching. Wo immer Sie können, sollten Sie die Daten einmal berechnen. Zum Beispiel brenne ich die Beleuchtung in die Szene ein. Es könnte eine dynamische Beleuchtung (im Bildschirmbereich, als Nachbearbeitung) verwenden, aber das Einbrennen der Beleuchtung bedeutet, dass ich die Normalen für die Dreiecke nicht einhalten muss, was bedeutet, dass ...
Ü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:
for (i = 0; i < chunkLength; i++) {
for (j = 0; j < chunkLength; j++) {
for (k = 0; k < chunkLength; k++) {
MyData[i][j][k] = newVal;
}
}
}
//Instead, do this:
for (i = 0; i < chunkLengthCubed; i++) {
//figure out x, y, z index of chunk using modulus and div operators on i
//myData should have chunkLengthCubed number of indices, obviously
myData[i] = newVal;
}
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