Meinen eigenen Szenengraphen rollen

23

Hallo Game Development SE!

Ich krieche meinen Weg durch OpenGL mit der Hoffnung, eine einfache und sehr leichte Spiel-Engine zu schaffen. Ich betrachte das Projekt als eine Lernerfahrung, die am Ende vielleicht ein wenig Geld bringt, aber auf jeden Fall Spaß macht.

Bisher habe ich GLFW verwendet, um einige grundlegende E / A, ein Fenster (mit einer ach so ausgefallenen F11-Vollbildtaste) und natürlich einen OpenGL-Kontext zu erhalten. Ich habe auch GLEW verwendet, um den Rest der OpenGL-Erweiterungen verfügbar zu machen, da ich Windows verwende und OpenGL 3.0+ verwenden möchte.

Was mich zum Szenengraphen bringt. Kurz gesagt, ich würde gerne meine eigenen rollen. Diese Entscheidung wurde getroffen, nachdem man sich mit OSG befasst und einige Artikel darüber gelesen hatte, wie das Konzept eines Szenegraphen verdreht, verbogen und gebrochen wurde. Ein solcher Artikel beschrieb, wie sich Szenendiagramme entwickelt haben, als ...

Dann fügten wir all diese zusätzlichen Dinge hinzu, wie z. B. hängende Ornamente an einem Weihnachtsbaum, mit der Ausnahme, dass einige der Ornamente schöne saftige Steaks und einige ganze lebende Kühe sind.

Der Analogie folgend hätte ich gerne das Steak, das Fleisch von dem, was eine Szenenkurve sein sollte, ohne Stapel zusätzlichen Codes oder ganze Kühe anschnallen zu müssen.

Vor diesem Hintergrund frage ich mich, was genau ein Szenendiagramm sein soll und wie ein einfaches Szenendiagramm implementiert werden soll. Folgendes habe ich bisher ...

Ein Eineltern-, N-Kinder-Baum oder DAG, der ...

  • Sollte Spielobjekttransformationen (Position, Rotation, Skalierung) verfolgen
  • Sollte Render-Status für Optimierungen enthalten
  • Sollte ein Mittel zum Keulen von Objekten bieten, die sich nicht im Sichtkegel befinden

Mit folgenden Eigenschaften ...

  • Alle Knoten sollten als wiedergebbar behandelt werden (auch wenn sie nicht gerendert werden). Dies bedeutet, dass sie ...

    • Sollten alle Methoden cull (), state () und draw () haben (0 zurückgeben, wenn nicht sichtbar)
    • cull () ruft rekursiv cull () für alle untergeordneten Knoten auf, wodurch ein vollständiges Cull-Mesh für den gesamten Knoten und alle untergeordneten Knoten generiert wird. Eine andere Methode, hasChanged (), könnte dazu führen, dass bei sogenannten statischen Netzen die Culling-Geometrie nicht für jeden Frame berechnet werden muss. Dies würde so funktionieren, dass, wenn sich ein Knoten im Unterbaum geändert hat, die gesamte Geometrie bis zur Wurzel neu erstellt wird.
  • Renderstatus werden in einer einfachen Aufzählung gespeichert. Jeder Knoten wählt aus dieser Aufzählung einen OpenGL-Statussatz aus, den er benötigt, und dieser Status wird eingerichtet, bevor draw () für diesen Knoten aufgerufen wird. Dies ermöglicht das Batching. Alle Knoten eines bestimmten Statussatzes werden zusammen gerendert. Anschließend wird der nächste Statussatz eingerichtet und so weiter.

  • Kein Knoten sollte direkt Geometrie- / Shader- / Texturdaten enthalten, stattdessen sollten Knoten auf gemeinsam genutzte Objekte verweisen (die möglicherweise von einem Singleton-Objekt wie einem Ressourcenmanager verwaltet werden).

  • Szenendiagramme sollten in der Lage sein, auf andere Szenendiagramme zu verweisen (möglicherweise unter Verwendung eines Proxy-Knotens), um Situationen wie diese zu ermöglichen. Auf diese Weise können komplexe Modelle / Objekte mit mehreren Maschen um das Szenendiagramm herum kopiert werden, ohne eine Tonne Daten hinzuzufügen.

