Ich schreibe ein Spiel in C ++ mit OpenGL.
Für diejenigen , die Sie mit der OpenGL API nicht kennen, machen eine Menge Anrufe , um Dinge wie glGenBuffers
und glCreateShader
etc. Diese Rückgabetypen von GLuint
denen sind eindeutige Kennungen zu dem, was Sie gerade erstellt haben . Das erstellte Objekt lebt im GPU-Speicher.
Da der GPU-Speicher manchmal begrenzt ist, möchten Sie nicht zwei Dinge erstellen, die gleich sind, wenn sie von mehreren Objekten verwendet werden sollen.
Zum Beispiel Shader. Sie verknüpfen ein Shader-Programm und haben dann ein GLuint
. Wenn Sie mit dem Shader fertig sind, sollten Sie anrufen glDeleteShader
(oder etwas in diesem Sinne).
Nehmen wir an, ich habe eine flache Klassenhierarchie wie:
class WorldEntity
{
public:
/* ... */
protected:
ShaderProgram* shader;
/* ... */
};
class CarEntity : public WorldEntity
{
/* ... */
};
class PersonEntity: public WorldEntity
{
/* ... */
};
Jeder Code, den ich jemals gesehen habe, würde erfordern, dass alle Konstruktoren eine ShaderProgram*
Übergabe an ihn haben, um in der gespeichert zu werden WorldEntity
. ShaderProgram
ist meine Klasse, die die Bindung von a GLuint
an den aktuellen Shader-Status im OpenGL-Kontext sowie einige andere hilfreiche Dinge, die Sie mit Shadern tun müssen, kapselt.
Das Problem, das ich damit habe, ist:
- Es sind viele Parameter erforderlich, um ein zu
WorldEntity
erstellen (bedenken Sie, dass es möglicherweise ein Netz, einen Shader, eine Reihe von Texturen usw. gibt, die alle gemeinsam genutzt werden können, sodass sie als Zeiger übergeben werden). - Was auch immer die
WorldEntity
Bedürfnisse schafft, muss wissen, wasShaderProgram
es braucht - Dies erfordert wahrscheinlich eine Art Gulp-
EntityManager
Klasse, die weiß, welche Instanz von wasShaderProgram
an verschiedene Entitäten übergeben werden soll.
Jetzt, da es eine gibt, müssen sich Manager
die Klassen entweder EntityManager
zusammen mit der benötigten ShaderProgram
Instanz bei der registrieren , oder ich brauche einen Big-Ass switch
im Manager, den ich für jeden neuen WorldEntity
abgeleiteten Typ aktualisieren muss .
Mein erster Gedanke war, eine ShaderManager
Klasse zu erstellen (ich weiß, Manager sind schlecht), die ich als Referenz oder Zeiger auf die WorldEntity
Klassen übergebe, damit sie erstellen können, was ShaderProgram
sie wollen, über die ShaderManager
und die ShaderManager
bereits vorhandenen ShaderProgram
s verfolgen können , damit dies möglich ist Geben Sie eine bereits vorhandene zurück oder erstellen Sie bei Bedarf eine neue.
(Ich könnte das ShaderProgram
s über den Hash der Dateinamen des ShaderProgram
tatsächlichen Quellcodes speichern. )
Also jetzt:
- Ich übergebe jetzt Zeiger auf
ShaderManager
stattShaderProgram
, daher gibt es immer noch viele Parameter - Ich brauche keine
EntityManager
, die Entitäten selbst wissen, welche InstanzShaderProgram
erstellt werden soll, undShaderManager
kümmern sich um die tatsächlichenShaderProgram
s. - Aber jetzt weiß ich nicht, wann ich
ShaderManager
sicher löschen kann,ShaderProgram
was es enthält.
Jetzt habe ich meiner ShaderProgram
Klasse eine Referenzzählung hinzugefügt , die das interne GLuint
Via löscht, glDeleteProgram
und ich verzichte darauf ShaderManager
.
Also jetzt:
- Ein Objekt kann alles erstellen, was
ShaderProgram
es benötigt - Aber jetzt gibt es Duplikate,
ShaderProgram
weil kein externer Manager den Überblick behält
Schließlich komme ich, um eine von zwei Entscheidungen zu treffen:
1. Statische Klasse
A static class
, das aufgerufen wird, um ShaderProgram
s zu erstellen . Es führt eine interne Verfolgung von ShaderProgram
s basierend auf einem Hash der Dateinamen durch - dies bedeutet, dass ich keine Zeiger oder Verweise mehr auf ShaderProgram
s oder ShaderManager
s übergeben muss, also weniger Parameter - WorldEntities
die alle Kenntnisse über die Instanz haben, die ShaderProgram
sie erstellen möchten
Dieses neue static ShaderManager
muss:
- Zählen Sie, wie oft a verwendet
ShaderProgram
wird, und ich macheShaderProgram
kein kopierbares ODER ShaderProgram
s zählen ihre Referenzen und rufenglDeleteProgram
ihren Destruktor nur auf, wenn die Zählung0
UND ist, und suchenShaderManager
regelmäßig nachShaderProgram
's mit einer Zählung von 1 und verwerfen sie.
Die Nachteile dieses Ansatzes, die ich sehe, sind:
Ich habe eine globale statische Klasse, die ein Problem sein könnte. Der OpenGL-Kontext muss vor dem Aufrufen von
glX
Funktionen erstellt werden. Möglicherweise wird also einWorldEntity
erstellt und versucht,ShaderProgram
vor der OpenGL-Kontexterstellung einen zu erstellen , was zu einem Absturz führt.Der einzige Weg, dies zu umgehen, besteht darin, alles als Zeiger / Referenzen weiterzugeben oder eine globale GLContext-Klasse zu haben, die abgefragt werden kann, oder alles in einer Klasse zu halten, die den Kontext bei der Erstellung erstellt. Oder vielleicht nur ein globaler Boolescher Wert
IsContextCreated
, der überprüft werden kann. Aber ich mache mir Sorgen, dass ich dadurch überall hässlichen Code bekomme.Was ich sehen kann, ist:
- Die große
Engine
Klasse, in der jede andere Klasse versteckt ist, damit sie die Bau- / Dekonstruktionsreihenfolge angemessen steuern kann. Dies scheint ein großes Durcheinander von Schnittstellencode zwischen dem Benutzer der Engine und der Engine zu sein, wie ein Wrapper über einem Wrapper - Eine ganze Reihe von "Manager" -Klassen, die Instanzen verfolgen und Dinge löschen, wenn dies erforderlich ist. Dies könnte ein notwendiges Übel sein?
- Die große
UND
- Wann soll man eigentlich
ShaderProgram
s aus dem räumenstatic ShaderManager
? Alle paar Minuten? Jede Spielschleife? Ich kümmere mich ordnungsgemäß um das Neukompilieren eines Shaders für den Fall, dass ein ShaderShaderProgram
gelöscht wurde, aber dann ein neuer ihnWorldEntity
anfordert. aber ich bin sicher, es gibt einen besseren Weg.
2. Eine bessere Methode
Darum bitte ich hier
quelle
WorldEntity
s zu konstruieren . Verschiebt das nicht einen Teil des Problems? Denn jetzt muss die WorldFactory-Klasse jeder WolrdEntity das richtige ShaderProgramm übergeben.Antworten:
Entschuldigung für die Nekromantie, aber ich habe so viele gesehen, die über ähnliche Probleme bei der Verwaltung von OpenGL-Ressourcen gestolpert sind, einschließlich mir in der Vergangenheit. Und so viele der Schwierigkeiten, mit denen ich zu kämpfen hatte und die ich bei anderen erkenne, sind auf die Versuchung zurückzuführen, die OGL-Ressourcen, die für das Rendern einer analogen Spieleinheit erforderlich sind, zu verpacken und manchmal zu abstrahieren und sogar zu kapseln.
Und der "bessere Weg", den ich gefunden habe (zumindest einer, der meine besonderen Kämpfe dort beendet hat), war, Dinge anders herum zu machen. Das heißt, beschäftigen Sie sich beim Entwerfen Ihrer Spielentitäten und -komponenten nicht mit den Aspekten von OGL auf niedriger Ebene und entfernen Sie sich von solchen Ideen, die Sie
Model
wie Dreiecks- und Scheitelpunktprimitive in Form von Objekten speichern müssen, die umhüllen oder sogar VBOs abstrahieren.Rendering-Bedenken vs. Game-Design-Bedenken
Es gibt Konzepte auf etwas höherer Ebene als GPU-Texturen, zum Beispiel mit einfacheren Verwaltungsanforderungen wie CPU-Images (und diese benötigen Sie zumindest vorübergehend, bevor Sie überhaupt eine GPU-Textur erstellen und binden können). Fehlende Bedenken hinsichtlich des Renderns eines Modells können ausreichen, um nur eine Eigenschaft zu speichern, die den Dateinamen angibt, der für die Datei verwendet werden soll, die die Daten für das Modell enthält. Sie können eine "Material" -Komponente haben, die übergeordneter und abstrakter ist und die Eigenschaften dieses Materials beschreibt als ein GLSL-Shader.
Und dann gibt es nur einen Platz in der Codebasis, der sich mit Dingen wie Shadern und GPU-Texturen sowie VAOs / VBOs und OpenGL-Kontexten befasst, und das ist die Implementierung des Renderingsystems . Das Rendering-System durchläuft möglicherweise die Entitäten in der Spielszene (in meinem Fall durchläuft es einen räumlichen Index, aber Sie können dies leichter verstehen und mit einer einfachen Schleife beginnen, bevor Sie Optimierungen wie das Ausstoßen von Kegelstumpf mit einem räumlichen Index implementieren) entdeckt Ihre übergeordneten Komponenten wie "Materialien" und "Bilder" und Modelldateinamen.
Und seine Aufgabe ist es, die übergeordneten Daten, die nicht direkt mit der GPU zu tun haben, zu nehmen und die erforderlichen OpenGL-Ressourcen zu laden / zu erstellen / zuzuordnen / zu binden / zu verwenden / zu trennen / zu zerstören, basierend auf dem, was sie in der Szene entdecken und was mit der passiert Szene. Und das beseitigt die Versuchung, Dinge wie Singletons und statische Versionen von "Managern" zu verwenden und was nicht, denn jetzt ist Ihre gesamte OGL-Ressourcenverwaltung auf ein System / Objekt in Ihrer Codebasis zentralisiert (obwohl Sie es natürlich in weitere gekapselte Objekte zerlegen könnten vom Renderer, um den Code übersichtlicher zu gestalten). Es vermeidet natürlich auch einige Auslösepunkte, wenn beispielsweise versucht wird, Ressourcen außerhalb eines gültigen OGL-Kontexts zu zerstören.
Designänderungen vermeiden
Darüber hinaus bietet dies viel Raum zum Atmen, um kostspielige zentrale Designänderungen zu vermeiden, da Sie im Nachhinein feststellen, dass für einige Materialien mehrere Rendering-Durchgänge (und mehrere Shader) zum Rendern erforderlich sind, z. B. ein Streuungsdurchlauf unter der Oberfläche und ein Shader für Hautmaterialien, während Sie dies zuvor getan haben wollte ein Material mit einem einzigen GPU-Shader zusammenführen. In diesem Fall gibt es keine kostspielige Designänderung an zentralen Schnittstellen, die von vielen Dingen verwendet werden. Sie aktualisieren lediglich die lokale Implementierung des Renderingsystems, um diesen zuvor unerwarteten Fall zu behandeln, wenn Skineigenschaften in Ihrer übergeordneten Materialkomponente auftreten.
Die Gesamtstrategie
Und das ist die Gesamtstrategie, die ich jetzt verwende, und sie wird umso hilfreicher, je komplexer Ihre Rendering-Bedenken sind. Als Nachteil erfordert es ein bisschen mehr Vorarbeit als das Injizieren von Shadern und VBOs und ähnlichen Dingen in Ihre Spieleinheiten, und es koppelt Ihren Renderer mehr an Ihre spezielle Spiel-Engine (oder deren Abstraktionen, obwohl im Gegenzug die höhere Ebene) Spielentitäten und -konzepte werden vollständig von Rendering-Problemen auf niedriger Ebene entkoppelt. Und Ihr Renderer benötigt möglicherweise Rückrufe, um ihn zu benachrichtigen, wenn Entitäten zerstört werden, damit er alle ihm zugeordneten Daten trennen und zerstören kann (Sie können hier oder hier die Nachzählung verwenden
shared_ptr
für gemeinsam genutzte Ressourcen, jedoch nur lokal im Renderer). Möglicherweise möchten Sie eine effiziente Methode zum Zuordnen und Aufheben der Zuordnung aller Arten von Rendering-Daten zu beliebigen Entitäten in konstanter Zeit (ein ECS bietet dies in der Regel jedem System auf Anhieb an, wie Sie neue Komponententypen im laufenden Betrieb zuordnen können, wenn dies der Fall ist ein ECS - wenn nicht, sollte es auch nicht zu schwierig sein) ... aber auf der anderen Seite werden all diese Dinge wahrscheinlich für andere Systeme als den Renderer nützlich sein.Zugegeben, die reale Implementierung wird viel nuancierter und verwischt diese Dinge möglicherweise ein bisschen mehr, als wenn Ihre Engine Dinge wie Dreiecke und Eckpunkte in anderen Bereichen als dem Rendern behandeln möchte (z. B. die Physik möchte, dass solche Daten eine Kollisionserkennung durchführen ). Aber wo das Leben (zumindest für mich) viel einfacher wurde, war es, diese Art der Umkehrung in Denkweise und Strategie als Ausgangspunkt zu nehmen.
Das Entwerfen eines Echtzeit-Renderers ist meiner Erfahrung nach sehr schwierig - das Schwierigste, das ich je entworfen habe (und das ich immer wieder neu entwerfe), mit schnellen Änderungen an Hardware, Schattierungsfunktionen und entdeckten Techniken. Dieser Ansatz beseitigt jedoch die unmittelbare Sorge, wann GPU-Ressourcen durch Zentralisierung all dessen für die Rendering-Implementierung erstellt / zerstört werden können, und noch vorteilhafter für mich ist, dass er die ansonsten kostspieligen und kaskadierenden Designänderungen (die sich auswirken könnten) verschoben hat Code, der sich nicht unmittelbar mit dem Rendern befasst), sondern nur mit der Implementierung des Renderers selbst. Und diese Reduzierung der Änderungskosten kann zu enormen Einsparungen führen, da sich die Anforderungen jedes oder jedes zweite Jahr so schnell ändern wie beim Echtzeit-Rendering.
Ihr Schattierungsbeispiel
Ich gehe Ihr Schattierungsbeispiel so an, dass ich mich nicht mit Dingen wie GLSL-Shadern in Dingen wie Auto- und Personeneinheiten beschäftige. Ich beschäftige mich mit "Materialien", die sehr leichte CPU-Objekte sind, die nur Eigenschaften enthalten, die beschreiben, um welche Art von Material es sich handelt (Haut, Autolack usw.). In meinem eigentlichen Fall ist es etwas raffiniert, da ich ein DSEL habe, das Unreal Blueprints ähnelt, um Shader in einer visuellen Sprache zu programmieren, aber in Materialien werden keine GLSL-Shader-Handles gespeichert.
Ich habe ähnliche Dinge getan, als ich diese Ressourcen außerhalb des Renderers gespeichert und verwaltet habe, weil meine frühesten naiven Versuche, die nur versuchten, diese Ressourcen in einem Destruktor direkt zu zerstören, oft versuchten, diese Ressourcen außerhalb von a zu zerstören gültiger GL-Kontext (und manchmal habe ich sogar versehentlich versucht, sie in einem Skript oder etwas anderem zu erstellen, wenn ich mich nicht in einem gültigen Kontext befand), daher musste ich die Erstellung und Zerstörung auf Fälle verschieben, in denen ich garantieren konnte, dass ich mich in einem gültigen Kontext befand die zu ähnlichen "Manager" -Designs führen, die Sie beschreiben.
All diese Probleme verschwinden, wenn Sie eine CPU-Ressource an ihrer Stelle speichern und der Renderer sich mit den Bedenken der GPU-Ressourcenverwaltung befasst. Ich kann einen OGL-Shader nirgendwo zerstören, aber ich kann ein CPU-Material überall zerstören und einfach verwenden
shared_ptr
und so weiter, ohne mich in Schwierigkeiten zu bringen.Jetzt ist dieses Problem selbst in meinem Fall schwierig, wenn Sie die GPU-Ressourcen effizient verwalten und auslagern möchten, wenn sie nicht mehr benötigt werden. In meinem Fall kann ich mich mit massiven Szenen befassen und arbeite eher in VFX als in Spielen, in denen Künstler möglicherweise besonders intensive Inhalte haben, die nicht für das Echtzeit-Rendering optimiert sind (epische Texturen, Modelle, die Millionen von Polygonen umfassen usw.).
Für die Leistung ist es sehr nützlich, nicht nur das Rendern zu vermeiden, wenn sie sich außerhalb des Bildschirms befinden (außerhalb des Betrachtungsstumpfs), sondern auch die GPU-Ressourcen zu entladen, wenn sie für eine Weile nicht mehr benötigt werden (sagen wir, der Benutzer schaut nicht auf etwas in der Ferne Platz für eine Weile).
Die Lösung, die ich am häufigsten verwende, ist die Art der "Zeitstempel" -Lösung, obwohl ich nicht sicher bin, wie sie bei Spielen anwendbar ist. Wenn ich anfange, Ressourcen zum Rendern zu verwenden / zu binden (z. B. sie bestehen den Kegelstumpf-Keulungstest), speichere ich die aktuelle Zeit bei ihnen. Dann wird regelmäßig überprüft, ob diese Ressourcen eine Weile nicht verwendet wurden, und wenn ja, werden sie entladen / zerstört (obwohl die ursprünglichen CPU-Daten, die zum Generieren der GPU-Ressource verwendet wurden, beibehalten werden, bis die tatsächliche Entität, die diese Komponenten speichert, zerstört ist oder bis diese Komponenten aus der Entität entfernt werden). Wenn die Anzahl der Ressourcen zunimmt und mehr Speicher verwendet wird, wird das System aggressiver beim Entladen / Zerstören dieser Ressourcen (die für einen alten Benutzer zulässige Leerlaufzeit).
Ich kann mir vorstellen, dass es sehr von Ihrem Spieldesign abhängt. Wenn Sie ein Spiel mit einem stärker segmentierten Ansatz mit kleineren Levels / Zonen haben, können Sie möglicherweise (und die einfachste Zeit, um die Frameraten stabil zu halten) alle für dieses Level erforderlichen Ressourcen im Voraus laden und sie entladen, wenn die Benutzer geht zum nächsten Level. Wenn Sie ein massives Open-World-Spiel haben, das auf diese Weise nahtlos ist, benötigen Sie möglicherweise eine viel ausgefeiltere Strategie, um zu steuern, wann diese Ressourcen erstellt und zerstört werden sollen, und es besteht möglicherweise eine größere Herausforderung, dies alles ohne Stottern zu tun. In meiner VFX-Domain ist ein kleiner Schluckauf bei den Frameraten keine so große Sache (ich versuche, sie innerhalb eines vernünftigen Rahmens zu beseitigen), da der Benutzer dadurch nicht überspielen wird.
All diese Komplexität ist in meinem Fall immer noch auf das Rendering-System beschränkt, und obwohl ich Klassen und Code zur Implementierung verallgemeinert habe, gibt es keine Bedenken hinsichtlich gültiger GL-Kontexte und Versuchungen, Globals oder ähnliches zu verwenden.
quelle
Anstatt die Referenzzählung in der
ShaderProgram
Klasse selbst durchzuführen, ist es besser, diese an eine Smart-Pointer-Klasse zu delegieren, wie zstd::shared_ptr<>
. Auf diese Weise stellen Sie sicher, dass jede Klasse nur einen einzigen Job zu erledigen hat.Um zu vermeiden, dass Ihre OpenGL-Ressourcen versehentlich erschöpft werden, können Sie sie
ShaderProgram
nicht kopierbar machen (privater / gelöschter Kopierkonstruktor und Kopierzuweisungsoperator).Um ein zentrales Repository mit
ShaderProgram
Instanzen zu verwalten, die gemeinsam genutzt werden können, können Sie Folgendes verwendenSharedShaderProgramFactory
(ähnlich wie bei Ihrem statischen Manager, jedoch mit einem besseren Namen):Die Factory-Klasse kann als statische Klasse, als Singleton oder als Abhängigkeit implementiert werden, die bei Bedarf übergeben wird.
quelle
Opengl wurde als C-Bibliothek konzipiert und weist die Merkmale von prozeduraler Software auf. Eine der OpenGL-Regeln, die aus einer C-Bibliothek stammen, sieht folgendermaßen aus:
Dies ist ein Merkmal der opengl-API. Grundsätzlich wird davon ausgegangen, dass sich Ihr gesamter Code in der Funktion main () befindet und alle diese Handles über die lokalen Variablen von main () übergeben werden.
Die Konsequenzen dieser Regel lauten wie folgt:
quelle
main
scheint mir die Idee, dass OGL auf dem gesamten Code basiert, der sich darin befindet , etwas schwierig (zumindest in der Formulierung).