Chapter 5. Animating Everything

In this chapter, we will cover:

  • Opening and closing doors

  • Playing a movie in the 3D world

  • Designing scrolling text

  • Implementing morph geometry

  • Fading in and out

  • Animating a flight on fire

  • Dynamically lighting within shaders

  • Creating a simple Galaxian game

  • Building a skeleton system

  • Skinning a customized mesh

  • Letting the physics engine be

Introduction

Computer animation means to generate moving images and render them on the screen one after another. The animated images make the viewers think that they are seeing smoothly moving objects. There must be at least 12 frames drawn per second to trick the human brain. And over 60 frames per second will create a perfect animating process.

Typical animation types in OSG include path animation (changing position, rotation, and scale factor of an object), texture animation (dynamically updating textures), morph animation (blending shapes and making changes per vertex), state animation (changing rendering states), particle animation, and skeletal animation (representing characters). The osgAnimation library provides a complete framework for handling different kinds of animations. And the osgParticle library makes use of another flexible framework to design and render particles. We will introduce both in different recipes. You may read the book "OpenSceneGraph 3.0: Beginner's Guide", Rui Wang and Xuelei Qian, Packt Publishing, for more information.

In this chapter, we will also integrate a famous physics engine into our OSG applications to improve the program's performance and support real-physics features of rigid bodies. And for game developers and lovers, we provide another simple but complete example that imitates the classic Galaxian.

Opening and closing doors

Opening and closing doors is a very common action in both daily life and computer games. You click with your mouse and slide the door open, and then you may meet either a girl or a monster behind it. Doors and entrances are also important in some spatial index algorithms such as the PVS (potentially visible set, refer to http://en.wikipedia.org/wiki/Potentially_visible_set). But in this recipe, we will only discuss how to animate the door object by applying actions to it.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/ShapeDrawable>
    #include <osg/MatrixTransform>
    #include <osgAnimation/BasicAnimationManager>
    #include <osgAnimation/UpdateMatrixTransform>
    #include <osgAnimation/StackedRotateAxisElement>
    #include <osgViewer/Viewer>
    #include <algorithm>
    
  2. Create the wall geometry which clamps the door:

    osg::Node* createWall()
    {
    osg::ref_ptr<osg::ShapeDrawable> wallLeft =
    new osg::ShapeDrawable( new osg::Box(osg::Vec3(-5.5f,
    0.0f, 0.0f), 10.0f, 0.3f, 10.0f) );
    osg::ref_ptr<osg::ShapeDrawable> wallRight =
    new osg::ShapeDrawable( new osg::Box(osg::Vec3(10.5f,
    0.0f, 0.0f), 10.0f, 0.3f, 10.0f) );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( wallLeft.get() );
    geode->addDrawable( wallRight.get() );
    return geode.release();
    }
    
    
  3. Create the door geometry with a slightly different color:

    osg::MatrixTransform* createDoor()
    {
    osg::ref_ptr<osg::ShapeDrawable> doorShape =
    new osg::ShapeDrawable( new osg::Box(osg::Vec3(2.5f,
    0.0f, 0.0f), 6.0f, 0.2f, 10.0f) );
    doorShape->setColor( osg::Vec4(1.0f, 1.0f, 0.8f, 1.0f) );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( doorShape.get() );
    osg::ref_ptr<osg::MatrixTransform> trans =
    new osg::MatrixTransform;
    trans->addChild( geode.get() );
    return trans.release();
    }
    
  4. The next function will compute the door opening or closing animation according to the closed parameter:

    void generateDoorKeyframes( osgAnimation::FloatLinearChannel*
    ch, bool closed )
    {
    osgAnimation::FloatKeyframeContainer* kfs =
    ch->getOrCreateSampler()->getOrCreateKeyframeContainer();
    kfs->clear();
    if ( closed )
    {
    kfs->push_back( osgAnimation::FloatKeyframe(0.0, 0.0f) );
    kfs->push_back( osgAnimation::FloatKeyframe(1.0, osg::PI_2) );
    }
    else
    {
    kfs->push_back( osgAnimation::FloatKeyframe(0.0, osg::PI_2) );
    kfs->push_back( osgAnimation::FloatKeyframe(1.0, 0.0f) );
    }
    }
    
  5. The OpenDoorHandler class receives user-click actions and checks if there is an intersection between the mouse coordinates and the door geometry. If so, it will generate opening/closing door animations and fill them into the keyframe container.

    class OpenDoorHandler : public osgCookBook::PickHandler
    {
    public:
    OpenDoorHandler() : _closed(true) {}
    virtual void doUserOperations(
    osgUtil::LineSegmentIntersector::Intersection& result )
    {
    osg::NodePath::iterator itr = std::find(
    result.nodePath.begin(), result.nodePath.end(),
    _door.get() );
    if ( itr!=result.nodePath.end() )
    {
    if ( _manager->isPlaying(_animation.get()) )
    return;
    osgAnimation::FloatLinearChannel* ch =
    dynamic_cast<osgAnimation::FloatLinearChannel*>(
    _animation->getChannels().front().get() );
    if ( ch )
    {
    generateDoorKeyframes( ch, _closed );
    _closed = !_closed;
    }
    _manager->playAnimation( _animation.get() );
    }
    }
    osg::observer_ptr<osgAnimation::BasicAnimationManager>
    _manager;
    osg::observer_ptr<osgAnimation::Animation> _animation;
    osg::observer_ptr<osg::MatrixTransform> _door;
    bool _closed;
    };
    
  6. In the main entry, we will create an animation channel which handles the pivoting animation along one axis. Each of its keyframes requires only one value that indicates the rotation in radians. The door animation will not be repeated.

    osg::ref_ptr<osgAnimation::FloatLinearChannel> ch =
    new osgAnimation::FloatLinearChannel;
    ch->setName( "euler" );
    ch->setTargetName( "DoorAnimCallback" );
    generateDoorKeyframes( ch.get(), true );
    osg::ref_ptr<osgAnimation::Animation> animation =
    new osgAnimation::Animation;
    animation->setPlayMode( osgAnimation::Animation::ONCE );
    animation->addChannel( ch.get() );
    
  7. We have to also add an updater to the door node to make it recognize the animation channel we set just now. It has the same name as the channel's target name, and has a stacked rotating parameter which records the pivot axis and the initial value.

    osg::ref_ptr<osgAnimation::UpdateMatrixTransform> updater =
    new osgAnimation::UpdateMatrixTransform(
    "DoorAnimCallback");
    updater->getStackedTransforms().push_back(
    new osgAnimation::StackedRotateAxisElement(
    "euler", osg::Z_AXIS, 0.0) );
    
  8. Add the animation to the manager.

    osg::ref_ptr<osgAnimation::BasicAnimationManager> manager =
    new osgAnimation::BasicAnimationManager;
    manager->registerAnimation( animation.get() );
    
  9. Create the scene graph with the updater and the manager set as node callbacks.

    osg::MatrixTransform* animDoor = createDoor();
    animDoor->setUpdateCallback( updater.get() );
    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( createWall() );
    root->addChild( animDoor );
    root->setUpdateCallback( manager.get() );
    
  10. Configure the handler and start the viewer.

    osg::ref_ptr<OpenDoorHandler> handler = new OpenDoorHandler;
    handler->_manager = manager.get();
    handler->_animation = animation.get();
    handler->_door = animDoor;
    osgViewer::Viewer viewer;
    viewer.addEventHandler( handler.get() );
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  11. Press Ctrl and click on the door with your mouse. Do you seen it opening smoothly? Now press Ctrl and click on it again to execute the closing animation. Reopen and reclose it more times if you wish.

How to do it...

How it works...

There are several important roles for an animation system to operate properly. The manager callback (osgAnimation::BasicAnimationManager and others) manages animation data including different types of channels and keyframes, it must be set to the root node to handle animations on all child nodes. The updaters (osgAnimation::UpdateMatrixTransform and others) must be set as callbacks of animating nodes. It can be connected with one or more channels that set it as the animation target. And its stacked elements, which represent different animating key types, must be matched to channels with the same name to read and use associated keyframes.

Compared with other 3D modelling and animating software like Autodesk 3dsmax, OSG's channels can be treated as tracks with keyframe data and interpolators, and stacked elements are in fact key filters which define animatable key types (position, rotation, and so on). In this example, we only construct one'euler' channel which is associated with the updater'DoorAnimCallback'. The stacked element'euler' is pushed into the updater to enable rotation along one Euler axis.

There's more...

osgAnimation presently supports five types of stacked elements, all of which are transformable elements. Material and morph updaters (the latter will be introduced in the fourth recipe of this chapter) don't need stacked elements at present. The stacked elements and their associatable channel types are listed in the following table:

Stacked element

Channel type

Key type

StackedMatrixElement

MatrixLinearChannel

osg::Matrix

StackedQuaternionElement

QuatSphericalLinearChannel

osg::Quat

StackedRotateAxisElement

FloatLinearChannel

float (along specific axis)

StackedScaleElement

Vec3LinearChannel

osg::Vec3

StackedTranslateElement

Vec3LinearChannel

osg::Vec3

Playing a movie in the 3D world

Have you ever dreamed of watching a movie in the 3D environment? We may consider creating a virtual cinema and put the movie on a big quad, a hemisphere, or some other irregular screens. The movie picture will be treated as the texture and mapped to the geometry mesh. OSG helps us handle the animating of the texture in an effective mode, that is, the osg::ImageStream class.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/ImageStream>
    #include <osg/Geometry>
    #include <osg/Geode>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. We provide two ways to show animated images here: one is to read an image sequence from the disk, another is to load frames from the webcam. The osgdb_ffmpeg plugin will do the low-level work for us here. But we have to first ensure that the webcam is the first reachable video-input device under Windows/Linux in this recipe.

    osg::ArgumentParser arguments( &argc, argv );
    osg::ref_ptr<osg::Image> image;
    if ( arguments.argc()>1 )
    image = osgDB::readImageFile( arguments[1] );
    else
    {
    #ifdef WIN32
    image = osgDB::readImageFile( "0.ffmpeg",
    new osgDB::Options("format=vfwcap frame_rate=25") );
    #else
    image = osgDB::readImageFile( "/dev/video0.ffmpeg" );
    #endif
    }
    
  3. Try to convert the loaded image to the image stream and start to play it. If we don't pass a movie filename (for example, AVI and MPG) as the argument, or if the webcam can't be initialized, we will get a NULL pointer here and all the following code may fail due to the same reason.

    osg::ImageStream* imageStream =
    dynamic_cast<osg::ImageStream*>( image.get() );
    if ( imageStream ) imageStream->play();
    
  4. Add the image to a 2D texture and apply it as the attribute of a simple quad with texture coordinates.

    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    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, 0.0f, 1.0f) );
    quad->getOrCreateStateSet()->setTextureAttributeAndModes(
    0, texture.get() );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( quad.get() );
    Start the viewer.
    osgViewer::Viewer viewer;
    viewer.setSceneData( geode.get() );
    return viewer.run();
    
  5. If you have already configured the webcam and didn't pass a filename instead, you may see your own figure in the 3D world (as shown in the following screenshot, it is just my room from a low-resolution webcam). It is an exciting functionality if you are developing a virtual chat room, or you want to do some image-recognition work and reflect the result in the 3D scene. Augmented Reality (AR) is also an interesting topic here with the movie texture implementation in this recipe as a foundation. Look for some extra materials by yourselves as these are already out of the scope of this book.

    How to do it...

