Jump to content
Unity Insider Forum

Ein Versuch: Voxel Terrain / Prozedurale Mesh Erstellung


schoen08

Recommended Posts

Ich habe mich in den letzten Tagen einmal intensiv mit der prozeduralen Mesh Erstellung in Form eines Voxel Terrains beschäftigt.

Mein Ergebnis könnt ihr unten sehen - mit dem Terrain selber bin ich sehr zufrieden, aber die Performance leidet leider inzwischen sehr.

 

Das Terrain besteht aus derzeit 400 sogenannten Chunks (also 20 x 20), die dann wiederum 32 x 32 Blöcke darstellen können. Dabei werden nur die sichtbaren Oberflächen gerendert - die Blöcke selber sind in einem dreidimensionalen Array gespeichert.

 

Über verschiedene Noise-Berechnungen wird die Form und der Typ des Terrains, das Wasser und die Berge gesetzt. Diese Berechnungen sind es leider, die so unglaublich lange dauern - wenn ich mit einem First Person Controller dynamisch Chunks nachlade und entferne, sinkt die Framerate ganz schnell zu unter 5 FPS - mal sehen was ich da noch optimieren kann, ich muss mich mal mit dem Unity Threading beschäftigen. Bisher wird alles im Main Thread berechnet ;).

 

Das Terrain kann auch verändert werden (Graben und Setzen vom Blöcken), dazu wird der entsprechende Chunk dann neu geladen - das klappt eigentlich ganz gut.

Das Terrain entsteht vollkommen zufällig über die Eingabe eines Seeds.

 

Ich fände ja ein Strategiespiel mit Voxelterrain unglaublich spannend zu programmieren... Mit Befehlen wie Tunnelgraben, Aufstauen von Flüssen etc...

 

Habt ihr irgendwelche Erfahrungen mit Voxelterrains oder Tipps für eine Perfomancesteigerung? Das Licht wird auch noch eine spannende Sache, bisher ist es einfaches Directional Light, ich hätte da aber lieber etwas Minecraft-ähnlicheres, aber das soll ja unglaublich kompliziert in der Berechnung sein ;)

post-2053-0-04608700-1398095641_thumb.png

post-2053-0-46690000-1398095654_thumb.png

post-2053-0-17043900-1398095670_thumb.png

Link zu diesem Kommentar
Auf anderen Seiten teilen

Dein Terrain sieht super aus :) bekommt man glatt Lust darin rum zu latschen.

 

Zum Thema Voxel und Performance möcht ich mal paar Schlagwörter in den Raum werfen. Ich weiß nicht in wie fern du die bei deinem Terrain berücksichtigt hast. Aber damit solltest du noch einiges rausholen können falls du das noch nicht getan hast.

  1. Nämlich den Marching Cubes Algoritmus
  2. Ocrees könnten dir beim Perfomanteren Ablauf helfen.
  3. sog. Hierachische Octee Texturen. bzw TileTrees
  4. Und mit dem Hidden Surface Removel Algoritmus kannst du checken was im Vordergrund bzw HIntergrund ist und somit definieren was gezeichnet werden muss und was nicht. Im Zusammenhang mit dem Z-Buffer Verfahren von Edwin Catmull könnte das vielleicht interessant für dich sein.

 

 

Vielleicht helfen dir diese Schlagwörter beim Optimieren.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Wow, sieht schon sehr gut aus das Ganze.

 

Da ich selbst schon einiges in diesem Gebiet gemacht habe und mich in viele Themen daher eingelesen und eingearbeitet habe hier ein paar Tipps:

 

Ich nehme an dass du die Chunks erst zur Laufzeit generierst, also dann wenn sie benötigt werden, das heißt dass nicht nur das Meshen Zeit in Anspruch nimmt sondern auch das auffüllen der Voxel. Ich sehe an den Bildern dass du ein natürliches Terrain anstrebst, daher ein Tipp: Generiere zuerst eine 2D Heightmap bei der Generierung und gehe dann von dem Punkt an Richtung Boden ab dem es keine Luft mehr gibt, dadurch ersparst du dir grade an der Oberfläche einiges an Generierungszeit.

Dadurch dass die Oberfläche vermutlich der Punkt ist an dem der Spieler startet wird der Untergrund sowieso schon generiert sein sollte sich der Spieler vertikal bewegen wollen.

 

Arbeite unbedingt (!) mit Threads, aber nicht zuviele auf einmal. Pro Core kannst du von 1 bis 4 Threads ausgehen, mehr wird dann oft kaum noch etwas bringen. Der Grund liegt an dem Threadmanagement jeder Wechsel eines aktiven Threads (pro Kern nur 1 aktiver Thread) geht viel Zeit verloren (wir reden hier natürlich NICHT von Sekunden oder schlimmeren). Wenn man es mit den Threads übertreibt geht mehr Zeit durchs wechseln verloren als die eigentliche Bearbeitung im Thread dauert.

 

Prioritisiere die Aufgaben (Tasks) die du an deine Arbeitsthreads verteilst, es macht kein Sinn weit entfernte Chunks oder Blöcke berechnen zu lassen obwohl es noch viele Elemente gibt die wichtiger wären, zB Chunks/Blöcke direkt in der Nähe des Spielers.

 

