A single particle

In this section, we will create a project that will model and draw one particle. It will be represented by our custom C++ class, Particle.

The best C++ programming style suggests declaring and implementing each new class in separate .h and .cpp files (in our case, it should be Particles.h and Particles.cpp) because it improves readability and reusability of the code. But for simplicity, we will declare and implement all the classes of the example only in testApp.h and testApp.cpp files respectively.

Note

This is example 03-Particles/01-SingleParticle.

The example is based on the emptyExample project in openFrameworks. In the testApp.h file, after the #include "ofMain.h" line, add the following declaration of a Particle class:

class Particle {
public:
  Particle();                //Class constructor
  void setup();              //Start particle
  void update( float dt );   //Recalculate physics
  void draw();               //Draw particle

  ofPoint pos;               //Position
  ofPoint vel;               //Velocity
  float time;                //Time of living
  float lifeTime;            //Allowed lifetime
  bool live;                 //Is particle live
};

The notable thing here is that the update() function has a parameter dt . This is a time step, that is, the time in seconds between the current and the previous callings of this function. This parameter will be used for physics computing.

The particle holds the following attributes: position (pos), velocity (vel), and time from when it's born (time). Other attributes—color and size—will be calculated based on the time value. The lifeTime is a constant value, meaning the maximal time of living for the particle; when time is greater than lifeTime, the particle dies, that is, it becomes inactive. The live value holds the current state of a particle's activity—is it live (true) or not (false)? An inactive (dead) particle is not updated and not being drawn.

Before implementing methods of this class, we need to represent the control parameters for particles that fly. Each parameter should be accessible by all the particles, and also can be changed in the testApp class. The simplest way to achieve this is by using a global variable. Also, it is better not to use many global variables, and hence we combine all the parameters in a separate class and declare just one global variable.

Control parameters

Let's discuss the control parameters we should use. We want our particle to be born inside a circular area; it means we have circular emitter, with the center eCenter and a radius eRad. A particle will start moving with its initial random velocity limited by some value (velRad):

Control parameters

A particle has a limited lifetime (lifeTime). Also, we want to have a possibility to rotate its velocity vector with a constant speed (rotate).

As a result, we obtain the following control parameters' class declaration, which you should add in the testApp.h file:

class Params {
public:
  void setup();
  ofPoint eCenter;    //Emitter center
  float eRad;         //Emitter radius
  float velRad;       //Initial velocity limit
  float lifeTime;     //Lifetime in seconds

  float rotate;   //Direction rotation speed in angles per second
};

extern Params param;  //Declaration of a global variable

The last line declares param as a global variable using the extern C++ keyword. It means that param is accessible in each C++ file, which includes the testApp.h file. Though in our example, it is not necessary (we will use param just in the testApp.cpp file); this method of defining global variables can be useful if you extend this project further.

Note, the extern Params param; line is just a declaration but not a definition of param. For successful compiling, we must add the Params param; line in the testApp.cpp file. Also, we should define the Params::setup() function, which sets the initial values for control parameters:

Params param;        //Definition of global variable

void Params::setup() {
  eCenter = ofPoint( ofGetWidth() / 2, ofGetHeight() / 2 );
  eRad = 50;
  velRad = 200;
  lifeTime = 1.0;

  rotate = 90;
}

Now, we are ready to define all the functions for the Particle class.

Defining the particle functions

In the testApp.cpp file, add the constructor of the Particle class:

Particle::Particle() {
  live = false;
}

It has no parameters, so this is a default constructor of the Particle class. In C++, such constructors are called automatically when an object of a corresponding class is created. In our case, the constructor just sets the live value to false. It means that all created particles will be inactive by default. To make them start flying, we need to directly call their setup() function.

Before defining the Particle::setup() function, we insert an additional function randomPointInCircle() definition, which returns a random vector lying in a circle with center (0, 0) and radius maxRad:

ofPoint randomPointInCircle( float maxRad ){
  ofPoint pnt;
  float rad = ofRandom( 0, maxRad );
  float angle = ofRandom( 0, M_TWO_PI );
  pnt.x = cos( angle ) * rad;
  pnt.y = sin( angle ) * rad;
  return pnt;
}

