OpenSceneGraph: Managing Scene Graph

0
3452
9 min read

 

OpenSceneGraph 3.0: Beginner’s Guide

OpenSceneGraph 3.0: Beginner's Guide

Create high-performance virtual reality applications with OpenSceneGraph, one of the best 3D graphics engines.

The Group interface

The osg::Group type represents the group nodes of an OSG scene graph. It can have any number of child nodes, including the osg::Geode leaf nodes and other osg::Group nodes. It is the most commonly-used base class of the various NodeKits—that is, nodes with various functionalities.


The osg::Group class derives from osg::Node, and thus indirectly derives from osg::Referenced. The osg::Group class contains a children list with each child node managed by the smart pointer osg::ref_ptr<>. This ensures that there will be no memory leaks whenever deleting a set of cascading nodes in the scene graph.

The osg::Group class provides a set of public methods for defining interfaces for handling children. These are very similar to the drawable managing methods of osg::Geode, but most of the input parameters are osg::Node pointers.

  1. The public method addChild() attaches a node to the end of the children list. Meanwhile, there is an insertChild() method for inserting nodes to osg::Group at a specific location, which accepts an integer index and a node pointer as parameters.
  2. The public methods removeChild() and removeChildren() will remove one or more child nodes from the current osg::Group object. The latter uses two parameters: the zero-based index of the start element, and the number of elements to be removed.
  3. The getChild() returns the osg::Node pointer stored at a specified zero-based index.
  4. The getNumChildren() returns the total number of children.

You will be able to handle the child interface of osg::Group with ease because of your previous experience of handling osg::Geode and drawables.

Managing parent nodes

We have already learnt that osg::Group is used as the group node, and osg::Geode as the leaf node of a scene graph. Additionally, both classes should have an interface for managing parent nodes.

OSG allows a node to have multiple parents. In this section, we will first have a glimpse of parent management methods, which are declared in the osg::Node class directly:

  1. The method getParent() returns an osg::Group pointer as the parent node. It requires an integer parameter that indicates the index in the parent’s list.
  2. The method getNumParents() returns the total number of parents. If the node has a single parent, this method will return 1, and only getParent(0) is available at this time.
  3. The method getParentalNodePaths() returns all possible paths from the root node of the scene to the current node (but excluding the current node). It returns a list of osg::NodePath variables.

The osg::NodePath is actually a std::vector object of node pointers, for example, assuming we have a graphical scene:

OpenSceneGraph

The following code snippet will find the only path from the scene root to the node child3:

osg::NodePath& nodePath = child3->getParentalNodePaths()[0];
for ( unsigned int i=0; i<nodePath.size(); ++i )
{
  osg::Node* node = nodePath[i];
  // Do something...
}

You will successively receive the nodes Root, Child1, and Child2 in the loop.

We don’t need to use the memory management system to reference a node’s parents. When a parent node is deleted, it will be automatically removed from its child nodes‘ records as well.

A node without any parents can only be considered as the root node of the scene graph. In that case, the getNumParents() method will return 0 and no parent node can be retrieved.

Time for action – adding models to the scene graph

In the past examples, we always loaded a single model, like the Cessna, by using the osgDB::readNodeFile() function. This time we will try to import and manage multiple models. Each model will be assigned to a node pointer and then added to a group node. The group node, which is defined as the scene root, is going to be used by the program to render the whole scene graph at last:

  1. Include the necessary headers:

    #include <osg/Group>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>

    
    
  2. In the main function, we will first load two different models and assign them to osg::Node pointers. A loaded model is also a sub-scene graph constructed with group and leaf nodes. The osg::Node class is able to represent any kind of sub graphs, and if necessary, it can be converted to osg::Group or osg::Geode with either the C++ dynamic_cast<> operator, or convenient conversion methods like asGroup() and asGeode(), which are less time-costly than dynamic_cast<>.
    osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile(
       "cessna.osg" );
    osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile( 
       "cow.osg" );
  3. Add the two models to an osg::Group node by using the addChild() method:

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( model1.get() );
    root->addChild( model2.get() );

    
    
  4. Initialize and start the viewer:
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
  5. Now you will see a cow getting stuck in the Cessna model! It is a little incredible to see that in reality, but in a virtual world, these two models just belong to uncorrelated child nodes managed by a group node, and then rendered separately by the scene viewer.

OpenSceneGraph

What just happened?

Both osg::Group and osg::Geode are derived from the osg::Node base class. The osg::Group class allows the addition of any types of child nodes, including the osg::Group itself. However, the osg::Geode class contains no group or leaf nodes. It only accepts drawables for rendering purposes.

It is convenient if we can find out whether the type of a certain node is osg::Group, osg::Geode, or other derived type especially those read from files and managed by ambiguous osg::Node classes, such as:

osg::ref_ptr<osg::Node> model = osgDB::readNodeFile( “cessna.osg” );


Both the dynamic_cast<> operator and the conversion methods like asGroup(), asGeode(), among others, will help to convert from one pointer or reference type to another. Firstly, we take the dynamic_cast<> operator as an example. This can be used to perform downcast conversions of the class inheritance hierarchy, such as:

osg::ref_ptr<osg::Group> model =
dynamic_cast<osg::Group*>( osgDB::readNodeFile(“cessna.osg”) );


The return value of the osgDB::readNodeFile() function is always osg::Node*, but we can also try to manage it with an osg::Group pointer. If, the root node of the Cessna sub graph is a group node, then the conversion will succeed, otherwise it will fail and the variable model will be NULL.

 

You may also perform an upcast conversion, which is actually an implicit conversion:

osg::ref_ptr<osg::Group> group = …;
osg::Node* node1 = dynamic_cast<osg::Node*>( group.get() );
osg::Node* node2 = group.get();


On most compilers, both node1 and node2 will compile and work fine.

The conversion methods will do a similar job. Actually, it is preferable to use those methods instead of dynamic_cast<> if one exists for the type you need, especially in a performance-critical section of code:

// Assumes the Cessna's root node is a group node.
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg");
osg::Group* convModel1 = model->asGroup(); // OK!
osg::Geode* convModel2 = model->asGeode(); // Returns NULL.

Traversing the scene graph

A typical traversal consists of the following steps:

  • First, start at an arbitrary node (for example, the root node).
  • Move down (or sometimes up) the scene graph recursively to the child nodes, until a leaf node is reached, or a node with no children is reached.
  • Backtrack to the most recent node that doesn’t finish exploring, and repeat the above steps. This can be called a depth-first search of a scene graph.

Different updating and rendering operations will be applied to all scene nodes during traversals, which makes traversing a key feature of scene graphs. There are several types of traversals, with different purposes:

  1. An event traversal firstly processes mouse and keyboard inputs, and other user events, while traversing the nodes.
  2. An update traversal (or application traversal) allows the user application to modify the scene graph, such as setting node and geometry properties, applying node functionalities, executing callbacks, and so on.
  3. A cull traversal tests whether a node is within the viewport and worthy of being rendered. It culls invisible and unavailable nodes, and outputs the optimized scene graph to an internal rendering list.
  4. A draw traversal (or rendering traversal) issues low-level OpenGL API calls to actually render the scene. Note that it has no correlation with the scene graph, but only works on the rendering list generated by the cull traversal.

In the common sense, these traversals should be executed per frame, one after another. But for systems with multiple processors and graphics cards, OSG can process them in parallel and therefore improve the rendering efficiency. The visitor pattern can be used to implement traversals.

Transformation nodes

The osg::Group nodes do nothing except for traversing down to their children. However, OSG also supports the osg::Transform family of classes, which is created during the traversal-concatenated transformations to be applied to geometry. The osg::Transform class is derived from osg::Group. It can’t be instantiated directly. Instead, it provides a set of subclasses for implementing different transformation interfaces.

When traversing down the scene graph hierarchy, the osg::Transform node always adds its own transformation to the current transformation matrix, that is, the OpenGL model-view matrix. It is equivalent to concatenating OpenGL matrix commands such as glMultMatrix(), for instance:

OpenSceneGraph

This example scene graph can be translated into following OpenGL code:

glPushMatrix();
glMultMatrix( matrixOfTransform1 );
renderGeode1(); // Assume this will render Geode1
glPushMatrix();
glMultMatrix( matrixOfTransform2 );
renderGeode2(); // Assume this will render Geode2
glPopMatrix();
glPopMatrix();


To describe the procedure using the concept of coordinate frame, we could say that Geode1 and Transform2 are under the relative reference frame of Transform1, and Geode2 is under the relative frame of Transform2. However, OSG also allows the setting of an absolute reference frame instead, which will result in the behavior equivalent to the OpenGL command glLoadMatrix():

transformNode->setReferenceFrame( osg::Transform::ABSOLUTE_RF );

And to switch back to the default coordinate frame:

transformNode->setReferenceFrame( osg::Transform::RELATIVE_RF );


LEAVE A REPLY

Please enter your comment!
Please enter your name here