How it works...

OSG uses the FFmpeg library (http://ffmpeg.org/) and related OSG plugin to decode and play different kinds of media files. As FFmpeg can hardly be built with Visual Studio under Windows, you may first obtain the SDK packages at http://ffmpeg.zeranoe.com/builds/.

And then reset related CMake options to ensure that OSG plugin can be generated.

As FFmpeg supports webcam video under Windows and Linux, we may easily make use of these features by force opening the device through the osgdb_ffmpeg plugin. The pseudo-loader mechanism adds a .ffmpeg postfix to the real device name (for example, 0 under Windows or /dev/video0 under Linux) so that it will be automatically transferred to the plugin with the same name.

There's more...

There are some other plugins for reading movie files as textures; each requiring extra dependencies and CMake options before they can be compiled:

You may also have a look at the osgART library, which provides a series of APIs for implementing AR functionalities in OSG. It uses its own way to render dynamic images from the webcam devices.

Designing scrolling text

Scrolling text is a classic functionality in many cases. For instance, HTML use the<marquee> tag to display texts sliding in and out on the web page, either from left to right or from right to left. They can be used for the purpose of UI design or emphasizing the importance of the contents. In this recipe, we will design simple one line scrolling texts which continuously move on the screen and change its content dynamically.

How to do it...

Let us start.

  1. Include necessary headers and define the macro for generating random numbers:

    #include <osgAnimation/EaseMotion>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    #include <sstream>
    #include <iomanip>
    #define RAND(min, max) ((min) + (float)rand()/(RAND_MAX+1) *
    ((max)-(min)))
    
  2. The ScrollTextCallback class will be set to the drawable to change its behaviors during the update traversal. It computes the Y position of a one-line text randomly, and then starts to move it along the X direction. When it reaches the right end of the screen, the callback will compute a new Y position and will restart from the left-hand side again.

    class ScrollTextCallback : public osg::Drawable::UpdateCallback
    {
    public:
    ScrollTextCallback()
    {
    _motion = new osgAnimation::LinearMotion;
    computeNewPosition();
    }
    virtual void update( osg::NodeVisitor* nv, osg::Drawable*
    drawable );
    void computeNewPosition()
    {
    _motion->reset();
    _currentPos.y() = RAND(50.0, 500.0);
    }
    protected:
    osg::ref_ptr<osgAnimation::LinearMotion> _motion;
    osg::Vec3 _currentPos;
    };
    
  3. In the operator() method, we process the text-moving animation using an osgAnimation::LinearMotion object, which returns the result of a linear interpolation. The actual X and Y values are also put into a string in every frame for dynamically changing the text content.

    osgText::Text* text = static_cast<osgText::Text*>( drawable );
    if ( !text ) return;
    _motion->update( 0.002 );
    float value = _motion->getValue();
    if ( value>=1.0f ) computeNewPosition();
    else _currentPos.x() = value * 800.0f;
    std::stringstream ss; ss << std::setprecision(3);
    ss << "XPos: " << std::setw(5) << std::setfill(' ')
    << _currentPos.x() << ";
    YPos: " << std::setw(5) << std::setfill(' ')
    << _currentPos.y();
    text->setPosition( _currentPos );
    text->setText( ss.str() );
    
  4. In the main entry, we simply use the convenient functions to create a text, add the update callback to it, and use an HUD camera to display it.

    osgText::Text* text = osgCookBook::createText(
    osg::Vec3(), "", 20.0f);
    text->addUpdateCallback( new ScrollTextCallback );
    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() );
    
  5. Start the viewer now.

    osgViewer::Viewer viewer;
    viewer.setSceneData( hudCamera.get() );
    return viewer.run();
    
  6. You will see the text is sliding from left to right, with the content varying all the time. You can easily modify this example to use it in your own applications and screensavers.

    How to do it...

How it works...

Here we introduced the osgAnimation::LinearMotion class, which belongs to the EaseMotion header. Easing means a change in speed. And in the osgAnimation implementation, ease motion means transition between the moving and stopping states. The velocity of a moving object must change when it is going to stop somewhere, and vice versa. Easing actually decides the acceleration values when velocity is changing.

An object starts and speeds up, this is called an 'in' motion. It slows down and finally stops, this is an 'out' motion. If we combine them with a half-and-half ratio, this is so called 'in-out' motion. Thus, an 'in-out-cubic' motion means a cubic equation will be used while computing the velocity values at the beginning and end parts of a motion curve.

Linear motion is special because it doesn't have velocity changes during the entire movement. The object will start and stop suddenly without any easing. For a scrolling text, which will only stop at the screen's right edge and start immediately at another edge, linear motion will be the most appropriate one to use.

There's more...

OSG provides the following types of ease motions besides linear motion, each with 'in', 'out', and 'in-out' forms: QuadMotion (quadratic equation), CubicMotion (cubic equation), QuartMotion (quartic equation), BounceMotion, ElasticMotion, SineMotion (sinusoidal equation), BackMotion, CircMotion (circular equation), and ExpoMotion (exponential equation).

For example, an 'in' type CubicMotion class is written as osgAnimation::InCubicMotion. You may call update() method with a time parameter (between 0 and 1) to update and get the result back with the getValue() method (also limited in [0, 1]).

Ease motion classes can be used separately for various purposes.

There is a good website explaining the concepts and implementations of different ease motions. You can read it for more details:

http://www.robertpenner.com/easing

Implementing morph geometry

Morphing is a special effect used in image processing and 3D animations. It always morphs the source image or model into another through a seamless transition. Modern morphing techniques require some advanced algorithms and operations and can work in very complex cases. But OSG provides a lightweight solution named osgAnimation::MorphGeometry, which can also produce fantastic results in real-time environments.

Although it is great to implement something like face morphing which is already possible in OSG now, we have to simplify the situation here by creating a really easy geometry and change it to another easy one. The emoticon (facial expressions represented by letters) may be simple and interesting enough for demonstration this time.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/Point>
    #include <osg/Geometry>
    #include <osg/Geode>
    #include <osgAnimation/MorphGeometry>
    #include <osgAnimation/BasicAnimationManager>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. The createEmoticonGeometry() function here will create an emoticon with two points representing the eyes, and another 13 points representing the mouth. We will pass a function address as the function argument, in which the mouth shape will be described and pushed into the vertex array.

    typedef void (*VertexFunc)( osg::Vec3Array* );
    osg::Geometry* createEmoticonGeometry( VertexFunc func )
    {
    osg::ref_ptr<osg::Vec3Array> vertices =
    new osg::Vec3Array(15);
    (*vertices)[0] = osg::Vec3(-0.5f, 0.0f, 1.0f);
    (*vertices)[1] = osg::Vec3(0.5f, 0.0f, 1.0f);
    (*func)( vertices.get() );
    osg::ref_ptr<osg::Vec3Array> normals =
    new osg::Vec3Array(15);
    for ( unsigned int i=0; i<15; ++i )
    (*normals)[i] = osg::Vec3(0.0f, -1.0f, 0.0f);
    osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
    geom->setVertexArray( vertices.get() );
    geom->setNormalArray( normals.get() );
    geom->setNormalBinding( osg::Geometry::BIND_PER_VERTEX );
    geom->addPrimitiveSet( new osg::DrawArrays(
    GL_POINTS, 0, 2) );
    geom->addPrimitiveSet( new osg::DrawArrays(
    GL_LINE_STRIP, 2, 13) );
    return geom.release();
    }
    
  3. The emoticonSource() function creates a normal mouth which is only a straight line (:|). It means that the person is annoyed.

    void emoticonSource( osg::Vec3Array* va )
    {
    for ( int i=0; i<13; ++i )
    (*va)[i+2] = osg::Vec3((float)(i-6)*0.15f, 0.0f, 0.0f);
    }
    
  4. The emoticonTarget() function will create a curved mouth instead. It actually looks like the famous emoticon (:)), that is, joking or with joy.

    void emoticonTarget( osg::Vec3Array* va )
    {
    float angleStep = osg::PI / 12.0f;
    for ( int i=0; i<13; ++i )
    {
    float angle = osg::PI - angleStep * (float)i;
    (*va)[i+2] = osg::Vec3(0.9f*cosf(angle), 0.0f,
    -0.2f*sinf(angle));
    }
    }
    
  5. The morph keyframes will indicate the next target geometry's index, which will be changed into from the source geometry (at index 0).

    void createMorphKeyframes( osgAnimation::FloatLinearChannel*
    ch )
    {
    osgAnimation::FloatKeyframeContainer* kfs =
    ch->getOrCreateSampler()->getOrCreateKeyframeContainer();
    kfs->push_back( osgAnimation::FloatKeyframe(0.0, 0.0) );
    kfs->push_back( osgAnimation::FloatKeyframe(2.0, 1.0) );
    }
    
  6. In the main entry, create the channel and animate the object for morphing. The channel must have a valid name for indicating the starting position of the morphing targets.

    osg::ref_ptr<osgAnimation::FloatLinearChannel> channel =
    new osgAnimation::FloatLinearChannel;
    channel->setName( "0" );
    channel->setTargetName( "MorphCallback" );
    createMorphKeyframes( channel.get() );
    osg::ref_ptr<osgAnimation::Animation> animation =
    new osgAnimation::Animation;
    animation->setPlayMode( osgAnimation::Animation::PPONG );
    animation->addChannel( channel.get() );
    
  7. Add the animation to the manager.

    osg::ref_ptr<osgAnimation::BasicAnimationManager> manager =
    new osgAnimation::BasicAnimationManager;
    manager->registerAnimation( animation.get() );
    manager->playAnimation( animation.get() );
    
  8. Create the morph geometry by duplicating the source emoticon (you may also create a new one, but you have to then add all vertices and primitives on your own), and add the target emoticon to the morph geometry for use. The morph geometry must be added to an osg::Geode node then. And we have to also add an osgAnimation::UpdateMorph object as the update callback for connecting the morph with the channel we set just now.

    osg::ref_ptr<osgAnimation::MorphGeometry> morph =
    new osgAnimation::MorphGeometry(
    *createEmoticonGeometry(emoticonSource) );
    morph->addMorphTarget( createEmoticonGeometry(emoticonTarget) );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( morph.get() );
    geode->addUpdateCallback(
    new osgAnimation::UpdateMorph("MorphCallback") );
    geode->getOrCreateStateSet()->setAttributeAndModes(
    new osg::Point(20.0f) );
    
  9. Add the node to the scene graph and start the viewer.

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( geode.get() );
    root->setUpdateCallback( manager.get() );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  10. You will find that the emoticon is changing from normal state to a smiling form, and then changing back. It is really rough, but still provides some of the basic conditions of implementing morph animations in OSG: the source and target must be both osg::Geometry objects, and must have the same number of vertices so that the morphing operation can be performed smoothly all the time.

    How to do it...

How it works...

The basic concept of morphing is to construct a source geometry and one or more target geometries. And change the original one into target ones with different weight settings. It requires an osgAnimation::MorphGeometry object which includes the source and target geometries, and an updater (osgAnimation::UpdateMorph) for connecting with the animation channel. The channel's name must be the same as the index of certain target geometry to make sure they are linked together.

There's more...

The example osganimationmorph is another good example for demonstrating the morphing animation. It loads two pre-created models with the same number of vertices and generates a transition from one to the other. You may find the implementation in the examples folder of the source code.

Fading in and out

In this recipe, we will try to implement a practical functionality. When a model is far away from the viewer, it will gradually be transparentized and finally disappear (fade out); and when the viewer moves towards the model and is near enough, the model will appear again. The fade in and out effects can be done by adding a material state to the model surface, or applying a texture with an alpha channel. We will choose the former as shown in the code segments.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/BlendFunc>
    #include <osg/Material>
    #include <osg/Node>
    #include <osgAnimation/EaseMotion>
    #include <osgDB/ReadFile>
    #include <osgUtil/CullVisitor>
    #include <osgViewer/Viewer>
    
  2. The fade in/out effects will be done in the node callback. It requires an osg::Material object which is also set to the node itself. The alpha component of the diffuse color will be changing during the traversal in every frame, so that the opacity of the node will vary as a result.

    class FadeInOutCallback : public osg::NodeCallback
    {
    public:
    FadeInOutCallback( osg::Material* mat )
    : _material(mat), _lastDistance(-1.0f), _fadingState(0)
    {
    _motion = new osgAnimation::InOutCubicMotion;
    }
    virtual void operator()( osg::Node* node, osg::NodeVisitor*
    nv );
    protected:
    osg::ref_ptr<osgAnimation::InOutCubicMotion> _motion;
    osg::observer_ptr<osg::Material> _material;
    float _lastDistance;
    int _fadingState;
    };
    
  3. In the operator() method, which is the actual implementation of the callback, we have two jobs to do: one is to change the diffuse color if the fading animation is running (_fadingState is non-zero). A cubic motion object will be used to compute a suitable alpha value (between 0.0 and 1.0, increasing or decreasing) here.

    if ( _fadingState!=0 )
    {
    _motion->update( 0.05 );
    float value = _motion->getValue();
    float alpha = (_fadingState>0 ? value : 1.0f - value);
    _material->setDiffuse( osg::Material::FRONT_AND_BACK,
    osg::Vec4(1.0f, 1.0f, 1.0f, alpha) );
    if ( value>=1.0f ) _fadingState = 0;
    traverse( node, nv ); return;
    }
    
  4. Another thing we have to check is the distance between the viewer's eye and the node's center. If there is no fade-in/out animation running currently, the check will be processed by calling the getDistanceFromEyePoint() method of osgUtil::CullVisitor class. Then we will decide if there should be a new fading-in or fading-out effect.

    osgUtil::CullVisitor* cv = static_cast<osgUtil::CullVisitor*
    >( nv );
    if ( cv )
    {
    float distance = cv->getDistanceFromEyePoint(
    node->getBound().center(), true );
    if ( _lastDistance>0.0f )
    {
    if ( _lastDistance>200.0f && distance<=200.0f )
    {
    _fadingState = 1; _motion->reset();
    }
    else if ( _lastDistance<200.0f && distance>=200.0f )
    {
    _fadingState =-1; _motion->reset();
    }
    }
    _lastDistance = distance;
    }
    traverse( node, nv );
    
  5. In the main entry, we load the model and apply a new material object to it. Don't forget to enable blending and transparent sorting on this node.

    osg::Node* loadedModel = osgDB::readNodeFile( "cessna.osg" );
    if ( !loadedModel ) return 1;
    osg::ref_ptr<osg::Material> material = new osg::Material;
    material->setAmbient( osg::Material::FRONT_AND_BACK,
    osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f) );
    material->setDiffuse( osg::Material::FRONT_AND_BACK,
    osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) );
    loadedModel->getOrCreateStateSet()->setAttributeAndModes(
    material.get(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE );
    loadedModel->getOrCreateStateSet()->setAttributeAndModes(
    new osg::BlendFunc );
    loadedModel->getOrCreateStateSet()->setRenderingHint(
    osg::StateSet::TRANSPARENT_BIN );
    
  6. Add the FadeInOutCallback as a cull callback which will be executed while culling scene objects.

    loadedModel->addCullCallback(
    new FadeInOutCallback(material.get()) );
    
  7. Add the node to the scene graph and start the viewer.

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( loadedModel );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  8. You can find the node rendered normally at first. Zoom out by pressing and dragging the right mouse button, and the Cessna will lighten and disappear. Zoom in and then you can see it coming out again. This provides a smooth effect when we go near and far from a model, without any bad pop-in effects such as the model suddenly presenting itself to the end users.

How it works...

The FadeInOutCallback class in this recipe uses a fixed value as a threshold. And start the fading-in or fading-out animation while the real distance from eye-to-model center comes across the threshold. It must be used as a cull callback because only the cull visitor (osgUtil::CullVisitor) can obtain model and view matrices and, thus, compute the position of the model in eye coordinates. You may rewrite this recipe and create a customized node type for such situations. The compass example in Chapter 2 will be a good reference.

Maybe you have also noticed that the Cessna model is not in good shape while being transparentized (but it requires very good eyesight here). That is because we can't perfectly sort the polygons in the Cessna model for alpha blending. If we can split these polygons up on the fly, sort, and redraw the Cessna in geometry level, the Cessna can be rendered well, but it will cause efficiency losses. And it is hard to design a perfect algorithm too. A possible solution for modern graphic cards is depth peeling. We will discuss that in the last chapter of this book.

Animating a flight on fire

OSG provides a complex particle framework, which can design the behaviour of each particle object from its birth to its death. The creation of new particles is realized by emitters, and all post-creation effects will be implemented by programs and its child operators. The basic attributes of each particle will be stored in a template that is managed by the particle system. A system is actually a drawable, and is often updated by a global updater node.

OSG supports multiple particle systems so we can have more than one emitter, program, and particle system. In this example, we will create a fire and a smoke particle system to simulate a Cessna model on fire.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/Point>
    #include <osg/PointSprite>
    #include <osg/MatrixTransform>
    #include <osgDB/ReadFile>
    #include <osgParticle/ModularEmitter>
    #include <osgParticle/ParticleSystemUpdater>
    #include <osgViewer/Viewer>
    
  2. First we use the createFireParticles() function to create a fire particle system, including the particle template settings and the emitter which handles the number, initial positions, and velocities of new-born particles.

    osgParticle::ParticleSystem* createFireParticles(
    osg::Group* parent )
    { ...
    }
    
  3. In the function, allocate the particle system and set up a suitable particle color and a smoke-like texture to simulate the tongue of flame.

    osg::ref_ptr<osgParticle::ParticleSystem> ps =
    new osgParticle::ParticleSystem;
    ps->getDefaultParticleTemplate().setLifeTime( 1.5f );
    ps->getDefaultParticleTemplate().setShape(
    osgParticle::Particle::QUAD );
    ps->getDefaultParticleTemplate().setSizeRange(
    osgParticle::rangef(3.0f, 1.5f) );
    ps->getDefaultParticleTemplate().setAlphaRange(
    osgParticle::rangef(1.0f, 0.0f) );
    ps->getDefaultParticleTemplate().setColorRange(
    osgParticle::rangev4(osg::Vec4(1.0f,1.0f,0.5f,1.0f),
    osg::Vec4(1.0f,0.5f,0.0f,1.0f)) );
    ps->setDefaultAttributes( "Images/smoke.rgb", true, false );
    
  4. Generate a random number of particles (between 30 and 50) in every frame. And set up the shooting direction range and initial speed.

    osg::ref_ptr<osgParticle::RandomRateCounter> rrc =
    new osgParticle::RandomRateCounter;
    rrc->setRateRange( 30, 50 );
    osg::ref_ptr<osgParticle::RadialShooter> shooter =
    new osgParticle::RadialShooter;
    shooter->setThetaRange( -osg::PI_4, osg::PI_4 );
    shooter->setPhiRange( -osg::PI_4, osg::PI_4 );
    shooter->setInitialSpeedRange( 5.0f, 7.5f );
    
  5. Set the counter and shooter to the emitter and add it to a parent node to ensure the emitter is part of the scene graph.

    osg::ref_ptr<osgParticle::ModularEmitter> emitter =
    new osgParticle::ModularEmitter;
    emitter->setParticleSystem( ps.get() );
    emitter->setCounter( rrc.get() );
    emitter->setShooter( shooter.get() );
    parent->addChild( emitter.get() );
    return ps.get();
    
  6. The creation of a smoke-particle system is very similar to the last function, except for some changes in particle attributes and emitter values. The smoke particle is usually darker, bigger, and moving faster than the flame, so some related attributes will be altered. Please refer to the source code package of this book for details of the implementation.

    osgParticle::ParticleSystem* createSmokeParticles(
    osg::Group* parent )
    {
    ...
    }
    
  7. In the main entry, we will create a transformation node which will be transformed to one of the wings of the Cessna model. Both emitters will be added to the transformation node to make sure all the particles are emitted under the specified local coordinates.

    osg::ref_ptr<osg::MatrixTransform> parent =
    new osg::MatrixTransform;
    parent->setMatrix( osg::Matrix::rotate(
    -osg::PI_2, osg::X_AXIS) * osg::Matrix::translate(
    8.0f,-10.0f,-3.0f) );
    osgParticle::ParticleSystem* fire = createFireParticles(
    parent.get() );
    osgParticle::ParticleSystem* smoke = createSmokeParticles(
    parent.get() );
    
  8. A particle system updater will be used to update all kinds of particle systems. And an osg::Geode node can display the particles managed by these two systems in the 3D world.

    osg::ref_ptr<osgParticle::ParticleSystemUpdater> updater =
    new osgParticle::ParticleSystemUpdater;
    updater->addParticleSystem( fire );
    updater->addParticleSystem( smoke );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( fire );
    geode->addDrawable( smoke );
    
  9. Add the Cessna model and all above nodes to the root node, and start the viewer.

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( osgDB::readNodeFile("cessna.osg") );
    root->addChild( parent.get() );
    root->addChild( updater.get() );
    root->addChild( geode.get() );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  10. Now we successfully made it happen: a Cessna is on fire! Its motor on one of the wings is blazing seriously. Will the Cessna crash after a while? Or someone could save it at the last moment? Now, you can continue writing the story by yourself.

How to do it...

How it works...

The osgParticle framework is as complex and powerful as osgAnimation. It is made up of at least a particle system, an emitter that controls the birth of particles, a program that controls a particle's behaviours after being born, and an updater that manages multiple systems and updates them. As emitters, programs, and updaters are all scene nodes, it is possible to put one or more of them under a transformation node to create different particle effects. Here we only place the emitters to the wing with an osg::MatrixTransform node so that all newly-allocated particles will come from the engine which seems to be broken, and the particles will also float in the sky if the flight is moving or falling.

There's more...

This example actually imitates an existing OSG model file named cessnafire.osg. You may look into the file content with any text editors and try to analyze the node and particle structures of it.

Dynamically lighting within shaders

Using shaders is a very popular topic now-a-days. As shading language is often the base of many advanced rendering effects, there is no reason not to use it in our OSG applications. In Chapter 2, we have already introduced the integration of OSG and NVIDIA Cg. But in this and the next chapter, we will return to GLSL and try to make use of different shaders with a lively mind. In this example, we are going to implement a simple phone shader and animate the light position so that diffuse and specular lights on the model surface will be animated at runtime.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/Program>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. First we will design the vertex shader. It requires a light position, and it computes the direction from eye to the light point, which will be used for per-pixel lighting implementation in the fragment shader.

    static const char* vertSource =
    {
    "uniform vec3 lightPosition;
    "
    "varying vec3 normal, eyeVec, lightDir;
    "
    "void main()
    "
    "{
    "
    "vec4 vertexInEye = gl_ModelViewMatrix * gl_Vertex;
    "
    "eyeVec = -vertexInEye.xyz;
    "
    "lightDir = vec3(lightPosition - vertexInEye.xyz);
    "
    "normal = gl_NormalMatrix * gl_Normal;
    "
    "gl_Position = ftransform();
    "
    "}
    "
    };
    
  3. In the fragment shader, we will compute a suitable surface color according to the varying normal and light direction vectors, and other uniform light parameters. The resultant color doesn't contain texture components, but with only a little shader programming experience, you will be able to add some more contents.

    static const char* fragSource =
    {
    "uniform vec4 lightDiffuse;
    "
    "uniform vec4 lightSpecular;
    "
    "uniform float shininess;
    "
    "varying vec3 normal, eyeVec, lightDir;
    "
    "void main (void)
    "
    "{
    "
    "vec4 finalColor = gl_FrontLightModelProduct.sceneColor;
    "
    "vec3 N = normalize(normal);
    "
    "vec3 L = normalize(lightDir);
    "
    "float lambert = dot(N,L);
    "
    "if (lambert > 0.0)
    "
    "{
    "
    "finalColor += lightDiffuse * lambert;
    "
    "vec3 E = normalize(eyeVec);
    "
    "vec3 R = reflect(-L, N);
    "
    "float specular = pow(max(dot(R, E), 0.0), shininess);
    "
    "finalColor += lightSpecular * specular;
    "
    "}
    "
    "gl_FragColor = finalColor;
    "
    "}
    "
    };
    
  4. The LightPosCallback class can handle specific GLSL uniform dynamically, and set a new value to it every frame. In this recipe, we simply set up a new position according to current frame number.

    class LightPosCallback : public osg::Uniform::Callback
    {
    public:
    virtual void operator()( osg::Uniform* uniform,
    osg::NodeVisitor* nv )
    {
    const osg::FrameStamp* fs = nv->getFrameStamp();
    if ( !fs ) return;
    float angle = osg::inDegrees( (float)fs->getFrameNumber() );
    uniform->set( osg::Vec3(20.0f * cosf(angle), 20.0f *
    sinf(angle), 1.0f) );
    }
    };
    
  5. In the main entry, we will first load a model, apply the osg::Program object with two shaders to its state set, and add uniforms with initial values.

    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile(
    "cow.osg" );
    osg::ref_ptr<osg::Program> program = new osg::Program;
    program->addShader( new osg::Shader(osg::Shader::VERTEX,
    vertSource) );
    program->addShader( new osg::Shader(osg::Shader::FRAGMENT,
    fragSource) );
    osg::StateSet* stateset = model->getOrCreateStateSet();
    stateset->setAttributeAndModes( program.get() );
    stateset->addUniform( new osg::Uniform("lightDiffuse",
    osg::Vec4(0.8f, 0.8f, 0.8f, 1.0f)) );
    stateset->addUniform( new osg::Uniform("lightSpecular",
    osg::Vec4(1.0f, 1.0f, 0.4f, 1.0f)) );
    stateset->addUniform( new osg::Uniform("shininess", 64.0f) );
    
  6. The light position uniform variable will be controlled by an update callback, as shown in the following code segment:

    osg::ref_ptr<osg::Uniform> lightPos = new osg::Uniform( "lightPosition", osg::Vec3() );
    lightPos->setUpdateCallback( new LightPosCallback );
    stateset->addUniform( lightPos.get() );
    Start the viewer.
    osgViewer::Viewer viewer;
    viewer.setSceneData( model.get() );
    return viewer.run();
    
  7. Here we actually use one moving light and compute the surface color according to user-defined shader parameters. You may create the same effect in traditional ways, but shaders provide more flexibility and they usually have better rendering results, for instance, per-pixel effects (which is impossible in fixed pipeline).

    How to do it...

How it works...

In this example, we defined few uniform light parameters in the shader code, so we can demonstrate the usage of uniform callbacks. It is also possible in GLSL to use the inbuilt uniform array gl_LightSource[]. For example, we can use gl_LightSource[0].position to represent the first light's position in the scene. And to change the position and make it work in shaders, we can add an osg::LightSource node in the scene graph, and use a node callback to execute osg::Light's setPosition() method.

The osg::Uniform class has an update callback and an event callback, using the same osg::Uniform::Callback structure. You may use either to update uniform variables on the fly.

Creating a simple Galaxian game

The Galaxian is a classic 2D game published in Japan in the 1980s. It includes a large number of aliens attacking the player by shooting bullets and making kamikaze-like operations. In this recipe, we will try our best to make such a Galaxian game in OSG and implement most of its features. We don't have enough space in this book to write thousands of lines of source code, so another important task is that we have to limit our code to at most 200-250 lines. But believe me, it is enough for implementing most kinds of functionalities, no matter how simple or complex.

Getting ready

Let us first prepare three RGBA pictures (in PNG format) named player.png, enemy.png, and bullet.png. They are going to be used for describing the roles' shapes in the game. Example images are shown in the following screenshot:

Getting ready

How to do it...

Let us start.

  1. Include necessary headers and define a macro for generating random numbers:

    #include <osg/Texture2D>
    #include <osg/Geometry>
    #include <osg/MatrixTransform>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    #define RAND(min, max) ((min) + (float)rand()/(RAND_MAX+1) *
    ((max)-(min)))
    
  2. Define a Player class which is actually a transformation node in the scene graph. The Player class not only means the player's role in the game, but also represents enemy aliens and bullets. Its width() and height() returns the size of the player in a 2D space (which is in fact an HUD camera in OSG, which ignores the Z direction but places all scene objects in the XOY plane), setSpeedVector() sets the speed of the vector which will make it move every frame, and setPlayerType() defines whether the node is a player, a player's bullet, an enemy, or an enemy's bullet.

    class Player : public osg::MatrixTransform
    {
    public:
    Player() : _type(INVALID_OBJ) {}
    Player( float width, float height, const std::string&
    texfile );
    float width() const { return _size[0]; }
    float height() const { return _size[1]; }
    void setSpeedVector( const osg::Vec3& sv )
    {
    _speedVec = sv;
    }
    const osg::Vec3& getSpeedVector() const
    {
    return _speedVec;
    }
    enum PlayerType
    {
    INVALID_OBJ=0, PLAYER_OBJ, ENEMY_OBJ,
    PLAYER_BULLET_OBJ, ENEMY_BULLET_OBJ
    };
    void setPlayerType( PlayerType t ) { _type = t; }
    PlayerType getPlayerType() const { return _type; }
    bool isBullet() const
    {
    return _type==PLAYER_BULLET_OBJ ||
    _type==ENEMY_BULLET_OBJ; }
    bool update( const osgGA::GUIEventAdapter& ea,
    osg::Group* root );
    bool intersectWith( Player* player ) const;
    protected:
    osg::Vec2 _size;
    osg::Vec3 _speedVec;
    PlayerType _type;
    };
    
  3. The constructor of the Player class can accept width and height parameters and a texture filename as the input arguments. It will create a textured quad, put it into an osg::Geode node, and add the geode as its own child at the end.

    Player::Player( float width, float height, const std::string&
    texfile )
    : _type(INVALID_OBJ)
    {
    _size.set( width, height );
    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    texture->setImage( osgDB::readImageFile(texfile) );
    osg::ref_ptr<osg::Drawable> quad =
    osg::createTexturedQuadGeometry(
    osg::Vec3(-width*0.5f, -height*0.5f, 0.0f),
    osg::Vec3(width, 0.0f, 0.0f), osg::Vec3(0.0f, height, 0.0f) );
    quad->getOrCreateStateSet()->setTextureAttributeAndModes(
    0, texture.get() );
    quad->getOrCreateStateSet()->setMode( GL_LIGHTING,
    osg::StateAttribute::OFF );
    quad->getOrCreateStateSet()->setRenderingHint(
    osg::StateSet::TRANSPARENT_BIN );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( quad.get() );
    addChild( geode.get() );
    }
    
  4. The update() method is the core function of the Player class. It defines the behaviors of the node when user events (including FRAME event) are coming. A player or enemy node may shoot bullets in the update() method, so the root node should also be passed to accept newly allocated bullet nodes.

    bool Player::update( const osgGA::GUIEventAdapter& ea,
    osg::Group* root )
    {
    ...
    }
    
  5. In the update() method, we will first check if user presses the left, right, or return key. It will cause a node of the player type to move and shoot. Enemy nodes will shoot randomly regardless of user inputs.

    bool emitBullet = false;
    switch ( _type )
    {
    case PLAYER_OBJ:
    if ( ea.getEventType()==osgGA::GUIEventAdapter::KEYDOWN )
    {
    switch ( ea.getKey() )
    {
    case osgGA::GUIEventAdapter::KEY_Left:
    _speedVec = osg::Vec3(-0.1f, 0.0f, 0.0f);
    break;
    case osgGA::GUIEventAdapter::KEY_Right:
    _speedVec = osg::Vec3(0.1f, 0.0f, 0.0f);
    break;
    case osgGA::GUIEventAdapter::KEY_Return:
    emitBullet = true;
    break;
    default: break;
    }
    }
    else if ( ea.getEventType()==osgGA::GUIEventAdapter::KEYUP )
    _speedVec = osg::Vec3();
    break;
    case ENEMY_OBJ:
    if ( RAND(0, 2000)<1 ) emitBullet = true;
    break;
    default: break;
    }
    
  6. Secondly, we will check if a new bullet should be generated and emitted. A player's bullet will start from the bottom of the screen and speed up to the top; and an enemy's bullet will go from the top to the bottom, trying to destroy the player's role. The bullet node should be added to the root node to make it visible in the space.

    osg::Vec3 pos = getMatrix().getTrans();
    if ( emitBullet )
    {
    osg::ref_ptr<Player> bullet = new Player(
    0.4f, 0.8f, "bullet.png");
    if ( _type==PLAYER_OBJ )
    {
    bullet->setPlayerType( PLAYER_BULLET_OBJ );
    bullet->setMatrix( osg::Matrix::translate(
    pos + osg::Vec3(0.0f, 0.9f, 0.0f)) );
    bullet->setSpeedVector( osg::Vec3(0.0f, 0.2f, 0.0f) );
    }
    else
    {
    bullet->setPlayerType( ENEMY_BULLET_OBJ );
    bullet->setMatrix( osg::Matrix::translate(
    pos - osg::Vec3(0.0f, 0.9f, 0.0f)) );
    bullet->setSpeedVector( osg::Vec3(0.0f,-0.2f, 0.0f) );
    }
    root->addChild( bullet.get() );
    }
    
  7. In the per-frame event, we actually add the speed vector to the transformation matrix, so that the player/enemy/bullet will actually start to move and fight.

    if ( ea.getEventType()!=osgGA::GUIEventAdapter::FRAME )
    return true;float halfW = width() * 0.5f, halfH = height() *
    0.5f;
    pos += _speedVec;
    // Don't update the player anymore if it is not in the visible //area.
    if ( pos.x()<halfW || pos.x()>ea.getWindowWidth()-halfW )
    return false;
    if ( pos.y()<halfH || pos.y()>ea.getWindowHeight()-halfH )
    return false;
    setMatrix( osg::Matrix::translate(pos) );
    return true;
    
  8. The intersectWith() function can quickly check if current node is intersected with another node. This is useful if we want to check whether the bullet hits another node and kills it.

    bool Player::intersectWith( Player* player ) const
    {
    osg::Vec3 pos = getMatrix().getTrans();
    osg::Vec3 pos2 = player->getMatrix().getTrans();
    return fabs(pos[0] - pos2[0]) < (width() + player->width())
    * 0.5f &&
    fabs(pos[1] - pos2[1]) < (height() + player->height())
    * 0.5f;
    }
    
  9. OK, now we must have a global game controller who will manage all roles in the game and update them one by one. It also removes nodes destroyed by bullets and bullets themselves.

    class GameControllor : public osgGA::GUIEventHandler
    {
    public:
    GameControllor( osg::Group* root )
    : _root(root), _direction(0.1f), _distance(0.0f) {}
    virtual bool handle( const osgGA::GUIEventAdapter& ea,
    osgGA::GUIActionAdapter& aa );
    protected:
    osg::observer_ptr<osg::Group> _root;
    float _direction;
    float _distance;
    };
    
  10. In the handle() method, we will manage a _distance and a _direction variable. They will help the enemy aliens move from left to right. As they will also randomly shoot bullets, it will be not easy for us to beat all of them.

    _distance += fabs(_direction);
    if ( _distance>30.0f )
    {
    _direction = -_direction;
    _distance = 0.0f;
    }
    
  11. The most important part of the game will be done here: All nodes registered in the controller will be updated; bullets out of the screen will be removed; the player and enemies will be destroyed when they knock into a bullet from the opposite roles. Although we don't have a 'welcome' and a 'game over' splash, and the aliens are so silly that they don't have any artificial intelligence, the game is still completed with most Galaxian features added.

    osg::NodePath toBeRemoved;
    for ( unsigned i=0; i<_root->getNumChildren(); ++i )
    {
    Player* player = static_cast<Player*>( _root->getChild(i) );
    if ( !player ) continue;
    // Update the player matrix, and remove the player if it is
    //a bullet outside the visible area
    if ( !player->update(ea, _root.get()) )
    {
    if ( player->isBullet() )
    toBeRemoved.push_back( player );
    }
    // Automatically move the enemies
    if ( player->getPlayerType()==Player::ENEMY_OBJ )
    player->setSpeedVector( osg::Vec3(_direction, 0.0f, 0.0f) );
    if ( !player->isBullet() ) continue;
    // Use a simple loop to check if any two of the players
    // (you, enemies, and bullets) are intersected
    for ( unsigned j=0; j<_root->getNumChildren(); ++j )
    {
    Player* player2 = static_cast<Player*>( _root->getChild(j) );
    if ( !player2 || player==player2 ) continue;
    if ( player->getPlayerType()==Player::ENEMY_BULLET_OBJ &&
    player2->getPlayerType()==Player::ENEMY_OBJ )
    {
    continue;
    }
    else if ( player->intersectWith(player2) )
    {
    // Remove both players if they collide with each other
    toBeRemoved.push_back( player );
    toBeRemoved.push_back( player2 );
    }
    }
    }
    
  12. At last, remove the nodes that will be destroyed.

    for ( unsigned i=0; i<toBeRemoved.size(); ++i )
    _root->removeChild( toBeRemoved[i] );
    return false;
    
  13. We have nearly finished the project. In the main entry, the last step is to add the player node to an HUD camera.

    osg::ref_ptr<Player> player = new Player(
    1.0f, 1.0f, "player.png");
    player->setMatrix( osg::Matrix::translate(
    40.0f, 5.0f, 0.0f) );
    player->setPlayerType( Player::PLAYER_OBJ );
    osg::ref_ptr<osg::Camera> hudCamera =
    osgCookBook::createHUDCamera(0, 80, 0, 30);
    hudCamera->addChild( player.get() );
    
  14. And so do the enemies. We will arrange them in a 5 x 10 cavalcade.

    for ( unsigned int i=0; i<5; ++i )
    {
    for ( unsigned int j=0; j<10; ++j )
    {
    osg::ref_ptr<Player> enemy = new Player(1.0f, 1.0f,
    "enemy.png");
    enemy->setMatrix( osg::Matrix::translate(
    20.0f+1.5f*(float)j, 25.0f-1.5f*(float)i, 0.0f) );
    enemy->setPlayerType( Player::ENEMY_OBJ );
    hudCamera->addChild( enemy.get() );
    }
    }
    
  15. Specify a black background and start the viewer.

    osgViewer::Viewer viewer;
    viewer.getCamera()->setClearColor( osg::Vec4(0.0f, 0.0f,
    0.0f, 1.0f) );
    viewer.addEventHandler( new GameControllor(hudCamera.get()) );
    viewer.setSceneData( hudCamera.get() );
    return viewer.run();
    
  16. Good job! Now make sure the PNG files are placed in the executable directory, press Left arrow and Right arrow buttons to move your fighter to avoid enemies' bullets, and press Return to fire. Your goal now is only one: defeat all bad aliens!

