2. Designing the Scene Graph – OpenSceneGraph 3 Cookbook

Chapter 2. Designing the Scene Graph

In this chapter, we will cover:

  • Using smart and observer pointers

  • Sharing and cloning objects

  • Computing the world bounding box of any node

  • Creating a running car

  • Mirroring the scene graph

  • Designing a breadth-first node visitor

  • Implementing a background image node

  • Making your node always face the screen

  • Using draw callbacks to execute NVIDIA Cg functions

  • Implementing a compass node

Introduction

In this chapter, we will show a series of interesting topics about configuring the scene graph and implementing some special effects with simple but effective methods. We assume that you have already understood the concepts of group nodes, leaf nodes (geodes) , and parent and child interfaces. If not, you can read the book "OpenSceneGraph 3.0: Beginner's Guide", Rui Wang and Xuelei Qian, Packt Publishing, first. So the main objective of the following few recipes will be to use nodes and callbacks in a flexible way.

Before we start, it is necessary to prepare some common functions and classes for use. These utilities can be used to quickly create nodes, event handlers, and other scene objects. As we have just started with the book, we will learn to handle some real programming cases; there will be only three components in the "common use" domain. The first one is the createHUDCamera() function:

osg::Camera* createHUDCamera( double left, double right, double bottom, double top )
{
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setReferenceFrame( osg::Transform::ABSOLUTE_RF );
camera->setClearMask( GL_DEPTH_BUFFER_BIT );
camera->setRenderOrder( osg::Camera::POST_RENDER );
camera->setAllowEventFocus( false );
camera->setProjectionMatrix(
osg::Matrix::ortho2D(left, right, bottom, top) );
camera->getOrCreateStateSet()->setMode(
GL_LIGHTING, osg::StateAttribute::OFF );
return camera.release();
}

This function will create an ordinary camera node which will be rendered on the top after the main scene is drawn. It can be used to display some heads-up display (HUD) texts and images. You may visit the following link to learn more about the concept of HUD: http://en.wikipedia.org/wiki/HUD_(video_gaming)

And it is necessary to have a function for creating HUD texts. Its content is shown in the following block of code:

osg::ref_ptr<osgText::Font> g_font = osgText::readFontFile("fonts/ arial.ttf");
osgText::Text* createText( const osg::Vec3& pos, const std::string& content, float size )
{
osg::ref_ptr<osgText::Text> text = new osgText::Text;
text->setDataVariance( osg::Object::DYNAMIC );
text->setFont( g_font.get() );
text->setCharacterSize( size );
text->setAxisAlignment( osgText::TextBase::XY_PLANE );
text->setPosition( pos );
text->setText( content );
return text.release();
}

Of course, it is already designed to work with the HUD camera node smoothly.

The last useful tool to implement is a picking-up handler with which we can quickly select a node or drawable displayed on the screen and retrieve information and parent node paths. It must be derived for practical use.

class PickHandler : public osgGA::GUIEventHandler
{
public:
// This virtual method must be overrode by subclasses.
virtual void doUserOperations(
osgUtil::LineSegmentIntersector::Intersection& ) = 0;
virtual bool handle( const osgGA::GUIEventAdapter& ea,
osgGA::GUIActionAdapter& aa )
{
if ( ea.getEventType()!=osgGA::GUIEventAdapter::RELEASE
||ea.getButton()!=osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON
||!(ea.getModKeyMask()&osgGA::GUIEventAdapter::MODKEY_CTRL) )
return false;
osgViewer::View* viewer = dynamic_cast<osgViewer::View*>(&aa);
if ( viewer )
{
osg::ref_ptr<osgUtil::LineSegmentIntersector>
intersector = new osgUtil::LineSegmentIntersector
(osgUtil::Intersector::WINDOW, ea.getX(), ea.getY());
osgUtil::IntersectionVisitor iv( intersector.get() );
viewer->getCamera()->accept( iv );
if ( intersector->containsIntersections() )
{
osgUtil::LineSegmentIntersector::Intersection&
result = *(intersector->getIntersections().begin());
doUserOperations( result );
}
}
return false;
}
};

When you are clicking on the screen to select an object, you must press Ctrl at the same time to distinguish the selecting operation with normal scene navigating.

All these utilities will be placed in the osgCookbook namespace to avoid ambiguous issues. And we will directly call them in a unified form such as the osgCookBook::createText() method, assuming that you have already put them in a suitable place for use.

Also, go through the code bundle of this chapter for the source code.

Using smart and observer pointers

You should be familiar with the smart pointer osg::ref_ptr<>, which manages allocated objects using reference counting, and deletes them when the counting number decreases to 0. In this case, osg::ref_ptr<> is actually a strong pointer that contributes to the life of the managed object.

This time we will come across another type of smart pointer, that is, the weak pointer. A weak pointer, that is, osg::observer_ptr<> in the OSG core library, doesn't own the object and won't change the reference counting number irrespective of it being attached or detached. But it has a property that when the object is deleted or recycled, it will be notified and set to NULL automatically to avoid using invalid pointers.

How to do it...

An interactive program will be created in this recipe to show the main feature of the osg::observer_ptr<> template class that checks if the pointer is valid or not spontaneously:

  1. Include necessary headers:

    #include <osg/ShapeDrawable>
    #include <osg/Geode>
    #include <osgViewer/Viewer>
    
  2. We are going to have a RemoveShapeHandler class derived from the osgCookBook::PickHandler auxiliary class. It simply checks and removes the picked drawable from its parent:

    class RemoveShapeHandler : public osgCookBook::PickHandler
    {
    virtual void doUserOperations( osgUtil::LineSegmentIntersector::
    Intersection& result )
    {
    if ( result.nodePath.size()>0 )
    {
    osg::Geode* geode = dynamic_cast<osg::Geode*>(
    result.nodePath.back() );
    if ( geode ) geode->removeDrawable(
    result.drawable.get() );
    }
    }
    };
    
  3. The ObserveShapeCallback class is used here to keep two drawables with the osg::observer_ptr<> template class. As a weak pointer, it will automatically reset the pointer to NULL if the referenced object is recycled for some reason. And the member _text variable here will record these changes and display them on the screen:

    class ObserveShapeCallback : public osg::NodeCallback
    {
    public:
    virtual void operator()( osg::Node* node, osg::NodeVisitor* nv )
    {
    std::string content;
    if ( _drawable1.valid() ) content += "Drawable 1; ";
    if ( _drawable2.valid() ) content += "Drawable 2; ";
    if ( _text.valid() ) _text->setText( content );
    }
    osg::ref_ptr<osgText::Text> _text;
    osg::observer_ptr<osg::Drawable> _drawable1;
    osg::observer_ptr<osg::Drawable> _drawable2;
    };
    
  4. In the main entry, we will first build the scene graph. It contains a HUD camera with text, and two basic drawables for use in this experiment:

    // Create the text and place it in an HUD camera
    osgText::Text* text = osgCookBook::createText(osg::Vec3( 50.0f, 50.0f, 0.0f), "", 10.0f);
    osg::ref_ptr<osg::Geode> textGeode = new osg::Geode;
    textGeode->addDrawable( text );
    osg::ref_ptr<osg::Camera> hudCamera = osgCookBook::createHUDCamera(0, 800, 0, 600);
    hudCamera->addChild( textGeode.get() );
    // Create two simple shapes and add both, as well as the camera, // to the root node
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( new osg::ShapeDrawable(new osg::Box(osg::Vec3(-2.0f,0.0f,0.0f), 1.0f)) );
    geode->addDrawable( new osg::ShapeDrawable(new osg::Sphere(osg::Ve c3(2.0f,0.0f,0.0f), 1.0f)) );
    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( hudCamera.get() );
    root->addChild( geode.get() );
    
  5. Create the update callback for the root node (or any other node in this case). Set up its public member variables in the following way:

    osg::ref_ptr<ObserveShapeCallback> observerCB = new ObserveShapeCallback;
    observerCB->_text = text;
    observerCB->_drawable1 = geode->getDrawable(0);
    observerCB->_drawable2 = geode->getDrawable(1);
    root->addUpdateCallback( observerCB.get() );
    
  6. Add the RemoveShapeHandler instance for interacting with drawables and start the viewer:

    osgViewer::Viewer viewer;
    viewer.addEventHandler( new RemoveShapeHandler );
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  7. Press Ctrl and click on one of the shapes to remove it from the scene graph, and you will see that the text shown at the bottom is changed immediately. The observer pointer that has already found the shape is not referenced by other objects anymore and, therefore, resets itself to avoid dangling pointer problems.

