📜 ⬆️ ⬇️

OpenSceneGraph: Basic Programming Techniques

image

Introduction


This article will focus not so much on graphics as on how an application should be organized, using it, taking into account the specifics of the OpenSceneGraph engine and the software tools it provides.

It is no secret that the key to the success of any software product is a well-designed architecture, which provides the possibility of maintaining and expanding the written code. In this sense, the engine we are considering is at a fairly high level, providing the developer with a very wide toolkit that provides for the construction of a flexible modular architecture.

This article is quite long and includes a general overview of various tools and techniques (design patterns, if you will) provided by the developer with the engine. All sections of the article are supplied with examples, the code of which can be taken in my repository .

1. Analysis of command line parameters


In C / C ++, command line parameters are passed through the arguments of the main () function. In the previous examples, we have carefully marked these parameters as unused, now we will use them to inform our program some data when it is launched.

OSG has built-in command line parsing tools.

Create the following example

Command-line example
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(); } 


Set the program startup parameters in QtCreator



Running the program for execution yields the result (the truck model is taken from the same OpenSceneGraph-Data )



Now let's look at the example line by line.

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

creates an instance of the command line parser class osg :: ArgumentParser. When creating a class constructor, the arguments that the main () function takes from the operating system are passed.

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

we parse the arguments, namely, we are looking for the key “–model” among them, putting its value in the string filename. Thus, by means of this key we transfer to the program the name of the file with the three-dimensional model. Next we load this model and display it.

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

The read () method of the osg :: ArgumentParser class has a lot of overloads that allow you to read from the command line not only string values, but also integers, floating-point numbers, vectors, etc. For example, you can read a certain parameter of type float

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

If the command line does not have this parameter, then its value will remain as it was after the initialization of the size variable.

2. The mechanism of notification and logging


OpenSceneGraph has a notification mechanism that allows you to display debug messages in the process of rendering, as well as initiated by the developer. This is a great help in tracing and debugging programs. The OSG notification system supports the display of diagnostic information (errors, warnings, notifications) at the engine level of the engine and plug-ins to it. The developer can display a diagnostic message while the program is running, using the osg :: notify () function.

This function works as a standard output stream of the standard C ++ library through operator operator overloading <<. It takes message level as an argument: ALWAYS, FATAL, WARN, NOTICE, INFO, DEBUG_INFO, and DEBUG_FP. for example

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

displays a warning with user-defined text.

OSG notifications can contain important information about the state of the program, extensions of the computer’s graphic subsystem, and possible problems with the engine.

In some cases, it is required to output this data not to the console, but to be able to redirect this output to a file (as a log) or to any other interface, including a graphical widget. The engine contains a special class osg :: NotifyHandler that provides redirection of notifications to the appropriate output stream for the developer.

Using a simple example, consider how you can redirect the output of notifications, say, to a text log file. Let's write the following code

Notify example
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(); } 


To redirect the output, we write the class LogFileHandler, which is the successor of osg :: NotifyHandler. The constructor and destructor of this class control the opening and closing of the _log output stream, to which the text file is associated. The notify () method is a similar method of the base class, which we redefined to output to the file notifications transmitted by OSG in the course of work through the msg parameter.

Class 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; }; 

Further, in the main program we perform the necessary settings.

 osg::setNotifyLevel(osg::INFO); 

We set the level of INFO notifications, that is, output to the log all information about the engine, including current notifications about normal operation.

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

Install the notification handler. Next, we process command line argrumenti in which the paths to the loaded models are passed.

 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; } 

At the same time, we process the situation of the lack of data in the command line, displaying a message in the log in manual mode using the macro OSG_FATAL. Run the program with the following arguments



getting output to a log file like this

OSG example
 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 


It doesn't matter that at the moment this information may seem senseless to you - in the future such a conclusion can help debug errors in your program.

By default, OSG sends messages to standard output std :: cout and error messages to the stream std :: cerr. However, by overriding the notification handler, as shown in the example, this output can be redirected to any output stream, including the elements of the graphical interface.

It should be remembered that when setting a high level of notifications (for example, FATAL), the system ignores all notifications of a lower level. For example, in a similar case

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

a custom message will simply not be displayed.

3. Interception of geometric attributes


