Version 1 (modified by osg, 10 years ago)

Initial copy of NPS tutorial

Finding and Manipulating a Switch and DOF Node


Given a scene graph that contains a switch node and a DOF bead search the scene graph for these nodes and update them.

Finding a named node within a scene graph - old school

Model files can contain a number of different nodes that allow users to update or articulate parts of the model. Multiswitch nodes enable programmers to select between a variety of model states. For example, the tank model used in previous tutorials has a multiswitch that lets the programmer select geometry and textures assocated with an intact or a damaged tank. The model also contains DOF nodes that allow parts of the tank to be articulated. The turret node can be rotated and the gun node can be elevated. When the turret is rotated, the turret body (including the gun) change heading relative to the tank's heading. When the gun is elevated, its pitch changes relative to the turret's pitch.

To update these nodes, we need a handle (pointer) to the node. To get a pointer to a node we first need to know the name of the node. There are an number of ways to find the name of a node: ask the modeler; open the file in appropriate file viewer (Creator or Vega for .flt files); or use Open Scene Graph. You can use osg in a few different ways. One technique would be to load the flt file in your scene graph, enter a simulation loop and then use the 'o' key to output the scene to an .osg file. This is an ascii format file which you can easily examine with any text processing application (word pad, note pad). In the case of the tank file from the previous example you would find a switch named 'sw1' that has two child nodes - 'good' and 'bad'. These correspond to undamaged and damaged states respectively. The .osg version of our tank model can be found here.

Now that we know the name of the switch we want to manipulate ("sw1") we need to get a handle (or pointer) to it so we can manipulate it. There are a few different ways to do this. The first method involves writing the code to traverse the scene graph. The second method involves taking advantage of the 'visitor' pattern and is presented later. From the previous tutorial we have the following code to load a flight file, add it to a scene and run a simulation loop.

    #include <osg/PositionAttitudeTransform>
    #include <osg/Group>
    #include <osg/Node>
    #include <osgDB/ReadFile> 
    #include <osgProducer/Viewer>

    int main()
       osg::Node* tankNode = NULL;
       osg::Group* root = NULL;
       osgProducer::Viewer viewer;
       osg::Vec3 tankPosit; 
       osg::PositionAttitudeTransform* tankXform;

       tankNode = osgDB::readNodeFile("Models/T72-tank/t72-tank_des.flt");

       root = new osg::Group();
       tankXform = new osg::PositionAttitudeTransform();


       tankXform->setPosition( tankPosit ); 

       viewer.setSceneData( root );

       while( !viewer.done() )
       return 0;

We need to modify this code by adding a function to find a node. The recursive function below accepts two arguments: a string to search for and a node from which to start the search. The function returns the first instance in the sub-tree represented by 'node' who's name matches the name passed to it. If the node is not found, the function returns NULL. It's worth noting that following the visitor pattern provides a more flexible way to do this. This code is useful for demonstrating hand-written scene-graph traversal.

    osg::Node* findNamedNode(const std::string& searchName, 
                                          osg::Node* currNode)
       osg::Group* currGroup;
       osg::Node* foundNode;

       // check to see if we have a valid (non-NULL) node.
       // if we do have a null node, return NULL.
       if ( !currNode)
          return NULL;

       // We have a valid node, check to see if this is the node we 
       // are looking for. If so, return the current node.
       if (currNode->getName() == searchName)
          return currNode;

       // We have a valid node, but not the one we are looking for.
       // Check to see if it has children (non-leaf node). If the node
       // has children, check each of the child nodes by recursive call.
       // If one of the recursive calls returns a non-null value we have
       // found the correct node, so return this node.
       // If we check all of the children and have not found the node,
       // return NULL
       currGroup = currNode->asGroup(); // returns NULL if not a group.
       if ( currGroup ) 
          for (unsigned int i = 0 ; i < currGroup->getNumChildren(); i ++)
             foundNode = findNamedNode(searchName, currGroup->getChild(i));
             if (foundNode)
                return foundNode; // found a match!
          return NULL; // We have checked each child node - no match found.
          return NULL; // leaf node, no match 