How it works...

The RemoveShapeHandler here re-implements the doUserOperation() method of its parent class to check if a shape is picked, and un-references it from the parent osg::Geode node. As no other smart pointers are referencing the shape, it is actually deleted from the system memory. The osg::observer_ptr<> template class, as a weak pointer will only observe the node's allocation and destroy it, and will automatically switch its data to NULL to prevent further improper usages.

The weak pointer is a great feature if we are going to observe or use a node in callbacks or user processes, without adding redundant references to it. Using a raw pointer is troublesome here because you have to always ensure the object is still valid; otherwise, your program may get crashed at once.

In multi-threaded applications, it is safe to use the lock() method to convert the weak pointer to a temporary strong pointer, to prevent synchronous object deletion in other threads. The code segments could be as follows:

// Define a member variable using osg::observer_ptr<>.
osg::observer_ptr<osg::Node> _memberNode;
// In a thread, when we want to obtain the member node.
osg::ref_ptr<osg::Node> tempRefOfNode;
if ( _memberNode.lock(tempRefOfNode) )
{
osg::Node* realNode = tempRefOfNode.get();
// Do something to the realNode.
// Don't worry if it is unreferenced or deleted in
// other threads, because tempRefOfNode can ensure
// it works during the lifetime of the smart pointer.
}

There's more...

You may refer to the Boost library and read some more about its shared_ptr (strong pointer) and weak_ptr implementations at the following sites:

http://www.boost.org/doc/libs/1_46_1/libs/smart_ptr/shared_ptr.htm

http://www.boost.org/doc/libs/1_46_1/libs/smart_ptr/weak_ptr.htm

And the MSDN site also contains the similar classes for use:

http://msdn.microsoft.com/en-us/library/bb982026.aspx

http://msdn.microsoft.com/en-us/library/bb982126.aspx

Sharing and cloning objects

The sharing of nodes and drawables is an important optimization for a huge 3D application based on OSG. But sometimes, duplicating a node without sharing any memory chunks between the previous node and the new one is also useful for handling user data. In this example, we will show both implementations in one interactive program and explain the main difference of the scene graphs we have.

How to do it...

We will clone a simple ball shape twice, each with a different mechanism (shallow copy or deep copy). The user can press Ctrl and click on a ball to change its color. Shallow copied balls will change together because they point to the same memory address, but a deep copied one will not.

  1. Include necessary headers:

    #include <osg/ShapeDrawable>
    #include <osg/Geode>
    #include <osg/MatrixTransform>
    #include <osgViewer/Viewer>
    
  2. This time we want to pick up any drawable and change its color if possible. The SetShapeColorHandler class here will do this for us. Every time we choose an osg::ShapeDrawable object, its color will be inverted. Thus we can quickly find out all the nodes that are sharing the same drawable:

    class SetShapeColorHandler : public osgCookBook::PickHandler
    {
    virtual void doUserOperations( osgUtil::LineSegmentIntersector
    ::Intersection& result )
    {
    osg::ShapeDrawable* shape = dynamic_cast<osg::ShapeDrawable*>
    ( result.drawable.get() );
    if ( shape ) shape->setColor( osg::Vec4(
    1.0f, 1.0f, 1.0f, 2.0f) - shape->getColor() );
    }
    };
    
  3. The createMatrixTransform() function here will just create a transformation node at a specified position, and add an osg::Geode node as its child:

    osg::Node* createMatrixTransform( osg::Geode* geode,
    const osg::Vec3& pos )
    {
    osg::ref_ptr<osg::MatrixTransform> trans =
    new osg::MatrixTransform;
    trans->setMatrix( osg::Matrix::translate(pos) );
    trans->addChild( geode );
    return trans.release();
    }
    
  4. In the main entry, create a basic sphere and disable the use of display lists on it. That is because its color may be dynamically changed later in the simulation loop:

    osg::ref_ptr<osg::ShapeDrawable> shape = new osg::ShapeDrawable( new osg::Sphere );
    shape->setColor( osg::Vec4(1.0f, 1.0f, 0.0f, 1.0f) );
    shape->setDataVariance( osg::Object::DYNAMIC );
    shape->setUseDisplayList( false );
    
  5. Now we will demonstrate different cloning types here. The original geode1 including the changeable sphere is duplicated into geode2 (shallow copy) and geode3 (deep copy). And all the three nodes are added to the root node with a proper translation:

    osg::ref_ptr<osg::Geode> geode1 = new osg::Geode;
    geode1->addDrawable( shape.get() );
    osg::ref_ptr<osg::Geode> geode2 = dynamic_cast<osg::Geode*>(
    geode1->clone(osg::CopyOp::SHALLOW_COPY) );
    osg::ref_ptr<osg::Geode> geode3 = dynamic_cast<osg::Geode*>(
    geode1->clone(osg::CopyOp::DEEP_COPY_ALL) );
    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( createMatrixTransform(geode1.get(),
    osg::Vec3(0.0f, 0.0f, 0.0f)) );
    root->addChild( createMatrixTransform(geode2.get(),
    osg::Vec3(-2.0f, 0.0f, 0.0f)) );
    root->addChild( createMatrixTransform(geode3.get(),
    osg::Vec3(2.0f, 0.0f, 0.0f)) );
    
  6. Before starting the viewer, don't forget to add the handler, with which you may click on the spheres to make the world colorful:

    osgViewer::Viewer viewer;
    viewer.addEventHandler( new SetShapeColorHandler );
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  7. You will soon realize the fact that geode1 (at the middle) and geode2 (at the left side) will act together when you pick either of them (press Ctrl at the same time). But geode3 (at the right side) is independent all the time.

How it works...

The geode3 node is deep copied so that if its member variables point to any objects, the allocated memories of these objects will be copied too. Instead, a shallow copy means the copied member pointers will share the same memory chunks with original ones. The difference between them can be seen in the following diagram:

The clone() method here will allocate a new object of the same type by calling the copy constructor. This is just a one-line implementation:

virtual osg::Object* clone(const osg::CopyOp& copyop) const
{ return new YourClass(*this, copyop); }

YourClass here means any class name used as OSG scene objects. And you can read the content of src/osg/CopyOp.cpp of the OSG source code for details about the second argument copyop.

Computing the world bounding box of any node

You may have already known that a node uses bounding sphere instead of the axis-aligned box by learning some other books and tutorials. You may also learn that the osg::ComputeBoundsVisitor class can compute the bounding box by traversing the node and its sub-graph. But in this recipe, we will introduce some more details about the whole computation process and the local-to-world transformation used here.

How to do it...

