OpenSceneGraph: Basic Programming Techniques

  • Tutorial
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 lengthy and includes an 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 carefully marked these parameters as unused, but now we will use them to tell our program some data when it starts.

OSG has built-in command line parsing tools.

Create the following example

Example command-line
main.h

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

main.cpp

#include"main.h"intmain(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();
}


Setting the program launch parameters in QtCreator After



running the program for execution, we get the result (the model of the truck 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 perform the analysis of 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 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 output of diagnostic information (errors, warnings, notifications) at the engine level 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 the 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.

For a simple example, consider how you can redirect the output of notifications, for example, 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"classLogFileHandler :public osg::NotifyHandler
{
public:
    LogFileHandler(conststd::string &file)
    {
        _log.open(file.c_str());
    }
    virtual ~LogFileHandler()
    {
        _log.close();
    }
    virtualvoidnotify(osg::NotifySeverity severity, constchar *msg){
        _log << msg;
    }
protected:
    std::ofstream   _log;
};
intmain(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

classLogFileHandler :public osg::NotifyHandler
{
public:
    LogFileHandler(conststd::string &file)
    {
        _log.open(file.c_str());
    }
    virtual ~LogFileHandler()
    {
        _log.close();
    }
    virtualvoidnotify(osg::NotifySeverity severity, constchar *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 does not 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 std :: cout standard output and error messages to the std :: cerr stream. 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.


virtualvoidapply( osg::Drawable::AttributeType type, 
					unsignedint 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::stringvec2str(const osg::Vec3 &v){
    std::string tmp = std::to_string(v.x());
    tmp += " ";
    tmp += std::to_string(v.y());
    tmp += " ";
    tmp += std::to_string(v.z());
    return tmp;
}
structFaceCollector
{voidoperator()(const osg::Vec3 &v1,
                    const osg::Vec3 &v2,
                    const osg::Vec3 &v3){
        std::cout << "Face vertices: "
                  << vec2str(v1)
                  << "; " << vec2str(v2)
                  << "; " << vec2str(v3) << std::endl;
    }
};
intmain(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

structFaceCollector
{voidoperator()(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 triangle vertices is displayed, which is simulated.



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

// Для точекvoidoperator()( const osg::Vec3&, bool );
// Для линийvoidoperator()( const osg::Vec3&, const osg::Vec3&, bool );
// Для треугольниковvoidoperator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool );
// Для четырехугольниковvoidoperator()( 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 relevant virtual functions for applying them to various 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 through the scene graph, visits each node and applies developer-defined operations to it. This is the main class used to intervene in 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 apply () overload methods provided by the osg :: NodeVisitor base class. These methods are available for most major OSG node types. 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, should 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 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.

virtualvoidapply( osg::Node& );
virtualvoidapply( osg::Geode& );
virtualvoidapply( osg::Group& );
virtualvoidapply( 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 - backward pass 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"//------------------------------------------------------------------------------////------------------------------------------------------------------------------classInfoVisitor :public osg::NodeVisitor
{
public:
    InfoVisitor() : _level(0)
    {
        setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN);
    }
    std::stringspaces(){
        returnstd::string(_level * 2, ' ');
    }
    virtualvoidapply(osg::Node &node);
    virtualvoidapply(osg::Geode &geode);
protected:
    unsignedint _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 (unsignedint i = 0; i < geode.getNumDrawables(); ++i)
    {
        osg::Drawable *drawable = geode.getDrawable(i);
        std::cout << spaces() << drawable->libraryName() << "::"
                  << drawable->className() << std::endl;
    }
    traverse(geode);
    _level--;
}
//------------------------------------------------------------------------------////------------------------------------------------------------------------------intmain(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

classInfoVisitor :public osg::NodeVisitor
{
public:
    InfoVisitor() : _level(0)
    {
        setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN);
    }
    std::stringspaces(){
        returnstd::string(_level * 2, ' ');
    }
    virtualvoidapply(osg::Node &node);
    virtualvoidapply(osg::Geode &geode);
protected:
    unsignedint _level;
};

The _level protected 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 OSG library code.

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 a 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 and repeating traverse () already for a subgraph starting at 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 type osg :: Geode its own overload is applied to the apply () method

void InfoVisitor::apply(osg::Geode &geode)
{
    std::cout << spaces() << geode.libraryName() << "::"
              << geode.className() << std::endl;
    _level++;
    for (unsignedint 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 is similar to working code, except that we display data on all geometric objects attached to the current geometric node

for (unsignedint 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 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 in the form of an OSG scene subgraph. Model geometry, transformations, particle effects that implement smoke and flame 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 it is possible to perform a lot of operations on modifying nodes during program execution.

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


An important trick to 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 want to add new functionality to several types of nodes. In this case, callbacks are used for nodes, which will be discussed below.

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"//------------------------------------------------------------------------------////------------------------------------------------------------------------------classAnimatingSwitch :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);
    virtualvoidtraverse(osg::NodeVisitor &nv);
protected:
    unsignedint _count;
};
void AnimatingSwitch::traverse(osg::NodeVisitor &nv)
{
    if (!((++_count) % 60) )
    {
        setValue(0, !getValue(0));
        setValue(1, !getValue(1));
    }
    osg::Switch::traverse(nv);
}
//------------------------------------------------------------------------------////------------------------------------------------------------------------------intmain(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.

classAnimatingSwitch :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);
    virtualvoidtraverse(osg::NodeVisitor &nv);
protected:
    unsignedint _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 a 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 overriding the traverse () method

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 performed 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 ()
Event geometryosg :: Drawable :: EventCallbackevent ()osg :: Drawable :: setEventCallback ()
Clipping geometryosg :: Drawable :: CullCallbackcull ()osg :: Drawable :: setCullCallback ()
Attribute Updateosg :: StateAttributeCallbackoperator ()osg :: StateAttribute :: setUpdateCallback ()
Attribute eventosg :: StateAttributeCallbackoperator ()osg :: StateAttribute :: setEventCallback ()
General updateosg :: Uniform :: Callbackoperator ()osg :: Uniform :: setUpdateCallback ()
General eventosg :: Uniform :: Callbackoperator ()osg :: Uniform :: setEvevtCallback ()
Callback for camera before drawingosg :: Camera :: DrawCallbackoperator ()osg :: Camera :: PreDrawCallback ()
Callback for camera after drawingosg :: Camera :: DrawCallbackoperator ()osg :: Camera :: PostDrawCallback ()


9. Switching osg :: Switch when updating the scene tree


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"//------------------------------------------------------------------------------////------------------------------------------------------------------------------classSwitchingCallback :public osg::NodeCallback
{
public:
    SwitchingCallback() : _count(0) {}
    virtualvoidoperator()(osg::Node *node, osg::NodeVisitor *nv);
protected:
    unsignedint _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);
}
//------------------------------------------------------------------------------////------------------------------------------------------------------------------intmain(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

classSwitchingCallback :public osg::NodeCallback
{
public:
    SwitchingCallback() : _count(0) {}
    virtualvoidoperator()(osg::Node *node, osg::NodeVisitor *nv);
protected:
    unsignedint _count;
};

The _count counter will control the switching of the osg :: Switch node from displaying 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 was triggered is passed to it through the node parameter. Since we know for sure that this will be a node of the osg :: Switch type, 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 be stopped.

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 the development of applications using the graphics engine OpenSceneGraph. However, these are far from all the points that I would like to touch (despite the fact that the article turned out to be quite long), so

Continuation should be ...

Also popular now: