Wie man Ressourcen in meinem Homebrew-Rendering-System zwischenspeichert

10

Hintergrund:

Ich entwerfe ein einfaches 3D-Render-System für eine Architektur vom Typ eines Entitätskomponentensystems unter Verwendung von C ++ und OpenGL. Das System besteht aus einem Renderer und einem Szenendiagramm. Wenn ich die erste Iteration des Renderers abgeschlossen habe, verteile ich möglicherweise das Szenendiagramm in der ECS-Architektur. Im Moment ist es auf die eine oder andere Weise ein Platzhalter. Wenn möglich, sind meine Ziele für den Renderer:

  1. Einfachheit . Dies ist für ein Forschungsprojekt und ich möchte meine Systeme leicht ändern und erweitern können (daher der ECS-Ansatz).
  2. Leistung . Meine Szene könnte viele kleine Modelle und auch große Volumen mit viel Geometrie haben. Es ist nicht akzeptabel, Objekte aus dem OGL-Kontext zu erfassen und die Geometrie in jedem Renderrahmen zu puffern. Ich strebe eine Datenlokalität an, um Cache-Fehler zu vermeiden.
  3. Flexibilität . Es muss in der Lage sein, Sprites, Modelle und Volumes (Voxel) zu rendern.
  4. Entkoppelt . Das Szenendiagramm kann nach dem Schreiben meines Renderers in die ECS-Kernarchitektur umgestaltet werden.
  5. Modular . Es wäre schön, verschiedene Renderer austauschen zu können, ohne mein Szenendiagramm zu ändern.
  6. Referenzielle Transparenz , dh ich kann ihr zu jedem Zeitpunkt eine gültige Szene geben und es wird immer das gleiche Bild für diese Szene gerendert. Insbesondere dieses Ziel ist nicht unbedingt erforderlich. Ich dachte, dies würde die Serialisierung von Szenen vereinfachen (ich muss in der Lage sein, Szenen zu speichern und zu laden) und mir die Flexibilität geben, zur Laufzeit verschiedene Szenen zu Test- / Experimentierzwecken auszutauschen.

Problem und Ideen:

Ich habe mir verschiedene Ansätze ausgedacht, um zu versuchen, aber ich habe Probleme damit, die OGL-Ressourcen (VAO, VBOs, Shader usw.) für jeden Renderknoten zwischenzuspeichern. Im Folgenden sind die verschiedenen Caching-Konzepte aufgeführt, die ich mir bisher ausgedacht habe:

  1. Zentraler Cache. Jeder Szenenknoten hat eine ID und der Renderer verfügt über einen Cache, der IDs den Renderknoten zuordnet. Jeder Renderknoten enthält die VAO und VBOs, die der Geometrie zugeordnet sind. Ein Cache-Fehler erfasst Ressourcen und ordnet die Geometrie einem Renderknoten im Cache zu. Wenn die Geometrie geändert wird, wird ein schmutziges Flag gesetzt. Wenn der Renderer beim Durchlaufen der Szenenknoten ein fehlerhaftes Geometrie-Flag sieht, werden die Daten mithilfe des Renderknotens zurückgewiesen. Wenn ein Szenenknoten entfernt wird, wird ein Ereignis gesendet und der Renderer entfernt den zugeordneten Renderknoten aus dem Cache, während Ressourcen freigegeben werden. Alternativ wird der Knoten zum Entfernen markiert und der Renderer ist für das Entfernen verantwortlich. Ich denke, dieser Ansatz erreicht das Ziel 6 am ehesten, während auch 4 und 5 berücksichtigt werden. 2 leidet unter der zusätzlichen Komplexität und dem Verlust der Datenlokalität bei Kartensuchen anstelle von Array-Zugriff.
  2. Verteilter Cache . Ähnlich wie oben, außer dass jeder Szenenknoten einen Renderknoten hat. Dies umgeht die Kartensuche. Um die Datenlokalität zu adressieren, könnten die Renderknoten im Renderer gespeichert werden. Dann könnten die Szenenknoten stattdessen Zeiger zum Rendern von Knoten haben, und der Renderer setzt den Zeiger auf einen Cache-Fehler. Ich denke, diese Art ahmt einen Entity-Component-Ansatz nach, sodass er mit dem Rest der Architektur übereinstimmt. Das Problem hierbei ist, dass jetzt Szenenknoten rendererimplementierungsspezifische Daten enthalten. Wenn ich ändere, wie Dinge im Renderer gerendert werden (wie das Rendern von Sprites oder Volumes), muss ich jetzt den Renderknoten ändern oder dem Szenenknoten weitere "Komponenten" hinzufügen (was bedeutet, dass auch das Szenendiagramm geändert wird). Auf der positiven Seite scheint dies der einfachste Weg zu sein, um meinen Renderer für die erste Iteration zum Laufen zu bringen.
  3. Verteilte Metadaten . In jedem Szenenknoten wird eine Renderer-Cache-Metadatenkomponente gespeichert. Diese Daten sind nicht implementierungsspezifisch, sondern enthalten eine ID, einen Typ und alle anderen relevanten Daten, die vom Cache benötigt werden. Anschließend kann die Cache-Suche mithilfe der ID direkt in einem Array durchgeführt werden, und der Typ kann angeben, welche Art von Rendering-Ansatz verwendet werden soll (z. B. Sprites oder Volumes).
  4. Besucher + verteiltes Mapping . Der Renderer ist ein Besucher und Szenenknoten sind Elemente im Besuchermuster. Jeder Szenenknoten enthält einen Cache-Schlüssel (wie die Metadaten, aber nur eine ID), den nur der Renderer bearbeitet. Die ID kann für das Array anstelle der allgemeinen Kartensuche verwendet werden. Der Renderer kann dem Szenenknoten erlauben, eine andere Renderfunktion basierend auf dem Typ des Szenenknotens auszulösen, und die ID kann von jedem Cache verwendet werden. Eine Standard-ID oder eine ID außerhalb des Bereichs würde einen Cache-Fehler anzeigen.

