OpenSceneGraph: Grundlegende Programmiertechniken

Bild

Einführung


Dieser Artikel konzentriert sich weniger auf die Grafiken als vielmehr darauf, wie die Anwendung, die sie verwendet, unter Berücksichtigung der Besonderheiten der OpenSceneGraph-Engine und der von ihr bereitgestellten Software organisiert werden sollte.

Es ist kein Geheimnis, dass der Schlüssel zum Erfolg eines Softwareprodukts eine gut gestaltete Architektur ist, die die Möglichkeit bietet, den geschriebenen Code zu pflegen und zu erweitern. In diesem Sinne befindet sich die Engine, die wir in Betracht ziehen, auf einem ziemlich hohen Niveau und bietet dem Entwickler ein sehr breites Toolkit, mit dem eine flexible modulare Architektur aufgebaut werden kann.

Dieser Artikel ist ziemlich lang und enthält eine Übersicht über die verschiedenen Tools und Techniken (Entwurfsmuster, falls gewünscht), die von der Entwickler-Engine bereitgestellt werden. Alle Abschnitte des Artikels enthalten Beispiele, deren Code in meinem Repository gespeichert werden kann .

1. Analysieren der Befehlszeilenoptionen


In C / C ++ werden Befehlszeilenparameter über die Argumente an die Funktion main () übergeben. In früheren Beispielen haben wir diese Parameter sorgfältig als nicht verwendet markiert. Jetzt werden wir sie verwenden, um unserem Programm beim Start einige Daten mitzuteilen.

OSG verfügt über integrierte Befehlszeilen-Parsing-Tools.

Erstellen Sie das folgende Beispiel

Befehlszeilenbeispiel
main.h

#ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif // MAIN_H 

main.cpp

 #include "main.h" int main(int argc, char *argv[]) { osg::ArgumentParser args(&argc, argv); std::string filename; args.read("--model", filename); osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Stellen Sie die Programmstartparameter in QtCreator ein



Wenn wir das Programm zur Ausführung ausführen, erhalten wir das Ergebnis (LKW-Modell aus denselben OpenSceneGraph-Daten )



Schauen wir uns nun ein Beispiel Zeile für Zeile an

 osg::ArgumentParser args(&argc, argv); 

Erstellt eine Instanz der Befehlszeilen-Parser-Klasse osg :: ArgumentParser. Beim Erstellen werden dem Klassenkonstruktor die Argumente übergeben, die von der Funktion main () vom Betriebssystem akzeptiert werden.

 std::string filename; args.read("--model", filename); 

Wir analysieren die Argumente, suchen nach dem Schlüssel „–model“ und setzen seinen Wert in den Dateinamen der Zeichenfolge. Mit diesem Schlüssel übertragen wir den Dateinamen mit einem dreidimensionalen Modell an das Programm. Als nächstes laden wir dieses Modell und zeigen es an

 osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); 

Die read () -Methode der osg :: ArgumentParser-Klasse weist viele Überladungen auf, sodass Sie nicht nur Zeichenfolgenwerte aus der Befehlszeile lesen können, sondern auch Ganzzahlen, Gleitkommazahlen, Vektoren usw. Sie können beispielsweise einen bestimmten Parameter vom Typ float lesen

 float size = 0.0f; args.read("--size", size); 

Wenn dieser Parameter nicht in der Befehlszeile angezeigt wird, bleibt sein Wert unverändert, nachdem die Größenvariable initialisiert wurde.

2. Benachrichtigungs- und Protokollierungsmechanismus


OpenSceneGraph verfügt über einen Benachrichtigungsmechanismus, mit dem Sie Debugging-Meldungen während des Rendervorgangs anzeigen und vom Entwickler initiieren können. Dies ist eine große Hilfe beim Verfolgen und Debuggen eines Programms. Das OSG-Benachrichtigungssystem unterstützt die Ausgabe von Diagnoseinformationen (Fehler, Warnungen, Benachrichtigungen) auf Motorkernebene und deren Plug-Ins. Der Entwickler kann während des Programmvorgangs mithilfe der Funktion osg :: notify () eine Diagnosemeldung anzeigen.

Diese Funktion arbeitet als Standardausgabestream der Standard-C ++ - Bibliothek durch Überladen des Operators <<. Es wird die Nachrichtenebene als Argument verwendet: IMMER, FATAL, WARN, NOTICE, INFO, DEBUG_INFO und DEBUG_FP. Zum Beispiel

 osg::notify(osg::WARN) << "Some warning message" << std::endl; 

Zeigt eine Warnung mit benutzerdefiniertem Text an.

OSG-Benachrichtigungen können wichtige Informationen über den Status des Programms, Erweiterungen des Grafiksubsystems des Computers und mögliche Probleme mit der Engine enthalten.

In einigen Fällen ist es erforderlich, diese Daten nicht an die Konsole auszugeben, sondern diese Ausgabe in eine Datei (in Form eines Protokolls) oder eine andere Schnittstelle, einschließlich eines Grafik-Widgets, umzuleiten. Die Engine enthält eine spezielle Klasse osg :: NotifyHandler, die die Umleitung von Benachrichtigungen an den vom Entwickler benötigten Ausgabestream ermöglicht.

Überlegen Sie anhand eines einfachen Beispiels, wie Sie die Ausgabe von Benachrichtigungen beispielsweise in eine Textprotokolldatei umleiten können. Schreiben Sie den folgenden Code

Beispiel benachrichtigen
main.h

 #ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <fstream> #endif // MAIN_H 

main.cpp

 #include "main.h" class LogFileHandler : public osg::NotifyHandler { public: LogFileHandler(const std::string &file) { _log.open(file.c_str()); } virtual ~LogFileHandler() { _log.close(); } virtual void notify(osg::NotifySeverity severity, const char *msg) { _log << msg; } protected: std::ofstream _log; }; int main(int argc, char *argv[]) { osg::setNotifyLevel(osg::INFO); osg::setNotifyHandler(new LogFileHandler("../logs/log.txt")); osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root) { OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl; return -1; } osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Um die Ausgabe umzuleiten, schreiben wir die LogFileHandler-Klasse, die der Nachfolger von osg :: NotifyHandler ist. Der Konstruktor und Destruktor dieser Klasse steuern das Öffnen und Schließen des _log-Ausgabestreams, dem die Textdatei zugeordnet ist. Die notify () -Methode ist eine ähnliche Basisklassenmethode, die wir neu definiert haben, um sie an die von OSG während des Betriebs über den Parameter msg gesendeten Dateibenachrichtigungen auszugeben.

Klasse LogFileHandler

 class LogFileHandler : public osg::NotifyHandler { public: LogFileHandler(const std::string &file) { _log.open(file.c_str()); } virtual ~LogFileHandler() { _log.close(); } virtual void notify(osg::NotifySeverity severity, const char *msg) { _log << msg; } protected: std::ofstream _log; }; 

Nehmen Sie als nächstes im Hauptprogramm die erforderlichen Einstellungen vor

 osg::setNotifyLevel(osg::INFO); 

Stellen Sie den Pegel der INFO-Benachrichtigungen ein, dh die Ausgabe aller Informationen über den Betrieb des Motors, einschließlich der aktuellen Benachrichtigungen über den normalen Betrieb, im Protokoll.

 osg::setNotifyHandler(new LogFileHandler("../logs/log.txt")); 

Installieren Sie den Benachrichtigungshandler. Als Nächstes verarbeiten wir Befehlszeilenargumente, in denen die Pfade zu den geladenen Modellen übergeben werden

 osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root) { OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl; return -1; } 

Gleichzeitig behandeln wir die Situation des Datenmangels in der Befehlszeile und zeigen eine Meldung im manuellen Protokollmodus mit dem Makro OSG_FATAL an. Führen Sie das Programm mit den folgenden Argumenten aus



Ausgabe in eine Protokolldatei wie diese

OSG-Protokollbeispiel
 Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll OSGReaderWriter wrappers loaded OK CullSettings::readEnvironmentalVariables() void StateSet::setGlobalDefaults() void StateSet::setGlobalDefaults() ShaderPipeline disabled. StateSet::setGlobalDefaults() Setting up GL2 compatible shaders CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() ShaderComposer::ShaderComposer() 0xa5ce8f0 CullSettings::readEnvironmentalVariables() ShaderComposer::ShaderComposer() 0xa5ce330 View::setSceneData() Reusing existing scene0xa514220 CameraManipulator::computeHomePosition(0, 0) boundingSphere.center() = (-6.40034 1.96225 0.000795364) boundingSphere.radius() = 16.6002 CameraManipulator::computeHomePosition(0xa52f138, 0) boundingSphere.center() = (-6.40034 1.96225 0.000795364) boundingSphere.radius() = 16.6002 Viewer::realize() - No valid contexts found, setting up view across all screens. Applying osgViewer::ViewConfig : AcrossAllScreens . . . . ShaderComposer::~ShaderComposer() 0xa5ce330 ShaderComposer::~ShaderComposer() 0xa5ce8f0 ShaderComposer::~ShaderComposer() 0xa5d6228 close(0x1)0xa5d3e50 close(0)0xa5d3e50 ContextData::unregisterGraphicsContext 0xa5d3e50 DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. ShaderComposer::~ShaderComposer() 0xa5de4e0 close(0x1)0xa5ddba0 close(0)0xa5ddba0 ContextData::unregisterGraphicsContext 0xa5ddba0 Done destructing osg::View DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll 


Es spielt keine Rolle, dass Ihnen diese Informationen im Moment möglicherweise sinnlos erscheinen. In Zukunft kann eine solche Schlussfolgerung dazu beitragen, Fehler in Ihrem Programm zu beheben.

Standardmäßig sendet OSG Nachrichten an die Standardausgabe von std :: cout und Fehlermeldungen an den Stream std :: cerr. Durch Überschreiben des Benachrichtigungshandlers, wie im Beispiel gezeigt, kann diese Ausgabe jedoch an einen beliebigen Ausgabestream einschließlich GUI-Elementen umgeleitet werden.

Beachten Sie, dass das System beim Festlegen einer hohen Benachrichtigungsstufe (z. B. FATAL) alle Benachrichtigungen einer niedrigeren Stufe ignoriert. Zum Beispiel in einem ähnlichen Fall

 osg::setNotifyLevel(osg::FATAL); . . . osg::notify(osg::WARN) << "Some message." << std::endl; 

Eine benutzerdefinierte Nachricht wird einfach nicht angezeigt.

3. Abfangen von geometrischen Attributen


Die Klasse osg :: Geometry verwaltet einen Datensatz, der Scheitelpunkte beschreibt, und zeigt ein Polygonnetz unter Verwendung eines geordneten Satzes von Grundelementen an. Diese Klasse hat jedoch keine Ahnung von Elementen der Topologie des Modells wie Flächen, Kanten und der Beziehung zwischen ihnen. Diese Nuance verhindert die Implementierung von Dingen wie dem Verschieben bestimmter Gesichter, beispielsweise beim Animieren von Modellen. OSG unterstützt diese Funktionalität derzeit nicht.

Die Engine implementiert jedoch eine Reihe von Funktoren, mit denen Sie die Geometrieattribute eines Objekts erneut lesen und zum Modellieren der Topologie des polygonalen Netzes verwenden können. In C ++ ist ein Funktor ein Konstrukt, mit dem Sie ein Objekt als Funktion verwenden können.