How to do it...

How it works...

Let us review the design of this naive game rapidly. A Player class is used for representing the player object, the enemies, and the bullets. It can move to a new position, shoot new bullets, and check if it is intersected with others. A global game controller is used to manage all these Player nodes and remove unused and destroyed ones. And if you like, you may also add some sentences to show 'you win' or 'you lose' pop ups, and improve the enemies' intelligence. No special functionailities are used except the event handlers and transformation nodes. But these are enough to build a simple game.

Considering more game-related features? Please continue reading the last example in this chapter which introduces integration with physics engines, and the next chapter which discusses rendering effects.

Building a skeleton system

Skeletal animation is important among all kinds of 3D scene animations. It needs a hierarchical set of bones that are connected to each other. Sometimes these bones are also called rigs. Animations on one or more bones will finally lead to complex character animations such as walking, running, and even fighting with somebody.

An OSG bone here means a joint at which two parts of the real human can make contact. OSG bones also have hierarchical structures as any of the bone nodes can have one or more children. To represent a bone's shape, it is always suggested to push an additional mesh to the parent. That is because the mesh and the bone are in the same coordinate frame, so they can be actually binded together to perform both the rendering and logic operations of a part of the complete skeleton.

We can directly add a certain model along with a bone so that it will follow the bone's translation and rotation. This can be used to produce some robot-like animations or simulate cartoon characters. A real human or animal has skin and muscles over the bones. The deformation of these muscles, or meshes in 3D developments, can be treated as the basis of real-character animations. To achieve this, we have to bind the mesh vertices to bones, and transform them according to bone's motion and vertex weights. The first implementation which doesn't have a skinning process will be demonstrated in this section. And a simple skinning work will be shown in the next one.

How to do it...

Let us start.

  1. Include necessary headers:

    #include <osg/LineWidth>
    #include <osg/Geometry>
    #include <osgAnimation/Bone>
    #include <osgAnimation/Skeleton>
    #include <osgAnimation/UpdateBone>
    #include <osgAnimation/StackedTranslateElement>
    #include <osgAnimation/StackedQuaternionElement>
    #include <osgAnimation/BasicAnimationManager>
    #include <osgViewer/Viewer>
    
  2. We create the bone shape by drawing a line from parent bone's original point (which is also the start point of current bone), to the end point of current bone. All will be done in parent bone's local coordinate system, so the generated osg::Geode node will be added to that bone too.

    osg::Geode* createBoneShape( const osg::Vec3& trans,
    const osg::Vec4& color )
    {
    osg::ref_ptr<osg::Vec3Array> va = new osg::Vec3Array;
    va->push_back( osg::Vec3() ); va->push_back( trans );
    osg::ref_ptr<osg::Vec4Array> ca = new osg::Vec4Array;
    ca->push_back( color );
    osg::ref_ptr<osg::Geometry> line = new osg::Geometry;
    line->setVertexArray( va.get() );
    line->setColorArray( ca.get() );
    line->setColorBinding( osg::Geometry::BIND_OVERALL );
    line->addPrimitiveSet( new osg::DrawArrays(GL_LINES, 0, 2) );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( line.get() );
    geode->getOrCreateStateSet()->setAttributeAndModes(
    new osg::LineWidth(15.0f) );
    geode->getOrCreateStateSet()->setMode( GL_LIGHTING,
    osg::StateAttribute::OFF );
    return geode.release();
    }
    
  3. Create the osgAnimation::Bone node and add it to the parent bone with an appropriate offset (trans). We will create translation and rotation animations for each bone, so there should be an osgAnimation::UpdateBone callback which records initial animation values. And to place the bone in its parent's local coordinates, we must consider its current matrix in the skeleton space while using setMatrixInSkeletonSpace() to apply the offset.

    osgAnimation::Bone* createBone( const char* name,
    const osg::Vec3& trans, osg::Group* parent )
    {
    osg::ref_ptr<osgAnimation::Bone> bone =
    new osgAnimation::Bone;
    parent->insertChild( 0, bone.get() );
    parent->addChild( createBoneShape(trans, osg::Vec4(
    1.0f, 1.0f, 1.0f, 1.0f)) );
    osg::ref_ptr<osgAnimation::UpdateBone> updater =
    new osgAnimation::UpdateBone(name);
    updater->getStackedTransforms().push_back( new
    osgAnimation::StackedTranslateElement("translate", trans) );
    updater->getStackedTransforms().push_back( new
    osgAnimation::StackedQuaternionElement("quaternion") );
    bone->setUpdateCallback( updater.get() );
    bone->setMatrixInSkeletonSpace( osg::Matrix::translate(trans)
    * bone->getMatrixInSkeletonSpace() );
    bone->setName( name );
    return bone.get();
    }
    
  4. While creating leaf bones of the skeleton, we add an additional shape node as the leaf bone's child. This is in fact not the leaf bone's own shape, but its child shape which will accept the leaf bone's animation and, thus, have transformations in the 3D world. This is the reason we must have an independent function to create these 'end bones' and their child shapes.

    osgAnimation::Bone* createEndBone( const char* name,
    const osg::Vec3& trans, osg::Group* parent )
    {
    osgAnimation::Bone* bone = createBone( name, trans, parent );
    bone->addChild( createBoneShape(trans, osg::Vec4(
    0.4f, 1.0f, 0.4f, 1.0f)) );
    return bone;
    }
    
  5. The createChannel() function will be used later to add rotation animations to bones.

    osgAnimation::Channel* createChannel( const char* name,
    const osg::Vec3& axis, float rad )
    {
    osg::ref_ptr<osgAnimation::QuatSphericalLinearChannel> ch =
    new osgAnimation::QuatSphericalLinearChannel;
    ch->setName( "quaternion" );
    ch->setTargetName( name );
    osgAnimation::QuatKeyframeContainer* kfs =
    ch->getOrCreateSampler()->getOrCreateKeyframeContainer();
    kfs->push_back( osgAnimation::QuatKeyframe(
    0.0, osg::Quat(0.0, axis)) );
    kfs->push_back( osgAnimation::QuatKeyframe(
    8.0, osg::Quat(rad, axis)) );
    return ch.release();
    }
    
  6. In the main entry, we first create the skeleton root and child bones. They are both OSG nodes so there is no difference in maintaining bones and normal OSG nodes.

    osg::ref_ptr<osgAnimation::Skeleton> skelroot =
    new osgAnimation::Skeleton;
    skelroot->setDefaultUpdateCallback();
    // Here the name 'bone0' means the root.
    // And 'bone1*' are bones of the next level.
    // The rest (bone2* - bone4*) may be deduced by analogy
    // and found in the source code package
    osgAnimation::Bone* bone0 = createBone( "bone0",
    osg::Vec3(0.0f,0.0f,0.0f), skelroot.get() );
    osgAnimation::Bone* bone11 = createBone( "bone11",
    osg::Vec3(0.5f,0.0f,0.0f), bone0 );
    osgAnimation::Bone* bone12 = createEndBone( "bone12",
    osg::Vec3(1.0f,0.0f,0.0f), bone11 );
    ...
    

    These code segments create a mechanical hand with four claws.

  7. Next we will create animations on different claw bones. This gives the hand a clasping action as an animation. Register the animation.

    osg::ref_ptr<osgAnimation::Animation> anim =
    new osgAnimation::Animation;
    anim->setPlayMode( osgAnimation::Animation::PPONG );
    anim->addChannel( createChannel("bone11", osg::Y_AXIS,
    osg::PI_4) );
    anim->addChannel( createChannel("bone12", osg::Y_AXIS,
    osg::PI_2) );
    ...// Find rest channel settings in the source code
    osg::ref_ptr<osgAnimation::BasicAnimationManager> manager =
    new osgAnimation::BasicAnimationManager;
    manager->registerAnimation( anim.get() );
    manager->playAnimation( anim.get() );
    
  8. Add them to the root node and start the viewer.

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild( skelroot.get() );
    root->setUpdateCallback( manager.get() );
    osgViewer::Viewer viewer;
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  9. You will see a mechanical hand rendered with lines in the space after starting the example. It will simulate the action of grabbing something with the four claws and then loosening. Bones are bound to their parents, so they won't break while the animation is running in ping-pong mode. Although it is a little too simple for a real application, it is a kind of character animation anyhow, and it can be extended to work with some very complex situations such as the human kinematics.

    How to do it...

