
Einführung
In einem
früheren Artikel haben wir uns die OpenSceneGraph-Assembly aus dem Quellcode angesehen und ein elementares Beispiel geschrieben, in dem eine graue Ebene in einer leeren lila Welt hängt. Ich stimme zu, nicht zu beeindruckend. Wie ich bereits sagte, gibt es in diesem kleinen Beispiel die Hauptkonzepte, auf denen diese Grafik-Engine basiert. Betrachten wir sie genauer. Das folgende Material verwendet Illustrationen aus
Alexander Bobkovs Blog über OSG (schade, dass der Autor das Schreiben über OSG aufgegeben hat ...). Der Artikel basiert auch auf Material und Beispielen aus dem Buch
OpenSceneGraph 3.0. AnfängerleitfadenIch muss sagen, dass die vorherige Veröffentlichung einer Kritik ausgesetzt war, der ich teilweise zustimme - das Material kam unausgesprochen heraus und wurde aus dem Zusammenhang gerissen. Ich werde versuchen, dieses Versäumnis unter dem Schnitt zu beheben.
1. Kurz über das Diagramm der Szene und ihrer Knoten
Das zentrale Konzept der Engine ist das sogenannte
Szenendiagramm (es ist kein Zufall, dass es im Namen des Frameworks hängen geblieben ist) - eine hierarchische Baumstruktur, mit der Sie eine logische und räumliche Darstellung einer dreidimensionalen Szene organisieren können. Das Szenendiagramm enthält den Wurzelknoten und die zugehörigen Zwischen- und Endknoten oder
-knoten .
Zum Beispiel

