Version 1 (modified by osg, 7 years ago)

Initial copy of NPS Version

Implementing a Camera that Follows a Node

Note (09 Aug 04):

With the release of osg 0.9.7, there is an osgGA::MatrixManipulator (TrackerManipulator) that allows you to 'attach' a camera to a node in a scene graph. This new manipulator effectively replaces the technique described below.
This tutorial will remain in place as an example of how to use Callbacks and NodePaths to retrieve the world coordinates of a node.

Goals

In a typical simulation, the user may have the option of selecting from a variety of vehicles or avatars they can follow through the scene. This section describes a technique for 'attaching' a camera to a node in a scene graph. The camera then will be positioned according to the world coordinates of that node.

Overview

The viewer class contains a set of MatrixManipulators (osgGA::MatrixManipulator). These provide the default 'Drive', 'Trackball', and 'Fly' interaction methods. The matrix manipulator class is designed to update a matrix that is used to position a camera. Matrix manipulators normally respond to GUI events (mouse click, drag, keypress, etc.) . The approach outlined here relies on updating the matrix used to position the camera based on the world coordinates of a node in the scene graph. In this manner, a camera could be assigned to follow a node in the scene graph.
To derive the world coordinates of a node in the scene graph we'll implement a class that uses a node visitor's node path. This class will provide a method that allows instances it to be attached to a scene graph, thus providing access to the world coordinates of any node. This matrix (the world coordinates of an arbitrary node in the scene) will be used by an instance of osgGA::MatrixManipulator as the matrix for positioning a camera.

The code

First we'll create a class that provides the accumulation of all of the transformation matrices above it in the scene graph. This is rather straightforward since every node visitor has access to the current node path. For update visitors, the node path is essentially a list of all the nodes from the root node to the current node. Given an instance of a node path, we can use the open scene graph method computeWorldToLocal( osg:!:NodePath) to retrieve a matrix that represents the world coordinates of a node.
The core of the class will be an update callback that updates the matrix that represents the accumlation of matrices above a given node. The entire class would be as follows:

    struct updateAccumlatedMatrix : public osg::NodeCallback
    {
       virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
       {
          matrix = osg::computeWorldToLocal(nv->getNodePath() );
          traverse(node,nv);
       }
       osg::Matrix matrix;
    }; 

Next we need to make sure this callback is initiated during the update traversal of the scene graph. To do this, we'll create a class that has an osg::Node instance as a data member. We'll make this node's update callback an instance of the above updateAccumulatedMatrix class and provide a way to make this node part of the scene. To allow access to the matrix representing the world coordinates of the node to which instances are attached, we'll need to provide a 'get' method for our matrix. We'll also provide a means to add this node to the scene graph. We need to be careful about how we let users attach this node within the scene. It's important for the node to have exactly one parent. To make sure instances of this class are only associated with one node, we'll keep track of the its parent node. The class declaration is as follows:

    struct transformAccumulator
    {
    public: 
       transformAccumulator();
       bool attachToGroup(osg::Group* g);
       osg::Matrix getMatrix();
    protected:
       osg::ref_ptr<osg::Group> parent;
       osg::Node* node;
       updateAccumlatedMatrix* mpcb;
    };

    The class implementation is as follows:

    transformAccumulator::transformAccumulator()
    {
       parent = NULL;
       node = new osg::Node;
       mpcb = new updateAccumlatedMatrix();
       node->setUpdateCallback(mpcb);
    }

    osg::Matrix transformAccumulator::getMatrix()
    {
       return mpcb->matrix;
    }

    bool transformAccumulator::attachToGroup(osg::Group* g)
    // Don't call this method from within a callback (see this post.) 
    {
       bool success = false;
       if (parent != NULL)
       {
          int n = parent->getNumChildren();
          for (int i = 0; i < n; i++)
          {
             if (node == parent->getChild(i) )
             {
                parent->removeChild(i,1);
                success = true;
             }
          }
          if (! success)
          {
             return success;
          }
       }
       g->addChild(node);
       return true;
    }

