Version 3 (modified by osg, 6 years ago)

--

Adding osgParticle Effects to a Scene

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

Goal

Add a customized osgParticle instance to a scene to simulate the dust cloud generated by a tank model traversing the terrain.

Overview

Adding particle effects can improve the overall appearance and realism of a simulation. Particle engines are typically used to simulate smoke, fire, dust and other similar effects. To add particle effects to an osg scene, we'll use the following classes:

Particle System (osgParticle::ParticleSystem) - holds a set of particles and manages their creation, update, rendering and destruction. The particle system is derived from the Drawable class. Rendering of particles can be controlled as with other drawable instances - by manipulating StateAttributes. A convience function is provided to let users manipulate three common state attributes. The method setDefaultAttributes allows users to specify a texture (or pass the null string to disable texturing), enable/disable additive blending, and enable/disable lighting.

Particle (osgParticle::Particle) - the basic unit of a particle system. Particles have both physical and graphic properties. A particle can be any of the following shapes: POINT, QUAD, QUAD_TRIPSTRIP, HEXAGON, LINE. Each particle has a life-span associated with it. The life span is the number of seconds each particle will exist. (Particles assigned a negative life span will exist indefinitely.) All particles have properties of SIZE, ALPHA, and COLOR. For each of these properties programmers can assign minimum and maximum values. As part of life-span management a particle system controls rendering of individual particles by varying these properties from their minimum to maximum values over their life span. (Linear interpolation from min to max based on ratio of elapsed time to life span.) Programmers can also specify their own method of interpolation between minimum and maximum values. (see osgParticle::Interpolator for details.)

Placer (osgParticle::Placer) - sets the initial position of a particle. There are a number of pre-defined placers or users can define their own. Existing types of placers include: PointPlacer (all particles originate from the same point) SectorPlacer (users describe a center point, radius range and range of angles. All particles originate from this sector) and a MultiSegmentPlacer (users specify a list of vertices; particles originate along the line segments these vertices describe.)

Shooter (osgParticle::Shooter) - assigns the initial velocity to particles. The RadialShooter allows users to specify a velocity range in meters per second and direction in radians. Direction is specified by providing two angles: theta - relative to the Z axis and phi - relative to the XY plane.

Counter (osgParticle::Counter) - controls how many particles should be created each frame. The RandomRateCounter allows users to specify the minimum and maximum number of particles that should be created each frame.

ModularEmitter (osgParticle::ModularEmitter) - for convenience a modular emitter contains a counter, a placer, and a shooter. This provides the user a single mechanism for controlling many of the elements of a particle system.

ParticleSystemUpdater (osgParticle::ParticleSystemUpdater) - used to automatically update particles. When part of a scene, on cull traversal calls the update method for all 'alive' particles.

ModularProgram (osgParticle::ModularProgram) - during a lifespan of an individual particle, users can control their position using a ModularProgram instance. ModularPrograms are composed by adding Operators to a program instance.

Operator (osgParticle::Operator) - provides a way to control the per-frame behavior of a particle over its lifespan. Users can vary parameters of existing Operator class instances or define their own Operator classes. Existing Operator classes include: AccelOperator (applies constant acceleration), AngularAccelOperator (applies constant angular acceleration), FluidFrictionOperator (calculates based on motion through fluid of specific Density and Viscosity) and ForceOperator (applies a constant force.)

The code

To create a highly customized particle system using the classes above can include the following steps. (All steps in parenthesis are optional and can be ommitted for a more basic particle system.)

  • Create a particle system instance and add it to the scene
  • (Set the particle system's state attributes)
  • Create a particle and associate it with the particle system
  • (Set parameters of the particle)
  • Create a particle system updater, associate it with our particle system and add it to the scene
  • (Create a modular emitter to customize: )
    • Counter - number of particles to create each frame
    • Placer - describes where particles originate
    • Shooter - assigns initial velocities
  • (Associate modular emitter with the particle system)
  • (Create a ModularProgram instance to control the particles over their lifespan)
    • Create and customize Operators
    • Add Operators to our modular program

The code to complete these steps follows. First we'll build a basic tank and terrain world. (We'll add the tank to the scene later.)

    osg::Group* rootNode = new osg::Group();

    osg::Node* terrainNode = new osg::Node();
    osgViewer::Viewer viewer;

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

    osg::Node* tankNode = osgDB::readNodeFile("\\Models\\T72-Tank\\T72-tank_des.flt");
    if ( ! tankNode)
    {
       std::cout << "no tank" << std::endl;
       return -1;
    }

The basic steps to a particle system: Create the particle system, an updater, and particles; setup the scene.

    // Create and initialize a particle system
    osgParticle::ParticleSystem *dustParticleSystem = new osgParticle::ParticleSystem;

    // Set the attributes 'texture', 'emmisive' and 'lighting'
    dustParticleSystem->setDefaultAttributes(
    "C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\images\\dust2.rgb", 
    false, false);

    // Since the particle system is derived from the class Drawable, we can create
    // add it to the scene as a child of a geode 
    osg::Geode *geode = new osg::Geode; 

    rootNode->addChild(geode);
    geode->addDrawable(dustParticleSystem);

    // Add an 'updater' to help per-frame management
    osgParticle::ParticleSystemUpdater *dustSystemUpdater = new osgParticle::ParticleSystemUpdater;

    // Associate this updater with our particle system 
    dustSystemUpdater->addParticleSystem(dustParticleSystem);
    // add the updater node to the scene graph
    rootNode->addChild(dustSystemUpdater);

    // Create a partical to be used by our particle system and define a few
    // of its properties
    osgParticle::Particle smokeParticle; 
    smokeParticle.setSizeRange(osgParticle::rangef(0.01,20.0)); // meters

    smokeParticle.setLifeTime(4); // seconds
    smokeParticle.setMass(0.01); // in kilograms
    // Make this our particle system's default particle 
    dustParticleSystem->setDefaultParticleTemplate(smokeParticle);

This section of code uses the modular emitter class to set some of basic parameters for our particles: number of paricles created each frame, location of new particles, and velocity of new particles.

    // Create a modular emitter (this contains default counter, placer and shooter.)
    osgParticle::ModularEmitter *emitter = new osgParticle::ModularEmitter;

    // Associate this emitter with the particle system 
    emitter->setParticleSystem(dustParticleSystem);

    // Get a handle to the emitter's counter and adjust the number of new particles
    // that will be added each frame
    osgParticle::RandomRateCounter *dustRate = 

    static_cast<osgParticle::RandomRateCounter *>(emitter->getCounter());
    dustRate->setRateRange(5, 10); // generate 5 to 10 particles per second

    // To customize the placer, create and initialize a multi-segment placer 

    osgParticle::MultiSegmentPlacer* lineSegment = new osgParticle::MultiSegmentPlacer();
    // Add vertices to our placer. This defines line seqments along which our particles will
    // originate. (If we co-locate a tank and this emitter, this will result in dust particles
    // originating from a line extending below and behind the tank model.) 

    lineSegment->addVertex(0,0,-2);
    lineSegment->addVertex(0,-2,-2);
    lineSegment->addVertex(0,-16,0);

    // Use this placer for our emitter 
    emitter->setPlacer(lineSegment);

    // To customize the shooter, create and initialize a radial shooter 
    osgParticle::RadialShooter* smokeShooter = new osgParticle::RadialShooter();

    // Set properties of this shooter 
    smokeShooter->setThetaRange(0.0, 3.14159/2); // radians, relative to Z axis.
    smokeShooter->setInitialSpeedRange(50,100); // meters/second
    // Use this shooter for our emitter

    emitter->setShooter(smokeShooter);

Here we add the emitter and a tank model to the scene under a transform node. Both the emitter and the tank will be positioned by the transform. The custom placer created above will position particles relative to this tranform.

    osg::MatrixTransform * tankTransform = new osg::MatrixTransform(); 
    tankTransform->setUpdateCallback( new orbit() ); // Callback that drives in a circle 

    // Add the emitter and the tank as children of this transform 
    tankTransform->addChild(emitter);
    tankTransform->addChild(tankNode);
    rootNode->addChild(tankTransform);

This section of code creates a modular program instance to control how particles are updated during their lifespan. The modular program applies operators to particles in the order the operators are added to the program.

    // Create a modular program and attach it to our particle system
    osgParticle::ModularProgram *moveDustInAir = new osgParticle::ModularProgram;
    moveDustInAir->setParticleSystem(dustParticleSystem);

    // Create an operator that simulates gravity, adjust it and add it to our program
    osgParticle::AccelOperator *accelUp = new osgParticle::AccelOperator;
    accelUp->setToGravity(-1); // scale factor for normal acceleration due to gravity. 
    moveDustInAir->addOperator(accelUp);

    // Add an operator to our program to calculate the friction of air.
    osgParticle::FluidFrictionOperator *airFriction = new osgParticle::FluidFrictionOperator;
    airFriction->setFluidToAir();

    //airFriction->setFluidDensity(1.2929/*air*/*5.0f);
    moveDustInAir->addOperator(airFriction);

    // Finally, add the program to the scene 
    rootNode->addChild(moveDustInAir);

The remaining code is the usual simulation loop.

    // add a viewport to the viewer and attach the scene graph.
    viewer.setSceneData(rootNode);

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

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

    return 0;