Ich hoffe auf ein wertvolles Feedback zu meinem aktuellen Design. Fehlt die Funktionalität? Gibt es einen weitaus besseren Weg / Entwurfsmuster? Fehlt mir ein größeres Konzept, das in diesem Entwurf für ein etwas einfaches 3D-Spiel enthalten sein muss? Etc.

Danke, -Cody

Cody Smith
quelle

Antworten:

15

Das Konzept

Grundsätzlich ist ein Szenengraph nichts anderes als ein bidirektionaler azyklischer Graph, der dazu dient, eine hierarchisch strukturierte Menge von räumlichen Beziehungen darzustellen.

Engines in freier Wildbahn neigen dazu, wie bereits erwähnt, andere Goodies in das Szenendiagramm aufzunehmen. Ob Sie das als Fleisch oder als Kuh sehen, hängt wahrscheinlich von Ihren Erfahrungen mit Motoren und Bibliotheken ab.

Leichtgewichtig bleiben

Ich bevorzuge den Unity3D-Stil, bei dem Ihr Szenendiagrammknoten (der im Mittelpunkt eher eine topologische als eine räumliche / topografische Struktur darstellt) räumliche Parameter und Funktionen enthält. In meiner Engine sind meine Knoten sogar noch leichter als in Unity3D, wo sie eine Menge unnötiger Junk-Mitglieder von Superklassen / implementierten Schnittstellen erben:

  • übergeordnete / untergeordnete Zeigermitglieder.
  • Raumparameterelemente vor der Transformation: xyz-Position, Neigung, Gieren und Rollen.
  • eine Transformationsmatrix; Die Matrizen in einer hierarchischen Kette können sehr schnell und einfach multipliziert werden, indem Sie rekursiv den Baum hinauf- und hinuntergehen. Auf diese Weise erhalten Sie die hierarchischen räumlichen Transformationen, die das Hauptmerkmal eines Szenendiagramms sind.
  • Eine updateLocal()Methode, die nur die Transformationsmatrizen dieses Knotens aktualisiert
  • Eine updateAll()Methode, die diese und die Transformationsmatrizen aller untergeordneten Knoten aktualisiert

... Ich füge meiner Knotenklasse auch Bewegungsgleichungslogik und damit Geschwindigkeits- / Beschleunigungsglieder (linear & winklig) hinzu. Sie können darauf verzichten und es stattdessen in Ihrem Haupt-Controller abwickeln, wenn Sie möchten. Aber das ist es - in der Tat sehr leicht. Denken Sie daran, Sie könnten diese auf Tausenden von Entitäten haben. Also, wie Sie vorgeschlagen haben, lassen Sie es leicht.

Hierarchien aufbauen

Was sagen Sie zu einem Szenendiagramm, das auf andere Szenendiagramme verweist? Ich warte auf die Pointe. Natürlich tun sie das. Das ist ihre Hauptverwendung. Sie können einen beliebigen Knoten zu einem beliebigen anderen Knoten hinzufügen. Transformationen sollten automatisch im lokalen Bereich der neuen Transformation stattfinden. Alles, was Sie tun, ist, einen Zeiger zu ändern. Es ist nicht so, als würden Sie Daten kopieren! Durch Ändern eines Zeigers erhalten Sie ein tieferes Szenendiagramm. Wenn die Verwendung von Proxies die Dinge effizienter macht, dann auf jeden Fall, aber ich habe nie die Notwendigkeit gesehen.

Vermeiden Sie renderbezogene Logik

Vergessen Sie das Rendern, während Sie Ihre Szenendiagramm-Knotenklasse schreiben, oder Sie werden die Dinge für sich selbst verwirren. Alles, was zählt, ist, dass Sie ein Datenmodell haben - ob es sich um das Szenendiagramm handelt oder nicht - und dass ein Renderer dieses Datenmodell inspiziert und Objekte in der Welt entsprechend rendert, ob es sich um 1, 2 handelt , 3 oder 7 Dimensionen. Der Punkt, den ich anspreche, ist: Verunreinigen Sie Ihr Szenendiagramm nicht mit Renderlogik. In einem Szenendiagramm geht es um Topologie und Topographie, dh Konnektivität und räumliche Eigenschaften. Dies ist der wahre Stand der Simulation und existiert auch ohne Rendering (das von der ersten Person über eine statistische Grafik bis hin zu einer textuellen Beschreibung jede Form unter der Sonne annehmen kann). Knoten verweisen nicht auf renderbezogene Objekte - möglicherweise ist dies jedoch umgekehrt. Beachten Sie auch Folgendes: Nicht jeder Szenendiagrammknoten in Ihrem gesamten Baum kann gerendert werden. Viele werden nur Container sein. Warum also überhaupt Speicher für ein Zeiger-auf-Render-Objekt reservieren? Selbst ein Zeigermitglied, das nie verwendet wird, belegt immer noch Speicher. Kehren Sie also die Zeigerrichtung um: Die renderbezogene Instanz verweist auf das Datenmodell (das möglicherweise Ihr Szenendiagrammknoten ist oder diesen enthält), NICHT umgekehrt. Wenn Sie auf einfache Weise die Liste der Controller durchsuchen und dennoch Zugriff auf die zugehörige Ansicht erhalten möchten, verwenden Sie ein Wörterbuch / eine Hashtabelle, die sich der Lesezugriffszeit von O (1) nähert. Auf diese Weise gibt es keine Verunreinigung, und Ihre Simulationslogik kümmert sich nicht darum, welche Renderer vorhanden sind, was Ihre Tage und Nächte der Codierung macht Warum also überhaupt Speicher für ein Zeiger-auf-Render-Objekt reservieren? Selbst ein Zeigermitglied, das nie verwendet wird, belegt immer noch Speicher. Kehren Sie also die Zeigerrichtung um: Die renderbezogene Instanz verweist auf das Datenmodell (das möglicherweise Ihr Szenendiagrammknoten ist oder diesen enthält), NICHT umgekehrt. Wenn Sie auf einfache Weise die Liste der Controller durchsuchen und dennoch Zugriff auf die zugehörige Ansicht erhalten möchten, verwenden Sie ein Wörterbuch / eine Hashtabelle, die sich der Lesezugriffszeit von O (1) nähert. Auf diese Weise gibt es keine Verunreinigung, und Ihre Simulationslogik kümmert sich nicht darum, welche Renderer vorhanden sind, was Ihre Tage und Nächte der Codierung macht Warum also überhaupt Speicher für ein Pointer-to-Render-Objekt reservieren? Selbst ein Zeigermitglied, das nie verwendet wird, belegt immer noch Speicher. Kehren Sie also die Zeigerrichtung um: Die renderbezogene Instanz verweist auf das Datenmodell (das möglicherweise Ihr Szenendiagrammknoten ist oder diesen enthält), NICHT umgekehrt. Wenn Sie auf einfache Weise die Liste der Controller durchsuchen und dennoch Zugriff auf die zugehörige Ansicht erhalten möchten, verwenden Sie ein Wörterbuch / eine Hashtabelle, die sich der Lesezugriffszeit von O (1) nähert. Auf diese Weise gibt es keine Verunreinigung, und Ihre Simulationslogik kümmert sich nicht darum, welche Renderer vorhanden sind, was Ihre Tage und Nächte der Codierung macht Wenn Sie auf einfache Weise die Liste der Controller durchsuchen und dennoch Zugriff auf die zugehörige Ansicht erhalten möchten, verwenden Sie ein Wörterbuch / eine Hashtabelle, die sich der Lesezugriffszeit von O (1) nähert. Auf diese Weise gibt es keine Verunreinigung, und Ihre Simulationslogik kümmert sich nicht darum, welche Renderer vorhanden sind, was Ihre Tage und Nächte der Codierung macht Wenn Sie auf einfache Weise die Liste der Controller durchsuchen und dennoch Zugriff auf die zugehörige Ansicht erhalten möchten, verwenden Sie ein Wörterbuch / eine Hashtabelle, die sich der Lesezugriffszeit von O (1) nähert. Auf diese Weise gibt es keine Verunreinigung, und Ihre Simulationslogik kümmert sich nicht darum, welche Renderer vorhanden sind, was Ihre Tage und Nächte der Codierung machtWelten leichter.

