Version 2 (modified by osg, 7 years ago)

Added menu

Using Multiple Independent Cameras to View a Scene

(Wiki editing note: Code needs conversion to osgViewer)
(Wiki editing note: Add link to complete source code at bottom)

Goal

Create several views into a scene. These views will each have independently controlled viewpoints:

  • Attach a 'tank driver' viewpoint (offset slightly above and behind) a tank model. The tank driver will have a wide field of view that covers the top third and entire width of the screen
  • Attach three separate views to the tank's turret. These views will coorespond to left, center and right gunner positions. These views will be oriented relative to the turret's heading with heading offset appropriate for each position. (The right gunner's view will be offset 45 degrees to the right of the turret.) The gunner's left, center and right views will occupy the lower one third of the screen. Each view will occupy approximately one third of the width of the display.

Overview

OpenSceneGraph provides a wide range of access with respect to the level of abstraction - programers can code at a fairly high level of abstraction using an osgProducer::viewer class instance. The viewer class encapsulates all of the basic elements of a visual simulation: keyboard/mouse interface, input area, rendering surface, scene management and frame control. Programmers are also free to program with more fundamental elements directly or create (or modify) their own encapsulating class. One of the advanatages of the open scene graph is that programming at higher levels does not restrict our access to lower levels of abstraction. In this tutorial, we'll integrate use of a viewer class instance with lower-level Producer::Camera elements to create independent views in the scene corresponding to different operator stations in a tank model.

For an explanation of Producer terminology (camera, lens, render surface, input area) see this page. More specific documentation starting with the camera class is provided here. For this tutorial, we'll use an instance of the Producer::CameraConfig class to provide access to a group of independent cameras we will create and control. We'll directly control the lens associated with the camera using Producer::Lens class methods.

The first step will be to set up a camera configuration instance that will allows us to specify individual camera and lense settings and indepently assign position and orientation. This configuration instance can then be used as an argument to the viewer class instance to describe camera parameters and control. For modularity, we'll write a specific function to create the desired instance of camera configuration.

    enum cameraIndex
    {
       DRIVER_CAMERA,
       GUNNER_LEFT_CAMERA,
       GUNNER_CENTER_CAMERA,
       GUNNER_RIGHT_CAMERA
    };

    Producer::CameraConfig* setupCameras()
    {
       Producer::CameraConfig* tankCameraConfig = 
          new Producer::CameraConfig();
       Producer::Camera* tankCameras[4];
       std::string cameraNames[4];
       cameraNames[0]= "Driver";
       cameraNames[1]= "GunnerLeft";
       cameraNames[2]= "GunnerCenter";
       cameraNames[3]= "GunnerRight";
       for (int i=0;i<4;i++)
       {
          tankCameras[i] = new Producer::Camera();
          tankCameras[i]->getLens()->setAutoAspect(true);
          tankCameras[i]->setShareLens(false);
          tankCameraConfig->addCamera(cameraNames[i],tankCameras[i]);
       }
       
       tankCameras[DRIVER_CAMERA]->
          setProjectionRectangle(0.05f, 0.95f, 0.4f, 0.8f);

       tankCameras[GUNNER_LEFT_CAMERA]->
          setProjectionRectangle(0.00, 0.30, 0.05f, 0.35);
       tankCameras[GUNNER_CENTER_CAMERA]->
          setProjectionRectangle(0.35f, 0.65f, 0.05f, 0.35f);
       tankCameras[GUNNER_RIGHT_CAMERA]->
          setProjectionRectangle(0.70f, 1.0f, 0.05f, 0.35f);

       Producer::RenderSurface* rsOne = 
          tankCameras[DRIVER_CAMERA]->getRenderSurface();

       tankCameras[GUNNER_RIGHT_CAMERA]->setRenderSurface( rsOne );
       tankCameras[GUNNER_CENTER_CAMERA]->setRenderSurface( rsOne );
       tankCameras[GUNNER_LEFT_CAMERA]->setRenderSurface( rsOne );

       return tankCameraConfig; 
    }

Now that we have a camera configuration set up to use later for our viewer class instance, we need to start building our scene. For this scene we'll have three main models: a terrain model - positioned at the origin, a 'damaged' tank positioned somewhere in the scene, and the tank model we'll associate with the viewpoints - this will be our 'ownTank' model. It will be positioned near the damaged tank so we have something to look at. Again for modularity, we'll put this in a setupScene function. To set up the scene and to attach viewpoints to the correct nodes in the scene, this function will need to provide handles to the root node and our 'ownTank' model. The following code achieves this. (This code repeats code from previous tutorials and can be skipped if you are familiar with loading models and setting multiSwitch nodes.)

    bool setupScene(Producer::ref_ptr<osg::Group> &rootNode,
                    Producer::ref_ptr<osg::Group> &ownTank  )
    // Setup a scene with a damaged tank near our own tank.
    // Return (via reference argument) a handle to a tank we'll
    //  eventually control and a handle to the root node.  
    // Function returns false if it can't load
    //  models or arguments are not NULL, true otherwise.
    {
       if (rootNode.get() || ownTank.get())
          return false;

       rootNode = new osg::Group();

       osgDB::FilePathList pathList = osgDB::getDataFilePathList();
       pathList.push_back
          ("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Models\\T72-Tank\\");
       pathList.push_back
          ("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Models\\JoeDirt\\");
       osgDB::setDataFilePathList(pathList);

       Producer::ref_ptr<osg::Node> terrainNode = 
          osgDB::readNodeFile("JoeDirt.flt");
       if (!terrainNode)
       {
          std::cout << " no terrain! " << std::endl;
          return false;
       }
       rootNode->addChild(terrainNode.get());

       ownTank = (osg::Group*) 
          osgDB::readNodeFile("T72-tank_des.flt");
       if( ! ownTank)
       {
          std::cout << "no Tank" << std::endl;
          return false;
       }
       osg::PositionAttitudeTransform* ownTankPAT = 
          new osg::PositionAttitudeTransform();
       ownTankPAT->setPosition( osg::Vec3(100,100,8) );
       rootNode->addChild(ownTankPAT);
       ownTankPAT->addChild(ownTank.get());

       Producer::ref_ptr<osg::Node> damagedTank = 
          osgDB::readNodeFile("T72-tank_des.flt");
       if( ! damagedTank )
       {
          std::cout << "no Tank" << std::endl;
          return false;
       }
       osg::PositionAttitudeTransform* damagedTankPAT = 
          new osg::PositionAttitudeTransform();
       damagedTankPAT->setPosition( osg::Vec3(90,110,8) );
       rootNode->addChild(damagedTankPAT);
       damagedTankPAT->addChild(damagedTank.get());

       findNodeVisitor findSwitch("sw1");
       damagedTank->accept(findSwitch);
       osgSim::MultiSwitch* damagedTankSwitch = 
          dynamic_cast <osgSim::MultiSwitch*> (findSwitch.getFirst());
       if (!damagedTankSwitch)
          return -1;
       damagedTankSwitch->setSingleChildOn(0,true);

       return true;
    }

Now that our basic scene is set up, we can set up the rest of the simulation. This section of code will configure our scene so we can position cameras relative to world coordinates of nodes in the scene graph. To retrieve the world coordinates of nodes in the scene, we'll use instances of the 'transformAcumulator' class developed in a previous tutorial. For the driver's view we'll add a slight offset (a few meters behind and higher) relative to the tank's position and use the world coordinates of this node. This camera will be controlled using the viewer's current matrix manipulator.
The other three cameras will be positioned relative to the turret. For the gunner views we'll add an offset (tranform node) to the turrent node. The center gunner camera will be positoned using a tranformAcumulator associated with this offset transform. (Giving the world coordinates of the offset from the turret.) To provide world coordinates for the left and right gunner positions, we'll add left and right offset tranforms to the center gunner node. Each of these will have a transform accumulator associated with it so we can retrieve world coordinates for positioning cameras.

    int main( int argc, char **argv )
    {
       Producer::ref_ptr<osg::Group> rootNode; 
       Producer::ref_ptr<osg::Group> ownTank;

       // build scene with terrain and two tanks
       if (!setupScene(rootNode,ownTank))
       {
          std::cout<< "problem setting up scene" << std::endl;
          return -1;
       }

       // Configure the cameras and use them to initialize a viewer.
       Producer::CameraConfig* tankCameras = setupCameras();
       osgProducer::Viewer viewer(tankCameras);

       // Set up the viewer
       viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
       viewer.setSceneData( rootNode.get() );

       // Get a handle to our tank's turret node
       findNodeVisitor findTurret("turret");
       ownTank->accept(findTurret);
       osgSim::DOFTransform* turretXForm = dynamic_cast <osgSim::DOFTransform*> 
          (findTurret.getFirst());

       // Declare an offset for the gunner and attach it to the turret
       osg::PositionAttitudeTransform* gunnerXForm = 
          new osg::PositionAttitudeTransform();
       gunnerXForm->setPosition( osg::Vec3(0,-1.5,3.0) );
       turretXForm->addChild(gunnerXForm);

       // Declare a transform accumulator to retrieve world coords of 
       // the center gunner. Associate transform accumulator with the offset
       // for the gunner.
       transformAccumulator* gunnerWorldCoords = new transformAccumulator();
       gunnerWorldCoords->attachToGroup(gunnerXForm);

       // Declare and set a transform for the left gunner's view 
       // (this will be a 45 degree rotation relative to the up axis.)
       // Attach this to the gunner transform.
       osg::PositionAttitudeTransform* leftGunnerPAT = 
          new osg::PositionAttitudeTransform();
       leftGunnerPAT->setAttitude( 
          osg::Quat( osg::DegreesToRadians(45.0) , osg::Vec3(0,0,1) ));
       gunnerXForm->addChild(leftGunnerPAT);

       // Declare a transform accumulator to retrieve world coordinates 
       // for the left gunner. Associate this accumulator with the left
       // gunner's transform.
       transformAccumulator* leftGunnerWC = new transformAccumulator();
       leftGunnerWC->attachToGroup(leftGunnerPAT);

       // repeat this process for the right gunner, the offset will be 
       // a -45 degree rotation relative to the up axis.
       osg::PositionAttitudeTransform* rightGunnerPAT = 
          new osg::PositionAttitudeTransform();
       rightGunnerPAT->setAttitude(
          osg::Quat( osg::DegreesToRadians(-45.0) , osg::Vec3(0,0,1) ));
       transformAccumulator* rightGunnerWC = new transformAccumulator();
       gunnerXForm->addChild(rightGunnerPAT);
       rightGunnerWC->attachToGroup(rightGunnerPAT);

       // Declare and initialize a transform for the driver,
       // add this to the tank node.
       osg::PositionAttitudeTransform* driverOffsetPAT = 
          new osg::PositionAttitudeTransform();
       driverOffsetPAT->setPosition(osg::Vec3(0,-15,4));
       driverOffsetPAT->setAttitude( 
          osg::Quat( osg::DegreesToRadians(-5.0f), osg::Vec3(1,0,0) ) );
       ownTank->addChild(driverOffsetPAT);

       // Declare a transform accumulator to retrieve world coordinates 
       // of the driver transform.
       transformAccumulator* driverWorldCoords = new transformAccumulator();
       driverWorldCoords->attachToGroup(driverOffsetPAT);

       // Use this for a new matrix manipulator that will follow the 
       // driver transform.
       viewer.getKeySwitchMatrixManipulator()->addMatrixManipulator
          ('m',"ft",new followNodeMatrixManipulator(driverWorldCoords));

Now that the simulation is set up, we ready for the simulation loop. This one will be a bit different than previous. After initiating the update traversal and before begining the cull traversal we'll manually set the position of the three cameras associated with the gunners. For each of these cameras, we'll use the Producer::Camera setViewByMatrix method. We can almost use the matrix from our transformAccumulator class instances directly - but need to make one important change. The matrix returned from the transformAccumulator class is expressed in Y up terms. The camera class' setViewByMatrix method expects a Z up matrix. To use this matrix, we must rotate it from Y up to Z up. The following code achieves this:

       // create the windows and run the threads.
       viewer.realize();

       while( !viewer.done() )
       {
          // wait for all cull and draw threads to complete.
          viewer.sync();

          // update the scene by traversing it with the the update visitor which will
          // call all node update callbacks and animations.
          viewer.update();

          tankCameras->findCamera("GunnerLeft")->setViewByMatrix( 
             Producer::Matrix(leftGunnerWC->getMatrix().ptr() )*
                Producer::Matrix::rotate( -M_PI/2.0, 1, 0, 0 ) );

          tankCameras->findCamera("GunnerCenter")->setViewByMatrix( 
             Producer::Matrix(gunnerWorldCoords->getMatrix().ptr() )*
                Producer::Matrix::rotate( -M_PI/2.0, 1, 0, 0 ) );

          tankCameras->findCamera("GunnerRight")->setViewByMatrix( 
             Producer::Matrix(rightGunnerWC->getMatrix().ptr() )*
                Producer::Matrix::rotate( -M_PI/2.0, 1, 0, 0 ) );

          // fire off the cull and draw traversals of the scene.
          viewer.frame();
       }

       // wait for all cull and draw threads to complete before exit.
       viewer.sync();

       return 0;
    }

Note: Pressing the 'm' key switches between cameras.