Die Klasse osg :: Drawable bietet dem Entwickler vier Arten von Funktoren:

  1. osg :: Drawable :: AttributeFunctor - Liest die Attribute von Scheitelpunkten als Array von Zeigern. Es verfügt über eine Reihe virtueller Methoden zum Anwenden von Scheitelpunktattributen verschiedener Datentypen. Um diesen Funktor verwenden zu können, müssen Sie die Klasse beschreiben und eine oder mehrere ihrer Methoden überschreiben, in denen die vom Entwickler geforderten Aktionen ausgeführt werden


 virtual void apply( osg::Drawable::AttributeType type, unsigned int size, osg::Vec3* ptr ) { //  3-     ptr. //      } 

  1. osg :: Drawable :: ConstAttributeFunctor - schreibgeschützte Version des vorherigen Funktors: Ein Zeiger auf ein Array von Vektoren wird als konstanter Parameter übergeben
  2. osg :: PrimitiveFunctor - imitiert den Prozess des Renderns von OpenGL-Objekten. Unter dem Deckmantel des Renderns eines Objekts werden vom Entwickler überschriebene Funktormethoden aufgerufen. Dieser Funktor hat zwei wichtige Vorlagenunterklassen: osg :: TemplatePrimitiveFunctor <> und osg :: TriangleFunctor <>. Diese Klassen empfangen primitive Eckpunkte als Parameter und übergeben sie mit dem Operator operator () an Benutzermethoden.
  3. osg :: PrimitiveIndexFunctor - führt dieselben Aktionen wie der vorherige Funktor aus, akzeptiert jedoch die Scheitelpunktindizes des Grundelements als Parameter.

Von osg :: Drawable abgeleitete Klassen wie osg :: ShapeDrawable und osg :: Geometry verfügen über eine accept () -Methode zum Anwenden verschiedener Funktoren.

4. Beispiel für die Verwendung des primitiven Funktors


Wir veranschaulichen die beschriebene Funktionalität am Beispiel des Sammelns von Informationen über dreieckige Flächen und Punkte einer zuvor bestimmten Geometrie.

Funktor Beispiel
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Geode> #include <osg/Geometry> #include <osg/TriangleFunctor> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" std::string vec2str(const osg::Vec3 &v) { std::string tmp = std::to_string(vx()); tmp += " "; tmp += std::to_string(vy()); tmp += " "; tmp += std::to_string(vz()); return tmp; } struct FaceCollector { void operator()(const osg::Vec3 &v1, const osg::Vec3 &v2, const osg::Vec3 &v3) { std::cout << "Face vertices: " << vec2str(v1) << "; " << vec2str(v2) << "; " << vec2str(v3) << std::endl; } }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array; vertices->push_back( osg::Vec3(0.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(0.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 1.5f) ); vertices->push_back( osg::Vec3(2.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(2.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(3.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(3.0f, 0.0f, 1.5f) ); vertices->push_back( osg::Vec3(4.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(4.0f, 0.0f, 1.0f) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) ); osg::ref_ptr<osg::Geometry> geom = new osg::Geometry; geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); geom->setNormalBinding(osg::Geometry::BIND_OVERALL); geom->addPrimitiveSet(new osg::DrawArrays(GL_QUAD_STRIP, 0, 10)); osg::ref_ptr<osg::Geode> root = new osg::Geode; root->addDrawable(geom.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); osg::TriangleFunctor<FaceCollector> functor; geom->accept(functor); return viewer.run(); } 


Lassen Sie uns Folgendes beachten, indem wir den von uns oft berücksichtigten Prozess der Erstellung von Geometrie weglassen. Wir definieren eine FaceCollector-Struktur, für die wir operator () wie folgt neu definieren

 struct FaceCollector { void operator()(const osg::Vec3 &v1, const osg::Vec3 &v2, const osg::Vec3 &v3) { std::cout << "Face vertices: " << vec2str(v1) << "; " << vec2str(v2) << "; " << vec2str(v3) << std::endl; } }; 

Wenn dieser Operator aufgerufen wird, zeigt er die Koordinaten der drei Eckpunkte an, die von der Engine an ihn übertragen werden. Die Funktion vec2str ist erforderlich, um die Komponenten des Vektors osg :: Vec3 in std :: string zu übersetzen. Um den Funktor aufzurufen, erstellen Sie eine Instanz davon und übergeben Sie sie über die Methode accept () an das Geometrieobjekt

 osg::TriangleFunctor<FaceCollector> functor; geom->accept(functor); 

Wie oben erwähnt, ahmt dieser Aufruf das Rendern der Geometrie nach und ersetzt die Zeichnung selbst durch Aufrufen einer überschriebenen Funktormethode. In diesem Fall wird es während des "Zeichnens" jedes der Dreiecke aufgerufen, aus denen die Geometrie des Beispiels besteht.

Auf dem Bildschirm erhalten wir eine solche Geometrie



und so ein Auspuff zur Konsole

 Face vertices: 0.000000 0.000000 0.000000; 0.000000 0.000000 1.000000; 1.000000 0.000000 0.000000 Face vertices: 0.000000 0.000000 1.000000; 1.000000 0.000000 1.500000; 1.000000 0.000000 0.000000 Face vertices: 1.000000 0.000000 0.000000; 1.000000 0.000000 1.500000; 2.000000 0.000000 0.000000 Face vertices: 1.000000 0.000000 1.500000; 2.000000 0.000000 1.000000; 2.000000 0.000000 0.000000 Face vertices: 2.000000 0.000000 0.000000; 2.000000 0.000000 1.000000; 3.000000 0.000000 0.000000 Face vertices: 2.000000 0.000000 1.000000; 3.000000 0.000000 1.500000; 3.000000 0.000000 0.000000 Face vertices: 3.000000 0.000000 0.000000; 3.000000 0.000000 1.500000; 4.000000 0.000000 0.000000 Face vertices: 3.000000 0.000000 1.500000; 4.000000 0.000000 1.000000; 4.000000 0.000000 0.000000 

Tatsächlich werden beim Aufrufen von geom-> accept (...) keine Dreiecke gerendert, OpenGL-Aufrufe simuliert und stattdessen Daten über die Eckpunkte des Dreiecks, deren Wiedergabe simuliert wird



Die Klasse osg :: TemplatePrimitiveFunctor sammelt nicht nur Daten zu Dreiecken, sondern auch zu anderen OpenGL-Grundelementen. Um die Verarbeitung dieser Daten zu implementieren, müssen Sie die folgenden Operatoren im Vorlagenargument überschreiben

 //   void operator()( const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool ); 


5. Das Besuchermuster


Das Besuchermuster wird verwendet, um auf Vorgänge zuzugreifen, um die Elemente des Szenendiagramms zu ändern, ohne die Klassen dieser Elemente zu ändern. Die Besucherklasse implementiert alle relevanten virtuellen Funktionen, um sie über einen doppelten Versandmechanismus auf verschiedene Arten von Elementen anzuwenden. Mit diesem Mechanismus kann der Entwickler seine eigene Besucherinstanz erstellen, indem er die von ihm benötigten Funktionen mithilfe spezieller Operatoren implementiert und den Besucher im laufenden Betrieb an verschiedene Arten von Szenendiagrammelementen bindet, ohne die Funktionalität der Elemente selbst zu ändern. Dies ist eine großartige Möglichkeit, die Funktionalität eines Elements zu erweitern, ohne Unterklassen dieser Elemente zu definieren.

Um diesen Mechanismus in OSG zu implementieren, wird die Klasse osg :: NodeVisitor definiert. Die von osg :: NodeVisitor geerbte Klasse bewegt sich im Szenendiagramm, besucht jeden Knoten und wendet die vom Entwickler definierten Operationen darauf an. Dies ist die Hauptklasse, die verwendet wird, um in den Prozess des Aktualisierens von Knoten und des Abschneidens unsichtbarer Knoten einzugreifen und einige andere Operationen zum Ändern der Geometrie von Knoten in der Szene anzuwenden, z. B. osgUtil :: SmoothingVisitor, osgUtil :: Simplifier und osgUtil :: TriStripVisitor.

Um den Besucher zu unterklassifizieren, müssen wir eine oder mehrere virtuell überladene apply () -Methoden überschreiben, die von der Basisklasse osg :: NodeVisitor bereitgestellt werden. Die meisten der wichtigsten OSG-Knotentypen verfügen über diese Methoden. Der Besucher ruft automatisch die apply () -Methode für jeden der besuchten Knoten auf, wenn er das Diagramm der Szenenszene durchläuft. Der Entwickler überschreibt die apply () -Methode für jeden der von ihm benötigten Knotentypen.

Bei der Implementierung der Methode apply () muss der Entwickler zum geeigneten Zeitpunkt die Methode traverse () der Basisklasse osg :: NodeVisitor aufrufen. Dies initiiert den Übergang des Besuchers zum nächsten Knoten, entweder zu einem untergeordneten Knoten oder zu einem Nachbarn auf Hierarchieebene, wenn der aktuelle Knoten keine untergeordneten Knoten hat, zu denen der Übergang erfolgen kann. Das Fehlen eines Aufrufs von traverse () bedeutet, dass das Durchlaufen des Szenendiagramms gestoppt wird und der Rest des Szenendiagramms ignoriert wird.

Überladungen der Methode apply () haben einheitliche Formate

 virtual void apply( osg::Node& ); virtual void apply( osg::Geode& ); virtual void apply( osg::Group& ); virtual void apply( osg::Transform& ); 

Um den Untergraphen des aktuellen Knotens für das Besucherobjekt zu umgehen, müssen Sie den Crawling-Modus festlegen, z.

 ExampleVisitor visitor; visitor->setTraversalMode( osg::NodeVisitor::TRAVERSE_ALL_CHILDREN ); node->accept( visitor ); 

Der Bypass-Modus wird von mehreren Enumeratoren festgelegt

  1. TRAVERSE_ALL_CHILDREN - Durchlaufen aller untergeordneten Knoten.
  2. TRAVERSE_PARENTS - Vom aktuellen Knoten zurückgeben und den Stammknoten nicht erreichen
  3. TRAVERSE_ACTIVE_CHILDREN - Umgeht ausschließlich aktive Knoten, dh solche, deren Sichtbarkeit über den Knoten osg :: Switch aktiviert wird.


6. Analyse der Struktur der brennenden Cessna


Der Entwickler kann immer den Teil des Szenendiagramms analysieren, der von dem aus der Datei geladenen Modell generiert wird.

Funktor Beispiel
main.h

 #ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class InfoVisitor : public osg::NodeVisitor { public: InfoVisitor() : _level(0) { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); } std::string spaces() { return std::string(_level * 2, ' '); } virtual void apply(osg::Node &node); virtual void apply(osg::Geode &geode); protected: unsigned int _level; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void InfoVisitor::apply(osg::Node &node) { std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; _level++; traverse(node); _level--; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void InfoVisitor::apply(osg::Geode &geode) { std::cout << spaces() << geode.libraryName() << "::" << geode.className() << std::endl; _level++; for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } traverse(geode); _level--; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root.valid()) { OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl; return -1; } InfoVisitor infoVisitor; root->accept(infoVisitor); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Wir erstellen die InfoVisitor-Klasse und erben sie von osg :: NodeVisitor

 class InfoVisitor : public osg::NodeVisitor { public: InfoVisitor() : _level(0) { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); } std::string spaces() { return std::string(_level * 2, ' '); } virtual void apply(osg::Node &node); virtual void apply(osg::Geode &geode); protected: unsigned int _level; }; 

Die Eigenschaft protected _level zeigt auf die Ebene des Szenendiagramms, auf der sich unsere Besucherklasse derzeit befindet. Initialisieren Sie im Konstruktor den Ebenenzähler und legen Sie den Modus für die Knotenüberquerung fest, um alle untergeordneten Knoten zu umgehen.

Definieren Sie nun die apply () -Methoden für Knoten neu

 void InfoVisitor::apply(osg::Node &node) { std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; _level++; traverse(node); _level--; } 

Hier geben wir den Typ des aktuellen Knotens aus. Die Methode libraryName () für den Knoten zeigt den Namen der OSG-Bibliothek an, in der dieser Knoten implementiert ist, und die Methode className zeigt den Namen der Knotenklasse an. Diese Methoden werden durch die Verwendung von Makros im Code von OSG-Bibliotheken implementiert.

 std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; 

Danach erhöhen wir den Zähler auf Diagrammebene und rufen die Methode traverse () auf, die einen Übergang zu einer höheren Ebene zum untergeordneten Knoten initiiert. Nach der Rückkehr von traverse () verringern wir erneut den Zählerwert. Es ist leicht zu erraten, dass traverse () einen wiederholten Aufruf der Methode apply () initiiert, wobei traverse () bereits für einen Untergraphen ab dem aktuellen Knoten wiederholt wird. Wir erhalten eine rekursive Besucherausführung, bis wir die Endknoten des Szenendiagramms erreichen.