How it works...

Skeleton animation uses the same osgAnimation framework as we discussed a while ago. A complete application with bone animations still needs to have an animation manager and several channels storing the animating data. It also has a sub-scene graph formed by parent and child bones. The root bone (bone0 in this recipe) is linked to the skeleton node. All other bones are its direct and indirect children. No orphan bone is allowed except the root one. A bone must have a callback (osgAnimation::UpdateBone) who records stacked elements to be associated with channels targeting them.

Be careful of the method setMatrixInSkeletonSpace(). It sets the bone in the skeleton space. So if you want to specify the bone in its parent bone's space (which is easier to understand here), you have to convert the offset matrix to skeleton matrix first. This requires you to ensure to first add the new bone into the skeleton scene graph; otherwise you will not be able to get a correct bone matrix for computation. The transformation of the bone can be easily written as the product of the new offset and the previous skeleton space matrix (which equals the parent bone's matrix initially).

bone->setMatrixInSkeletonSpace(
osg::Matrix::translate(trans) *
bone->getMatrixInSkeletonSpace() );

Another thing to note is, in order to add renderables to represent each bone's shape, we have to push the geometry node to the end of the parent bone's child list, and insert the bone node at the beginning of the list. See the following code snippet:

osg::ref_ptr<osgAnimation::Bone> bone =
new osgAnimation::Bone;
parent->insertChild( 0, bone.get() );
parent->addChild( createBoneShape(trans, osg::Vec4(
1.0f, 1.0f, 1.0f, 1.0f)) );

To explain the reason in short: This ensures all bones will be traversed and updated before any geometry is drawn. Just treat this as a rule to follow.

Skinning a customized mesh

Let us continue the last recipe which creates a simple skeleton for representing a mechanical hand with four claws. This time we will do the skinning work, that is, bind vertices of the character geometry with bones. Each bone can be associated with some portion of the vertices, and each vertex can be associated with multiple bones, each with a weight factor which will change the effect of the bone on the vertex. You may find some detailed information in the following link, as well as some shader code:

http://tech-artists.org/wiki/Vertex_Skinning

To calculate the final position of one vertex, we must collect all bones associated with it and apply each bone's transformation matrix to the vertex's position, as well as scale the matrix by corresponding weight. Fortunately, OSG does everything we have just described for us. The only thing we have to do is to build a map of the vertex and its associated bone name and weight. We will work on the last example code to add such implementations.

How to do it...