Now that we have a class that provides a way to retrieve a matrix that represents the world coordinate position of a node in the scene, we need a way to use the matrix to update a matrix used to position a camera. The osgGA::MatrixManipulator class is designed to provide an updated matrix for positioning a camera. We will derive a class from MatrixManipulator that uses the matrix representing the world coordinates of a node in the scene for positioning the camera. To achieve this, the class will need a data member that is a handle to an instance of the accumulateTransform class we created above. The class will also need a data member to store the matrix for positioning the camera.
The core of the MatrixManipulator class is the 'handle' method. This method checks for selected GUI events and responds to them. For our class, the only GUI event we need to respond to is the 'FRAME' event. At every frame event we need to set our copy of the matrix to be used to position the camera equal to the matrix that represents the transformAccumulator matrix. We'll create a short updateMatrix method as part of our class to do this. Because the base class is abstract, several additional methods need to be defined (setting and getting the matrix or its inverse). Based on this, the class declaration would be as follows:

    class followNodeMatrixManipulator : public osgGA::MatrixManipulator
    {
    public:
       followNodeMatrixManipulator( transformAccumulator* ta); 
       bool handle (const osgGA::GUIEventAdapter&ea, osgGA::GUIActionAdapter&aa);
       void updateTheMatrix();
       virtual void setByMatrix(const osg::Matrixd& mat) {theMatrix = mat;}
       virtual void setByInverseMatrix(const osg::Matrixd&mat) {}
       virtual osg::Matrixd getInverseMatrix() const;
       virtual osg::Matrixd getMatrix() const;
    protected:
       ~followNodeMatrixManipulator() {}
       transformAccumulator* worldCoordinatesOfNode;
       osg::Matrixd theMatrix;
    };

The class implementation is as follows:

    followNodeMatrixManipulator::followNodeMatrixManipulator( transformAccumulator* ta)
    {
       worldCoordinatesOfNode = ta; theMatrix = osg::Matrixd::identity();
    }
    void followNodeMatrixManipulator::updateTheMatrix()
    {
       theMatrix = worldCoordinatesOfNode->getMatrix();
    }
    osg::Matrixd followNodeMatrixManipulator::getMatrix() const
    {
       return theMatrix;
    }
    osg::Matrixd followNodeMatrixManipulator::getInverseMatrix() const
    {
    // rotate the matrix from Y up to Z up. 
       osg::Matrixd m;
       m = theMatrix * osg::Matrixd::rotate(-M_PI/2.0, osg::Vec3(1,0,0) );
       return m;
    }
    void followNodeMatrixManipulator::setByMatrix(const osg::Matrixd& mat)
    {
       theMatrix = mat;
    }
    void followNodeMatrixManipulator::setByInverseMatrix(const osg::Matrixd& mat)
    {
       theMatrix = mat.inverse();
    } 

    bool followNodeMatrixManipulator::handle
    (const osgGA::GUIEventAdapter&ea, osgGA::GUIActionAdapter&aa)
    {
       switch(ea.getEventType())
       {
          case (osgGA::GUIEventAdapter::FRAME):
          {
             updateTheMatrix();
             return false;
          }
       }
       return false;
    }

Once these classes have been defined, using them should be straightforward. We need to declare an instance of the transformAccumulator class. This instance should then be attached to some node in the scene graph. Next we need to declare an instance of our nodeFollowerMatrixManipulator class. The constructor to this manipulator takes a pointer to our instance of the transformAccumulator class. Lastly, this matrix manipuator needs to be added to our viewer's list of matrix manipulators. The steps to do this are as follows:

    // ... code to set up scene and viewer (including adding a
    // tankTransform node) ...

    transformAccumulator* tankWorldCoords = new transformAccumulator();
    tankWorldCoords->attachToGroup(tankTransform);
    followNodeMatrixManipulator* followTank = 
       new followNodeMatrixManipulator(tankWorldCoords);
    osgGA::KeySwitchMatrixManipulator *ksmm = 
       viewer.getKeySwitchMatrixManipulator();
    if (!ksmm)
       return -1;
    // add the tank follower matrix manipulator. Selecting the 'm' key 
    // with switch the viewer to this manipulator.
    ksmm->addMatrixManipulator('m',"tankFollower",followTank);

    // ... code to enter simulation loop ...