Zum Keulen siehe oben. Das Culling von Interessengebieten ist ein Konzept der Simulationslogik. Das heißt, Sie bearbeiten die Welt nicht außerhalb dieses (normalerweise kastenförmigen, kreisförmigen oder kugelförmigen) Bereichs. Dies erfolgt in der Haupt-Controller / Game-Schleife, bevor das Rendern erfolgt. Kegelstumpf-Keulen ist dagegen rein renderbezogen. Also vergessen Sie jetzt das Keulen. Es hat nichts mit Szenendiagrammen zu tun, und wenn Sie sich darauf konzentrieren, werden Sie den wahren Zweck dessen, was Sie erreichen möchten, verschleiern.

Ein letzter Hinweis ...

Ich habe das starke Gefühl, dass Sie aus einem Flash-Hintergrund (speziell AS3) stammen, wenn man alle Details zum Rendern berücksichtigt, die hier enthalten sind. Ja, das Flash Stage / DisplayObject-Paradigma enthält die gesamte Renderlogik als Teil des Szenegraphen. Aber Flash geht von vielen Annahmen aus, die Sie nicht unbedingt machen möchten. Für eine vollwertige Spiel-Engine ist es aus Gründen der Leistung, des Komforts und der Kontrolle der Codekomplexität besser, die beiden nicht zu kombinieren .

Ingenieur
quelle
1
Danke Nick. Ich bin eigentlich ein 3D-Animator (echtes 3D nicht Flash), der zum Programmierer geworden ist, daher neige ich dazu, grafisch zu denken. Wenn das nicht schlimm genug ist, habe ich in Java angefangen und mich von der in dieser Sprache vermittelten Mentalität "Alles muss ein Objekt sein" abgehoben. Sie haben mich davon überzeugt, dass das Szenendiagramm von Rendering- und Culling-Code getrennt werden sollte. Jetzt dreht sich alles darum, wie dies erreicht werden soll. Ich denke daran, den Renderer wie ein eigenes System zu behandeln, das auf das Szenendiagramm für Transformationsdaten usw. verweist.
Cody Smith,
1
Ich bin froh, dass es geholfen hat. Schamloser Plug, aber ich pflege ein Framework, bei dem es nur um SoC / MVC geht. Dabei bin ich mit dem traditionelleren Lager in der Branche in Konflikt geraten, das darauf besteht, dass sich alles in einem zentralen, monolithischen Objekt befindet. Aber selbst sie würden Ihnen allgemein sagen - halten Sie Ihr Rendering von Ihrem Szenendiagramm getrennt. SoC / SRP kann ich nicht genug betonen - mischen Sie niemals mehr Logik in eine einzelne Klasse, als Sie benötigen. Ich würde sogar komplexe OO-Vererbungsketten gegenüber gemischter Logik in derselben Klasse befürworten, wenn Sie mir eine Waffe auf den Kopf stellen!
Ingenieur
Nein, das Konzept gefällt mir. Und Sie haben Recht, dies ist die erste Erwähnung von SoC, die ich seit Jahren beim Lesen über Game Design gesehen habe. Danke noch einmal.
Cody Smith
@CodySmith Schneller Gedanke beim erneuten Durchsuchen. Im Allgemeinen ist es gut, die Dinge entkoppelt zu halten. Für verschiedene Typen von Model-Controller-Objekten in Ihrer Codebasis, die gerendert werden, ist es jedoch in Ordnung, Sammlungen von Renderables (einer Schnittstelle oder abstrakten Klasse) intern für diese zentralen Model-Controller-Objekte zu speichern. Gute Beispiele hierfür sind Entities oder UI-Elemente. Auf diese Weise können Sie schnell auf nur die Renderer zugreifen, die für dieses bestimmte Kernobjekt relevant sind - ohne Implementierungsspezifikationen, die die Entitätsklasse und damit die Verwendung von Schnittstellen kontaminieren würden.
Ingenieur
@CodySmith Der Nutzen liegt auf der Hand bei Entitäten, die z. habe Darstellungen sowohl im World Viewport als auch auf einer Minikarte. Daher die Sammlung. Alternativ können Sie nur einen Renderer-Slot für jedes Modell-Controller-Objekt intern für dieses Objekt zulassen. Behalten Sie aber die allgemeine Oberfläche bei! Keine Besonderheiten - nur Renderer.
Ingenieur