Wie würden Sie dieses Problem lösen? Oder haben Sie Vorschläge? Danke, dass du meine Textwand gelesen hast!

Sean
quelle
1
Hast du irgendwelche Fortschritte gemacht?
Andreas
Dies ist eine äußerst komplexe Frage, die wahrscheinlich in mehrere separate Fragen unterteilt werden sollte. Dies ist im Wesentlichen die Frage "Wie soll ich meinen Motor konstruieren?" Mein Rat wäre, zuerst etwas zu entwerfen, das die einfacheren Komponenten unterstützt, und dann Funktionen hinzuzufügen und umzugestalten, wenn Sie fortfahren. Suchen Sie auch nach Designbüchern für 3D-Game-Engines, die viele der gesuchten Informationen enthalten sollten.
Ian Young

Antworten:

2

Nachdem Sie Ihre Frage erneut gelesen haben, habe ich das starke Gefühl, dass Sie das Problem überkomplizieren. Hier ist der Grund:

  1. Es gibt nur zwei Arten von Rendering-Systemen: Forward und Deferred, von denen keines von einem Szenendiagramm abhängt.

  2. Die einzigen Leistungsprobleme, die bei einem Rendering-System auftreten sollten, sollten auf eine hohe Polyanzahl und ineffizienten Shader- und Client-Code zurückzuführen sein.

  3. Cache-Fehler verringern zwar die Leistung, aber sie sind nicht ganz die Monster, von denen Sie vielleicht denken, dass sie sie sind. 80% Ihrer Leistungsverbesserungen werden durch einen effizienteren Algorithmus erzielt. Machen Sie nicht den Fehler, Ihren Code vorab zu optimieren.

Das gesagt:

Wenn Sie einen Homebrew-Szenegraphen verwenden, sollten Sie bereits eine "Renderer" -Schnittstelle (oder Basisklasse) verwenden, um den Rendering-Teil Ihres Szenegraph-Codes zu entwerfen. Ein Besuchermuster mit doppeltem Versand ist ein guter Ansatz, da Sie möglicherweise viele Arten von Diagrammknoten wie Farbe, Textur, Netz, Transformation usw. verwenden. Auf diese Weise muss der Renderer während des Renderzyklus lediglich etwas tun Gehen Sie mit der Tiefe zuerst durch die Struktur des Szenenbaums und geben Sie sich selbst als Argument weiter. Auf diese Weise ist der Renderer im Grunde nur eine Sammlung von Shadern und möglicherweise ein oder zwei Framebuffern. Dies hat zur Folge, dass für das Rendering-System kein Such- / Entfernungscode mehr erforderlich ist, sondern nur noch der Szenegraph.

Es gibt sicherlich andere Möglichkeiten, um die Probleme anzugehen, mit denen Sie konfrontiert sind, aber ich möchte keine zu langwierige Antwort geben. Mein bester Rat ist also, zuerst etwas Einfaches zum Laufen zu bringen, es dann zu erweitern, um seine Schwächen zu finden, dann mit anderen Ansätzen zu experimentieren und herauszufinden, wo ihre Stärken / Schwächen in der Praxis liegen.

Dann sind Sie gut aufgestellt, um eine fundierte Entscheidung zu treffen.

Ian Young
quelle