Vermeide Locks. Jeder Lock bringt einen Thread dazu, sollte er warten müssen, dass seine ganze verbleibende zugewiesesene Arbeitszeit verloren geht, dies kann sich stark aufsummieren, grade wenn der Spieler sich bewegt müssen viele Elemente generiert werden (neue Chunks, etc) dies führt zu vielen einzelnen Aufgaben für die Threads wodurch jede Zuweisung im schlechtesten Fall durch ein Lock begleitet wird.

Threadsynchronisierung geht auch auf andere Wege als simple Locks.

 

Halte arbeiten vom Hauptthread/UIThread fern! Wenn du in die Situation kommst dass du Dinge auf dem Hauptthread ausführen musst (In Unity die Resourcenerzeugung wie Meshs, GOs, etc) dann versuche ob du die anfallende Last nicht auf mehrere Frames verteilen kannst. zb jeder Frame darf nur max 1 Resource erzeugen. Dies verhindert den von dir beschriebenen Fall bei dem du Zeitweise nur 5 FPS hast.

Denn es kann vorkommen dass die Threads 20 Meshs fertig haben und der Hauptthread soll diese nun erzeugen. Während er nun 5 Meshs erzeugt hat haben andere Threads nochmal 20 Meshs erzeugt die wiederum abgearbeitet werden müssen, also nun ganze 35 verbleibende Meshs, dies kann sich noch weiter aufsummieren, bis zum schlimmsten Fall: Unity crasht

 

Je nachdem welche Polygenisierungsvariante du benutzt macht es sehr viel Sinn LookupTables zu verwenden (MarshingCubes nutzt von sich aus schon sehr viele). Ich habs geschafft von 10ms auf 2ms nur durch diese Umstellung zu kommen.

 

Versuche Optimierungsalgorithmen zu benutzen die es dir ermöglichen gleiche Bereiche des Voxelchunks zu überspringen, der berühmteste SVO (Sparse Voxel Octree) bietet sich hier an.

 

Verwende soviel wie möglich wieder. Ein Cube hat 8 Ecken (8 Voxel), es macht nicht Sinn jede Ecke erneut zu berechnen (1 Ecke wird meist von bis zu 8 Cubes benutzt).

 

Benutze ein Profiler, ehrlich, die Teile zeigen dir ALLES.

 

Erstelle keine Tasks fürs Polygonisieren BEVOR die Voxel generiert wurden, dies blockiert nur unnötig Lange die Threads fürs Polygonisieren.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Das klingt sehr interessant :).

 

Ich nehme an dass du die Chunks erst zur Laufzeit generierst, also dann wenn sie benötigt werden, das heißt dass nicht nur das Meshen Zeit in Anspruch nimmt sondern auch das auffüllen der Voxel. Ich sehe an den Bildern dass du ein natürliches Terrain anstrebst, daher ein Tipp: Generiere zuerst eine 2D Heightmap bei der Generierung und gehe dann von dem Punkt an Richtung Boden ab dem es keine Luft mehr gibt, dadurch ersparst du dir grade an der Oberfläche einiges an Generierungszeit.

Dadurch dass die Oberfläche vermutlich der Punkt ist an dem der Spieler startet wird der Untergrund sowieso schon generiert sein sollte sich der Spieler vertikal bewegen wollen.

Ja, die Chunks werden erst zur Laufzeit generiert - und gerade das Berechnen des Chunkinhalts dauert viel Zeit. Zu dem Punkt mit der Heightmap - genauso mache ich das derzeit: Eine Heightmap (bzw. 2 für die Berge) und dann wird der Chunk ab der Oberfläche gefüllt - oder meinst Du etwas anderes?

 

Halte arbeiten vom Hauptthread/UIThread fern! Wenn du in die Situation kommst dass du Dinge auf dem Hauptthread ausführen musst (In Unity die Resourcenerzeugung wie Meshs, GOs, etc) dann versuche ob du die anfallende Last nicht auf mehrere Frames verteilen kannst. zb jeder Frame darf nur max 1 Resource erzeugen. Dies verhindert den von dir beschriebenen Fall bei dem du Zeitweise nur 5 FPS hast.

Denn es kann vorkommen dass die Threads 20 Meshs fertig haben und der Hauptthread soll diese nun erzeugen. Während er nun 5 Meshs erzeugt hat haben andere Threads nochmal 20 Meshs erzeugt die wiederum abgearbeitet werden müssen, also nun ganze 35 verbleibende Meshs, dies kann sich noch weiter aufsummieren, bis zum schlimmsten Fall: Unity crasht

Die Idee finde ich super! Derzeit stockt das Game immer beim Aufbauen neuer Chunks, ich denke das kann ich durch eine Aufteilung stark verbessern.

 

Versuche Optimierungsalgorithmen zu benutzen die es dir ermöglichen gleiche Bereiche des Voxelchunks zu überspringen, der berühmteste SVO (Sparse Voxel Octree) bietet sich hier an.

 

Verwende soviel wie möglich wieder. Ein Cube hat 8 Ecken (8 Voxel), es macht nicht Sinn jede Ecke erneut zu berechnen (1 Ecke wird meist von bis zu 8 Cubes benutzt).

 

Werde ich mir ansehen!

