Top-Down-Octree-Erzeugung von prozeduralem Terrain

8

Ich versuche, ein voxelbasiertes Terrain-Generierungssystem in Unity3d (C #) zu implementieren. Ich habe erfolgreich ein einheitliches 3D-Rastersystem implementiert und die Isofläche mithilfe von Marching Cubes- und Surface Nets-Algorithmen extrahiert.

Ich stieß schnell auf die inhärenten Probleme bei der Darstellung des gesamten Raums mit einem 3D-Raster. Ein Großteil dieses Raums befand sich entweder über oder unter der Oberfläche, und ich wandte mich der Raumaufteilung mit Oktrees zu, da dies nicht allzu schwierig schien. Eines der großartigen Dinge an Octrees ist, dass Octree-Knoten, deren Kanten nicht von der Oberfläche geschnitten werden, nicht erneut geteilt werden müssen.

Ich recherchierte und fand ein paar Ressourcen, um meinen Code zu erstellen. Eines ist Volume GFX und ein anderes ist das Original-Dual Contouring-Papier von Ju et al. Meine Kantenprüfung erfolgt durch die Überprüfung des Marching Cube anhand von Paul Bourkes Code. Wenn der "Cubeindex" entweder 0 oder 255 ist, wird keine Kante geschnitten, und der Octree-Knoten muss nicht geteilt werden.

Mein Octree-Generierungscode funktioniert für so etwas wie eine Viertelkugel, bei der die Oberflächenmerkmale äußerst normal sind: Octree Node-Visualisierung und entsprechende Dual Contouring-Oberfläche

Wie im Bild zu sehen ist, werden nur Octree-Knoten, die die Oberfläche enthalten, unterteilt. Arbeitet großartig.

Wenn wir uns jedoch etwas Komplexerem zuwenden, wie der PerlinNoise-Funktion von Unity3d: Perlin Noise mit Octree GASP! Was macht das Loch im Netz in der unteren rechten Ecke? Bei näherer Betrachtung stellen wir fest, dass das Octree nicht richtig unterteilt wurde (rote Linien, die die Abmessungen des betreffenden Knotens hervorheben): Geben Sie hier die Bildbeschreibung ein Dies ist das gleiche Problem, das Jules Bloomenthal in ihrem Artikel über Polygonisierung , Seite 10, Abbildung 10, hervorhebt . Traditionelle Methoden zum Generieren Top-Down-Octrees ("Adaptive Subdivision") reagieren empfindlich auf feine Oberflächenmerkmale im Verhältnis zur Größe des Octree-Knotens.

Das Wesentliche:

X-------X
|       |
|       |  <- Node
X--/\---X     X's - tested values, all outside of the surface!
  /  \  <- surface

Die Oberfläche bricht die Oberfläche, kommt aber wieder herunter, bevor sie über einen Scheitelpunkt geht. Da wir Kantenschnittpunkte berechnen, indem wir die Zeichen an den beiden Eckpunkten betrachten, wird die Kante dadurch nicht als gekreuzt gekennzeichnet.

Gibt es eine Methode, um festzustellen, ob diese Anomalien vorliegen? Vorzugsweise funktioniert die Lösung nicht nur für die Unity3d-Rauschfunktion, sondern für jede 3D-Rauschfunktion (für Klippen / Überhänge / schwimmende Inseln usw.).

AKTUALISIEREN

Dank Jasons großartiger Antwort konnte ich meine eigenen Fragen beantworten (in den Kommentaren unter seiner Antwort). Das Problem, das ich hatte, war, dass ich aufgrund ihrer periodischen Natur nicht verstand, wie ich eine Begrenzungsfunktion für trigonometrische Funktionen (Sinus, Cosinus usw.) erstellen konnte.

Für diese Leute ist die Periodizität der Schlüssel zu Grenzfunktionen. Wir wissen, dass sin / cos ihre Extremwerte in einem festgelegten Intervall erreichen, insbesondere in jedem π/2. Wenn also das Intervall, das wir überprüfen, ein Vielfaches (cos) oder ein halbes Vielfaches (sin) als 1 / -1 enthält, ist dies das bestimmte Extrem. Wenn es kein Vielfaches enthält (dh das Intervall [0.1,0.2]), entspricht der Bereich einfach den Werten der Funktion, die an ihren Endpunkten ausgewertet wird.

UPDATE 2:

Wenn das oben Gesagte keinen Sinn ergibt, überprüfen Sie Jasons Antwort auf meine Kommentare.

TheVulch
quelle

Antworten:

6

Ich gehe davon aus, dass Sie eine Funktion haben f, durch die die Oberfläche definiert ist f(x,y,z)==0. Im Moment haben Sie nur die Funktion fselbst, und ohne weitere Informationen ist es unmöglich zu wissen, wann Sie sie unterteilen müssen. Es ist immer möglich, dass die Oberfläche das tut, was Sie beschreiben, dh die Funktion fkann beliebig dünne "Flossen" haben, was erfordern kann, dass Sie beliebig tief in den Oktree gehen.

Die einzige Lösung, die eine Top-Down-Generierung ermöglicht, besteht darin, zusätzliche Informationen zu haben f. Im Moment haben Sie eine Punktabfrage : Sie können bestimmen, wie hoch der Wert fan einem bestimmten Punkt ist, aber Sie haben keine Garantie dafür, wie hoch der Wert an einem anderen Punkt sein wird, auch nicht direkt daneben. Stattdessen benötigen Sie eine Begrenzungsabfrage : Bei einem AABB wird bestimmt, welcher Maximal- und Minimalwert füber diesem Bereich liegt.

Jetzt ist es ganz einfach: Führen Sie für jeden Octree-Knoten eine Grenzabfrage durch. Wenn das Minimum größer als Null oder das Maximum kleiner als Null ist, unterteilen Sie nicht. Andernfalls unterteilen Sie, es sei denn, Sie haben die kleinste Größe. In diesem Fall können Sie die Punktabfrage an den Ecken wie gewohnt verwenden, um die Oberfläche zu konstruieren.

Es ist einfach, die Punktabfrage für interessante Funktionen zu schreiben, aber es ist nicht klar, wie die entsprechende Grenzfunktion geschrieben werden soll. Unsere Arbeit wird jedoch dadurch erleichtert, dass wir nicht präzise sein müssen: Die Berechnung der genauen Min / Max-Werte wird ohnehin nicht besonders nützlich sein, da wir sie nur mit Null vergleichen werden. Wir müssen also wirklich nur [a, b] berechnen und intervallieren, das [true_min, true_max] vollständig über die angegebenen AABB-Grenzen enthält. Wenn wir uns dem wahren Bereich nähern, werden wir hauptsächlich Dinge unterteilen, die tatsächlich die Oberfläche enthalten, und wenn wir locker sind und viel größere Bereiche angeben, werden wir am Ende mehr Knoten teilen als nötig. Solange wir jedoch sicherstellen, dass der wahre Bereich eingehalten wird, werden wir niemals einen Teil der Oberfläche verpassen.

Als Beispiel können wir die Grenzfunktion für die Kugelfunktion konstruieren. (Um zu sehen, warum Ihre Technik auch in diesem Fall nicht funktioniert, beginnen Sie mit einer Kugel, die vollständig im Startwürfel enthalten ist. Der Algorithmus wird sofort beendet!). Die Punktfunktion ist f(x,y,z) = dist(<x,y,z>, center) - radius. Wir sehen, dass diese Funktion nur von der Entfernung vom Punkt abhängt center, also im Kern eine 1D-Funktion ist. Zunächst bestimmen wir, welchen Radiusbereich unser AABB abdeckt. Ein einfacher, aber langsamer Weg, dies zu tun, besteht darin, die Abstände für die 8 Ecken zu bestimmen und die minimalen und maximalen Abstände zu nehmen. Der einzige Sonderfall ist, dass das Minimum Null ist, wenn das Zentrum im AABB enthalten ist. Jetzt haben wir einen Bereich von Radien, und durch Einstecken der Funktion erhalten Sie f(r) = r - radiusden gewünschten Bereich für Werte von f.

Das war natürlich ein einfaches Beispiel; Sie wollen wahrscheinlich etwas komplizierteres als Kugeln. Das Schöne an diesem Ansatz ist, dass Sie solche Grenzfunktionen kombinieren können. Wenn Sie beispielsweise zwei Funktionen addieren, f + gkann die Grenzfunktion für die Summe einfach auf konservative Weise konstruiert werden: Min und Max sind nur die Summe der Minuten bzw. Max der beiden Funktionen. Um zu sehen, dass dies konservativ ist, betrachten Sie die Funktionen sin(x)und -sin(x)in 1D. Über jeden Bereich von mehr als 2 pi haben diese eindeutig einen Bereich [-1,1], und daher beträgt der berechnete Bereich [-2,2], aber der reale Bereich ist das Intervall [0,0], das nur 0 enthält! Es geht jedoch nicht alle Hoffnung verloren: Wenn wir einen kleinen Bereich für x vergrößern, wird auch das Intervall kleiner (näher an [0,0]).

Das heißt, wenn Sie eine Oktave Perlin-Rauschen binden können, können Sie ein vollständiges fraktales Rauschen binden, das nur eine Summe vieler einzelner Oktaven ist. Wenn wir einzelne Funktionen binden können, können wir tatsächlich die Regeln der Intervallarithmetik verwenden , um interessantere Funktionen zu konstruieren. Insbesondere für die prozedurale Synthese können Summation, Minimum / Maximum, Domänenverzerrungen und Skalierung durch Konstanten durch Intervallarithmetik behandelt werden, wodurch wir Oberflächentexturen hinzufügen, Objekte platzieren und Verzerrungen und Skalierungen vornehmen können, ohne komplizierte Grenzfunktionen schreiben zu müssen. So können wir schreiben f(x) + min(g(x), h(x)) * 4 + h(2*x)für die Punktabfrage und automatisch (aber ungenau) konstruieren die entsprechenden Grenzen Funktion, vorausgesetzt , wir die Grenzen Funktionen für haben f, gund h.

Schließlich lassen sich einige Funktionen nicht einfach als einfache Kombination anderer Funktionen ausdrücken. In diesem Fall können Sie die Ableitung häufig binden und damit den Wert der Funktion binden. Dies ist jedoch etwas mathematischer, daher werde ich hier nicht darauf eingehen.

Implementierungshinweis

Es ist wahrscheinlich am einfachsten, eine Funktion fdurch ein Funktionspaar darzustellen :

  1. Eine Punktabfrage, bei f_pointder es sich um eine Funktion handelt, die einen Punkt zu einem Float bringt.
  2. eine Grenzabfrage, f_boundsdie einen AABB auf ein Intervall [min, max] bringt.

Dann können Sie Kombinatoren wie Summe, Skalierung usw. schreiben, die diese Paare nehmen und neue zurückgeben, die der angegebenen Kombination entsprechen. Sie können diese je nach Sprache als Tupel, Objekte usw. darstellen.


quelle
Dies ist eine großartige Antwort. Könnten Sie vielleicht klären, wie Sie eine "Grenzfunktion" für so etwas wie eine Oktave Perlin-Rauschen bestimmen können? Ich verstehe, dass ich für mein bestimmtes Rauschen immer nur das absolute Min / Max verwenden könnte, aber wie würde ich vorgehen, um eine Funktion zu entwerfen, die unterschiedliche [min, max] für unterschiedliche Intervalle schätzt? Ich glaube, das liegt in der Lösung lokaler Extrema, aber ich hatte gehofft, dass Sie sich von einem weiteren Leckerbissen der Weisheit trennen können. Vielen Dank!
TheVulch
Ich glaube, ich verstehe jetzt ... Perlin-Rauschen ist nur eine Kombination vieler einfacher Funktionen. Wenn wir also die Grenzen dieser einfachen Funktionen kombinieren, können wir eine komplexe Grenzfunktion für die gesamte Oktave erhalten. Ist das der richtige Gedankengang? Ich verstehe immer noch nicht, wie ich etwas so Einfaches wie die Grenzfunktion der Sünde konstruieren kann.
TheVulch
Das Begrenzen von Perlin-Rauschen ist aufgrund der stückweisen Definition schwierig (an den Zellenrändern ändert sich die Formel). Beachten Sie für die Sünde, dass die Grenzen einer monotonen Funktion (immer größer oder immer kleiner) an den Rändern in einer Dimension gefunden werden. Auf diese Weise können wir das Abfrageintervall in Teile aufteilen, einen für jeden pi-großen Block, über den sin monoton ist. Wenn es mehr als zwei Teile gibt, geben Sie [-1,1] zurück (überlegen Sie, warum). Andernfalls geben Sie einfach das Min / Max zwischen den 2/3 Endpunkten zurück (möglicherweise wird einer gemeinsam genutzt). Der gleiche Trick beim Aufteilen der Abfrage funktioniert auch für Perlin.