Bei einem Endknoten vom Typ osg :: Geode wird die Überladung der Methode apply () überschrieben

 void InfoVisitor::apply(osg::Geode &geode) { std::cout << spaces() << geode.libraryName() << "::" << geode.className() << std::endl; _level++; for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } traverse(geode); _level--; } 

mit ähnlich funktionierendem Code, außer dass wir Daten zu allen geometrischen Objekten anzeigen, die an den aktuellen geometrischen Knoten angehängt sind

 for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } 

In der Funktion main () verarbeiten wir Befehlszeilenargumente, über die wir eine Liste der in die Szene geladenen Modelle übergeben und die Szene bilden

 osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root.valid()) { OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl; return -1; } 

Gleichzeitig verarbeiten wir Fehler im Zusammenhang mit dem Fehlen von Modelldateinamen in der Befehlszeile. Jetzt erstellen wir eine Besucherklasse und übergeben sie zur Ausführung an das Szenendiagramm

 InfoVisitor infoVisitor; root->accept(infoVisitor); 

Als nächstes folgen die Schritte zum Starten des Viewers, die wir bereits viele Male ausgeführt haben. Nach dem Starten des Programms mit Parametern

 $ visitor ../data/cessnafire.osg 

Wir werden die folgende Ausgabe an die Konsole sehen

 osg::Group osg::MatrixTransform osg::Geode osg::Geometry osg::Geometry osg::MatrixTransform osgParticle::ModularEmitter osgParticle::ModularEmitter osgParticle::ParticleSystemUpdater osg::Geode osgParticle::ParticleSystem osgParticle::ParticleSystem osgParticle::ParticleSystem osgParticle::ParticleSystem 

Tatsächlich haben wir einen vollständigen Baum der geladenen Szene erhalten. Entschuldigung, wo sind so viele Knoten? Alles ist sehr einfach - Modelle des * .osg-Formats selbst sind Container, in denen nicht nur Daten zur Modellgeometrie, sondern auch andere Informationen zu seiner Struktur in Form eines Teilgraphen der OSG-Szene gespeichert werden. Die Geometrie des Modells, Transformationen und Partikeleffekte, die Rauch und Flammen realisieren, sind Knotenpunkte des OSG-Szenendiagramms.Jede Szene kann entweder von * .osg heruntergeladen oder vom Viewer in das * .osg-Format entladen werden.

Dies ist ein einfaches Beispiel für die Anwendung der Besuchermechanik. Tatsächlich können Sie innerhalb von Besuchern viele Vorgänge ausführen, um die Knoten zu ändern, wenn das Programm ausgeführt wird.

7. Steuern des Verhaltens von Knoten im Szenendiagramm durch Überschreiben der Methode traverse ()


Eine wichtige Möglichkeit, mit OSG zu arbeiten, besteht darin, die Methode traverse () zu überschreiben. Diese Methode wird jedes Mal aufgerufen, wenn ein Frame gezeichnet wird. Sie akzeptieren einen Parameter vom Typ osg :: NodeVisitor &, der angibt, welche Passage des Szenendiagramms gerade ausgeführt wird (Aktualisierung, Ereignisverarbeitung oder Clipping). Die meisten OSG-Hosts überschreiben diese Methode, um ihre Funktionalität zu implementieren.

Es ist zu beachten, dass das Überschreiben der Methode traverse () gefährlich sein kann, da dies den Prozess des Durchlaufens des Szenendiagramms beeinflusst und zu einer falschen Anzeige der Szene führen kann. Es ist auch unpraktisch, wenn Sie mehreren Knotentypen neue Funktionen hinzufügen möchten. In diesem Fall werden Knotenrückrufe verwendet, deren Konversation etwas geringer ausfällt.

Wir wissen bereits, dass der osg :: Switch-Knoten die Anzeige seiner untergeordneten Knoten steuern kann, einschließlich der Anzeige einiger Knoten und des Ausschaltens der Anzeige anderer Knoten. Da er jedoch nicht weiß, wie dies automatisch zu tun ist, erstellen wir einen neuen Knoten auf der Grundlage des alten Knotens, der entsprechend dem Wert des internen Zählers zu unterschiedlichen Zeitpunkten zwischen untergeordneten Knoten wechselt.

