Version 6 (modified by osg, 7 years ago)

--

Changing Models Using Update Callbacks

(Wiki editing note: Add link to complete source code at bottom)

Goals

Setup a callback to update a node of a scene graph. A previous tutorial demonstrated how to update a DOF or switch node within a scenegraph prior to entering the main simulation loop. This tutorial demonstrates how to use a callback to continually update one of these nodes as part of the update traversal performed once each frame.

Callbacks overview

Users can interact with a scene graph using callbacks. Callbacks can be thought of as user-defined functions that are automatically executed depending on the type of traversal (update, cull, draw) being performed. Callbacks can be associated with individual nodes or they can be associated with selected types (or subtypes) of nodes. During each traversal of a scene graph if a node is encountered that has a user-defined callback associated with it, that callback is executed. For more information on traversals and callbacks see chapter four of David Eberly's '3D Game Engine Design' and SGI's Chapter Four of the 'Performer Programmer's Guide'. For a more complete example see osgExampleCallback.

Creating an update callback

An update callback will be executed each time a scene graph update traversal is performed. Since the code associated with update callbacks happens once per frame and before the cull traversal is executed the code could be inserted in the main simulation loop between the viewer.updateTraversal() and viewer.renderingTraversals() calls. While the effect of the code is the same, callbacks provide an interface that is easier to update and maintain. Code that takes advantage of callbacks can also be more efficient when a multithreaded processing mode is used.

Extending the example from a previous tutorial, if we want to automatically update the DOF nodes associated with a tank model's turret rotation and gun elevation there are a number of techniques we could use. One alternative would be to write individual callbacks for each of the nodes we want to articulate: a callback associated with the gun node, a callback associated with the turret node, and so on. A disadvantage of this technique is that the functions associated with an individual model will not be centralized - complicating the process of reading, maintaining and updating the code. A second (extreme) alternative would be to write a single update callback that applies to the entire scene. This would essentially share part the same problem as putting all the code within the simulation loop. As the complexity of the simulation increased the single update callback method would become difficult to read, update and maintain. There are no set rules for how to assign callbacks to nodes/subtrees of a scene graph. A useful guideline would be to associate one callback with each entity (model) within the scene. In this example we will create a single callback for a tank node. This callback will update the turret and gun DOF nodes of the tank model.

To implement this callback, we will need some data that is not already part of the node. We will need handles to the DOF nodes associated with the turret and gun and we will need updated values for turret rotation and gun elevation. These values should be based on the most recent data. Since callbacks are initiated as part of a traversal, the only parameters we normally have access to are the pointer to the node that has the callback associated with it and a pointer to the nodeVisitor performing the traversal. The challenge of providing access to the additional required data (handles to turret and gun DOF, values for rotation and elevation) can be met by using the 'userData' data member of the node class. The userData member is a pointer to an instance of a user defined class containing any data fields the user wants to associate with a particular node. The only requirement for this user defined class is that it must be derived from the osg::Referenced class.The referenced class helps users manage memory by providing smart pointers. Smart pointers keep a count of the number of references assigned to a single class instance. These class instances are only deleted when their reference count reaches zero. For a detailed description of the osg::Referenced class see this page. For a more recent update see this correction. Based on the requirements outlined above, the data we want to associate with the tank node includes:

    class tankDataType : public osg::Referenced
    {
    public:
    // ... (public methods to follow) ... 
    protected:
       osgSim::DOFTransform* tankTurretNode;
       osgSim::DOFTransform* tankGunNode;
       double rotation;
       double elevation;
    };

To implement our tankData class, we need a means to get handles to the DOF nodes. This can be done in the class' constructor using the findNodeVisitor class defined in a previous tutorial. A findNodeVisitor traversal is initiated from a starting node. In this case we want to start the traversal from the root of the subtree that represents the tank, therefore we will need to pass a pointer to the tank node to the constructor for our tankData type. Based on this, the constructor for the class tankDataType would be as follows: (the steps to assign user data to specific nodes is covered a bit later.)

    tankDataType::tankDataType(osg::Node* n)
    {
       rotation = 0;
       elevation = 0;

       findNodeVisitor findTurret("turret"); 
       n->accept(findTurret);
       tankTurretNode = 
          dynamic_cast <osgSim::DOFTransform*> (findTurret.getFirst());

       findNodeVisitor findGun("gun"); 
       n->accept(findGun);
       tankGunNode = 
          dynamic_cast< osgSim::DOFTransform*> (findGun.getFirst());
    }

We can also define methods within the tankDataType class for updating the turret rotation and gun elevation. For now these methods will simply update the turret heading and gun pitch a fixed amount each frame. For the gun elevation we will check to see if the elevation is above some arbitrary limit. If this limit is reached, the elevation will be reset to zero. The turret will continually rotate through 2 PI radians.

    void tankDataType::updateTurretRotation()
    { 
       rotation += 0.01;
       tankTurretNode->setCurrentHPR( osg::Vec3(rotation,0,0) );
    }

    void tankDataType::updateGunElevation()
    {
       elevation += 0.01;
       tankGunNode->setCurrentHPR( osg::Vec3(0,elevation,0) );
       if (elevation > .5)
          elevation = 0.0;
    }

    Adding these methods to the class definition, our new class declaration would look like this:

    class tankDataType : public osg::Referenced
    {
    public:
       tankDataType(osg::Node*n); 
       void updateTurretRotation();
       void updateGunElevation();
    protected:
       osgSim::DOFTransform* tankTurretNode;
       osgSim::DOFTransform* tankGunNode;
       double rotation; // (radians) 
       double elevation; // (radians) 
    }; 

The next step is to create the callback and associate it with the tank node we want to articulate. To create the callback we need to define a class that is derived from nodeCallback class. Within this class we will override the operator () that takes two parameters: a node pointer and a nodeVisitor pointer. In this function we will implement the code to update the DOFs. We can do this by invoking the update methods of the tankData instance we will associate with the tank node using the tank node's userData field. The pointer to the tank data can be retrieved using the node's getUserData method. Since this method returns a pointer to the base class osg::Referenced, the result needs to be safely cast to a pointer to our tankDataType class. To make sure we keep an accurate count of the number of references to our user data, we'll cast the point to user data to a variable of type osg::ref_ptr<tankDataType>. The entire class definition is as follows:

    class tankNodeCallback : public osg::NodeCallback 
    {
    public:
       virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
       {
          osg::ref_ptr<tankDataType> tankData = 
             dynamic_cast<tankDataType*> (node->getUserData() );
          if(tankData)
          {
             tankData->updateTurretRotation();
             tankData->updateGunElevation();
          }
          traverse(node, nv); 
       }
    };

The next step is to 'install' the callback -- associate it with the tank we want to articulate so our update methods get called once per frame. To do this, we first have to make sure the tank node has appropriate user data (an instance of tankDataType class.) We can then use the osg::Node class' setUpdateCallback method to associate our callback with the correct node. The code for this is as follows:

    // .. code to initialize variables, load model, build scene ...

       tankDataType* tankData = new tankDataType(tankNode);

       tankNode->setUserData( tankData );
       tankNode->setUpdateCallback(new tankNodeCallback);

Once we have created a callback we enter our simulation loop. The code for the simulation loop does not change from previous examples. When we call the update() method for our viewer class instance we initiate an update traversal. When the update traversal reaches our tank node, the version of the operator () we provided in our tankNodeCallback class is invoked.

    // .. code to initialize viewer, etc ... 

       osgViewer::Viewer viewer;

       viewer.setSceneData(tankNode);

       return viewer.run();
    }

Continue with tutorial Keyboard handler class