So now we can add this function to our code and use it to get a handle to any named node in our scene. Note this is a depth-first search and returns the first matching node.
The next step will be to call this function at some point after we have set up our scene but before we enter the simulation loop. The handle (pointer) to a switch that this function returns can be used to update the switch's state. The following code inserted at some point after the models are loaded and added to the scene will provide the handle.

       osg::Switch* tankStateSwitch = NULL;
       osg::Node* foundNode = NULL;

       foundNode = findNamedNode("sw1",root); 
       tankStateSwitch = (osg::Switch*) foundNode;
       if ( !tankStateSwitch)
          std::cout << "tank state switch node not found, quitting." << std::endl;
          return -1;

Finding a named node using the Visitor pattern.

The visitor pattern is designed to allow users to apply some type-node specific function to each node based on the type of traversal that is performed. Types of traversals are defined as NODE_VISITOR, UPDATE_VISITOR, COLLECT_OCCLUDER_VISITOR, and CULL_VISITOR. Since we're not yet concerned about updating, occluder nodes or culling, it's most appropriate to use a 'node_visitor' traversal. The visitor pattern also allows users to specify the traversal mode. Options are TRAVERSE_NONE, TRAVERSE_PARENTS, TRAVERSE_ALL_CHILDREN, and TRAVERSE_ACTIVE_CHILDREN. In this case we will select 'traverse all children' mode.
Next, we need to define what function we want to apply at each node. In this case we want to compare a user-defined string with the name of the node. If the name of the node matches this string, the node will be added to a list of nodes. When the traversal is complete, the list will contain all of the nodes who's name matches the search string.
To take advantage of the visitor pattern, we can derive a specialized version of node visitor (call it findNodeVisitor) from the base class osg::NodeVisitor?. Our class will need two new data members: a std::string to compare against the named node we are looking for, and a list (std::vector<osg::Node*>) of nodes with names that match the string we are searching for. To implement the operation we want to perform all we need to do is override the 'apply' method. The 'apply' method of the base class has been defined for each type of node (all classes derived from type osg::Node.) This allows you to write 'apply' methods that operate on specific types of nodes. Since we want the operation to be the same for each node, all we have to do is provide an 'apply' method that works for osg::Node types. The header file for this class is listed below. The source code is available [here].


    #include <osg/NodeVisitor>
    #include <osg/Node>

    class findNodeVisitor : public osg::NodeVisitor 

       // Default constructor - initialize searchForName to "" and 
       // set the traversal mode to TRAVERSE_ALL_CHILDREN

       // Constructor that accepts string argument
       // Initializes searchForName to user string
       // set the traversal mode to TRAVERSE_ALL_CHILDREN
       findNodeVisitor(const std::string &searchName);

       // The 'apply' method for 'node' type instances.
       // Compare the 'searchForName' data member against the node's name.
       // If the strings match, add this node to our list
       virtual void apply(osg::Node &searchNode);

       // Set the searchForName to user-defined string
       void setNameToFind(const std::string &searchName);

       // Return a pointer to the first node in the list
       // with a matching name
       osg::Node* getFirst();

       // typedef a vector of node pointers for convenience
       typedef std::vector<osg::Node*> nodeListType; 

       // return a reference to the list of nodes we found
       nodeListType& getNodeList() { return foundNodeList; }


       // the name we are looking for
       std::string searchForName; 

       // List of nodes with names that match the searchForName string
       nodeListType foundNodeList;



Now that we have created a class that will: initiate a node-visitor traversal, visit each child of the subtree we start with, compare the node's name to a user defined string, and build a list of nodes with names equal to the search string, how do we initiate this process? To initiate a node-visitor traversal, we use the osg::Node class' 'accept' method. We control where the traversal starts by selecting which node invokes the accept method. (Direction of traversal is controlled by the traversal mode and type-node specific functions are created by overridding the 'apply' method for the classes we want to affect.) Accept initiates the type of traversal requested and invokes the 'apply' method for each type (sub-class) of node the user has defined. In our case we have overridden the version of apply for generic 'nodes' and selected TRAVERSE_ALL_CHILDREN traversal mode thus, apply will be performed on each node in our subtree that invokes the accept method.

For this example, we'll load three versions of the tank. The first model we will leave unchanged, the second model we will select the multSwitch associated with the damaged state. For the third tank we will rotate the turret and elevate the gun.