We will create a simple scene with animations and compute the bounding box of some objects in realtime, with the resultant bounding box displayed.

  1. Include necessary headers:

    #include <osg/ComputeBoundsVisitor>
    #include <osg/ShapeDrawable>
    #include <osg/AnimationPath>
    #include <osg/MatrixTransform>
    #include <osg/PolygonMode>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. The BoundingBoxCallback class can compute the real-world bounding box for us. We will have to pass a list of nodes to it and expand the world box by adding the local bound of each node one by one:

    class BoundingBoxCallback : public osg::NodeCallback
    {
    public:
    virtual void operator()( osg::Node* node, osg::NodeVisitor* nv )
    {
    }
    osg::NodePath _nodesToCompute;
    };
    
  3. In the operator() implementation, we will do the trick: The osg::ComputeBoundsVisitor class calculates the bounding box of a node in its parent's reference frame. Then we must re-compute the vertices in the world coordinates before adding them to the world box variable using the localToWorld matrix:

    osg::BoundingBox bb;
    for ( unsigned int i=0; i<_nodesToCompute.size(); ++i )
    {
    osg::Node* node = _nodesToCompute[i];
    osg::ComputeBoundsVisitor cbbv;
    node->accept( cbbv );
    osg::BoundingBox localBB = cbbv.getBoundingBox();
    osg::Matrix localToWorld = osg::computeLocalToWorld(
    node->getParent(0)->getParentalNodePaths()[0] );
    for ( unsigned int i=0; i<8; ++i )
    bb.expandBy( localBB.corner(i) * localToWorld );
    }
    
  4. Apply the result (world coordinates) to the transformation node and make it visible in the whole scene:

    osg::MatrixTransform* trans =
    static_cast<osg::MatrixTransform*>(node);
    trans->setMatrix(
    osg::Matrix::scale(bb.xMax()-bb.xMin(), bb.yMax()-bb.yMin(),
    bb.zMax()-bb.zMin()) *
    osg::Matrix::translate(bb.center()) );
    
  5. We would like to create a function named createAnimationPath() for creating animation path here, and make the computation of the bounding box more dynamic:

    osg::AnimationPath* createAnimationPath( float radius, float time )
    {
    osg::ref_ptr<osg::AnimationPath> path =
    new osg::AnimationPath;
    path->setLoopMode( osg::AnimationPath::LOOP );
    unsigned int numSamples = 32;
    float delta_yaw = 2.0f * osg::PI/((float)numSamples - 1.0f);
    float delta_time = time / (float)numSamples;
    for ( unsigned int i=0; i<numSamples; ++i )
    {
    float yaw = delta_yaw * (float)i;
    osg::Vec3 pos( sinf(yaw)*radius, cosf(yaw)*radius, 0.0f );
    osg::Quat rot( -yaw, osg::Z_AXIS );
    path->insert( delta_time * (float)i,
    osg::AnimationPath::ControlPoint(pos, rot) );
    }
    return path.release();
    }
    
  6. In the main entry, we first create the scene with a Cessna flying in a circle, a truck, and the example terrain. All the model files can be found in the OSG sample dataset:

    osg::ref_ptr<osg::MatrixTransform> cessna = new osg::MatrixTransform;
    cessna->addChild(
    osgDB::readNodeFile("cessna.osgt.0,0,90.rot") );
    osg::ref_ptr<osg::AnimationPathCallback> apcb =
    new osg::AnimationPathCallback;
    apcb->setAnimationPath( createAnimationPath(50.0f, 6.0f) );
    cessna->setUpdateCallback( apcb.get() );
    osg::ref_ptr<osg::MatrixTransform> dumptruck =
    new osg::MatrixTransform;
    dumptruck->addChild( osgDB::readNodeFile("dumptruck.osgt") );
    dumptruck->setMatrix( osg::Matrix::translate(0.0f, 0.0f, -100.0f) );
    osg::ref_ptr<osg::MatrixTransform> models =
    new osg::MatrixTransform;
    models->addChild( cessna.get() );
    models->addChild( dumptruck.get() );
    models->setMatrix( osg::Matrix::translate(0.0f, 0.0f, 200.0f) );
    
  7. The Cessna and the truck will be added for computing the world bounding box in a complex way:

    osg::ref_ptr<BoundingBoxCallback> bbcb =
    new BoundingBoxCallback;
    bbcb->_nodesToCompute.push_back( cessna.get() );
    bbcb->_nodesToCompute.push_back( dumptruck.get() );
    
  8. Construct the box shape for representing the bound in the scene graph:

    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( new osg::ShapeDrawable(new osg::Box) );
    osg::ref_ptr<osg::MatrixTransform> boundingBoxNode =
    new osg::MatrixTransform;
    boundingBoxNode->addChild( geode.get() );
    boundingBoxNode->setUpdateCallback( bbcb.get() );
    boundingBoxNode->getOrCreateStateSet()->setAttributeAndModes(
    new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK,
    osg::PolygonMode::LINE) );
    boundingBoxNode->getOrCreateStateSet()->setMode(
    GL_LIGHTING, osg::StateAttribute::OFF );
    
  9. Build the scene and start the viewer:

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( models.get() );
    root->addChild( osgDB::readNodeFile("lz.osgt") );
    root->addChild( boundingBoxNode.get() );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  10. You will see that the wireframe box is changing its size while the Cessna is moving. But it exactly contains the Cessna and the truck models all the time, as shown in the following screenshot:

  11. Try commenting the line inputted in step 3 in the following type:

    osg::Matrix localToWorld;/* = osg::computeLocalToWorld(
    node->getParent(0)->getParentalNodePaths()[0] ); */
    

    Then rebuild to see the difference. Can you figure out the reason for the change?

How it works...

Every node in the scene graph has its own local coordinate system. When you translate and rotate a transformation node, it means that you change its position and attitude in its parent's coordinate system. This makes all the transformations occur in local space rather than the world one. And the matrix applied to the node can also be treated as the transpose matrix that maps the node space to its parent's space.

To compute the world coordinates of a specified point, we have to first find out the local space it lives in, and then fetch the node's parent, parent's parent, and so on, until we reach the scene root. Then we will have a node path from the root to the node containing the point. With the node path, we can multiply all the transpose matrices and get a complete local-to-world matrix.

The collection of parental node paths is done with the getParentalNodePaths() method. It has multiple paths because an OSG node may have more than one parent node. And to compute the local-to-world matrix, use osg::computeLocalToWorld() function directly with the node path as argument. There is another function named osg::computeWorldToLocal(), which is for computing the local representation of a point in world space.

Creating a running car

The goal here is easy to understand but not easy to achieve. It requires exactly another book to tell how to make a realistic enough car model and load it into the scene graph efficiently, as well as how to assemble components and have the wheels rotating. So we have to simplify the problem here—we are going to implement some very ugly car parts only with basic shapes, and demonstrate the use of the scene graph in the assembly process.

How to do it...