Diese Grafik zeigt eine Szene, die aus einem Haus und einem Tisch besteht. Das Haus hat eine bestimmte geometrische Darstellung und befindet sich auf eine bestimmte Weise im Raum relativ zu einem bestimmten grundlegenden Koordinatensystem, das dem Wurzelknoten (Wurzel) zugeordnet ist. Die Tabelle wird auch durch eine Geometrie beschrieben, die sich in irgendeiner Weise relativ zum Haus und zusammen mit dem Haus befindet - relativ zum Wurzelknoten. Alle Knoten, die eine gemeinsame Eigenschaft haben, weil sie von einer osg :: Node-Klasse erben, werden entsprechend ihrem funktionalen Zweck in Typen unterteilt
- Gruppenknoten (osg :: Group) - sind die Basisklasse für alle Zwischenknoten und dienen zum Kombinieren anderer Knoten zu Gruppen
- Transformationsknoten (osg :: Transform und seine Nachkommen) - zur Beschreibung der Transformation von Objektkoordinaten
- Geometrische Knoten (osg :: Geode) - Endknoten (Blattknoten) des Szenendiagramms, die Informationen zu einem oder mehreren geometrischen Objekten enthalten.
Die Geometrie von Szenenobjekten in OSG wird in einem eigenen lokalen Koordinatensystem des Objekts beschrieben. Transformationsknoten, die sich zwischen diesem Objekt und dem Wurzelknoten befinden, implementieren Matrixkoordinatentransformationen, um die Position des Objekts im Basiskoordinatensystem zu erhalten.
Die Knoten führen viele wichtige Funktionen aus, insbesondere speichern sie den Status der Anzeige von Objekten, und dieser Status wirkt sich nur auf den diesem Knoten zugeordneten Untergraphen aus. Knoten im Szenendiagramm können mehrere Rückrufe zugeordnet werden, Ereignishandler, mit denen Sie den Status des Knotens und den damit verbundenen Untergraphen ändern können.
Alle globalen Operationen im Szenendiagramm, die mit dem Erhalten des Endergebnisses auf dem Bildschirm verbunden sind, werden automatisch von der Engine ausgeführt, indem das Diagramm regelmäßig in der Tiefe durchlaufen wird.
In dem
zuletzt untersuchten Beispiel bestand unsere Szene aus einem einzelnen Objekt - einem Flugzeugmodell, das aus einer Datei geladen wurde. Mit Blick auf die Zukunft werde ich sagen, dass dieses Modell der Blattknoten des Szenendiagramms ist. Es ist fest mit dem globalen Basiskoordinatensystem des Motors verschweißt.
2. OSG-Speicherverwaltung
Da die Knoten des Szenendiagramms viele Daten über Szenenobjekte und Operationen auf diesen speichern, ist es erforderlich, Speicher zuzuweisen, auch dynamisch, um diese Daten zu speichern. In diesem Fall müssen Sie beim Bearbeiten des Szenendiagramms und beispielsweise beim Löschen einiger seiner Knoten sorgfältig überwachen, dass die gelöschten Knoten des Diagramms nicht mehr verarbeitet werden. Dieser Prozess ist immer mit Fehlern und zeitaufwändigem Debuggen verbunden, da es für den Entwickler ziemlich schwierig ist zu verfolgen, welche Zeiger auf Objekte auf vorhandene Daten verweisen und welche gelöscht werden sollten. Ohne effektive Speicherverwaltung treten eher Segmentierungsfehler und Speicherlecks auf.
Die Speicherverwaltung ist eine wichtige Aufgabe in OSG und ihr Konzept basiert auf zwei Punkten:
- Zuweisung von Speicher: Sicherstellen der Zuweisung der zum Speichern eines Objekts erforderlichen Speichermenge.
- Speicher freigeben: Gibt den zugewiesenen Speicher an das System zurück, wenn er nicht benötigt wird.
Viele moderne Programmiersprachen wie C #, Java, Visual Basic .Net und dergleichen verwenden den sogenannten Garbage Collector, um den zugewiesenen Speicher freizugeben. Das Konzept der C ++ - Sprache sieht einen solchen Ansatz nicht vor, wir können ihn jedoch mithilfe der sogenannten Smart Pointer nachahmen.
Heute hat C ++ intelligente Zeiger in seinem Arsenal, das als "out of the box" bezeichnet wird (und der C ++ 17-Standard hat es bereits geschafft, die Sprache von einigen veralteten Arten intelligenter Zeiger zu befreien), aber dies war nicht immer der Fall. Die früheste der offiziellen OSG-Versionen mit der Nummer 0.9 wurde 2002 geboren, und es gab noch drei Jahre vor der ersten offiziellen Veröffentlichung. Zu dieser Zeit sah der C ++ - Standard noch keine intelligenten Zeiger vor, und selbst wenn Sie an
einen historischen Exkurs glauben, erlebte die Sprache selbst schwere Zeiten. Das Erscheinungsbild eines Fahrrads in Form eigener Smart Pointer, die in OSG implementiert sind, ist daher keineswegs überraschend. Dieser Mechanismus ist tief in die Struktur des Motors integriert, daher ist es von Anfang an unbedingt erforderlich, dessen Funktionsweise zu verstehen.
3. Die Klassen osg :: ref_ptr <> und osg :: Referenced
OSG bietet einen eigenen intelligenten Zeigermechanismus, der auf der Vorlagenklasse osg :: ref_ptr <> basiert, um die automatische Speicherbereinigung zu implementieren. Für den ordnungsgemäßen Betrieb stellt OSG eine weitere osg :: Referenced-Klasse zum Verwalten von Speicherblöcken bereit, für die der Verweis auf diese gezählt wird.
Die Klasse osg :: ref_ptr <> bietet mehrere Operatoren und Methoden.
- get () ist eine öffentliche Methode, die einen Rohzeiger zurückgibt. Wenn Sie beispielsweise die Vorlage osg :: Node als Argument verwenden, gibt diese Methode osg :: Node * zurück.
- operator * () ist tatsächlich der Dereferenzierungsoperator.
- Mit operator -> () und operator = () können Sie osg :: ref_ptr <> als klassischen Zeiger verwenden, wenn Sie auf die Methoden und Eigenschaften der durch diesen Zeiger beschriebenen Objekte zugreifen.
- operator == (), operator! = () und operator! () - ermöglichen das Ausführen von Vergleichsoperationen für intelligente Zeiger.
- valid () ist eine öffentliche Methode, die true zurückgibt, wenn der verwaltete Zeiger den richtigen Wert hat (nicht NULL). Der Ausdruck some_ptr.valid () entspricht dem Ausdruck some_ptr! = NULL, wenn some_ptr ein intelligenter Zeiger ist.
- release () ist eine öffentliche Methode, die nützlich ist, wenn Sie eine verwaltete Adresse von einer Funktion zurückgeben möchten. Darüber wird später ausführlicher beschrieben.
Die Klasse osg :: Referenced ist die Basisklasse für alle Elemente des Szenendiagramms, z. B. Knoten, Geometrie, Rendering-Zustände und andere Objekte, die auf der Bühne platziert sind. Wenn wir also den Wurzelknoten der Szene erstellen, erben wir indirekt alle Funktionen, die von der Klasse osg :: Referenced bereitgestellt werden. Daher gibt es in unserem Programm eine Ankündigung
osg::ref_ptr<osg::Node> root;
Die Klasse osg :: Referenced enthält einen Ganzzahlzähler für Verweise auf den zugewiesenen Speicherblock. Dieser Zähler wird im Klassenkonstruktor auf Null initialisiert. Sie wird um eins erhöht, wenn das Objekt osg :: ref_ptr <> erstellt wird. Dieser Zähler nimmt ab, sobald ein Verweis auf das durch diesen Zeiger beschriebene Objekt gelöscht wird. Ein Objekt wird automatisch zerstört, wenn keine intelligenten Zeiger mehr darauf verweisen.
Die Klasse osg :: Referenced verfügt über drei öffentliche Methoden:
- ref () ist eine öffentliche Methode, die um 1 Referenzanzahl erhöht wird.
- unref () ist eine öffentliche Methode, die um 1 Referenzanzahl verringert wird.
- referenceCount () ist eine öffentliche Methode, die den aktuellen Wert des Referenzzählers zurückgibt. Dies ist beim Debuggen von Code hilfreich.
Diese Methoden sind in allen Klassen verfügbar, die von osg :: Referenced abgeleitet sind. Es sollte jedoch beachtet werden, dass die manuelle Steuerung des Verbindungszählers zu unvorhersehbaren Konsequenzen führen kann. Wenn Sie dies verwenden, sollten Sie klar verstehen, was Sie tun.
4. Wie OSG Müll sammelt und warum er benötigt wird
Es gibt mehrere Gründe, warum intelligente Zeiger und Speicherbereinigung verwendet werden sollten:
- Minimierung kritischer Fehler: Durch die Verwendung intelligenter Zeiger können Sie die Zuweisung und Freigabe von Speicher automatisieren. Es gibt keine gefährlichen Rohzeiger.
- Effektive Speicherverwaltung: Der für das Objekt zugewiesene Speicher wird sofort freigegeben, sobald das Objekt nicht mehr benötigt wird, was zu einer wirtschaftlichen Nutzung der Systemressourcen führt.
- Erleichterung des Debuggens von Anwendungen: Da wir die Anzahl der Verknüpfungen zu einem Objekt klar verfolgen können, haben wir die Möglichkeit, verschiedene Arten von Optimierungen und Experimenten durchzuführen.
Angenommen, ein Szenendiagramm besteht aus einem Wurzelknoten und mehreren Ebenen von untergeordneten Knoten. Wenn der Stammknoten und alle untergeordneten Knoten mit der Klasse osg :: ref_ptr <> verwaltet werden, kann die Anwendung nur den Zeiger auf den Stammknoten verfolgen. Das Entfernen dieses Knotens führt zu einem sequentiellen, automatischen Entfernen aller untergeordneten Knoten.

Intelligente Zeiger können als lokale Variablen, globale Variablen und Klassenmitglieder verwendet werden und verringern automatisch die Referenzanzahl, wenn der intelligente Zeiger den Gültigkeitsbereich verlässt.
Intelligente Zeiger werden von OSG-Entwicklern für die Verwendung in Projekten dringend empfohlen. Es gibt jedoch einige wichtige Punkte, die Sie beachten sollten:
- Instanzen von osg :: Referenced und seinen Derivaten können ausschließlich auf dem Heap erstellt werden. Sie können nicht als lokale Variablen auf dem Stapel erstellt werden, da die Destruktoren dieser Klassen als geschützt deklariert sind. Zum Beispiel
osg::ref_ptr<osg::Node> node = new osg::Node;
- Sie können temporäre Szenenknoten mit regulären C ++ - Zeigern erstellen. Dieser Ansatz ist jedoch nicht sicher. Es ist besser, intelligente Zeiger zu verwenden, um sicherzustellen, dass das Szenendiagramm korrekt verwaltet wird.
osg::Node *tmpNode = new osg::Node;
- In keinem Fall sollten Sie zyklische Verknüpfungsszenen im Baum verwenden, wenn der Knoten über mehrere Ebenen direkt oder indirekt auf sich selbst verweist