The osg :: Geometry class manages a set of data that describes vertices and displays a polygonal mesh using an ordered set of primitives. However, this class has no idea about such elements of the model topology as faces, edges and relations between them. This nuance hinders the implementation of such things as moving certain faces, for example when animating models. Currently OSG does not support this functionality.

However, the engine has implemented a number of functors that allow you to re-read the geometry attributes of any object and use them for modeling the polygon mesh topology. In C ++, a functor is a construction that allows using an object as a function.

The osg :: Drawable class provides the developer with four types of functors:

  1. osg :: Drawable :: AttributeFunctor - reads vertex attributes as an array of pointers. It has a number of virtual methods for applying the vertex attributes of different data types. To use this functor, you must describe the class and override one or more of its methods, within which the actions required by the developer are performed.


 virtual void apply( osg::Drawable::AttributeType type, unsigned int size, osg::Vec3* ptr ) { // Читаем 3-векторы в буфер с указателем ptr. // Первый параметр определяет тип атрибута } 

  1. osg :: Drawable :: ConstAttributeFunctor - read-only version of the previous functor: a pointer to an array of vectors is passed as a constant parameter
  2. osg :: PrimitiveFunctor - simulates the rendering process for OpenGL objects. Under the guise of rendering an object, the developer's redefined methods of the functor are called. This functor has two important template subclasses: osg :: TemplatePrimitiveFunctor <> and osg :: TriangleFunctor <>. These classes receive as parameters the vertices of the primitive and pass them to user methods using operator ().
  3. osg :: PrimitiveIndexFunctor - performs the same actions as the previous functor, but takes the indexes of the vertices of the primitive as a parameter.

Classes derived from osg :: Drawable, such as osg :: ShapeDrawable and osg :: Geometry, have an accept () method that allows you to apply different functors.

4. An example of using the primitive functor


We illustrate the described functionality with an example of collecting information about triangular faces and points of some geometry defined by us in advance.

Functor example
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(); } 


Omitting the process of creating geometry that we considered many times, let us pay attention to the following. We define a FaceCollector structure, for which we override operator () as follows

 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; } }; 

This operator, when called, will display the coordinates of the three vertices passed to it by the engine. The vec2str function is required to translate the components of the vector osg :: Vec3 to std :: string. To call the functor, create an instance of it and pass it to the geometry object via the accept () method

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

This call, as mentioned above, simulates the drawing of geometry, replacing the drawing itself with a call to the overridden functor method. In this case, it will be called when "drawing" each of the triangles from which the geometry of the example is composed.

On the screen we get this geometry



and such exhaust in the console

 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 

In fact, when calling geom-> accept (...), drawing triangles does not occur, OpenGL calls are simulated, and instead, data about the vertices of the triangle, the rendering of which is simulated



The osg :: TemplatePrimitiveFunctor class collects data not only about triangles, but also about any other OpenGL primitives. To implement the processing of this data, it is necessary to redefine the following operators in the template argument.

 // Для точек 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. Pattern "Visitor"


The visitor pattern is used to access operations for changing the elements of a scene graph without changing the classes of these elements. The visitor class implements all the corresponding virtual functions for applying them to different types of elements through a double dispatching mechanism. Using this mechanism, a developer can create his own instance of a visitor by implementing the necessary functionality with the help of special operators and bind the visitor to various types of elements of the scene graph on the fly, without changing the functionality of the elements themselves. This is a great way to extend the functionality of an element without defining subclasses of these elements.

To implement this mechanism, OSG defines the class osg :: NodeVisitor. A class inherited from osg :: NodeVisitor moves around the scene graph, visits each node and applies developer-defined operations to it. This is the main class used to interfere with the process of updating nodes and cutting off invisible nodes, as well as applying some other operations related to modifying the geometry of scene nodes such as osgUtil :: SmoothingVisitor, osgUtil :: Simplifier and osgUtil :: TriStripVisitor.

To create a visitor subclass, we must override one or more virtual overloaded apply () methods provided by the osg :: NodeVisitor base class. Most of the major OSG node types have these methods. The visitor will automatically call the apply () method for each of the nodes visited when traversing the scene graph. The developer overrides the apply () method for each of the node types he needs.

In the implementation of the apply () method, the developer, at the appropriate moment, must call the traverse () method of the base class osg :: NodeVisitor. This initiates a visitor's transition to the next node, either a child or a hierarchically adjacent one, if the current node does not have any child nodes to which the transition can be made. The absence of a traverse () call means stopping the traversing of the scene graph and the rest of the scene graph is ignored.

The apply () method overloads have uniform formats.

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

To bypass the subgraph of the current node for the visitor object, you need to specify a bypass mode, for example

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

The traversal mode is specified by several enumerators.

  1. TRAVERSE_ALL_CHILDREN - move through all child nodes.
  2. TRAVERSE_PARENTS - pass back from the current node, not reaching the root node
  3. TRAVERSE_ACTIVE_CHILDREN — bypassing exclusively active nodes, that is, those whose visibility is activated through the osg :: Switch node.


6. Analysis of the structure of the burning Cessna


The developer can always analyze the part of the scene graph that is generated by the model loaded from the file.

Functor example
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(); } 


Create an InfoVisitor class, inheriting it from 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; }; 

The protected _level property will indicate the level of the scene graph where our visitor class is currently located. In the constructor, we initialize the level counter and set the mode for traversing the nodes — bypassing all the child nodes.

Now override the apply () methods for the nodes.

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

Here we will display the type of the current node. The libraryName () method for a node displays the name of the OSG library where the node is implemented, and the className method displays the name of the node class. These methods are implemented by using macros in the code of OSG libraries.

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

After that, we increment the count level counter and call the traverse () method, initiating a transition to a higher level, to the child node. After returning from traverse (), we again decrease the value of the counter. It is not hard to guess that traverse () initiates the repeated call of the apply () method to repeat the traverse () already for a subgraph starting from the current node. We get the recursive execution of the visitor, until we rest on the end nodes of the scene graph.

For the end node of the osg :: Geode type, its own apply () method override is overridden

 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--; } 

c in the same way as working code, except for the fact that we display data on all geometric objects attached to the current geometric node

 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 the main () function, we process the command line arguments through which we pass the list of models loaded into the scene and form the scene.

 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; } 

In doing so, we handle errors related to the absence of model file names in the command line. Now we create a class visitor and pass it to the scene graph to perform

 InfoVisitor infoVisitor; root->accept(infoVisitor); 

Next are the actions to launch the viewer, which we have already done many times. After starting the program with parameters

 $ visitor ../data/cessnafire.osg 

we will see the following output to the console

 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 

In fact, we got a full tree of the loaded scene. Let, where so many nodes? Everything is very simple - the * .osg format models themselves are containers in which not only the geometry data of the model is stored, but also other information about its structure as an OSG scene subgraph. Model geometry, transformations, particle effects that implement smoke and flames are all nodes of the OSG scene graph.Any scene can be either loaded from * .osg, or unloaded from the viewer in * .osg format.

This is a simple example of how visitors use mechanics. In fact, inside visitors can perform a lot of operations on the modification of nodes during the execution of the program.

7. Controlling the behavior of the scene graph nodes by overriding the traverse () method


An important technique for working with OSG is overriding the traverse () method. This method is called every time a frame is drawn. They take an osg :: NodeVisitor & type parameter that tells you which passage of the scene graph is currently being performed (update, event handling, or clipping). Most OSG nodes override this method to implement their functionality.

It should be remembered that redefining the traverse () method can be dangerous, since it affects the process of traversing the scene graph and can cause the scene to be displayed incorrectly. This is also inconvenient if you wish to add new functionality to several types of nodes. In this case, use callbacks nodes, talk about which will go a little lower.

We already know that the osg :: Switch node can control the display of its child nodes, including the display of some nodes and turning off the display of others. But he does not know how to do this automatically, so we will create a new node based on the old one that will switch between the child nodes at different points in time, in accordance with the value of the internal counter.

Animswitch example
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(); } 


Let us analyze this example on the shelves. We are creating a new class, AnimatingSwitch, which inherits from osg :: Switch.

 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); } 

This class contains the default constructor.

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

and copy constructor created according to OSG requirements

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

The constructor for copying must contain as parameters: a constant reference to the instance of the class to be copied and the osg :: CopyOp parameter that sets the copy settings for the class. This is followed by rather strange letters.

 META_Node(osg, AnimatingSwitch); 

This is a macro that forms the structure necessary for an heir of a class derived from osg :: Node. We do not attach importance to this macro yet - it is important that it should be present when inheriting from osg :: Switch when defining all descendant classes. The class contains a protected _count field - the same counter on the basis of which we perform the switch. Switching is implemented when the traverse () method is overridden

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

Switching the display status of nodes will occur every time when the value of the counter (incrementing each method call) is a multiple of 60. Compile the example and run it



Since the traverse () method is constantly redefined for various types of nodes, it must provide a mechanism for obtaining transformation matrices and render states for further use by their algorithm implemented by overloading. The input parameter osg :: NodeVisitor is the key to various operations with nodes. In particular, it indicates the type of the current traversal of the scene graph, such as updating, event handling, and cutting off invisible faces. The first two are associated with callback nodes and will be considered when studying animation.

The clipping pass can be identified by converting the object osg :: NodeVisitor to osg :: CullVisitor

 osgUtil::CullVisitor *cv = dynamic_cast<osgUtil::CullVisitor *>(&nv); if (cv) { /// Выполняем что-то тут, характерное для обработки отсечения } 


8. Callback mechanism


In the previous article, we implemented animation of the scene object by changing the parameters of its transformation within the scene rendering cycle. As already mentioned many times, such an approach harbors the potentially dangerous behavior of an application in multi-threaded rendering. To solve this problem, a callback mechanism is used that is executed when traversing a scene graph.

In the engine there are several types of callbacks. Callbacks are implemented by special classes, among which osg :: NodeCallback is designed to handle the process of updating the scene nodes, and osg :: Drawable :: UpdateCallback, osg :: Drawable :: EventCallback and osg :: Drawable: CullCallback - perform the same functions, but for geometry objects.

The osg :: NodeCallback class has an overridable virtual operator operator () provided by the developer to implement its own functionality. In order for the callback to work, you must attach an instance of the call class to the node for which it will be processed by calling the setUpdateCallback () or addUpdateCallback () method. The operator operator () is automatically called during the update of the nodes of the scene graph when rendering each frame.

The following table lists the callbacks available to the developer on OSG.

NameCallback functorVirtual methodMethod to attach to the object
Knot revivalosg :: NodeCallbackoperator ()osg :: Node :: setUpdateCallback ()
Event nodeosg :: NodeCallbackoperator ()osg :: Node :: setEventCallback ()
Node clippingosg :: NodeCallbackoperator ()osg :: Node :: setCullCallback ()
Geometry 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 при обновлении дерева сцены


Just above, we wrote an example with switching two models of aircraft. Now we will repeat this example, but we’ll do it right using the OSG callback mechanism.

Callbackswith example
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(); } 


You must create a class, inheriting it from osg :: NodeCallback, which controls the osg :: Switch node

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

The _count counter will control the switching of the osg :: Switch node from the display of one child node to another, depending on its value. In the constructor, we initialize the counter, and the virtual method operator () overrides

 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); } 

The node on which the call worked is passed to it through the node parameter. Since we know for sure that this will be an osg :: Switch node, we perform a static cast of a pointer to a node to a pointer to a switch node

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

Switching the displayed child nodes will be performed with a valid value of this pointer, and when the counter value is a multiple of 60

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

Don't forget to call the traverse () method to continue recursively traversing the scene graph.

 traverse(node, nv); 

The rest of the program code is trivial, except for the line

 root->setUpdateCallback( new SwitchingCallback ); 

where we assign the callback we created to the root node with the osg :: Switch type. The work of the program is similar to the previous example.



So far, we have used the mysterious traverse () method for two purposes: redefining this method in successor classes and calling this method in the osg :: NodeVisitor class to continue traversing the scene graph.

In the example just discussed, we use the third variant of the call to traverse (), passing the pointer to the node and the pointer to the visitor instance as parameters. As in the first two cases, if there is no call for traverse () on this node, the traversing of the scene graph will stop.

The addUpdateCallback () method also serves to add a callback to the node. Unlike setUpdateCallback (), it is used to add another callback to the existing ones. Thus, there may be multiple callbacks for the same node.

Conclusion


We reviewed the basic techniques used in developing applications that use the graphics engine OpenSceneGraph. However, these are not all the points that I would like to touch upon (while the article was quite long), therefore

To be continued...

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