All we need to do here is use the transformation node, which is one the most basic classes in the OSG library. But it is not easy to be skillful with it.

  1. Include necessary headers:

    #include <osg/ShapeDrawable>
    #include <osg/AnimationPath>
    #include <osg/MatrixTransform>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. The convenient function createTransformNode() will create a transformation node for every part shape we have:

    osg::MatrixTransform* createTransformNode( osg::Drawable*
    shape, const osg::Matrix& matrix )
    {
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( shape );
    osg::ref_ptr<osg::MatrixTransform> trans =
    new osg::MatrixTransform;
    trans->addChild( geode.get() );
    trans->setMatrix( matrix );
    return trans.release();
    }
    
  3. We want to make the wheel turn rapidly using an animation path callback. Note that we also added a very small offset on the Z axis while rotating the wheel. It makes the animation a little more realistic here:

    osg::AnimationPathCallback* createWheelAnimation(
    const osg::Vec3& base )
    {
    osg::ref_ptr<osg::AnimationPath> wheelPath =
    new osg::AnimationPath;
    wheelPath->setLoopMode( osg::AnimationPath::LOOP );
    wheelPath->insert( 0.0, osg::AnimationPath::ControlPoint(
    base, osg::Quat()) );
    wheelPath->insert( 0.01, osg::AnimationPath::ControlPoint(
    base + osg::Vec3(0.0f, 0.02f, 0.0f), osg::Quat(
    osg::PI_2, osg::Z_AXIS)) );
    wheelPath->insert( 0.02, osg::AnimationPath::ControlPoint(
    base + osg::Vec3(0.0f,-0.02f, 0.0f), osg::Quat(
    osg::PI, osg::Z_AXIS)) );
    osg::ref_ptr<osg::AnimationPathCallback> apcb =
    new osg::AnimationPathCallback;
    apcb->setAnimationPath( wheelPath.get() );
    return apcb.release();
    }
    
  4. In the main entry, there are mainly four parts of our ugly car: four wheels, the coupling rod between each two wheels, the car body (here we only use a box to represent it) and the main rod which connects all of them, as shown in the following diagram:

  5. By default, the geometric centers of each part's prototype are all placed at the origin point; and the height directions of the cylinders are the Z-axis:

    // The prototype of the main rod
    osg::ref_ptr<osg::ShapeDrawable> mainRodShape =
    new osg::ShapeDrawable( new osg::Cylinder(
    osg::Vec3(), 0.4f, 10.0f) );
    // The prototype of the coupling (wheel) rod
    osg::ref_ptr<osg::ShapeDrawable> wheelRodShape =
    new osg::ShapeDrawable( new osg::Cylinder(
    osg::Vec3(), 0.4f, 8.0f) );
    // The prototypes of the wheel and the car body
    osg::ref_ptr<osg::ShapeDrawable> wheelShape =
    new osg::ShapeDrawable( new osg::Cylinder(
    osg::Vec3(), 2.0f, 1.0f) );
    osg::ref_ptr<osg::ShapeDrawable> bodyShape =
    new osg::ShapeDrawable( new osg::Box(
    osg::Vec3(), 6.0f, 4.0f, 14.0f) );
    
  6. The wheels will be moved to the ends of the coupling rod:

    osg::MatrixTransform* wheel1 = createTransformNode(
    wheelShape.get(), osg::Matrix::translate(0.0f, 0.0f,-4.0f) );
    wheel1->setUpdateCallback(
    createWheelAnimation(osg::Vec3(0.0f, 0.0f,-4.0f)) );
    osg::MatrixTransform* wheel2 = createTransformNode(
    wheelShape.get(), osg::Matrix::translate(0.0f, 0.0f, 4.0f) );
    wheel2->setUpdateCallback(
    createWheelAnimation(osg::Vec3(0.0f, 0.0f, 4.0f)) );
    
  7. And the coupling rod itself will be rotated and moved to the end of the main rod too:

    osg::MatrixTransform* wheelRod1 = createTransformNode(
    wheelRodShape.get(),
    osg::Matrix::rotate(osg::Z_AXIS, osg::X_AXIS) *
    osg::Matrix::translate(0.0f, 0.0f,-5.0f) );
    wheelRod1->addChild( wheel1 );
    wheelRod1->addChild( wheel2 );
    
  8. For another coupling rod, we will directly copy from the transformation node wheelRod1. It is good to do a shallow copy here as the child wheels and animations will be shared. After moving the cloned rod to a suitable place, now we can have a complete wheel system:

    osg::MatrixTransform* wheelRod2 =
    static_cast<osg::MatrixTransform*>(
    wheelRod1->clone(osg::CopyOp::SHALLOW_COPY) );
    wheelRod2->setMatrix( osg::Matrix::rotate(osg::Z_AXIS,
    osg::X_AXIS) * osg::Matrix::translate(0.0f, 0.0f, 5.0f) );
    
  9. Finally, move the car body onto the main rod and finish the assembly work. All three parts should be added to the main rod node to make sure they are under its local coordinates:

    osg::MatrixTransform* body = createTransformNode(
    bodyShape.get(), osg::Matrix::translate(0.0f, 2.2f, 0.0f) );
    osg::MatrixTransform* mainRod = createTransformNode(
    mainRodShape.get(), osg::Matrix::identity() );
    mainRod->addChild( wheelRod1 );
    mainRod->addChild( wheelRod2 );
    mainRod->addChild( body );
    
  10. Create the root node and start the viewer now:

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( mainRod );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  11. You will see the car running in the viewer. Of course, it has no textures, doors, windows, or aerodynamic bodyworks. But, why don't you just create some beautiful models in some other software like 3dsmax, Maya, or Blender3D, and replace the basic shapes used here? Try it if you have any interest in building a better-looking scene.

How it works...