Let us start.

  1. First, we don't need the createBoneShape() function anymore. Because we are not going to draw the bone directly in the 3D world this time. In the createBone() and createEndBone() functions, remove the line that adds the return value of createBoneShape() to the parent node.

    osgAnimation::Bone* createBone( const char* name,
    const osg::Vec3& trans, osg::Group* parent )
    {
    osg::ref_ptr<osgAnimation::Bone> bone =
    new osgAnimation::Bone;
    parent->insertChild( 0, bone.get() );
    osg::ref_ptr<osgAnimation::UpdateBone> updater =
    new osgAnimation::UpdateBone(name);
    updater->getStackedTransforms().push_back( new
    osgAnimation::StackedTranslateElement("translate", trans) );
    updater->getStackedTransforms().push_back( new
    osgAnimation::StackedQuaternionElement("quaternion") );
    bone->setUpdateCallback( updater.get() );
    bone->setMatrixInSkeletonSpace( osg::Matrix::translate(trans)
    * bone->getMatrixInSkeletonSpace() );
    bone->setName( name );
    return bone.get();
    }
    osgAnimation::Bone* createEndBone( const char* name,
    const osg::Vec3& trans, osg::Group* parent )
    {
    osgAnimation::Bone* bone = createBone( name, trans, parent );
    return bone;
    }
    

    And there are no changes to the createChannel() function.

  2. Now we should create two new functions, and use osgAnimation::RigGeometry class to record relations between bones and vertices. The addVertices() function adds vertices of one claw to the geometry object, and sets up bones which are bound to these newly allocated vertices.

    void addVertices( const char* name1, float length1,
    const char* name2, float length2,
    const osg::Vec3& dir, osg::Geometry* geom,
    osgAnimation::VertexInfluenceMap* vim )
    {
    osg::Vec3Array* va = static_cast<osg::Vec3Array*>(
    geom->getVertexArray() );
    unsigned int start = va->size();
    // The bone shape is supposed to have 5 vertices, the first
    // two of which are unmovable
    va->push_back( dir * 0.0f );
    va->push_back( dir * length1 );
    // The last 3 points will be fully controled by the rig
    // geometry so they should be assoicated with the influence
    // map
    va->push_back( dir * length1 );
    (*vim)[name1].push_back(
    osgAnimation::VertexIndexWeight(start+2, 1.0f) );
    va->push_back( dir * length2 );
    (*vim)[name1].push_back(
    osgAnimation::VertexIndexWeight(start+3, 1.0f) );
    va->push_back( dir * length2 );
    (*vim)[name2].push_back(
    osgAnimation::VertexIndexWeight(start+4, 1.0f) );
    // Push the very, very simple shape definition (actually
    // line strips) to the rig geometry
    geom->addPrimitiveSet( new osg::DrawArrays(
    GL_LINE_STRIP, start, 5) );
    }
    
  3. The createBoneShapeAndSkin() function will simultaneously create the geometry of the mechanical hand and finish the skinning work. It uses the addVertices() function we just introduced internally.

    osg::Geode* createBoneShapeAndSkin()
    {
    ...
    }
    
  4. In this function, first we have to initialize the geometry and the osgAnimation::VertexInfluenceMap object, which is the association table of all the vertices and bones. Then we allocate the vertices of each claw geometry, assemble them, and bind them to specific bones.

    osg::ref_ptr<osg::Geometry> geometry = new osg::Geometry;
    geometry->setVertexArray( new osg::Vec3Array );
    osg::ref_ptr<osgAnimation::VertexInfluenceMap> vim =
    new osgAnimation::VertexInfluenceMap;
    (*vim)["bone11"].setName( "bone11" );
    (*vim)["bone12"].setName( "bone12" );
    ... // Please find details in the source code
    addVertices( "bone11", 0.5f, "bone12", 1.5f, osg::X_AXIS,
    geometry.get(), vim.get() );
    ... // Please find details in the source code
    
  5. Then we use the osgAnimation::RigGeometry object to accept the geometry and the influence map and add it to a node, which will be added to the scene graph later.

    osg::ref_ptr<osgAnimation::RigGeometry> rigGeom =
    new osgAnimation::RigGeometry;
    rigGeom->setSourceGeometry( geometry.get() );
    rigGeom->setInfluenceMap( vim.get() );
    rigGeom->setUseDisplayList( false );
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( rigGeom.get() );
    geode->getOrCreateStateSet()->setAttributeAndModes(
    new osg::LineWidth(15.0f) );
    geode->getOrCreateStateSet()->setMode( GL_LIGHTING,
    osg::StateAttribute::OFF );
    return geode.release();
    
  6. The only change in the main entry is to add the rigs and vertex binding data to the skeleton node.

    skelroot->addChild( createBoneShapeAndSkin() );
    
  7. OK, now the claws work as they did in the Building a skeleton system recipe. But this time it has a solid mesh and you will see that the mesh is deforming along with the bones' animations. The muscle movement may not be realistic at present, as we have only configured simple weights of each bone on vertices, and the number of changeable vertices is not enough for performing precise movements. Exporting models, skeletons, and animations from some other modeling tools may be a good idea if you need more complex characters in the application. Remember that OSG supports characters in Collada DAE and Autodesk FBX formats currently.

How to do it...

How it works...

In the Building a skeleton system recipe, we added a set of line geometries to the bone structure. Each bone had one geometry to represent its shape and animation states. It required bone nodes and geometry nodes to be mixed in one sub-scene graph. But this time in the skinning example, we keep the bone hierarchy unchanged and directly add a geometry node to the skeleton itself. Then we bind each bone with a number of vertices and set the vertex weight. The character updating and rendering work will be done in OSG backend internally. You may use either software or hardware technique to render the character's rig geometry.

There's more...

OSG provides two examples to explain the usage of skeleton animation clearly: The osganimationskinning example shows how to build a simple skeleton and skin it; the osganimationhardware example describes how to change the transform technique used in osgAnimation::RigGeometry (software or hardware).

Last but not least, don't try to build a complete human model and skeleton and animate it by programming. It is possible but needs heavy work to implement. Consider converting your character from some standard formats and commonly-used modeling software. The DAE and FBX formats are good choices for such purposes.

Letting the physics engine be

It is a great enhancement to your applications to integrate with certain physics engines, and, thus, have the ability to compute collisions between scene objects, simulate rigid, soft body, fluid, and cloth behaviours in a virtual physics world. Especially in games, physics support can make the scenario more realistic and interesting, and provide comfortable user interactions and feedbacks.

In the last recipe of this chapter, we are going to see a simple example of physics integration in OSG. It requires the famous NVIDIA PhysX library as dependence, and can finally produce an interactive program demonstrating the most common rigid-collision functionality in an OSG world.

Getting ready

You have to first download the PhysX SDK from the NVIDIA developer website. Remember to download version 2.8, as the latest 3.0 version totally changes the API interface and won't work with this recipe.

Visit the following link and register to download:

http://developer.nvidia.com/physx-downloads

Then we can configure the CMake script to add PhysX dependence directories and libraries:

FIND_PATH(PHYSX_SDK_DIR Physics/include/NxPhysics.h)
FIND_LIBRARY(PHYSX_LIBRARY PhysXLoader.lib libPhysXLoader.so)
SET(EXTERNAL_INCLUDE_DIR
"${PHYSX_SDK_DIR}/PhysXLoader/include"
"${PHYSX_SDK_DIR}/Physics/include"
"${PHYSX_SDK_DIR}/Foundation/include")
TARGET_LINK_LIBRARIES(${EXAMPLE_NAME} ${PHYSX_LIBRARY})

How to do it...