In dem Beispieldiagramm des Szenendiagramms bezieht sich der untergeordnete 1.1-Knoten auf sich selbst, und der untergeordnete 2.2-Knoten bezieht sich auch auf den untergeordneten 1.2-Knoten. Solche Links können zu einer falschen Berechnung der Anzahl der Links und zu einem unbestimmten Verhalten des Programms führen.
5. Verfolgte verwaltete Objekte
Um die Funktionsweise des Smart-Pointer-Mechanismus in OSG zu veranschaulichen, schreiben wir das folgende synthetische Beispiel
main.h #ifndef MAIN_H #define MAIN_H #include <osg/ref_ptr> #include <osg/Referenced> #include <iostream> #endif // MAIN_H
main.cpp #include "main.h" class MonitoringTarget : public osg::Referenced { public: MonitoringTarget(int id) : _id(id) { std::cout << "Constructing target " << _id << std::endl; } protected: virtual ~MonitoringTarget() { std::cout << "Dsetroying target " << _id << std::endl; } int _id; }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0); std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl; osg::ref_ptr<MonitoringTarget> anotherTarget = target; std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl; return 0; }
Wir erstellen eine osg :: Referenced-Nachkommenklasse, die nichts anderes tut als im Konstruktor und Destruktor, der meldet, dass ihre Instanz erstellt wurde, und den Bezeichner anzeigt, der beim Erstellen der Instanz ermittelt wird. Erstellen Sie eine Instanz der Klasse mit dem Smart-Pointer-Mechanismus
osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0);
Als nächstes zeigen wir den Referenzzähler für das Zielobjekt an
std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl;
Erstellen Sie anschließend einen neuen intelligenten Zeiger und weisen Sie ihm den Wert des vorherigen Zeigers zu
osg::ref_ptr<MonitoringTarget> anotherTarget = target;
und erneut den Referenzzähler anzeigen
std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl;
Mal sehen, was wir durch die Analyse der Ausgabe des Programms erhalten haben
15:42:39: Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Dsetroying target 0 15:42:42:
Wenn der Klassenkonstruktor gestartet wird, wird eine entsprechende Meldung angezeigt, die uns mitteilt, dass der Speicher für das Objekt zugewiesen ist und der Konstruktor ordnungsgemäß funktioniert hat. Nach dem Erstellen eines intelligenten Zeigers sehen wir außerdem, dass der Referenzzähler für das erstellte Objekt um eins erhöht wurde. Wenn Sie einen neuen Zeiger erstellen und ihm den Wert des alten Zeigers zuweisen, wird im Wesentlichen eine neue Verknüpfung zu demselben Objekt erstellt, sodass der Referenzzähler um einen anderen erhöht wird. Beim Beenden des Programms wird der Destruktor der MonitoringTarget-Klasse aufgerufen.

Lassen Sie uns ein weiteres Experiment durchführen, indem wir solchen Code am Ende der main () - Funktion hinzufügen
for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i); }
was zu einem solchen "Auspuff" -Programm führt
16:04:30: Constructing target 0 Referenced count before referring: 1 Referenced count after referring: 2 Constructing target 1 Dsetroying target 1 Constructing target 2 Dsetroying target 2 Constructing target 3 Dsetroying target 3 Constructing target 4 Dsetroying target 4 Dsetroying target 0 16:04:32:
Wir erstellen mehrere Objekte im Hauptteil der Schleife mit einem intelligenten Zeiger. Da sich der Bereich des Zeigers in diesem Fall nur auf den Körper der Schleife erstreckt, wird der Destruktor beim Beenden automatisch aufgerufen. Dies würde ganz offensichtlich nicht passieren, wir würden die üblichen Zeiger verwenden.
Die automatische Speicherfreigabe ist ein weiteres wichtiges Merkmal der Arbeit mit intelligenten Zeigern. Da der Destruktor der abgeleiteten Klasse osg :: Referenced geschützt ist, können wir den Löschoperator nicht explizit aufrufen, um das Objekt zu löschen. Die einzige Möglichkeit, ein Objekt zu löschen, besteht darin, die Anzahl der Links zu diesem Objekt zurückzusetzen. Aber dann wird unser Code während der Multithread-Datenverarbeitung unsicher - wir können von einem anderen Thread auf ein bereits gelöschtes Objekt zugreifen.
Glücklicherweise bietet OSG mithilfe des Objektentfernungsplans eine Lösung für dieses Problem. Dieser Scheduler basiert auf der Verwendung der Klasse osg :: DeleteHandler. Es funktioniert so, dass ein Objekt nicht sofort gelöscht wird, sondern nach einer Weile. Alle zu löschenden Objekte werden vorübergehend gespeichert, bis der Moment zum sicheren Löschen gekommen ist, und dann werden sie alle auf einmal gelöscht. Der osg :: DeleteHandler-Entfernungsplaner wird vom OSG-Render-Backend gesteuert.
6. Rückkehr von der Funktion
Fügen Sie unserem Beispielcode die folgende Funktion hinzu
MonitoringTarget *createMonitoringTarget(int id) { osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id); return target.release(); }
und ersetzen Sie den Aufruf des neuen Operators in der Schleife durch den Aufruf dieser Funktion
for (int i = 1; i < 5; i++) { osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i); }
Der Aufruf von release () reduziert die Anzahl der Verweise auf das Objekt auf Null. Anstatt den Speicher zu löschen, wird der tatsächliche Zeiger direkt auf den zugewiesenen Speicher zurückgegeben. Wenn dieser Zeiger einem anderen intelligenten Zeiger zugewiesen wird, treten keine Speicherlecks auf.
Schlussfolgerungen
Die Konzepte des Szenendiagramms und der intelligenten Zeiger sind grundlegend für das Verständnis des Funktionsprinzips und damit für die effektive Verwendung von OpenSceneGraph. Denken Sie bei OSG-Smart-Zeigern daran, dass deren Verwendung unbedingt erforderlich ist, wenn
- Eine langfristige Lagerung der Anlage wird erwartet.
- Ein Objekt speichert eine Verknüpfung zu einem anderen Objekt
- Sie müssen einen Zeiger von einer Funktion zurückgeben
Der im Artikel bereitgestellte Beispielcode
ist hier verfügbar .
Fortsetzung folgt...