delta3d

Particle Systems Manipulation

This tutorial is a followup to the GM Tutorial, and will teach you how to manipulate particle systems through code.

Note*- this is for version 1.3 of the delta3d code base.

Introduction:

All particle effects can be made this way through the PSE, and loaded on models for effects. Currently, the only control you have is to turn the emitter on and off. Having control over our particles allows users to feel more immersed in the game or simulation. Anything such as wind speed, to temperature, to even a sound presence could trigger an event causing our particles to act differently. This tutorial will allow you to do just that, such as changing colors, velocity’s, lifetimes, mass, and so much more!

For this demonstration we will be changing the particle trail behind the tank according to the tanks speed. This will make the dust particles stay up in the air longer when you’re driving faster, just like in real life! You should already have the particle effect we’ll be using from the previous tutorial. This is the particle editor that we use to create our effects with.


Particle Know How:

How do we get at our particle data?

Note: The following code is wrapped up already in dtCore::ParticleSystem SetupLayers function. This is to show how to access this and give you a better sense to what we are getting to. Once the particle is initialized the user will be dealing with each of the separate layers through the GetSingleLayer(“Layer 0”), function call.

Getting to the particle data requires us to work directly with the OpenSceneGraph(OSG) code. Once in the OSG code, we will search through the particles systems, find the one we want to change, and change it. Might be easier said then done, so let’s jump in!

First we will declare a class that will hold all of our particle info, for each layer of our particle system. The class will hold the following member variables that are the meat of the particle system class. This class will be called ParticleLayer

From dtCore::ParticleSystem (h and cpp)


/**
* The geode that holds the drawable particle system, and whose name is
* the name of the layer.
*/
RefPtr<osg::Geode> mGeode;

/**
* The active particle system.
*/
RefPtr<osgParticle::ParticleSystem> mParticleSystem;

/**
* The transform that controls the position of the emitter.
*/
RefPtr<osg::MatrixTransform> mEmitterTransform;

/**
* The active emitter.
*/
RefPtr<osgParticle::ModularEmitter> mModularEmitter;

/**
* The active program.
* Can be modular or fluid
*/
RefPtr<osgParticle::Program> mProgram;

/**
* Name of the layer
*/
std::string mstrLayerName;

All of the above variables are filled in during the SetupParticleLayers functions. They each serve a purpose that you can manipulate to adjust the particle effect. Filling this class in is not hard from the outside, but the code is not set up in a hierarchal form you would expect.

The following method is called when you create a particle system by the LoadFile method, and never needs to be called directly. Once LoadFile is complete, you can use the ParticleLayer class to change the values you want to be different. You do not need to write this code, but is provided to help you understand the OSG particle system’s guts.

From dtCore::ParticleSystem (h and cpp)


/**
 *  Called from LoadFile() function, should never be called
 *  from user
 */
void ParticleSystem::SetupParticleLayers()
{
   // particle system group of the root note from the loaded particle 
   // system file. will only be valid after you call load file on an 
   // object, thus its protected
   osg::Group* newParticleSystemGroup = (osg::Group*)mLoadedFile.get();

   // node we are going to reuse over and over again to search through 
   // all the children of the osg root node
   osg::Node*  searchingNode = NULL;
   
   // iterating through children var
   unsigned int i = 0;

Note: that we will be looping through all the children twice, this is done once to setup the initial particle system, and then a second time to set the other variables to tie in with it. Its done like this since we could be getting the children in any order, but need to know the particle system layer variable before the others.


   // Not everything has a name.... which sucks. usually only the geode
   // we will bind the geode name to the whole class with newly 
   // created var mstrLayername
   // Osg checks for the same particle system, instead of names
   for(i=0;i<newParticleSystemGroup->getNumChildren();i++)
   {
      searchingNode = newParticleSystemGroup->getChild(i);
      ParticleLayer layer;

      if(dynamic_cast<osgParticle::ParticleSystemUpdater*>(searchingNode))
      {
        // This is when you import multiple osg files in one system. 
        // This wont be done in delta3d since it was set up where 
        // you can only load in 1  per system. Which makes sense. 
        // One for each file could be loaded, to tell the system everything 
        // else is in here.
      }

      // See if this is the particle system of the geode
      if(dynamic_cast<osg::Geode*>(searchingNode))
      {
         // well its a geometry node.
         layer.mGeode = (osg::Geode*)searchingNode;

         // see if the geometry node has drawables that are the 
         // particle system we are looking for
         for(unsigned int j=0;j<layer.mGeode->getNumDrawables();j++)
         {
             osg::Drawable* drawable    = layer.mGeode->getDrawable(j);

             // seems like we found the particle system, continue on!
             if(dynamic_cast<osgParticle::ParticleSystem*>(drawable))
             {
                layer.mParticleSystem = (osgParticle::ParticleSystem*)drawable;
                layer.mstrLayerName     = layer.mGeode->getName();

               // We're done setting values up, push it onto the list
               mlLayers.push_back(layer);
             }
         }
      } // end if                        
   }// end for loop
      
   // we do this in two seperate processes since the top particle 
   // system nodes and the cousins could be in any order.
   for(i=0;i<newParticleSystemGroup->getNumChildren();i++)
   {
      searchingNode = newParticleSystemGroup->getChild(i);

      // Node can't be a matrix and a program
      // reason for if / else if
      if(dynamic_cast<osg::MatrixTransform*>(searchingNode))
      {
         // the transform in space
       osg::MatrixTransform* newEmitterTransform = 
            (osg::MatrixTransform*)searchingNode;

         for(unsigned int j=0;j<newEmitterTransform->getNumChildren();j++)
         {
            osg::Node* childNode = newEmitterTransform->getChild(j);

             if(dynamic_cast<osgParticle::ModularEmitter*>(childNode))
             {
    osgParticle::ModularEmitter* newModularEmitter = 
                   (osgParticle::ModularEmitter*)childNode;
               
               // Go through the populated particle system list and 
               // see where this belongs too.
               for(std::list<ParticleLayer>::iterator layerIter = mlLayers.begin();
               layerIter != mlLayers.end(); layerIter++)
               {
                  // check for pointers, osg comparison
                  if(layerIter->mParticleSystem == newModularEmitter->getParticleSystem())
                  {
                     // set the data in our layer
                     layerIter->mEmitterTransform    = newEmitterTransform;
                     layerIter->mModularEmitter      = newModularEmitter;
                  }
               }
            }
         } // end for loop
      } // end if check

      // particle cant be a fluid and modular program
      // reason for else if/ else if
      else if(dynamic_cast<osgParticle::ModularProgram*>(searchingNode))
      {
         osgParticle::ModularProgram* newModularProgram = (osgParticle::ModularProgram*)searchingNode;
         // Go through the populated particle system list and 
         // see where this belongs too.
         for(std::list<ParticleLayer>::iterator layerIter = mlLayers.begin();
         layerIter != mlLayers.end(); layerIter++)
         {
            // check for pointers, osg comparison
            if(layerIter->mParticleSystem == newModularProgram->getParticleSystem())
            {
               // set the data in our layer
               layerIter->SetModoluarProgram(true);
               layerIter->mProgram  = newModularProgram;
               break;
            }
         } // end for loop
      } // end if check
      // check and see if this is a fluid program
      else if(dynamic_cast<osgParticle::FluidProgram*>(searchingNode))
      {
         osgParticle::FluidProgram* newFluidProgram = (osgParticle::FluidProgram*)searchingNode;
         // Go through the populated particle system list and 
         // see where this belongs too.
         for(std::list<ParticleLayer>::iterator layerIter = mlLayers.begin(); 
               layerIter != mlLayers.end(); layerIter++)
         {
            // check for pointers, osg comparison
            if(layerIter->mParticleSystem == newFluidProgram->getParticleSystem())
            {
               // set the data in our layer
               layerIter->SetFluidProgram(true);
               layerIter->mProgram  = newFluidProgram;
               break;
            }
         } // end for loop
      } // end if check
   } // end for loop
}// end function call

From here, we have a list of all the particle layers loaded in through our OSG file. Each of the effects in the file is on a different ParticleLayer in a std::list. Getting the layer back to change all the effects put in, is as simple as calling GetSingleLayer() and sending in a string of the name you want.

This will return to you a reference to the ParticleLayer, which in turn will give you access to change anything you could want with the particle effects. What we want is in the default Particle.

Hierarchal Overview:

For the most part, everything in our class is under the Particle Processor section. Its a common base interface for those classes which need to do something on particles. Such classes are, for example emitter (particle generation) and program (particle animation). This class holds some properties, like a reference frame and a reference to a ParticleSystem; descendant classes should process the particles taking into account the reference frame, computing the right transformations when needed.

OpenScene Graph Documentation For the Overview

OSG Particle System:

Our osg particle system is mostly a utility class. This class can tell you to freeze the whole system, return the number of particles, determine on what type of surface they are drawn on, etc. Utilizing this system would work great on a game that uses freeze frame / stop animation, and you can freeze your particle system to give you good effects.

OpenScene Graph Documentation For the particle system

The Default Particle:

Let it be known that all Particles are made off of a Template Particle. That is, the settings that were first loaded in through file, make a default Template Particle, that all particles copy from. Through OSG the Default Template is just like any other Particle, except it is used as the template to create each new particle. Now lets move on.

To get access to the default particle we must go through the Particle System and get it from there. Consequently when you’re done, you should be putting the default particle back into the Particle System for all new particles to take advantage of your changes. Since we are returning everything by reference however, in this situation you will not have to feed it back into the system.

The following a list of some of the members you can change on the Default particle.

void setLifeTime (double t) Set the life time of the particle.
void setSizeRange (const rangef &r) Set the minimum and maximum values for polygon size.
void setAlphaRange (const rangef &r) Set the minimum and maximum values for alpha.
void setColorRange (const rangev4 &r) Set the minimum and maximum values for color.
void setSizeInterpolator (Interpolator *ri) Set the interpolator for computing size values.
void setAlphaInterpolator (Interpolator *ai) Set the interpolator for computing alpha values.
void setColorInterpolator (Interpolator *ci) Set the interpolator for computing color values
void setRadius (float r) Set the physical radius of the particle.
void setMass (float m) Set the mass of the particle.
void setPosition (const osg::Vec3 &p) Set the position vector.
void setVelocity (const osg::Vec3 &v) Set the velocity vector.

For A full list of everything on a particle, follow this link:

OpenScene Graph Documentation for a particle class

Once you are done changing values, simply reinsert the particle into the Default particle Template, and all new particles created off that template / emitter will have the desired effects you want. Note that this will not change the current particles you have in effect. Typically, the old particles and new particles blend together naturally.

Modular Emitter:

The modular emitter is used to determine the amount of particles you can make, or how much it has to make to catch up to when you lag by frame time or another abnormality occurs. Changing this amount would give you the effect of spawning more particles or spawning less. This would be great to use on a fire growing and dampening over time.

This emitter has a Shooter and a Placer object contained within. Placers are where the particles are made at, while shooters control where they go after they have been “Placed”.

OpenScene Graph Documentation for a modular emitter

Emitter Transform:

The emitter transform is used to move the emitter around and is the position of it. A way you could use this, is if you had a ship sailing down the river and wanted to create different wave effects from the offset of the ship.

OpenScene Graph Documentation for a transform

Setting the layer:

Since we caught the variable by reference through GetSingleLayer() all changes will be made without calling a set method. However if in your code, you ever need to set the layer back in you can call SetSingleLayer().

Interacts with tank simulation:

Through the tank simulation program, we are going to change the dusty smoke trail attached to the tank, to provide more realism based on the velocity of the tank. In short, we’re going to make the Life Time of the particles short or longer based on the speed of the tank.

On the MoveTheTank function of the tank, I added the following:


//particle fun
if(mDust.valid() && mIsEngineRunning && mVelocity != 0)
{
   // Get the layer we want
   dtCore::ParticleLayer& pLayerToSet = *mDust->GetSingleLayer("Layer 0");
      
   // make a temp var for changing particle default template.
   osgParticle::Particle& defaultParticle = pLayerToSet.GetParticleSystem().getDefaultParticleTemplate();

   // do our funky changes
   float lifetime = max(2.0f, abs(mVelocity) * .4f);
   defaultParticle.setLifeTime(lifetime);
   

   // for some added fun you can add in this line to create lots of 
   // particles, feel free to change the float value around to see 
   // the differences you can make       
   pLayerToSet.GetModularEmitter().setNumParticlesToCreateMovementCompenstationRatio(abs(mVelocity) * 0.10f);    
}

And there you go, now when you move your tank around you will have your particles change according to the speed of your vehicle. Neato!


Here you can find the complete Tutorial Zip From the GM tutorial, you will have to manually add in the particle effects code listed above : Link

Trackback

Trackback URL for this entry: http://delta3dengine.org/trackback.php?id=20060622140124995

No trackback comments for this entry.

About delta3d

delta3d is a game and simulation engine appropriate for a wide variety of simulation and entertainment applications. delta3d uses best-of-breed open source technologies to create a fully integrated game engine and with content creation tools.MORE

Twitter

User Functions





Don't have an account yet? Sign up as a New User!

Lost your password?