Vielen Dank für eure Tipps, ich bin gespannt was ich da noch rausholen kann ;).

 

Zum Licht: Ich habe in dem bekannten UnityForum Thema "After Playing Minecraft" gelesen, dass das Licht mit den Vertex Colors an das Mesh übergeben wird - eigentlich ja eine gute Idee. Dabei wird jedem Block, der direkt von oben sichtbar ist, 100% Licht gegeben - von diesen ausgehend werden dann die Nachbarblocks mit 80%, 60% etc. versehen. Ich denke das wird auch noch einiges an Berechnung erfordern, denn wenn jetzt ein neuer Chunk generiert wird muss ja auch das Licht von den umgebenden Chunks neu berechnet werden.

Im Forum von Blokworld hat der Entwickler das Verfahren auch noch einmal beschrieben: http://blokworld.forumotion.co.uk/t6-lighting-technique

Die Ergebnisse sehen echt klasse aus, vor allem da über die Vertex Colors ja auch eine Art Ambient Occlusion möglich ist.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Wie genau baust du die Chunks denn auf?

 

Mir ist dazu noch was eingefallen:

 

Jede mini Änderung kann ordentlich Geschwindigkeit bringen (pro Voxel summiert sich ja einiges auf). Versuche zB möglichst viel wiederzuverwenden, versuche Bereiche einzugrenzen in denen du etwas berechnen musst, setze nur Daten die anders sind als der Standardwert. So verbrätst du nur Zeit bei Voxeln die anders sind.

 

Eingrenzen geht so: Du hast ja Areale in denen diverse Mineralien vorkommen, diese brauchst du nicht in Höhlen berechnen oder im Wasser oder in der Luft. etc.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Wenn niemand was dagegen hat, würde ich gerne mal für einen Moment vom eigentlich Thema ablenken, obwohls eigentlich schon damit zu tun hat.

 

In eurem Fall besteht die komplette Welt ja aus diesen einzelnen Blöcken die alles zusammen setzen, und so auch zerstört werden können, würdet ihr dieses Prinzip auch auf normale Objekte anwenden? Nehmen wir z.B. Battlefield, dort können ganze Häuser in kleine Brocken zerlegt werden, würdet ihr diese (Auch in diesem Stil!) auch so aufbauen, oder doch eher Bruchteile instanzieren lassen und mit Partikeleffekten ausschmücken ?

 

Mir geht es hierbei um kleine, normale Objekte, die auch beeinflussbar seien sollen.

Z.B. Ein Ballon der zerplatzt, ein Auto das zerstört wird, usw.

Link zu diesem Kommentar
Auf anderen Seiten teilen

@ DieKuensteDesBosses

 

So funktinieren Voxel nicht:

Voxel können zb auch nicht animiert werden bei Battelfield wurde keine Voxel verwendet.

Voxel sind auch nicht zwangleufig lauter kleine Blöcke wie man sie aus Minecraft kennt. Eine Optimerungsmöglich ist zb nicht Tausende Würfel zu erstellen sondern wenige grossen Würfel. Da Voxel Volumenkörper sind, kann man diese bei bedarf weiter unterteilen.

 

Für Voxel gibt es zb beim Marching cube Allgorithmus nur 2hoch8 = 256 Möglichkeiten ein Voxel durch einen Voxel eine Fläche darzustellen durch dieses Verfahren muss ein Voxel nicht zwangleufig komplett mit einem Volumen gefüllt werden sondern kann ähnlich wie ein Polygon eine Hülle haben.

 

Bei der Erstellung mit Marching Cubes kann hierbei auf eine Tabelle mit den 256 Möglichkeiten zurückgegriffen werden. Ein ständiges neuberechen der Voxel ist dann nichtmehr nötig.

 

Beim Octree Allgorithmus wird ähnlich wie beim Marching Cubes verfahren ein Voxel untersucht und geprüft ob mindestens ein Eckpunkt innerhalb eines Voxel liegt. Wenn nicht wird das Voxel nicht wieter beachtet, wenn sich die ein Eckpunkt innerhalb eines Voxel befindetm kann das Voxel in weitere Teile zerlegt werden.

 

Durch dieses Verfahren könnte dann zb ein einiger Chunk ein Voxel mit einem unbekannten Inhalt sein, nährt sich der Player zb diesem Chunk kann der Rechner dieses Riesen Voxel in kleinere Zerlegen. Damit spart man sehr sehr viel Rechenleistung, das verfahren ist indirekt mit dem Kamera Occlusion Culling verwandt , nur das bei diesem Verfahren keine Objekt sichtbar oder unsichtbar gemacht werden sondern erst dann genieriert wenn sie gebraucht werden.

 

Hier stehen auch noch mehr Infos dazu:

http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter37.html

An dem Hasen erkennt man sehr schön die unterschiedlich grossen Voxel.

 

Umgekehrt können durch diesen Weg auch unnötige Chunks aus der Berechnung rausgeschmissen werden, zb wo es nichts zu berechnen gibt usw.

 

Das sieht man hier sehr gut.

 

http://www.nekocake.com/cv/voxel/voxel2.png

Link zu diesem Kommentar
Auf anderen Seiten teilen

  • 4 weeks later...

Hallo Leute,