Let us start.

  1. Include necessary headers:

    #include <NxPhysics.h>
    #include <osg/ShapeDrawable>
    #include <osg/MatrixTransform>
    #include <osgDB/ReadFile>
    #include <osgViewer/Viewer>
    
  2. We will create an independent class to manage all PhysX functions. It is designed to use the singleton design pattern and create and manage physics objects with unique ID numbers. It supports creating a few types of rigid bodies and setting their velocities and matrices later with the ID.

    class PhysXInterface : public osg::Referenced
    {
    public:
    static PhysXInterface* instance();
    void createWorld( const osg::Plane& plane, const osg::Vec3&
    gravity );
    void createBox( int id, const osg::Vec3& dim, double mass );
    void createSphere( int id, double radius, double mass );
    void setVelocity( int id, const osg::Vec3& pos );
    void setMatrix( int id, const osg::Matrix& matrix );
    osg::Matrix getMatrix( int id );
    void simulate( double step );
    protected:
    PhysXInterface();
    virtual ~PhysXInterface();
    void createActor( int id, NxShapeDesc* shape,
    NxBodyDesc* body );
    typedef std::map<int, NxActor*> ActorMap;
    ActorMap _actors;
    NxPhysicsSDK* _physicsSDK;
    NxScene* _scene;
    };
    
  3. The instance() function returns the only instance of the PhysXInterface class.

    PhysXInterface* PhysXInterface::instance()
    {
    static osg::ref_ptr<PhysXInterface> s_registry =
    new PhysXInterface;
    return s_registry.get();
    }
    
  4. In the constructor, we initialize the PhysX SDK object.

    PhysXInterface::PhysXInterface() : _scene(NULL)
    {
    NxPhysicsSDKDesc desc;
    NxSDKCreateError errorCode = NXCE_NO_ERROR;
    _physicsSDK = NxCreatePhysicsSDK(NX_PHYSICS_SDK_VERSION,
    NULL, NULL, desc, &errorCode);
    if ( !_physicsSDK )
    {
    OSG_WARN << "Unable to initialize the PhysX SDK, error
    code: " << errorCode << std::endl;
    }
    }
    
  5. And in the destructor, we will release all registered PhysX actors (which are actually objects in the physics world), the _scene variable which describes the physics world, and the SDK variable to make sure all PhysX objects are deleted from the memory.

    PhysXInterface::~PhysXInterface()
    {
    if ( _scene )
    {
    for ( ActorMap::iterator itr=_actors.begin();
    itr!=_actors.end(); ++itr )
    _scene->releaseActor( *(itr->second) );
    _physicsSDK->releaseScene( *_scene );
    }
    NxReleasePhysicsSDK( _physicsSDK );
    }
    
  6. The createWorld() method will allocate a new physics world with specified gravity and material values, and create a static ground object at the same time. The protected createActor() will be always called to create new NxActor objects and save it to the actor map.

    void PhysXInterface::createWorld( const osg::Plane& plane,
    const osg::Vec3& gravity )
    {
    NxSceneDesc sceneDesc;
    sceneDesc.gravity = NxVec3(gravity.x(), gravity.y(),
    gravity.z());
    _scene = _physicsSDK->createScene( sceneDesc );
    NxMaterial* defaultMaterial =
    _scene->getMaterialFromIndex(0);
    defaultMaterial->setRestitution( 0.5f );
    defaultMaterial->setStaticFriction( 0.5f );
    defaultMaterial->setDynamicFriction( 0.5f );
    // Create the ground plane
    NxPlaneShapeDesc shapeDesc;
    shapeDesc.normal = NxVec3(plane[0], plane[1], plane[2]);
    shapeDesc.d = plane[3];
    createActor( -1, &shapeDesc, NULL );
    }
    
  7. The createBox() method creates a dynamic box object in the world. To note, it doesn't do anything to the rendering result at present. The function only affects the 'physics' world.

    void PhysXInterface::createBox( int id, const osg::Vec3&
    dim, double mass )
    {
    NxBoxShapeDesc shapeDesc; shapeDesc.dimensions =
    NxVec3(dim.x(), dim.y(), dim.z());
    NxBodyDesc bodyDesc; bodyDesc.mass = mass;
    createActor( id, &shapeDesc, &bodyDesc );
    }
    
  8. The createSphere() method will create a dynamic sphere.

    void PhysXInterface::createSphere( int id, double radius,
    double mass )
    {
    NxSphereShapeDesc shapeDesc; shapeDesc.radius = radius;
    NxBodyDesc bodyDesc; bodyDesc.mass = mass;
    createActor( id, &shapeDesc, &bodyDesc );
    }
    
  9. After a rigid object is created, we can call setVelocity() method with the object ID to set the velocity value.

    void PhysXInterface::setVelocity( int id, const osg::Vec3&
    vec )
    {
    NxActor* actor = _actors[id];
    actor->setLinearVelocity( NxVec3(vec.x(), vec.y(), vec.z()) );
    }
    
  10. We can also set the matrix (the translation and rotation components) value of a created object.

    void PhysXInterface::setMatrix( int id, const osg::Matrix&
    matrix )
    {
    NxF32 d[16];
    for ( int i=0; i<16; ++i )
    d[i] = *(matrix.ptr() + i);
    NxMat34 nxMat; nxMat.setColumnMajor44( &d[0] );
    NxActor* actor = _actors[id];
    actor->setGlobalPose( nxMat );
    }
    
  11. The getMatrix() method is important because it can obtain the latest matrix value of a physics object, which may be moved or smashed during the simulation. The value can be set to related OSG node to ensure that the changes in the physics world can be reflected to the rendering window too.

    osg::Matrix PhysXInterface::getMatrix( int id )
    {
    float mat[16];
    NxActor* actor = _actors[id];
    actor->getGlobalPose().getColumnMajor44( mat );
    return osg::Matrix(&mat[0]);
    }
    
  12. The simulate() method must be executed in every frame to make sure the physics simulation loop is running.

    void PhysXInterface::simulate( double step )
    {
    _scene->simulate( step );
    _scene->flushStream();
    _scene->fetchResults( NX_RIGID_BODY_FINISHED, true );
    }
    
  13. The internal createActor() function will create an actor according to the shape and body descriptions, and push it into the map for future uses.

    void PhysXInterface::createActor( int id, NxShapeDesc* shape,
    NxBodyDesc* body )
    {
    NxActorDesc actorDesc;
    actorDesc.shapes.pushBack( shape );
    actorDesc.body = body;
    NxActor* actor = _scene->createActor( actorDesc );
    _actors[id] = actor;
    }
    
  14. After completing the PhysX interface class, we will have to establish relationships between the physics elements and scene graph nodes, and update the physics world in every frame. In the PhysicsUpdater class, we use a NodeMap data type which records physics ID and OSG node in a key-value map. These IDs also correspond to physics actors in the PhysX layer, so we can actually connect the physics and the rendering layer together here.

    class PhysicsUpdater : public osgGA::GUIEventHandler
    {
    public:
    PhysicsUpdater( osg::Group* root ) : _root(root) {}
    void addGround( const osg::Vec3& gravity );
    void addPhysicsBox( osg::Box* shape, const osg::Vec3& pos,
    const osg::Vec3& vel, double mass );
    void addPhysicsSphere( osg::Sphere* shape,
    const osg::Vec3& pos, const osg::Vec3& vel, double mass );
    bool handle( const osgGA::GUIEventAdapter& ea,
    osgGA::GUIActionAdapter& aa );
    protected:
    void addPhysicsData( int id, osg::Shape* shape,
    const osg::Vec3& pos, const osg::Vec3& vel, double mass );
    typedef std::map<int, osg::observer_ptr<
    osg::MatrixTransform> > NodeMap;
    NodeMap _physicsNodes;
    osg::observer_ptr<osg::Group> _root;
    
  15. In the addGround() method, we create a huge box with a very small height to simulate the ground, and create the world element in PhysX as well.

    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( new osg::ShapeDrawable(
    new osg::Box(osg::Vec3(0.0f, 0.0f,-0.5f), 100.0f,
    100.0f, 1.0f)) );
    osg::ref_ptr<osg::MatrixTransform> mt =
    new osg::MatrixTransform;
    mt->addChild( geode.get() );
    _root->addChild( mt.get() );
    PhysXInterface::instance()->createWorld( osg::Plane(
    0.0f, 0.0f, 1.0f, 0.0f), gravity );
    
  16. In the addPhysicsBox() method, we create a physics box actor and use addPhysicsData() to register it and create corresponding OSG node in the scene graph.

    int id = _physicsNodes.size();
    PhysXInterface::instance()->createBox(
    id, shape->getHalfLengths(), mass );
    addPhysicsData( id, shape, pos, vel, mass );
    
  17. In the addPhysicsSphere() method, we have similar work to do as in addPhysicsBox().

    int id = _physicsNodes.size();
    PhysXInterface::instance()->createSphere(
    id, shape->getRadius(), mass );
    addPhysicsData( id, shape, pos, vel, mass );
    
  18. The handle() method has two events to handle. If the FRAME event comes, it will update the physics world with a delta time value. Then all IDs registered in the NodeMap variable will be traversed to retrieve matrix data from the physics element and apply it to scene graph nodes. And if user presses the Return key, the updater will create a new dynamic ball at the eye position with an initial velocity. So we can interactively shoot at any other element in the scene and see how they collapse, roll, and fly off.

    osgViewer::View* view = static_cast<osgViewer::View*>( &aa );
    if ( !view || !_root ) return false;
    switch ( ea.getEventType() )
    {
    case osgGA::GUIEventAdapter::KEYUP:
    if ( ea.getKey()==osgGA::GUIEventAdapter::KEY_Return )
    {
    osg::Vec3 eye, center, up, dir;
    view->getCamera()->getViewMatrixAsLookAt( eye, center, up );
    dir = center - eye; dir.normalize();
    addPhysicsSphere( new osg::Sphere(osg::Vec3(), 0.5f),
    eye, dir * 60.0f, 2.0 );
    }
    break;
    case osgGA::GUIEventAdapter::FRAME:
    PhysXInterface::instance()->simulate( 0.02 );
    for ( NodeMap::iterator itr=_physicsNodes.begin();
    itr!=_physicsNodes.end(); ++itr )
    {
    osg::Matrix matrix = PhysXInterface::instance()-
    >getMatrix(itr->first);
    itr->second->setMatrix( matrix );
    }
    break;
    default: break;
    }
    return false;
    
  19. The protected addPhysicsData() method will add a newly allocated OSG node to the root, and set its ID and physics attributes as well.

    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable( new osg::ShapeDrawable(shape) );
    osg::ref_ptr<osg::MatrixTransform> mt =
    new osg::MatrixTransform;
    mt->addChild( geode.get() );
    _root->addChild( mt.get() );
    PhysXInterface::instance()->setMatrix(
    id, osg::Matrix::translate(pos) );
    PhysXInterface::instance()->setVelocity( id, vel );
    _physicsNodes[id] = mt;
    
  20. Now we will get into the main entry, first we will create the root node and the updater object.

    osg::ref_ptr<osg::Group> root = new osg::Group;
    osg::ref_ptr<PhysicsUpdater> updater = new PhysicsUpdater(
    root.get() );
    
  21. And then we create the ground and a wall made up of many small boxes and add them to the updater (and to the _root node which is stored in the updater class).

    updater->addGround( osg::Vec3(0.0f, 0.0f,-9.8f) );
    for ( unsigned int i=0; i<10; ++i )
    {
    for ( unsigned int j=0; j<10; ++j )
    {
    updater->addPhysicsBox( new osg::Box(osg::Vec3(), 0.99f),
    osg::Vec3((float)i, 0.0f, (float)j+0.5f), osg::Vec3(),
    1.0f );
    }
    }
    
  22. Start the viewer at last:

    osgViewer::Viewer viewer;
    viewer.addEventHandler( updater.get() );
    viewer.setSceneData( root.get() );
    return viewer.run();
    
  23. Now we can run the application and adjust the view matrix by dragging mouse buttons. First we can see a wall stand on the ground. It is obviously made up of many boxes, and may even shake slightly, but won't fall down without an outside force.

    How to do it...
  24. Aim with your eye and press the Return key to shoot a ball! Can you knock and smash the wall with only one hit?

    How to do it...
  25. Let us have a look at the ruins we finally created. Are you imagining making an 'Angry Birds' game by yourself?

How to do it...

How it works...

NVIDIA PhysX is a powerful and legible physics library, and so it is easy to embed it into OSG scene. The only data shared by the physics level and the rendering level is the unique object IDs.

The meaning of an ID in the PhyxXInterface class is to identify a unique rigid element, for instance, a rigid box, a sphere, or the ground plane. They are not visible on the screen but can be used to carry out physics computation and calculate the resultant positions and rotations.

The same ID in the PhysicsUpdater class is used to distinguish scene nodes. They use the same dimensions as the physics elements, but are used for rendering purpose only. This design separates the rendering and physics computing work so that there will be less coupling in the application.

There's more...

You may use the same idea to design your own physics integrations. There are many other choices such as ODE (http://www.ode.org/), Bullet Physics (http://bulletphysics.org/), Newton (http://newtondynamics.com/), and so on. Consider using one or more classes to encapsulate the physics functionalities and don't merge them into the scene graph rashly. A mixure of rendering objects and physics objects in the project may cause confusion for reading and understanding.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset