Version 2 (modified by osg, 7 years ago)

Added menu

Adding Billboards to a Scene

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

Goal

Add billboards to a scene to simulate desert shrubs.

Overview

Billboards are nodes that rotate about a user defined axis (or point) to face a given direction. Typically billboards that rotate about the up (Z) axis and face the viewer are used to simulate trees. Online documentation for the billboard class can be found here. The example with the osg distribution (example osgBillboard) demonstrates a variety of billboard modes by specifying an axis of rotation and normal (direction to face). This tutorial creates tree-style billboards - ones that rotate about the up axis and always face the viewer.

The Billboard Class (osg::Billboard) - contains geometry that can be rotated about an axis or point to face a specific direction. The billboard class is derived from the geode class. This means that instances derived from the drawable class (including geometry) can be added to a billboard class object. This allows us to attach geometry that can be drawn to our billboard. Since the billboard class is derived from the geode class, we can also associate StateSets with them.
In addition to the methods and members inherited from the Geode class, the billboard class has members and methods to control the feature specific to billboards: whether it rotates about a point or an axis; and if it rotates about an axis, what direction it should face. To control the type of billboard, use the setMode(osg::Billboard::Mode) method. Valid arguments are POINT_ROT_EYE (rotate about a point, relative to the eye position), POINT_ROT_WORLD (rotate about a point, relative to world coordinates), AXIAL_ROT (rotate about an axis.) If the mode AXIAL_ROT is specified users can then set the axis about which the billboard will rotate using the setAxis(osg::Vec3) method. They can also define the direction the billboard will face using the setNormal(osg::Vec3) method. To help place billboards there is an overload to osg::Geode's addGeometry method. This method takes two arguments: a drawable instance and an osg::Vec3 instance for position.
One final consideration for creating billboards: since the geometry will rotate to face the viewer using normals for lighting calculations will result in strange behavior. (Lighting can change abruptly as the viewpoint changes.) To create billboards that appear the same from every direction we need to make sure lighting is disabled for billboards. With all this in mind, the basic steps to add some billboards to a scene are:

  • Create a billboard instance and add it to the scene
  • Create a stateset with lighting disabled
  • Create geometry (applying the stateset above) and add it to the billboard

The code

Required includes:

    #include <osg/Geometry>
    #include <osg/Texture2D>
    #include <osg/Billboard>
    #include <osg/BlendFunc>
    #include <osgDB/Registry>
    #include <osgDB/ReadFile>
    #include <osgProducer/Viewer>
    #include <osg/PositionAttitudeTransform>

First, a convience function to create geometry to associate with our billboard. The function will take two arguments - a float representing the scale factor, and a pointer to a state that we will apply to the geometry instance we create. The function will return a pointer to the geometry. We'll use a QUAD for the geometry. The method for specifying vertex and texture coordinate arrays for the QUAD can be found here. The only special consideration in creating this geometry is that we want it to be centered relative to the axis of rotation. The function definition is as follows:

    osg::Drawable* createShrub(const float & scale, osg::StateSet* bbState)
    {
       float width = 1.5f;
       float height = 3.0f;

       width *= scale;
       height *= scale;

       osg::Geometry* shrubQuad = new osg::Geometry;

       osg::Vec3Array* shrubVerts = new osg::Vec3Array(4);
       (*shrubVerts)[0] = osg::Vec3(-width/2.0f, 0, 0);
       (*shrubVerts)[1] = osg::Vec3( width/2.0f, 0, 0);
       (*shrubVerts)[2] = osg::Vec3( width/2.0f, 0, height);
       (*shrubVerts)[3] = osg::Vec3(-width/2.0f, 0, height);

       shrubQuad->setVertexArray(shrubVerts);

       osg::Vec2Array* shrubTexCoords = new osg::Vec2Array(4);
       (*shrubTexCoords)[0].set(0.0f,0.0f);
       (*shrubTexCoords)[1].set(1.0f,0.0f);
       (*shrubTexCoords)[2].set(1.0f,1.0f);
       (*shrubTexCoords)[3].set(0.0f,1.0f);
       shrubQuad->setTexCoordArray(0,shrubTexCoords);

       shrubQuad->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4));

       // Need to assign a color to the underlying geometry, otherwise we'll get
       // whatever color is current applied to our geometry.
       // Create a color array, add a single color to use for all the vertices

       osg::Vec4Array* colorArray = new osg::Vec4Array;
       colorArray->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); // white, fully opaque

       // An index array for assigning vertices to colors (based on index in the array)
       osg::TemplateIndexArray
          <unsigned int, osg::Array::UIntArrayType,4,1> *colorIndexArray;
       colorIndexArray = 
          new osg::TemplateIndexArray<unsigned int, osg::Array::UIntArrayType,4,1>;
       colorIndexArray->push_back(0);

       // Use the index array to associate the first entry in our index array with all 
       // of the vertices.
       shrubQuad->setColorArray( colorArray);
       shrubQuad->setColorIndices(colorIndexArray);
       shrubQuad->setColorBinding(osg::Geometry::BIND_OVERALL);

       shrubQuad->setStateSet(bbState); 

       return shrubQuad;
    }

Next, the code to set up the scene and run a simulation loop. The code related to billboards is highlighted in red. This code relies on the function we defined above to add drawables (QUADS) to a billboard instance. (Since the billboard class is derived from the geode class, the billboard can contain drawables.) This code also sets the axis of rotation and direction the billboards will face.

    int main( int argc, char **argv )
    {
       osgProducer::Viewer viewer;

       osg::Group* rootNode = new osg::Group();
       osg::Node* tankNode = NULL;
       osg::Node* terrainNode = NULL;
       osg::PositionAttitudeTransform* tankXform = 
          new osg::PositionAttitudeTransform();

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

       tankNode = osgDB::readNodeFile("T72-tank_des.flt");
       terrainNode = osgDB::readNodeFile("JoeDirt.flt");
       if (! (tankNode && terrainNode))
       {
          std::cout << "Couldn't load models, quitting." << std::endl;
          return -1;
       }
       rootNode->addChild(terrainNode);
       rootNode->addChild(tankXform);
       tankXform->addChild(tankNode);

       tankXform->setPosition( osg::Vec3(10,10,8) );
       tankXform->setAttitude( 
          osg::Quat(osg::DegreesToRadians(-45.0), osg::Vec3(0,0,1) ) );
       viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);

       viewer.setSceneData(rootNode);
       viewer.realize();

Create the billboards and set them up. We want these billboards to rotate about the up axis (0,0,1) and always face the viewer. To get them to face the viewer we'll define their normals as facing the screen (0,-1,0)

       osg::Billboard* shrubBillBoard = new osg::Billboard();
       rootNode->addChild(shrubBillBoard);

       shrubBillBoard->setMode(osg::Billboard::AXIAL_ROT);
       shrubBillBoard->setAxis(osg::Vec3(0.0f,0.0f,1.0f));
       shrubBillBoard->setNormal(osg::Vec3(0.0f,-1.0f,0.0f));

Here's the code to set up the state we want to apply to our QUADS. We'll use alpha blending so our simple quads will look like they have complex underlying geometry.

    osg::Texture2D *ocotilloTexture = new osg::Texture2D;
    ocotilloTexture->setImage(osgDB::readImageFile("images\\ocotillo.png"));

    osg::AlphaFunc* alphaFunc = new osg::AlphaFunc;
    alphaFunc->setFunction(osg::AlphaFunc::GEQUAL,0.05f);

    osg::StateSet* billBoardStateSet = new osg::StateSet;

    billBoardStateSet->setMode( GL_LIGHTING, osg::StateAttribute::OFF );
    billBoardStateSet->setTextureAttributeAndModes
          (0, ocotilloTexture, osg::StateAttribute::ON );
    billBoardStateSet->setAttributeAndModes
          (new osg::BlendFunc, osg::StateAttribute::ON );
    osg::AlphaFunc* alphaFunction = new osg::AlphaFunc;
    alphaFunction->setFunction(osg::AlphaFunc::GEQUAL,0.05f);
    billBoardStateSet->setAttributeAndModes( alphaFunc, osg::StateAttribute::ON );

Next we need to create some drawables and add them to our billboard instance. We'll use the function we defined above to create the geometry; use the Billboard class' overload of the addDrawable method to add and position the QUADS.

    // Create some drawables of varying scale, assign the state we created 
    // above to the drawables.
    osg::Drawable* shrub1Drawable = createShrub( 1.0f, billBoardStateSet);
    osg::Drawable* shrub2Drawable = createShrub( 2.0f, billBoardStateSet);
    osg::Drawable* shrub3Drawable = createShrub( 1.2f, billBoardStateSet);

    // Add these drawables to our billboard at various positions 
    shrubBillBoard->addDrawable( shrub1Drawable , osg::Vec3(12,3,8) );
    shrubBillBoard->addDrawable( shrub2Drawable , osg::Vec3(10,18,8));
    shrubBillBoard->addDrawable( shrub3Drawable , osg::Vec3(6,10,8) )

The remainder of the code is our usual

       while( !viewer.done() )
       {
          // wait for all cull and draw threads to complete.
          viewer.sync();
          // update the scene by traversing it with the update visitor which will
          // call all node update callbacks and animations.
          viewer.update();
          // fire off the cull and draw traversals of the scene.
          viewer.frame(); 
       }

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