Animswitch Beispiel
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class AnimatingSwitch : public osg::Switch { public: AnimatingSwitch() : osg::Switch(), _count(0) {} AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} META_Node(osg, AnimatingSwitch); virtual void traverse(osg::NodeVisitor &nv); protected: unsigned int _count; }; void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); osg::ref_ptr<AnimatingSwitch> root = new AnimatingSwitch; root->addChild(model1.get(), true); root->addChild(model2.get(), false); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Schauen wir uns dieses Beispiel an. Wir erstellen eine neue AnimatingSwitch-Klasse, die von osg :: Switch erbt.

 class AnimatingSwitch : public osg::Switch { public: AnimatingSwitch() : osg::Switch(), _count(0) {} AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} META_Node(osg, AnimatingSwitch); virtual void traverse(osg::NodeVisitor &nv); protected: unsigned int _count; }; void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } 

Diese Klasse enthält den Standardkonstruktor.

 AnimatingSwitch() : osg::Switch(), _count(0) {} 

und Konstruktor zum Kopieren, erstellt gemäß den Anforderungen von OSG

 AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} 

Der Konstruktor zum Kopieren sollte als Parameter enthalten: einen konstanten Verweis auf die zu kopierende Klasseninstanz und den Parameter osg :: CopyOp, der die Kopiereinstellungen der Klasse angibt. Es folgen ziemlich seltsame Buchstaben

 META_Node(osg, AnimatingSwitch); 

Dies ist ein Makro, das die Struktur bildet, die für den Nachkommen einer von osg :: Node abgeleiteten Klasse erforderlich ist. Bis wir diesem Makro Bedeutung beimessen, ist es wichtig, dass es beim Erben von osg :: Switch vorhanden ist, wenn alle untergeordneten Klassen definiert werden. Die Klasse enthält das geschützte Feld _count - genau den Zähler, anhand dessen wir wechseln. Wir implementieren das Umschalten beim Überschreiben der traverse () -Methode

 void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } 

Das Umschalten des Anzeigestatus von Knoten erfolgt jedes Mal, wenn der Wert des Zählers (Inkrementieren jedes Methodenaufrufs) ein Vielfaches von 60 ist. Wir kompilieren das Beispiel und führen es aus



Da die traverse () -Methode für verschiedene Knotentypen ständig neu definiert wird, sollte sie einen Mechanismus zum Abrufen von Transformationsmatrizen und Renderzuständen zur weiteren Verwendung durch ihren überladenen Algorithmus bereitstellen. Der Eingabeparameter osg :: NodeVisitor ist der Schlüssel für verschiedene Operationen mit Knoten. Insbesondere wird die Art der aktuellen Durchquerung des Szenendiagramms angegeben, z. B. Aktualisieren, Verarbeiten von Ereignissen und Abschneiden unsichtbarer Gesichter. Die ersten beiden beziehen sich auf Knotenrückrufe und werden beim Studium der Animation berücksichtigt.

Der Beschneidungsdurchlauf kann identifiziert werden, indem das Objekt osg :: NodeVisitor in das Objekt osg :: CullVisitor konvertiert wird

 osgUtil::CullVisitor *cv = dynamic_cast<osgUtil::CullVisitor *>(&nv); if (cv) { ///  - ,     } 


8. Rückrufmechanismus


Im vorherigen Artikel haben wir die Animation eines Szenenobjekts implementiert, indem wir die Parameter seiner Transformation innerhalb des Szenenwiedergabezyklus geändert haben. Wie bereits mehrfach erwähnt, enthält dieser Ansatz potenziell gefährliches Anwendungsverhalten beim Multithread-Rendering. Um dieses Problem zu lösen, wird ein Rückrufmechanismus verwendet, der beim Durchlaufen des Szenendiagramms ausgeführt wird.

Es gibt verschiedene Arten von Rückrufen in der Engine. Rückrufe werden von speziellen Klassen implementiert, von denen osg :: NodeCallback für die Aktualisierung von Szenenknoten ausgelegt ist und osg :: Drawable :: UpdateCallback, osg :: Drawable :: EventCallback und osg :: Drawable: CullCallback - dieselben Funktionen ausführen, jedoch für Geometrieobjekte.

Die osg :: NodeCallback-Klasse verfügt über einen überschreibbaren virtuellen operator () -Operator, der vom Entwickler zur Implementierung ihrer eigenen Funktionalität bereitgestellt wird. Damit der Rückruf funktioniert, müssen Sie eine Instanz der Aufrufklasse an den Knoten anhängen, für den sie verarbeitet werden soll, indem Sie die Methode setUpdateCallback () oder addUpdateCallback () aufrufen. Der Operator operator () wird beim Aktualisieren der Knoten im Szenendiagramm beim Rendern jedes Frames automatisch aufgerufen.

In der folgenden Tabelle sind die Rückrufe aufgeführt, die dem Entwickler in OSG zur Verfügung stehen.

VornameRückruf-FunktorVirtuelle MethodeMethode zum Anhängen an ein Objekt
Knotenaktualisierungosg :: NodeCallbackoperator ()osg :: Node :: setUpdateCallback ()
Knotenereignisosg :: NodeCallbackoperator ()osg :: Node :: setEventCallback ()
Knotenausschnittosg :: NodeCallbackoperator ()osg :: Node :: setCullCallback ()
Geometrie-Updateosg::Drawable::UpdateCallbackupdate()osg::Drawable::setUpdateCallback()
osg::Drawable::EventCallbackevent()osg::Drawable::setEventCallback()
osg::Drawable::CullCallbackcull()osg::Drawable::setCullCallback()
osg::StateAttributeCallbackoperator()osg::StateAttribute::setUpdateCallback()
osg::StateAttributeCallbackoperator()osg::StateAttribute::setEventCallback()
osg::Uniform::Callbackoperator()osg::Uniform::setUpdateCallback()
osg::Uniform::Callbackoperator()osg::Uniform::setEvevtCallback()
osg::Camera::DrawCallbackoperator()osg::Camera::PreDrawCallback()
osg::Camera::DrawCallbackoperator()osg::Camera::PostDrawCallback()


9. osg::Switch


Etwas höher haben wir ein Beispiel für den Wechsel zweier Flugzeugmodelle geschrieben. Jetzt werden wir dieses Beispiel wiederholen, aber mit dem OSG-Rückrufmechanismus alles richtig machen.

Rückrufe mit Beispiel
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()(osg::Node *node, osg::NodeVisitor *nv); protected: unsigned int _count; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osg::Switch *switchNode = static_cast<osg::Switch *>(node); if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } traverse(node, nv); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1, true); root->addChild(model2, false); root->setUpdateCallback( new SwitchingCallback ); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