The code below shows how to load three tank models from a file and add these to a scene. Two of the tanks will be added as children of transform nodes so they can be positioned away from the origin.

       // Declare a group for the root of our tree and 
       // three groups to contain individual tank models
       osg::Group* root = new osg::Group();
       osg::Group* tankOneGroup = NULL;
       osg::Group* tankTwoGroup = NULL;
       osg::Group* tankThreeGroup = NULL;

       // Load the models from the same file
       tankOneGroup = dynamic_cast<osg::Group*> 
       tankTwoGroup = dynamic_cast<osg::Group*>
       tankThreeGroup = dynamic_cast<osg::Group*> 

       // add the first tank as a child of the root node

       // declare a transform for positioning the second tank
       osg::PositionAttitudeTransform* tankTwoPAT = 
          new osg::PositionAttitudeTransform();
       // move the second tank five units right, five units forward
       tankTwoPAT->setPosition( osg::Vec3(5,5,0) );
       // add the tank as a child of its transform to the scene

       // declare a transform for positioning the third tank
       osg::PositionAttitudeTransform* tankThreePAT = 
          new osg::PositionAttitudeTransform();
       // move the third tank ten units right
       tankThreePAT->setPosition( osg::Vec3(10,0,0) );
       // rotate the tank model 22.5 degrees to the left
       // (to demonstrate that rotation of the turret will be 
       // relative to the tank's heading)
       tankThreePAT->setAttitude( osg::Quat(3.14159/8.0, osg::Vec3(0,0,1) ));
       // add the tank as a child of its transform to the scene

Since we want to use the damaged state for the second model, we'll use the findNodeVisitor class to get a handle to the multiSwitch that controls the scond tank's state. This node visitor should be initiated from the group that contains the second tank. This section of code demonstrates how to declare and initialize a findNodeVisitor instance and initiate the traversal. After the traversal is complete we can retrieve the handle to the first node in our list of nodes whose names matched the string we were looking for. This will give us a handle to the multiSwitch we want to control.

       // Declare an instance of 'findNodeVisitor' class and set its
       // searchForName string equal to "sw1"
       findNodeVisitor findNode("sw1"); 

       // Initiate traversal of this findNodeVisitor instance starting
       // from tankTwoGroup, searching all its children. Build a list
       // of nodes whose names matched the searchForName string above.

       // Declare a switch type and assign it to the first node
       // in our list of matching nodes.
       osgSim::MultiSwitch* tankSwitch = NULL; 
       tankSwitch = dynamic_cast <osgSim::MultiSwitch*> (findNode.getFirst());

Updating a switch node

Once we have a valid handle to a switch node, the next trick will be to change from one model state to the next. This is done using the setSingleChildOn method. The setSingleChildOn() method takes two arguments - an unsigned integer corresponding to the index of the switchSet to manipulate and an unsigned integer cooresponding to the position you want to set. In the tank example, there is only one switchSet. It can be set to undamaged and damaged states as follows:

       // make sure it's a valid handle. If it is, set the first (only) 
       // multi-switch:
       if (tankSwitch)
          //tankSwitch->setSingleChildOn(0,false); // good model
          tankSwitch->setSingleChildOn(0,true); // bad model

Updating a DOF node

The tank model also has two DOF nodes named 'turret' and 'gun'. Handles to these nodes can be found using the findNodeVisitor class as described above. (In this case the traversal should be initiated from the group that contains the third tank model.) Once a valid handle to a DOF node has been obtained, the setCurrentHPR method can be used to update the transformation matrix associated with these nodes. The setCurrentHPR method takes a single argument - an osg::Vec3 instance whose values coorespond to the heading, pitch and roll measured in radians. (To express values in degrees you can use the osg::DegreesToRadians? method.)

       // Declare an instance of 'findNodeVisitor', set the name to search
       // for to "turret"
       findNodeVisitor findTurretNode("turret");

       // Initiate a traversal starting from the subtree that represents 
       // the third tank model we loaded.

       // Make sure we found a node and it's the correct type 
       osgSim::DOFTransform * turretDOF = 
          dynamic_cast<osgSim::DOFTransform *> (findTurretNode.getFirst());

       // if it's a valid DOF node, set the heading of the turret
       // to 45 degrees right relative to the tank's heading.
       if (turretDOF)
          turretDOF->setCurrentHPR( osg::Vec3(-3.14159/4.0,0.0,0.0) );

The DOF associated with the gun can be changed in a similar manner:

       findNodeVisitor findGunNode("gun");

       osgSim::DOFTransform * gunDOF = 
          dynamic_cast<osgSim::DOFTransform *> (findGunNode.getFirst()) ;

       if (gunDOF)
          gunDOF->setCurrentHPR( osg::Vec3(0.0,3.14159/8.0,0.0) );

That's it! The rest of the code is the usual simulation loop.