Ich bin schoen08s Partner in diesem Projekt und wir kommen bis auf ein Paar Sachen eigentlich gut voran. Erstmal ein Update was sich geändert hat.

  • Es gibt eine Kamera die sich wie in einem RTS steuern lässt.
  • Es wurde eine Drawdistance eingeführt, die bestimmt wie viele Chunks in jeder richtung dargestellt werden.
  • Es gibt Höhlen
  • Es gibt Eigene Wasserchunks.
  • Der Noise wird zuerst global Erzeugt
  • Das Licht wird über die VertexColors kontrolliert, was zu einer Art Ambient Occlusion führt.

Nun ein paar Bilder:

14224478965_598777e043_o.jpg

14222136962_d592ce4305_o.jpg

14037895647_3fd4dabe2b_o.jpg

14037864620_bd2e317f8b_o.jpg

14201335466_0ffdf36b6a_o.jpg

14224480885_85f5cde19e_o.jpg

14201335416_11965939b8_o.jpg

 

Das sind die Problemen die aufgetaucht sind:

Problem 1:

Unsere Gewünschte Kartengröße liegt zischen 1024x1024 und 1580x1580 aber leider lässt die Speicherbegrenzung von x32Programmen dies nicht zu. Das Maximum das zur Zeit mit der Anzeige der gesamten Karte möglich ist, ist 928x928. Unser Lösungsansatz für dieses Problem war die Einführung der Drawdistance. Als Folge daraus wurden um die Kamera herum nur noch 225 (7->(2*Drawdistance+1)^2->64) Chunks angezeigt ((2*Drawdistance+1)^2). Ich bin mir nicht sicher warum, aber der Speicherverbrauch von Unity sinkt nicht wenn Chunks per mesh.Clear() gelöscht werden. Meine Theorie ist, dass der LOH (Large Object Heap) von C#/Mono/Unity von Speicherfragmentierung betroffen ist und deshalb nicht genug Platz für neu generierte Chunks vorhanden ist. Ist dies der Fall und wenn ja gibt es einen Weg das zu verhindern?

 

Problem 2:

Unsere Kamera muss einen großen Terrainausschnitt betrachten können, das bedeutet aber auch, dass viele Chunks vorgeladen werden müssen. Für das generieren jedes Chunks starten wir eine Coroutine, aber das verhindert trotzdem nicht den immensen FPS-Drop der auftritt wenn eine Spalte Chunks geladen wird. Da es das reine Generiern von Meshes (Hauptsächlich List.toArray() Aufrufe sowie mesh.Optimize() und mesh.RecalculateNormals()) ist das so viel Zeit in Anspruch nimmt kann man es nicht in einen Thread auslagern und das warten bis zum nächsten Frame hat das Problem auch nicht gelöst. Wie kann man also diese Meshes möglichst parallel erzeugen?

 

Wem es hilft. Hier sind noch Zeitangaben was bei einer 928x928 Karte wie lange benötigt:

Noise: 39,43s

Licht: 5,48s

Meshes Aufbauen: 82,64s

Gesamt 130,49s

 

 

Ich hoffe ihr könnt helfen :)

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hi, zum Speicherproblem:

 

Verbrauchen die Meshs denn soviel Speicher oder haltet ihr zusätzlich noch die Voxeldaten selbst im Speicher, nachdem das Mesh generiert wurde?

 

Wenn das Mesh soviel Speicher verbraucht (was mich wundern würde), entfernt ihr versteckte Flächen und fasst gleiches zusammen (1 riesen Quadrat anstatt viele kleine Quadrate).

 

Wie genau sieht eure Voxelstruktur aus, vermutlich gibts da Optimierungspotential.

 

Unitys Mesh Klasse kann leider nicht in separaten Threads benutzt werden, ihr könnt aber eine eigene schreiben um diese später auf dem Hauptthread in ein Unity Mesh umzuwandeln, dadurch könnt ihr natürlich Threads dafür verwenden. Was genau Optimize macht weiß ich nicht genau, vermutlich werden die ganzen Daten intern Cache freundlich umgelegt. Den Rest kann man allerdings relativ einfach nachbauen.

Ich dürfte irgendwo was dazu herumliegen haben wenn Bedarf besteht.

 

Zu deinen Zeiten, ich kenne es leider nur so dass die einzelnen Chunks gemessen werden, also wie lange dauert es einen 16³ Block aufzubauen. Wäre es möglich diese Zeiten zu messen? Ansonsten:

 

16x16

 

Noise: 679ms

Licht: 94ms

Meshs Aufbauen: 1424ms

Gesamt: 2249ms

 

Was genau macht ihr beim Noise? Und was beim aufbauen des Meshs? Wie baut ihr das Mesh auf, ich vermute dass es dort enormen Optimierungsbedarf gibt.

 

Ich habe bei meinem nicht Block Terrain bei 16^3 etwas von wegen <10ms fürs Generieren und Meshen. Nur damit ihr eine Ahnung davon habt wie sehr ihr euer Terrain noch optimieren könnt, vor allem da ihr Blöcke habt was das ganze noch schneller machen dürfte.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Sieht sehr schön aus. Kenne mich mit dem Thema nicht aus, aber da ist Marrrk ja der richtige Ansprechpartner ;)

 

Thema find ich aber sehr spannend und sieht wirklich visuell Hammer aus was ihr das bis jetzt gezaubert hat. Ich denke ich weiß was ich nächste Wochenende zumindestens mal anfangen werde, dann aber in C++

Link zu diesem Kommentar
Auf anderen Seiten teilen

Vielen Dank Marrrk für die ausführliche Antwort :).

 

Zur Noise Generierung:

Wir verwenden den Simplex Noise von Alexzzz aus dem Unity3D-Forum - ich hänge die ZIP mal an den Post ran. Der Noise hat verschiedene Funktionen, um einen 2D-Noise und einen 3D-Noise zu erzeugen. Der 2D-Noise für die sanfte Hügellandschaft auf den Bergen und am Fuße der Berge entsteht durch ein 2D-Array, welches der Simplex Noise einmalig mit Werten füllt - Damit werden dann über drei for-Schleifen und mehrere if-Abfragen das Gras, die Erde, der Sand und der Stein gesetzt (abhängig von Wert und Höhe).

Der 3D-Noise funktioniert anders: Hier werden über drei For-Schleifen (die gleichen die für das Setzen des 2D-Noise arbeiten) jedem Block im Raum ein Wert zugewiesen (einzelne Abfragen), und je nach Größe des Werts wird der Block gesetzt oder nicht. Der 3D-Noise erzeugt in der Welt die Überhänge und Felswände.

 

Vergleichswerte mit und Verwendung des 3D-Noise in einem 16*16*128 Chunk:

Ohne 3D-Noise (ohne Berge): 320ms

Mit 3D-Noise (mit Berge): bis zu 741ms

 

Das Problem ist hier also eindeutig der 3D-Noise - Der Unterschied ist ja doch recht groß.

Erstaunlich ist auch, dass Alexzzz mit diesem Noise unglaublich schnelle Ergebnisse erzielt.

 

Verbrauchen die Meshs denn soviel Speicher oder haltet ihr zusätzlich noch die Voxeldaten selbst im Speicher, nachdem das Mesh generiert wurde?

 

Wenn das Mesh soviel Speicher verbraucht (was mich wundern würde), entfernt ihr versteckte Flächen und fasst gleiches zusammen (1 riesen Quadrat anstatt viele kleine Quadrate).

 

Wir haben ein großes 3D-Array in einer World-Klasse, welches in Integer die verschiedenen Blocktypen speichert. Nach diesem wird auch das Mesh aufgebaut - Bei der späteren Bearbeitung des Terrain wird einfach der Wert im Array ausgetauscht und der Chunk der den Block erhält nach diesem Array wieder aufgebaut.

Ansonsten werden etwa die Arrays der Vertices, Triangles, UV und ColliderDaten von jedem Chunk nach dem Aufbauen über .Clear() gelöscht.

Die nicht sichtbaren Flächen werden gar nicht aufgebaut: Die Aufbauen Methode überprüft pro Block des Chunks über das globale World-Array, ob in den Nachbar-Blöcken Luft ist - wenn ja, wird diese Fläche dargestellt.

 

Ich habe bei meinem nicht Block Terrain bei 16^3 etwas von wegen <10ms fürs Generieren und Meshen. Nur damit ihr eine Ahnung davon habt wie sehr ihr euer Terrain noch optimieren könnt, vor allem da ihr Blöcke habt was das ganze noch schneller machen dürfte.

 

Das klingt extrem gut - Das ist doch mal ein Ziel :D

 

Zum Threading kann Dir ForTheRecord mehr sagen - da habe ich mich rausgehalten.

 

Viele Grüße und Danke,

Dominik

noise.zip

Link zu diesem Kommentar
Auf anderen Seiten teilen

Mit den Threads und Coroutines ist es im Moment folgendermaßen. Am Anfang Wird die Noise-Erzeugung in einer Coroutine gestartet. Währenddessen "wartet" der Haupthread. Sinn davon ist dass das Spiel nicht komplett einfriert (Für eventuelle Ladescreens). Danach werden die Chunks instantiiert und das Licht in Threads berechnet (Ein Thread pro Chunk) und ebenfalls in einem dreidimensionalen Array gespeichert. Die Lichtberechnung ist eine rekursive variante der bekannten Tiefensuche (iterativ haben wir sie nicht hinbekommen), die vom höchsten nicht-Luft-Block, mit der Tiefe 11, Lichtwerte berechnet. Noch während das Licht berechnet wird geht der Haupthread in eine while-Schleife die alle Chunks durchgeht. Chunks besitzen z.Z vier Zustände. Frisch instanziiert, Licht berechnet, Mesh-Daten berechnet und Mesh erzeugt. Die While Schleife ist dafür verantwortlich für jeden Chunk den nächsten Schritt auszuführen. Bis auf die letztendliche Erzeugung der Meshes läuft alles in Threads.

 

So läuft es zumindest wenn wir eine gesamte Karte erzeugen.

Mit der teilweise dargestellten Karte per DrawDistance ist es nach der Lichterzeugung anders. Hier wird in der bei jedem Frame geprüft, welche Chunks den Sichtbereich verlassen und welche gerade kommen und werden jeweils erzeugt, oder zerstört (mesh.Clear() und mesh=null). Hier ist mir aufgefallen, dass, obwohl die Anzahl der existierenden Chunks konstant war, der RAM-Verbrauch von Unity immer weiter gewachsen ist. Ich arbeite gerade daran dies möglichst parallel zu gestalten. Ich bräuchte einen Weg, Methoden von Threads aus auf dem Hauptthread auszuführen. Soweit ich bisher gelesen habe ist das in .NET C# möglich; ob es in Mono Unity C# auch geht wird sich herausstellen.

 

Unitys Mesh Klasse kann leider nicht in separaten Threads benutzt werden, ihr könnt aber eine eigene schreiben um diese später auf dem Hauptthread in ein Unity Mesh umzuwandeln, dadurch könnt ihr natürlich Threads dafür verwenden. Was genau Optimize macht weiß ich nicht genau, vermutlich werden die ganzen Daten intern Cache freundlich umgelegt. Den Rest kann man allerdings relativ einfach nachbauen.

Ich dürfte irgendwo was dazu herumliegen haben wenn Bedarf besteht.

 

Ich würde mir zu gerne mal deinen Ansatz zur Mesh generierung in Threads ansehen, Marrrk. :)

Hoffentlich blick ich da durch, das ist mir nämlich immer sehr wichtig.

 

Wenn das Mesh soviel Speicher verbraucht (was mich wundern würde), entfernt ihr versteckte Flächen und fasst gleiches zusammen (1 riesen Quadrat anstatt viele kleine Quadrate).

 

Die Meshes werden bezüglich Vertices und Quadraten nicht optimiert. Da wüsste ich jetzt auch nicht wie man das konkret machen sollte (oder unter welchem Stichwort man googlen sollte). Zum Speicher Hier mal ein Vergleich (die Maßstäbe sind nicht einheitlich):

post-4597-0-27698300-1400637828_thumb.png

 

Zu deinen Zeiten, ich kenne es leider nur so dass die einzelnen Chunks gemessen werden, also wie lange dauert es einen 16³ Block aufzubauen. Wäre es möglich diese Zeiten zu messen?

Unsere Chunks haben leider feste Dimensionen von 32x32x128. Vergleichswerte müssen wir also davon runterrechnen. Ein solcher 32x32x128 Chunk benötigt bei uns noch 1,7s.

 

Ich habe bei meinem nicht Block Terrain bei 16^3 etwas von wegen <10ms fürs Generieren und Meshen. Nur damit ihr eine Ahnung davon habt wie sehr ihr euer Terrain noch optimieren könnt, vor allem da ihr Blöcke habt was das ganze noch schneller machen dürfte.

 

:o <10ms... Kudos to you.

 

Ich denke ich weiß was ich nächste Wochenende zumindestens mal anfangen werde, dann aber in C++

Hach, C++ Wäre jetzt praktisch...

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hallo For the Record,

 

schade das Unity noch Mono bzw. altes Mono benutzt denn das "Frisch instanziiert, Licht berechnet, Mesh-Daten berechnet und Mesh erzeugt" wäre mal eines der Paradebeispiele für Pipelining (http://msdn.microsoft.com/en-us/library/ff963548.aspx) und Async/Await hätte sowieso hier und da seine Vorteile wie auch immer sind alles sehr Bodenständig aus was ihr das da macht

 

Wird Zeit das Wochenende wird damit ich das Thema auch angehen kann, da das ja hier ein Thread generell über das Thema ist, werde ich (soweit ich welche habe...) Ergebnisse hier auch mit reinhauen und auf Hilfe hoffen

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hi, ich empfehle euch den UnityThreadHelper (Im AssetStore oder hier im Thread downloaden: http://forum.unity3d.com/threads/90128-Unity-Threading-Helper):

 

Ich hab den geschrieben und verwende den auch in meinen Projekten, er gibt einen diverse Möglichkeiten in die Hand um Aktionen in Threads auszuführen und um von dort Aktionen wieder auf dem Hauptthread aufzurufen. Teilweise orientiert sich das an die TPL wodurch Chaining/Pipelining zumindest ermöglicht wird.

 

Da ihr Coroutinen habt verteilt ihr die Last im besten Fall nur auf mehrere Frames ohne wirklich einen Vorteil daraus zu ziehen.

 

@For The Record: Mein Ansatz zur Generierung der Meshs in einem seperaten Thread sieht so aus dass ich mir nur die einzelnen Arrays für die Unity Mesh Klasse generiere und diese im Hauptthread der konkreten Mesh Klasse zuweise. Darauf basiert zumindest alles, anders gehts auch nicht.

 

Kann ich mal eure Worldgenerierung sehen? Also wie genau ihr den Noise anwendet. 0.7s finde ich schon sehr krass.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hi, die Noise Generierung sieht wie folgt aus. Das Array blocks[,,| speichert - wie oben beschrieben - die verschiedenen Typen an Blöcken ab.

1 = Stein

2 = Helles Gras

3 = Dunkles Gras

4 = Wasser

5 = Sand

 

Zur tatsächlichen Noise-Generierung verwenden wir den Noise, den ich oben gepostet habe.

Viele Grüße,

Dominik

 

 

private IEnumerator GenerateNoise(){
 ChunkNoise noise = new ChunkNoise(seed);
 NoiseBase simplex = new SimplexNoise(seed);
 //Hügel
 float[,] heightmap = new float[worldX,worldZ];
 heightmap = noise.FillMap2D(heightmap, 0, 0, 5, 0.03f, 45);
 float[,] heightmap1 = new float[worldX,worldZ];
 heightmap1 = noise.FillMap2D(heightmap1, 0+seed, 0+seed, 5, 0.03f, 65);
 //Höhlen
 float[,] cavemap = new float[worldX,worldZ];
 cavemap = simplex.CreateSinusMap(worldX, 4, 1.0f, 20.0f, false);
 float[,] caveheight = new float[worldX,worldZ];
 caveheight = simplex.CreateSinusMap(worldX, 4, 1.0f, 1.5f, false);

 float[,] randompoints = new float[worldX,worldZ];
 randompoints = noise.FillMap2D(randompoints, 0+seed*2, 0+seed*2, 5, 0.03f, 35);

 for (int localX=0; localX<worldX; localX++){
  for (int localZ=0; localZ<worldZ; localZ++){
/////////////////////////////
//Hügel
/////////////////////////////
int height = Mathf.RoundToInt(42 + heightmap[localX, localZ]);
int height1 = Mathf.RoundToInt(42 + heightmap1[localX, localZ]);
for (int y = 0; y < height; y++){
 if(y>height-2){
  if(randompoints[localX,localZ]<1){
   blocks[localX, y, localZ] = 2;
  }
  else{
   blocks[localX, y, localZ] = 3;
  }
 }
 else{
  blocks[localX, y, localZ] = 1;
 }

}
/////////////////////////////
//Berge
/////////////////////////////
for (int yt = height; yt < height1+29; yt++){
  //Noise für Berge
  float noiseValue3D = noise.GetValue3D(localX, yt, localZ, 6, 0.05f, 0.01f);
  if (noiseValue3D > 0&&yt>0){
   //Block ist Stein
   blocks[localX, yt, localZ] = 1;
   if (yt>height1+27){
	if(randompoints[localX,localZ]<1){
	 blocks[localX, yt, localZ] = 2;
	}
	else{
	 blocks[localX, yt, localZ] = 3;
	}
   }

  }
  else{
   if(yt>0){
	if (blocks[localX, yt-1, localZ]==1){
	 if(randompoints[localX,localZ]<1){
	  blocks[localX, yt, localZ] = 2;
	 }
	 else{
	  blocks[localX, yt, localZ] = 3;
	 }
	}
	else{
	 blocks[localX, yt, localZ] = 0;
	}
   }
  }
}
/////////////////////////////
//Wasser
/////////////////////////////
for(int y=38;y>0;y--){
 if(blocks[localX, y, localZ]==0){
  blocks[localX, y, localZ]=4;
 }
}
/////////////////////////////
//Höhlen
/////////////////////////////
//Debug.Log (localX + " / " + localZ + ": " + cavemap[localX,localZ]);
for(int i=0; i<cavemap[localX,localZ]*15; i++){
 if(cavemap[localX,localZ]>0.3f){
  int y = 10+i+Mathf.FloorToInt(caveheight[localX,localZ]*20);
  if(y>127){y=127;};
  if(y<1){y=1;};
  if(blocks[localX, y, localZ]!=4){
   blocks[localX, y, localZ] = 0;
  }
 }
}
/////////////////////////////
//Strand&Sand
/////////////////////////////
for(int y=40;y>0;y--){
 if(blocks[localX, y, localZ]==4&&blocks[localX, y-1, localZ]!=4){
  blocks[localX, y, localZ]=5;
  blocks[localX, y-1, localZ]=5;
  blocks[localX, y-2, localZ]=1;
 }
 if(blocks[localX, y, localZ]==4&&y>37){
  blocks[localX, y, localZ]=0;
 }
}

  }

  yield return new WaitForSeconds(0);
 }
 yield return new WaitForSeconds(0);

}

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Mhh ich habe momentan nicht die Zeit mir alles anzusehen aber ich habe eine ungefähre Idee davon bekommen wie ihr euer Terrain aufbaut. Wenn ihr Multithreading verwenden wollt müsst ihr da vieles sowieso umschreiben, aber hier ein paar allgemeine Tipps:

 

1. Benutzt weniger Branches (if)

 

2. Bevor ihr einen Block mit Daten befüllt prüft ob an der Stelle überhaupt jemals etwas gesetzt werden kann. zB habt ihr ja die Heightmap, ihr könnt diese benutzen um alles was über dieser Heightmap liegt zu ignorieren.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Original von Suckerpunch:

schade das Unity noch Mono bzw. altes Mono benutzt denn das "Frisch instanziiert, Licht berechnet, Mesh-Daten berechnet und Mesh erzeugt" wäre mal eines der Paradebeispiele für Pipelining (http://msdn.microsof...y/ff963548.aspx) und Async/Await hätte sowieso hier und da seine Vorteile wie auch immer sind alles sehr Bodenständig aus was ihr das da macht

Uhhh, ein neues Design Pattern. Sehr interessant. In dem jetztigen Zustand brauche ich dies allerdings nicht mehr, zumal es vorher schon stark einer solchen Pipeline ähnelte. Könntest du die Geschichte mit Async/Await nochmal genauer erläutern?

 

Original von Marrrk:

Da ihr Coroutinen habt verteilt ihr die Last im besten Fall nur auf mehrere Frames ohne wirklich einen Vorteil daraus zu ziehen.

Ziel war es nicht einen Performancevorteil zu erlangen sondern vielmehr das Einfrieren des Spiels zu verhindern. Allerdings wird sich diese Coroutine mit dem Threaden des Noises erübrigen.

 

Original von Marrrk:

@For The Record: Mein Ansatz zur Generierung der Meshs in einem seperaten Thread sieht so aus dass ich mir nur die einzelnen Arrays für die Unity Mesh Klasse generiere und diese im Hauptthread der konkreten Mesh Klasse zuweise. Darauf basiert zumindest alles, anders gehts auch nicht.

Dann war mein Ansatz sogar "richtig", mir hat eben nur die Möglichkeit gefehlt Befehle explizit auf dem Hauptthread auszuführen. Hier hat dein UnityThreadingHelper extrem geholfen. Mit seiner Hilfe konnte ich auf einer 928x928 Karte die Mesherzeugungszeit von 82s auf 37s verbessern.

Gerade eben habe ich unsere Arrays auf den Datentyp byte umgstellt und es hat eine EEENORME Speichereinsparung gebracht. Bei 928x928 von ~1.8GB auf ~200MB. Allerdings verstehe ich nicht, warum jetzt der Rest diesen neu geschaffenen Platz komplett einnimmt. Liegt das vielleicht an den Threads die nun großflächig eingesetzt werden?

Kleine Frage am Rande: Gibt es bei deinem ThreadHelper so Sachen wie wait() und notify()?

 

14239284894_e60ae096b6_o.jpg

 

 

Original von Marrrk:

Wenn das Mesh soviel Speicher verbraucht (was mich wundern würde), entfernt ihr versteckte Flächen und fasst gleiches zusammen (1 riesen Quadrat anstatt viele kleine Quadrate).

 

Die Vertices der Sichtbaren Meshes zu reduzieren geht aufgrund des Lichts nicht, aber die Collidervertices ließen sich reduzieren. Wisst ihr wie man sowas am besten macht?

 

Die gesamte Berechnung und Mesherzeugung wird jetzt, bis auf die konkrete Zuweisung der Objekte, über den TaskDistributor von Marrrks Tool geregelt. Das löschen von Meshes wird auch über den TaskDistributor geregelt. Leider kommt es immer noch zu FPS Drops auf 3.3 FPS wenn neue Meshes hinzukommen. Hier mal die Methode die von der Kamera in der Update-Methode aufgerufen wird:

 

 

public void manageChunks(float x, float z, int drawDistance){
 //Calculate the Chunk the Position is in
 int chunkX =(int) x / 32;
 int chunkZ =(int) z / 32;
 //Traverse all the Chunks in absolute draw Distance+1
 for (int i=chunkX-drawDistance-1; i<=chunkX+drawDistance+1; i++) {
  for(int j=chunkZ-drawDistance-1; j<=chunkZ+drawDistance+1; j++){
   //Check Bounds
   if(i>=0 && j>=0 && i<terrainchunks.GetLength(0) && j<terrainchunks.GetLength(1)){
 //IF chunk leaves the render Distance
 if( i<chunkX-drawDistance || i>chunkX+drawDistance || j<chunkZ-drawDistance || j>chunkZ+drawDistance){
  //IF the chunk is there but should't be displayed
  if(terrainchunks[i,j].display) terrainchunks[i,j].destroy();  //Is run as Thread
  if(waterchunks[i,j].display) waterchunks[i,j].destroy();  //Is run as Thread
 } else {
  //If the chunk should be displayed but isn't there
  if(!terrainchunks[i,j].display){
   terrainchunks[i,j].buildMesh(); //Is run as Thread
  }
  if(!waterchunks[i,j].display){
   waterchunks[i,j].buildMesh(); //Is run as Thread
  }
 }
   }
  }
 }
 ready = true;
}

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ein weiterer Durchbruch! Die Meshes werden gelöscht! Dank dieser hilfreichen Seiten:

http://answers.unity...the-editor.html

http://forum.unity3d...enerated-meshes

Fragt mich nicht warum ich die erst jetzt finde :blink:

 

Ich verwende jetzt UnityEngine.Object.Destroy(Object obj) um meine Meshes und Collider zu löschen. Nach den for-Schleifen in manageChunks rufe ich jetzt zusätzlich noch Resources.UnloadUnusedAssets() auf. Ich werde morgen mal testen ob das einen Unterschied macht, oder ob es vielleicht sogar reicht die Referenz zu löschen und dann UnloadUnusedAssets() aufzurufen.. Wie sich herausstellt behält Unity selbst noch eine Referenz auf Meshes. Ist dann klar, dass der Garbage Collector die Meshes dann nicht löscht.

Leider gibt es immer noch irgendwo einen Memory Leak denn wenn ich einmal durch die Welt marschiere und wieder zurück habe ich im Vergleich zu vorher ~200MB mehr im RAM. Das könnte auch an Unity liegen, deswegen werde ich hier ebenfalls weitere Tests durchführen.

 

Übrigens haben wir unsere Mesh Farben auf color32 umgestellt. 32bit bekommen dem Speicher besser als 128bit.

 

Gute Nacht :D

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...