We will use this function for initializing a particle's position and velocity. Though the randomPointInCircle( maxRad ) function returns a random vector inside a circle, the resultant probability distribution is not uniform (when maxRad is greater than zero). For our example, such nonuniformity is not important but is interesting.

Now we define the Particle::setup() function. It initializes all the parameters and sets the value of live to true so the particle becomes active and begins to fly:

void Particle::setup() {
  pos = param.eCenter + randomPointInCircle( param.eRad );
  vel = randomPointInCircle( param.velRad );
  time = 0;
  lifeTime = param.lifeTime;
  live = true;
}

This function uses all the control parameters held in a param object, except the velRotate value. This value will be used in the Particle::update() function, so a user can change this parameter dynamically and it will affect the particle system.

Next, the Particle::update() function's code checks whether the particle is active and then rotates the velocity vector, updates the position, and checks the particle's lifetime. The input parameter dt is a time step:

void Particle::update( float dt ){
  if ( live ) {
      //Rotate vel
      vel.rotate( 0, 0, param.rotate * dt );

      //Update pos
      pos += vel * dt;    //Euler method

      //Update time and check if particle should die
      time += dt;
      if ( time >= lifeTime ) {
          live = false;   //Particle is now considered as died
      }
  }
}

The first notable thing here is how we rotate the vel vector using the vel.rotate() function. This function performs rotation of vel, considered as a vector in 3D space, by specifying three parameters as rotation angles in x, y, and z axes respectively. So, in the code, we rotate in the z axis only; therefore, vel rotates just in the xy plane. This is exactly what we need.

The second thing to mention is the use of the Euler method for updating the position using velocity.

Note

The Euler method is a popular method used for an approximate integration. It states that for the given continuous functions, f( t ) and g( t ), if f( t ) is equal to g'( t ), and f( t0 ) is given, we can use the following formula for an approximate computing of f( t0 + dt ):

f( t0 + dt ) = f( t0 ) + g( t0 )· dt

In our case, velocity is derivative of position. Following the Euler method, if we know the current position pos of a particle, after dt seconds, it will be equal to the sum of pos and vel multiplied by dt (( pos + vel ) * dt). We don't care about the previous pos values, so just replace the pos value with a new one as follows:

pos += vel * dt;

See more information on the Euler method at en.wikipedia.org/wiki/Euler_method. There is another popular integration method, which is more accurate than the Euler method and often used for particles' physics computing. It is called the Verlet integration; see en.wikipedia.org/wiki/Verlet_integration for further details.

Finally, we define the body of the drawing function Particle::draw().This function checks whether the particle is active and then computes the size and color of a particle in dependence of time. During its lifetime, the size increases from 1 to 3 and then decreases back, and the color hue is constantly changing. The particle is rendered as a circle:

void Particle::draw(){
  if ( live ) {
      //Compute size
      float size = ofMap( 
          fabs(time - lifeTime/2), 0, lifeTime/2, 3, 1 );

      //Compute color
      ofColor color = ofColor::red;
      float hue = ofMap( time, 0, lifeTime, 128, 255 );
      color.setHue( hue );
      ofSetColor( color );

      ofCircle( pos, size );  //Draw particle
  }
}

We specify the Particle and Params classes and now use them in the project.

Implementing a particle in the project

Let's implement one particle object in the project's testApp class. Also, we will add a possibility for particles to leave trails that will slowly disappear. We will implement it using the offscreen buffer FBO (see the Using FBO for offscreen drawing section in Chapter 2, Drawing in 2D).

In the testApp.h file, in the testApp class declaration, add the following declarations:

Particle p;           //Particle
ofFbo fbo;            //Offscreen buffer for trails
float history;        //Control parameter for trails
float time0;          //Time value for computing dt

The history variable will take values in the range [0, 1]. It controls the decaying time of the trails. Value 0.0 means that trails disappear immediately (so there are no trails), and value 1.0 means that trails are infinite. The dependence between history and trails' length is nonlinear; trails are slightly visible when history is about 0.5, and trails become long only when history is more than 0.8.

In the testApp.cpp file, fill the body of the testApp::setup() function with the following code, which sets up buffer and parameters:

void testApp::setup(){
  ofSetFrameRate( 60 );    //Set screen frame rate

  //Allocate drawing buffer
  int w = ofGetWidth();
  int h = ofGetHeight();
  fbo.allocate( w, h, GL_RGB32F_ARB );

  //Fill buffer with white color
  fbo.begin();
  ofBackground(255, 255, 255);
  fbo.end();

  //Set up parameters
  param.setup();          //Global parameters
  history = 0.995;

  time0 = ofGetElapsedTimef();
}

The notable part here is the last parameter of the fbo.allocate() function calling, namely, GL_RGB32F_ARB.

Note

The code to call fbo.allocate() with the last optional argument GL_RGB32F_ARB is as follows:

fbo.allocate( w, h, GL_RGB32F_ARB );

The preceding line of code means that fbo will hold the pixel color components as the float values. This is a much more accurate representation of colors than what we find in the default mode (in which pixels' components are the unsigned char values). It is unimportant when we use fbo just for accumulating drawings. But when we are gradually erasing the buffer's content, the accuracy of the unsigned char values is insufficient and leads to visual artifacts.

Note, the float fbo occupies four times more video memory. Also, it may not work on old or integrated video cards. In case of problems, you can allocate fbo using the ordinary method fbo.allocate( w, h ), though, the picture with trails will not be so clean and perfect.

The testApp::update() function computes dt, activates a particle if it is not alive, and updates the particle state:

void testApp::update(){
    //Compute dt
    float time = ofGetElapsedTimef();
    float dt = ofClamp( time - time0, 0, 0.1 );
    time0 = time;
    
    //If the particle is not active - activate it
    if ( !p.live ) {
         p.setup();
    }

    //Update the particle
    p.update( dt );
}

The dt is a time step value that is computed as a time difference between the current time and time of previous calling of the update() function. We use the ofClamp() function for limiting its value by 0.1. The reason for this is that sometimes time - time0 can be a large value. (For example, if the user drags the window or hides the application's window, update() callings can be paused—it depends on the operating system.) So if we don't limit this, formulas in the Euler method will work in an unstable way, and the model will literally explode.

The testApp::draw() function performs drawing in the fbo buffer and then draws it on the screen:

void testApp::draw(){
  ofBackground( 255, 255, 255 );  //Set white background

  //1. Drawing to buffer
  fbo.begin();

  //Draw semi-transparent white rectangle
  //to slightly clearing a buffer (depends on history value)

  ofEnableAlphaBlending();         //Enable transparency

  float alpha = (1-history) * 255;
  ofSetColor( 255, 255, 255, alpha );
  ofFill();
  ofRect( 0, 0, ofGetWidth(), ofGetHeight() );

  ofDisableAlphaBlending();        //Disable transparency

  //Draw the particle
  ofFill();
  p.draw();

  fbo.end();

  //2. Draw buffer on the screen
  ofSetColor( 255, 255, 255 );
  fbo.draw( 0, 0 );
}

Note that drawing in buffer consists of two steps: slightly erasing the current buffer's content (level of erasing depends on the history value) and drawing the particle. Erasing is performed by drawing a semitransparent white rectangle in the buffer. For achieving it, we enable working with transparency by calling the ofEnableAlphaBlending() function, and after that we disable it by calling ofDisableAlphaBlending(). See details on working with transparency in the Transparency section in Chapter 4, Images and Textures.

Run the project. It will activate the single particle; this particle will fly and get deactivated when its lifeTime exceeds param.lifeTime, which is 1.0 seconds. So each second particle will be activated in a random place with random velocity. The buffer keeps the trails, so you will see a picture as shown in the following screenshot:

Implementing a particle in the project

Notice that the old trails gradually disappear. Also, notice that the particle's trajectories are curvilinear because its velocity vector rotates (due to the rotate parameter), and the particle changes its color from aqua to red. You can play with the control parameters and see how it affects the particle's behavior.

Now, let's add to the project the capability to working with many particles.

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

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