Although the result is not so exciting and refined, it should be a good example for demonstrating the use of local coordinates, as well as the basic concepts of character bones (we were working on a car's bones in the previous section).

Of course there are more ways to implement such a composite car model, and you will be able to import some beautiful models to replace our basic ones. Just try it by yourselves.

The structure of the recipe's scene graph is shown in the following diagram:

Mirroring the scene graph

Mirroring the scene graph, or in another words, putting a scene inside a mirror as the "reflection", can also be done by specifying another transformation node as the parent of the origin scene. It requires rendering everything for a second time, and can be integrated with some render-to-texture techniques to simulate real mirrors, water reflections, shadows, and some other reflective effects. The solution described here will be used again in Chapter 6 to create simple water effects.

How to do it...

The following code will be short but can be reused later in other chapters.

  1. Include necessary headers:

    #include <osg/ClipNode>
    #include <osg/MatrixTransform>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    Load a model into the scene graph first:
    osg::ArgumentParser arguments( &argc, argv );
    osg::ref_ptr<osg::Node> scene = osgDB::readNodeFiles(
    arguments );
    if ( !scene ) scene = osgDB::readNodeFile("cessna.osg");
    
  2. The next important step is to work out the mirror matrix. We are going to make a mirror of the model against the XOY plane by flipping it upside down, as well as a small translation along the Z axis too, to represent the height of the mirror. The osg::Matrix::scale() function here will put the reflected model opposite on the Z axis, and the osg::Matrix::translate() function is used to set the scale pivot point:

    float z = -10.0f;
    osg::ref_ptr<osg::MatrixTransform> reverse =
    new osg::MatrixTransform;
    reverse->preMult(osg::Matrix::translate(0.0f, 0.0f, -z) *
    osg::Matrix::scale(1.0f, 1.0f, -1.0f) *
    osg::Matrix::translate(0.0f, 0.0f, z) );
    reverse->addChild( scene.get() );
    
  3. Enable clipping to remove anything that is poking out of the mirrored graph through the mirror. It may have no effect here but will help later when we are working on the water simulation example:

    osg::ref_ptr<osg::ClipPlane> clipPlane = new osg::ClipPlane;
    clipPlane->setClipPlane( 0.0, 0.0, -1.0, z );
    clipPlane->setClipPlaneNum( 0 );
    osg::ref_ptr<osg::ClipNode> clipNode = new osg::ClipNode;
    clipNode->addClipPlane( clipPlane.get() );
    clipNode->addChild( reverse.get() );
    
  4. Now add both the origin scene and the reversed one to the root node and start the viewer:

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( scene.get() );
    root->addChild( clipNode.get() );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  5. The result is shown in the following screenshot. It may not be interesting enough at present, but you will soon find that it is the basis of some other complex effects such as water reflection and shadow implementations.

There's more...

If you have more interests in implementing an inverse scene, there are some more examples for you to understand, for instance, the NeHe OpenGL tutorials at http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=26.

The OSG source code also provides a good one using the osg::Stencil class for stencil tests. Refer to examples/osgreflect for details.

Designing a breadth-first node visitor

In graphics applications, a breadth-first-search (BFS) is a search algorithm that begins at the root node and traverses all the neighboring nodes before it goes deeper. That is different from the default behavior of the osg::NodeVisitor class, which is depth-first-search (DFS) . We will try to implement a BFS visitor in this section to show how to make changes to this basic OSG class for your own use.

How to do it...

First we have to declare a new node visitor class inherited from the osg::NodeVisitor class. The headers to be included here are:

#include <osg/NodeVisitor>
#include <deque>

Two virtual functions must be overrode to make the visitor work: One is the reset() method, which will reset all member variables to their initial states; the other one is the apply() method, which accepts osg::Node class as the input argument. All OSG nodes will be redirected to this method while traversing a scene graph, so we will place our own traverseBFS() here for the purpose of creating a BFS visitor:

class BFSVisitor : public osg::NodeVisitor
{
public:
BFSVisitor() { setVisitorType(TRAVERSE_ALL_CHILDREN); }
virtual void reset() { _pendingNodes.clear(); }
virtual void apply( osg::Node& node ) { traverseBFS(node); }
protected:
virtual ~BFSVisitor() {}
void traverseBFS( osg::Node& node );
std::deque<osg::Node*> _pendingNodes;
};

The new traversal mechanism of scene graph is implemented as follows. We will find all the child nodes and push them into a queue, and then handle the queue from the first. Actually the queue can be treated as a FIFO (first in, first out) pipe. Neighboring nodes will be explored and handled together, and nodes at a lower level should always wait until nodes at higher levels are finished, and so on:

void BFSVisitor::traverseBFS( osg::Node& node )
{
osg::Group* group = node.asGroup();
if ( !group ) return;
for ( unsigned int i=0; i<group->getNumChildren(); ++i )
{
_pendingNodes.push_back( group->getChild(i) );
}
while ( _pendingNodes.size()>0 )
{
osg::Node* node = _pendingNodes.front();
_pendingNodes.pop_front();
node->accept(*this);
}
}

Now we can use the BFSVisitor class in practical work.

  1. First include other necessary headers:

    #include <osgDB/ReadFile>
    #include <osgUtil/PrintVisitor>
    #include <iostream>
    
  2. We would like to print each node's class name while traversing the scene graph:

    class BFSPrintVisitor : public BFSVisitor
    {
    public:
    virtual void apply( osg::Node& node )
    {
    std::cout << node.libraryName() << "::"
    <<node.className() << std::endl;
    traverseBFS(node);
    }
    };
    
  3. In the main entry, let us read a model from file first:

    osg::ArgumentParser arguments( &argc, argv );
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles( arguments );
    if ( !root ) root = osgDB::readNodeFile("osgcool.osg");
    
  4. The osgUtil::PrintVisitor class is used here to show the traversal sequence of DFS visitors:

    std::cout << "DFS Visitor traversal: " << std::endl;
    osgUtil::PrintVisitor pv( std::cout );
    root->accept( pv );
    std::cout << std::endl;
    
  5. Now use the new BFS visitor to print the node information. You may have to start a terminal and run the program in text mode:

    std::cout << "BFS Visitor traversal: " << std::endl;
    BFSPrintVisitor bpv;
    root->accept( bpv );
    return 0;
    
  6. The comparative result is listed as follows. You can easily find differences between DFS and BFS visitors.

There's more...

The breadth-first searching can be used to find the shortest path between two nodes, or implement some other algorithms. In a word, it is not suitable for the updating and rendering processes, which encapsulate the OpenGL state transitions and local-to-world transformations in a tree-like structure. The depth-first solution, which goes as far as possible along each branch and then backtracks, is still preferred for most scene graph visitors, such as the osg::NodeVisitor class.

Some more information about breadth-first and depth-first searching can be found at the following links:

http://en.wikipedia.org/wiki/Breadth-first_search

http://en.wikipedia.org/wiki/Depth-first_search

Implementing a background image node

Maybe you have also tried to implement a background image before but failed. The difficulty here is that if you ever try to use an HUD system to apply a background image, which will always be rendered after the main scene, it's difficult to deal with the depth buffer values set by the main scene. Fortunately, in this recipe, we have a solution for this problem using the depth tests.

How to do it...

Specify any image as the background image and load an arbitrary scene, and check if it is displayed before the background to verify the correctness of our solution.

  1. Include necessary headers:

    #include <osg/Geometry>
    #include <osg/Geode>
    #include <osg/Depth>
    #include <osg/Texture2D>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. Load the background image and map it to a quadrangle geometry:

    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    osg::ref_ptr<osg::Image> image = osgDB::readImageFile(
    "Images/osg256.png" );
    texture->setImage( image.get() );
    osg::ref_ptr<osg::Drawable> quad =
    osg::createTexturedQuadGeometry( osg::Vec3(),
    osg::Vec3(1.0f, 0.0f, 0.0f), osg::Vec3(0.0f, 1.0f, 0.0f) );
    quad->getOrCreateStateSet()->setTextureAttributeAndModes(
    0, texture.get() );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( quad.get() );
    
  3. Prepare an HUD camera for the background image. It must completely fill the screen so we have to use orthogonal projection here. Some other important points here include disabling culling on the camera and setting the clear mask to 0. That is because the background should never be culled, and it should neither affect color nor depth buffer generated by the main scene.

    osg::ref_ptr<osg::Camera> camera = new osg::Camera;
    camera->setCullingActive( false );
    camera->setClearMask( 0 );
    camera->setAllowEventFocus( false );
    camera->setReferenceFrame( osg::Transform::ABSOLUTE_RF );
    camera->setRenderOrder( osg::Camera::POST_RENDER );
    camera->setProjectionMatrix( osg::Matrix::ortho2D(
    0.0, 1.0, 0.0, 1.0) );
    camera->addChild( geode.get() );
    
  4. Prevent the background from being affected by the light, and set up the depth test values. We will explain the reason later:

    osg::StateSet* ss = camera->getOrCreateStateSet();
    ss->setMode( GL_LIGHTING, osg::StateAttribute::OFF );
    ss->setAttributeAndModes( new osg::Depth(
    osg::Depth::LEQUAL, 1.0, 1.0) );
    
  5. Now add the background camera and any other scene to the root node and see what we have now:

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( camera.get() );
    root->addChild( osgDB::readNodeFile("cessna.osg") );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  6. Everything seems to work well, as shown in the following screenshot. Believe it or not, the most important line in the program is the addition of the state attribute osg::Depth here. Try hiding it and see what the difference is.

How it works...

The key of the implementation of background images can be concentrated into one line, that is, re-map the depth values of the background image to [1.0, 1.0].

This ensures that each depth value of the post-rendered background is 1.0, and it won't pass the depth test unless the original depth value is equal or greater than 1.0 (the latter is certainly impossible), as shown in the following code segment:

setAttributeAndModes( new osg::Depth(osg::Depth::LEQUAL,
1.0, 1.0) );

So when will the original depth value be 1.0? The answer is obvious: It happens when there is nothing displayed! And the real meaning of a "background" is exactly what needs to be shown when there is no other scene object. So we have just finished the work in a perfect way now.

Making your node always face the screen

Make something face the screen? Yes, this is exactly what the osg::Billboard class has done for you, and the osgText::Text class has a similar feature that rotates the text to screen automatically. But this time we will work on a node, and show how to alter transformation nodes according to the global model-view matrix. The method used here can also be extended to implement other small functionalities, for instance, to show small XYZ axes for reference in a model editor window, or a front sight following the mouse in a shooting game.

How to do it...

This recipe will be simple enough for reading and understanding. But the usage of cull callbacks here may also help in the following chapters to implement some complex examples. Just keep it in mind or place a bookmark if you can.

  1. Include necessary headers:

    #include <osg/MatrixTransform>
    #include <osgDB/ReadFile>
    #include <osgUtil/CullVisitor>
    #include <osgViewer/Viewer>
    
  2. Declare a node callback and we will change the transformation matrix of specified node to make sure it is always facing the screen, which is actually a billboard node's behavior:

    class BillboardCallback : public osg::NodeCallback
    {
    public:
    BillboardCallback( osg::MatrixTransform* billboard )
    : _billboardNode(billboard) {}
    virtual void operator()( osg::Node* node, osg::NodeVisitor* nv )
    {
    ...
    }
    protected:
    osg::observer_ptr<osg::MatrixTransform> _billboardNode;
    };
    
  3. In the operator() implementation, first be careful of the dynamic type casting. We are trying to convert the input-node visitor pointer to an osgUtil::CullVisitor object. It can only be retrieved in the cull traversal.

    osgUtil::CullVisitor* cv =
    dynamic_cast<osgUtil::CullVisitor*>(nv);
    if ( _billboardNode.valid() && cv )
    {
    osg::Vec3d translation, scale;
    osg::Quat rotation, so;
    cv->getModelViewMatrix()->decompose( translation, rotation,
    scale, so );
    osg::Matrixd matrix( rotation.inverse() );
    _billboardNode->setMatrix( matrix );
    }
    traverse( node, nv );
    

    This code segment decomposes the matrix into translation, rotation, scale vector, and scale orientation.

  4. To make a node face the screen all the time, all we should do is remove the rotation component from the model-view matrix applying on it. That is why we set the inverse rotation matrix here. And this and the previous rotation component will cancel each other out during the matrix multiplication process.

  5. In the main entry, load the Cessna model and add it to a transformation node, which will only accept the inverse rotation matrix:

    osg::ref_ptr<osg::MatrixTransform> billboardNode =
    new osg::MatrixTransform;
    billboardNode->addChild( osgDB::readNodeFile("cessna.osg") );
    
  6. Add the billboard node and a terrain model for reference to the root node:

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( billboardNode.get() );
    root->addChild( osgDB::readNodeFile("lz.osg") );
    root->addCullCallback(
    new BillboardCallback(billboardNode.get()) );
    

    Later we will explain the reason we put the BillboardCallback on the root node, rather than the billboard node itself.

  7. Start the viewer:

    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  8. The Cessna is still at the right place and has correct hiding relations with the terrain. But you will soon find that you can see only one side of the Cessna, as if it is 2D. That is to say, the Cessna is facing the screen now, as shown in the following screenshot:

How it works...

The difference between adding the callback to the root node and to billboardNode is the priority order of setting matrix and applying matrix. Let us look at the first case; when the callback is set to root, it will be executed when the cull visitor reaches the root node and call the setMatrix() method of the transformation node billboardNode. After that, when the cull visitor is traversing the node billboardNode, the transformation matrix will be applied to the billboard and become effective during the rendering. It leads to the corrected orientation (facing the screen) of the node.

But if we set the callback to the node billboardNode directly, some problems may appear. The newly set matrix won't work immediately in a cull callback. So the new orientation value can only take effect in the next frame. In fact, this will cause the model to twinkle, and thus lead to unexpected results.

There's more...

There are several types of visitors that may traverse the scene graph and trigger a callback. You may obtain them by dynamic type casting in the traverse() method of your custom node, or in the operator() of callbacks. The following table shows these node visitors, type enumerations (can be obtained by calling the getVisitorType() method), and descriptions:

Visitor

Type enumeration

Related callback

Description

osgGA:: EventVisitor

EVENT_VISITOR

setEvent Callback()

The event visitor

osgUtil:: UpdateVisitor

UPDATE_VISITOR

setUpdate Callback()

The update visitor

osgUtil:: CullVisitor

CULL_VISITOR

setCull Callback()

The cull visitor

osgUtil:: GLObjectsVisitor

NODE_VISITOR

None

The visitor for compiling OpenGL objects

osg:: CollectOccludersVisitor

COLLECT_OCCLUDER_VISITOR

None

The visitor for collecting culling occluders

osgUtil:: IntersectionVisitor

NODE_VISITOR

None

The visitor for intersections

For other implementations of billboards, see the declarations of the osg::Billboard and osg::AutoTransform classes. And there are some related examples at examples/osgforest and examples/osgautotransform in the OSG source code too.

Using draw callbacks to execute NVIDIA Cg functions

The Cg language (C for Graphics) is a high-level shading language developed by NVIDIA. It is suitable for GPU programming and can support both the DirectX (HLSL) and OpenGL (GLSL) shader programs. It is widely used in modern PC games and 3D applications.

Of course, although we can't make use of any HLSL features of the Cg language, it is still worth integrating it with the OSG functionalities. Before considering using shader parameters, parameter buffers, CgFX, and other advanced Cg features, we could first attempt to run some very easy Cg programs. This time we will use osg::Camera's draw callbacks for such purposes.

Getting ready

Review and download the Cg toolkit at the NVIDIA website first. It supports Linux, Mac OS X, and Windows systems too.

http://developer.nvidia.com/cg-toolkit

We don't have space to introduce the Cg grammar and example code here. You can check out some tutorials on the Internet.

The CMake script of your program should be modified to find Cg include directory and libraries. The following code segment should be an easy-to-read example here:

FIND_PATH(CG_INCLUDE_PATH Cg/cg.h)
FIND_LIBRARY(CG_GL_LIBRARY CgGL)
FIND_LIBRARY(CG_LIBRARY Cg)
INCLUDE_DIRECTORIES(${CG_INCLUDE_PATH })
TARGET_LINK_LIBRARIES(${EXAMPLE_NAME}
${CG_LIBRARY} ${CG_GL_LIBRARY})

How to do it...

Now let us first create the draw callbacks for rendering with Cg program states.

  1. Include necessary headers and start to construct some classes for integrating Cg shading features:

    #include <Cg/cg.h>
    #include <Cg/cgGL.h>
    #include <osg/Camera>
    
  2. The most important steps when using Cg programs and profiles are to enable them before actual drawing, and disable them after; this can enable specific shaders to work before real-drawing operations, and disable them after to make sure they won't affect other processing steps. To implement this with camera callbacks, you have to design a pre-drawing and a post-drawing callback, both with the same Cg variables. Therefore, we can just have a base callback class which manages a list of CGprofile and CGprogram objects:

    class CgDrawCallback : public osg::Camera::DrawCallback
    {
    public:
    void addProfile( CGprofile profile ) {
    _profiles.push_back(profile); }
    void addCompiledProgram( CGprogram prog ) {
    _programs.push_back(prog); }
    protected:
    std::vector<CGprofile> _profiles;
    std::vector<CGprogram> _programs;
    };
    
  3. The CgStartDrawCallback and CgEndDrawCallback classes will have different behaviors while handling the same Cg objects. Note that the CgStartDrawCallback class has an extra _initialized variable to help initialize the programs the first time it is executed:

    class CgStartDrawCallback : public CgDrawCallback
    {
    public:
    CgStartDrawCallback() : _initialized(false) {}
    virtual void operator()( osg::RenderInfo&
    renderInfo ) const;
    protected:
    mutable bool _initialized;
    };
    class CgEndDrawCallback : public CgDrawCallback
    {
    public:
    virtual void operator()( osg::RenderInfo&
    renderInfo ) const;
    };
    
  4. Here is the implementation of these two drawing callbacks. The two operator() methods here will be executed just before and after the drawing process of the camera's children:

    void CgStartDrawCallback::operator()( osg::RenderInfo&
    renderInfo ) const
    {
    if ( !_initialized )
    {
    // Load all Cg shader programs
    for ( unsigned int i=0; i<_programs.size(); ++i )
    cgGLLoadProgram( _programs[i] );
    _initialized = true;
    }
    // Bind the programs to current graphics context
    for ( unsigned int i=0; i<_programs.size(); ++i )
    cgGLBindProgram( _programs[i] );
    // Enable Cg profiles to work under specified devices
    for ( unsigned int i=0; i<_profiles.size(); ++i )
    cgGLEnableProfile( _profiles[i] );
    }
    void CgEndDrawCallback::operator()( osg::RenderInfo&
    renderInfo ) const
    {
    // Disable profiles after the drawing
    for ( unsigned int i=0; i<_profiles.size(); ++i )
    cgGLDisableProfile( _profiles[i] );
    }
    
  5. After finishing the callback classes, now it's time to create a small example using OSG and NVIDIA Cg. First let us include the headers and create a very simple Cg program rendering vertex normal as final pixel colors:

    static const char* cgProgramCode = {
    "struct app_input {\n"
    "float4 vertex : POSITION;\n"
    "float4 normal : NORMAL;\n"
    "};\n"
    "struct vertex_to_fragment {\n"
    "float4 position : POSITION;\n"
    "float3 normal3 : TEXCOORD0;\n"
    "};\n"
    "vertex_to_fragment vertex_main(app_input input)\n"
    "{\n"
    "vertex_to_fragment output;\n"
    "output.position = mul(glstate.matrix.mvp,
    input.vertex);\n"
    "output.normal3 = input.normal.xyz;\n"
    "return output;\n"
    "}\n"
    "float4 fragment_main(vertex_to_fragment input) : COLOR\n"
    "{\n"
    "float4 output = float4(input.normal3.x, input.normal3.y,
    input.normal3.z, 1.0);\n"
    "return output;\n"
    "}\n"
    };
    
  6. The Cg context must be global, and we will set up an error callback for any Cg-related problems:

    CGcontext g_context;
    void error_callback()
    {
    CGerror lastError = static_cast<CGerror>( cgGetError() );
    OSG_WARN << "Cg error: " << cgGetErrorString(lastError)
    << std::endl;
    if ( lastError == CG_COMPILER_ERROR )
    OSG_WARN << std::string(cgGetLastListing(g_context))
    << std::endl;
    }
    
  7. In the main entry, first we load a model and allocate the two callbacks:

    osg::ArgumentParser arguments( &argc, argv );
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(
    arguments );
    if ( !root ) root = osgDB::readNodeFile( "cow.osg" );
    osg::ref_ptr<CgStartDrawCallback> preCB =
    new CgStartDrawCallback;
    osg::ref_ptr<CgEndDrawCallback> postCB =
    new CgEndDrawCallback;
    
  8. Initialize the viewer, and what is more important, initialize the graphics context by calling the setUpViewInWindow() method:

    osgViewer::Viewer viewer;
    viewer.getCamera()->setPreDrawCallback( preCB.get() );
    viewer.getCamera()->setPostDrawCallback( postCB.get() );
    viewer.setSceneData( root.get() );
    viewer.setUpViewInWindow( 100, 100, 800, 600 );
    
  9. Initialize Cg variables before adding them to the callback objects. Since the initialization process requires OpenGL context to be created and made current, we must get the graphics context used in the current camera and set up the internal OpenGL rendering context. Now you will understand why we should initialize the graphics context before:

    CGprofile vertProfile, fragProfile;
    CGprogram vertProg, fragProg;
    osg::GraphicsContext* gc =
    viewer.getCamera()->getGraphicsContext();
    if ( gc )
    {
    gc->realize();
    gc->makeCurrent();
    g_context = cgCreateContext();
    cgSetErrorCallback( error_callback );
    vertProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
    vertProg = cgCreateProgram(
    g_context, CG_SOURCE, cgProgramCode, vertProfile,
    "vertex_main", NULL );
    fragProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
    fragProg = cgCreateProgram(
    g_context, CG_SOURCE, cgProgramCode, fragProfile,
    "fragment_main", NULL );
    gc->releaseContext();
    }
    
  10. Add the initialized variables to the callbacks and start the viewer:

    preCB->addProfile( vertProfile );
    preCB->addProfile( fragProfile );
    preCB->addCompiledProgram( vertProg );
    preCB->addCompiledProgram( fragProg );
    postCB->addProfile( vertProfile );
    postCB->addProfile( fragProfile );
    postCB->addCompiledProgram( vertProg );
    postCB->addCompiledProgram( fragProg );
    viewer.run();
    
  11. Lastly, don't forget to release allocated Cg variables:

    if ( gc )
    {
    cgDestroyProgram( vertProg );
    cgDestroyProgram( fragProg );
    cgDestroyContext( g_context );
    }
    return 0;
    
  12. OK, now what is the feeling of successfully integrating another shading language into OSG? If you are familiar with the Cg language, just try some other shaders and see if they could also work for you.

How it works...

A remarkable feature of this recipe is that it forces the construction of the graphics context and uses it to specify the OpenGL device and execute commands. You might remember there is a createGraphicsContext() method that creates new contexts according to user-specified traits. Yes, it could work here too. And the setUpViewInWindow() method actually executes this function internally with an auto-configured traits object.

There are some other setUpView*() methods, all of which can build a graphics context of different behaviors for use. You are able to retrieve the osg::GraphicsContext object and use it to execute OpenGL calls before the simulation starts.

So there are at least three ways to integrate OpenGL commands and libraries based on OpenGL with OSG now. The first is to derive and customize the osg::Drawable class. The second one is the pre-draw and post-draw callbacks defined in the osg::Camera class, which can manage some external states of child nodes but may cause OpenGL command coupling sometimes. The last one, that makes use of the rendering context directly, is applicable when you are going to make some initializations or tests; but can also cause serious threading problems in the multi-threaded mode because the same context may have to be used by other OSG graphic objects simultaneously.

Integration with other libraries is an interesting topic, so it will be mentioned again in other chapters. Try to find the pros and cons of the three methods discussed above by learning this and the following recipes, but use them at your own risk in different applications.

There's more...

Another good integration of OSG and NVIDIA Cg can be found in the third-party osgXI project (http://sourceforge.net/projects/osgxi/). Its osgCg module now supports Cg and CgFX by accepting them as state attributes.

Last but not least, to learn more about NVIDIA Cg, the free Cg tutorial is always preferred for reading, and can be found at http://developer.nvidia.com/object/cg_tutorial_home.html.

Implementing a compass node

Now, here comes the last recipe in this chapter, and we could do something really interesting this time. We will try to implement a compass and use it in a simple earth scene. A compass can help us identify the directions in a 3D world. And as far as we know, it makes our applications look professional and useful, if we are working on some 3D geographic information systems (GIS) or computer games.

How to do it...

  1. Declare the Compass class. It contains a transformable dial plate and a needle. The orientation will be read and computed from the current view matrix of the main scene camera, which should be set before the simulation starts:

    class Compass : public osg::Camera
    {
    public:
    Compass();
    Compass( const Compass& copy, osg::CopyOp
    copyop=osg::CopyOp::SHALLOW_COPY );
    META_Node( osg, Compass );
    void setPlate( osg::MatrixTransform* plate ) {
    _plateTransform = plate; }
    void setNeedle( osg::MatrixTransform* needle ) {
    _needleTransform = needle; }
    void setMainCamera( osg::Camera* camera ) {
    _mainCamera = camera; }
    virtual void traverse( osg::NodeVisitor& nv );
    protected:
    virtual ~Compass();
    osg::ref_ptr<osg::MatrixTransform> _plateTransform;
    osg::ref_ptr<osg::MatrixTransform> _needleTransform;
    osg::observer_ptr<osg::Camera> _mainCamera;
    };
    
  2. Implement the copy constructor of the Compass class. Without a copy constructor, you will not be able to use the META_Node macro to define the standard node methods:

    Compass::Compass( const Compass& copy, osg::CopyOp copyop ):
    osg::Camera(copy, copyop),
    _plateTransform(copy._plateTransform),
    _needleTransform(copy._needleTransform),
    _mainCamera(copy._mainCamera)
    {
    }
    
  3. The traverse() method will be called during the event, update, and cull traversals of the entire scene graph in every frame. Override it and we will have custom behaviors for own node types.

  4. For the compass, we have to compute the angle between the present viewer orientation and the north vector (the earth's geographic pole), and rotate the needle or plate node to align itself. Here we will read the current view matrix from the main camera and move the plate to fit it. This can be done during the cull traversal as there are few factors affecting the viewer's position and direction:

    void Compass::traverse( osg::NodeVisitor& nv )
    {
    if ( _mainCamera.valid() &&
    nv.getVisitorType()==osg::NodeVisitor::CULL_VISITOR )
    {
    osg::Matrix matrix = _mainCamera->getViewMatrix();
    matrix.setTrans( osg::Vec3() );
    osg::Vec3 northVec = osg::Z_AXIS * matrix;
    northVec.z() = 0.0f;
    northVec.normalize();
    osg::Vec3 axis = osg::Y_AXIS ^ northVec;
    float angle = atan2(axis.length(), osg::Y_AXIS*northVec);
    axis.normalize();
    if ( _plateTransform.valid() )
    _plateTransform->setMatrix( osg::Matrix::rotate(
    angle, axis) );
    }
    _plateTransform->accept( nv );
    _needleTransform->accept( nv );
    osg::Camera::traverse( nv );
    }
    
  5. Later we will explain why we directly call accept() here, and why the _plateTransform and _needleTransform nodes are never added as the compass' children.

    The compass class can be used in any applications now. Let's try it now.

  6. First there are header files for use:

    #include <osg/ShapeDrawable>
    #include <osg/MatrixTransform>
    #include <osg/Texture2D>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  7. You may have many ways to design your own compass needle and plate. But in this recipe, we will choose to use textured quads. Create a needle image with transparent background, superimpose it onto the plate image, and the result will be nice enough for our case (a 2D compass). The example images are shown in the following diagram:

  8. Create a function for the needle or plate node. The height parameter is for computing the Z-order of these two components:

    osg::MatrixTransform* createCompassPart( const std::string&
    image, float radius, float height )
    {
    osg::Vec3 center(-radius, -radius, height);
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable(
    createTexturedQuadGeometry(center, osg::Vec3(radius*2.0f,0.0f,0.0f),
    osg::Vec3(0.0f,radius*2.0f,0.0f)) );
    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    texture->setImage( osgDB::readImageFile(image) );
    osg::ref_ptr<osg::MatrixTransform> part =
    new osg::MatrixTransform;
    part->getOrCreateStateSet()->setTextureAttributeAndModes(
    0, texture.get() );
    part->getOrCreateStateSet()->setRenderingHint(
    osg::StateSet::TRANSPARENT_BIN );
    part->addChild( geode.get() );
    return part.release();
    }
    
  9. Create a demo earth model:

    osg::Geode* createEarth( const std::string& filename )
    {
    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    texture->setImage( osgDB::readImageFile(filename) );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( new osg::ShapeDrawable(
    new osg::Sphere(osg::Vec3(), osg::WGS_84_RADIUS_POLAR)) );
    geode->getOrCreateStateSet()->setTextureAttributeAndModes(
    0, texture.get() );
    return geode.release();
    }
    
  10. In the main entry, create the viewer and attach the main camera to the compass, which is shown orthographic:

    osgViewer::Viewer viewer;
    osg::ref_ptr<Compass> compass = new Compass;
    compass->setMainCamera( viewer.getCamera() );
    compass->setViewport( 0.0, 0.0, 200.0, 200.0 );
    compass->setProjectionMatrix( osg::Matrixd::ortho(
    -1.5, 1.5, -1.5, 1.5, -10.0, 10.0) );
    
  11. Add the plate and the needle images to the compass node. The needle must appear on top of the plate, so it has a larger height value here:

    compass->setPlate( createCompassPart("compass_plate.png",
    1.5f, -1.0f) );
    compass->setNeedle( createCompassPart("compass_needle.png",
    1.5f, 0.0f) );
    
  12. The 2D compass is in fact a HUD camera. The following code defines its basic behaviors:

    compass->setRenderOrder( osg::Camera::POST_RENDER );
    compass->setClearMask( GL_DEPTH_BUFFER_BIT );
    compass->setAllowEventFocus( false );
    compass->setReferenceFrame( osg::Transform::ABSOLUTE_RF );
    compass->getOrCreateStateSet()->setMode( GL_LIGHTING,
    osg::StateAttribute::OFF );
    compass->getOrCreateStateSet()->setMode( GL_BLEND,
    osg::StateAttribute::ON );
    
  13. Add the earth and the compass to the root node and start the viewer. The earth image file can be found in the OSG sample dataset.

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(
    createEarth("Images/land_shallow_topo_2048.jpg") );
    root->addChild( compass.get() );
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  14. You may start navigating the scene and don't worry about determining the direction in the virtual world. Of course, if you really get lost inside a complex 3D scene one day, maybe quitting the program and restarting will be easier.

How it works...

In this recipe, the north vector is defined as the Z axis in the world coordinates. All we have to do here is figure out how the compass' magnetized needle is pulled towards the North Pole, and rotate the transformation nodes (_plateTransform or _needleTransform) correspondingly. Watch the Google Earth application carefully and you will see that the compass plate is rotating while you look around.

Imagine you are facing the true north in our simple 3D world, your compass needle should point to the top of the screen at that time, that is, actually the positive Y axis. So if there are any orientation changes, it can just be considered as the angle difference between the Y axis in the eye coordinates and the computed earth's north vector in eye coordinates.

Transform the world north vector with the view matrix, regardless of the position offset. After that, calculate the rotation axis (cross product of the Y axis and the north vector, as they are both in the view coordinate system) and angle, and apply them to the needle or plate node at your own discretion.

osg::Vec3 axis = osg::Y_AXIS ^ northVec;
float angle = atan2(axis.length(), osg::Y_AXIS*northVec);
axis.normalize();

Another question you may have if you have already read the example source code is: Why didn't we add the needle and plate nodes to the compass, and how could they still work without being considered as children? Good question! And if you have ever read the implementation of the osg::Group class, you may have found out the answer yourself:

void Group::traverse(NodeVisitor& nv)
{
for(NodeList::iterator itr=_children.begin();
itr!=_children.end(); ++itr)
{
(*itr)->accept(nv);
}
}

While calling the traverse() method of its super class, the Compass class (and other classes derived from osg::Group) will actually iterate each child and call the accept() method on them, to make the traversal continue. But here, the work is done by directly calling the accept() method on the transformation nodes _plateTransform and _needleTransform. It means that these two nodes will be traversed as if they were children of the compass. This sometimes brings flexibility.

Note

But be careful, without being added to the scene graph, a node will lose some functionalities such as contributing to the bounding box computation and executing update callbacks applied on it.

Note that the osg::Camera class doesn't override the traverse() method; it simply calls the osg::Group's traverse() method. That is why our strategy works here. And of course, everything would work well if you decide to add the needle and plate as children of the compass.