Sie müssen eine Klasse erstellen, die von osg :: NodeCallback erbt und den osg :: Switch-Knoten steuert

 class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()(osg::Node *node, osg::NodeVisitor *nv); protected: unsigned int _count; }; 

Der _count-Zähler steuert das Umschalten des osg :: Switch-Knotens von der Zuordnung eines untergeordneten Knotens zu einem anderen, abhängig von seinem Wert. Im Konstruktor initialisieren wir den Zähler und definieren die virtuelle operator () -Methode neu

 void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osg::Switch *switchNode = static_cast<osg::Switch *>(node); if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } traverse(node, nv); } 

Der Knoten, auf dem der Aufruf ausgeführt wurde, wird über den Knotenparameter an ihn übergeben. Da wir sicher wissen, dass dies ein Knoten vom Typ osg :: Switch sein wird, führen wir eine statische Umwandlung des Zeigers auf den Knoten auf den Zeiger auf den Switch-Knoten durch

 osg::Switch *switchNode = static_cast<osg::Switch *>(node); 

Wir werden die angezeigten untergeordneten Knoten mit dem gültigen Wert dieses Zeigers und wenn der Zählerwert ein Vielfaches von 60 ist, vertauschen

 if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } 

Vergessen Sie nicht, die Methode traverse () aufzurufen, um die rekursive Durchquerung des Szenendiagramms fortzusetzen

 traverse(node, nv); 

Der Rest des Programmcodes ist bis auf die Zeile trivial

 root->setUpdateCallback( new SwitchingCallback ); 

Dort weisen wir den von uns erstellten Rückruf dem Stammknoten vom Typ osg :: Switch zu. Das Programm funktioniert ähnlich wie im vorherigen Beispiel



Bisher haben wir die mysteriöse Methode traverse () für zwei Zwecke verwendet: Überschreiben dieser Methode in Nachfolgeklassen und Aufrufen dieser Methode für die Klasse osg :: NodeVisitor, um das Durchlaufen des Szenendiagramms fortzusetzen.

In dem gerade untersuchten Beispiel verwenden wir die dritte Option zum Aufrufen von traverse (), wobei ein Zeiger auf den Knoten und ein Zeiger auf die Besucherinstanz als Parameter übergeben werden. Wie in den ersten beiden Fällen wird das Crawlen des Szenendiagramms gestoppt, wenn auf diesem Knoten kein Aufruf von traverse () erfolgt.

Die Methoden addUpdateCallback () dienen auch dazu, dem Knoten einen Rückruf hinzuzufügen. Im Gegensatz zu setUpdateCallback () wird es verwendet, um vorhandenen Rückrufen einen weiteren hinzuzufügen. Somit kann es mehrere Rückrufe für denselben Knoten geben.

Fazit


Wir haben die grundlegenden Techniken untersucht, die bei der Entwicklung von Anwendungen mithilfe der OpenSceneGraph-Grafik-Engine verwendet werden. Dies ist jedoch weit entfernt von allen Punkten, die ich ansprechen möchte (trotz der Tatsache, dass der Artikel ziemlich lang war)

Fortsetzung folgt...

Source: https://habr.com/ru/post/de437688/


All Articles