Creating a particle system in 2D

In this recipe, we are going to learn how we can build a basic particle system in two dimensions using the Verlet algorithm.

Getting ready

We will need to create two classes, a Particle class representing a single particle, and a ParticleSystem class to manage our particles.

Using your IDE of choice, create the following files:

  • Particle.h
  • Particle.cpp
  • ParticleSystem.h
  • ParticleSystem.cpp

How to do it…

We will learn how we can create a basic particle system. Perform the following steps to do so:

  1. First, let's declare our Particle class in the Particle.h file and include the necessary Cinder files:
    #pragma once
    
    #include "cinder/gl/gl.h"
    #include "cinder/Vector.h"
    
    class Particle{
    };
  2. Let's add, to the class declaration, the necessary member variables – ci::Vec2f to store the position, previous position, and applied forces; and float to store particle radius, mass, and drag.
    ci::Vec2f position, prevPosition;
    ci::Vec2f forces;
    float radius;
    float mass;
    float drag;
  3. The last thing needed to finalize the Particle declaration is to add a constructor that takes the particle's initial position, radius, mass, and drag, and methods to update and draw the particle.

    The following is the final Particle class declaration:

    class Particle{
    public:
    
    Particle( const ci::Vec2f& position, float radius, 
    float mass, float drag );
    
    void update();
    void draw();
    
    ci::Vec2f position, prevPosition;
    ci::Vec2f forces;
    float radius;
    float mass;
    float drag;
    };
  4. Let's move on to the Particle.cpp file and implement the Particle class.

    The first necessary step is to include the Particle.h file, as follows:

    #include "Particle.h"
  5. We initialize the member variables to the values passed in the constructor. We also initialize forces to zero and prevPosition to the initial position.
    Particle::Particle( const ci::Vec2f& position, float radius, float mass, float drag ){
      this->position = position;
      this->radius = radius;
      this->mass = mass;
      this->drag = drag;
      prevPosition = position;
      forces = ci::Vec2f::zero();
    }
  6. In the update method, we need to create a temporary ci::Vec2f variable to store the particle's position before it is updated.
    ci::Vec2f temp = position;
  7. We calculate the velocity of the particle by finding the difference between current and previous positions and multiplying it by drag. We store this value in ci::Vec2f temporarily for clarity.
    ci::Vec2f vel = ( position – prevPosition ) * drag;
  8. To update the particle's position, we add the previously calculated velocity and add forces divided by mass.
    position += vel + forces / mass;
  9. The final steps in the update method are to copy the previously stored position to prevPosition and reset forces to a zero vector.

    The following is the complete update method implementation:

    void Particle::update(){
        ci::Vec2f temp = position;
        ci::Vec2f vel = ( position - prevPosition ) * drag;
        position += vel + forces / mass;
        prevPosition = temp;
        forces = ci::Vec2f::zero();
    }
  10. In the draw implementation, we simply draw a circle at the particle's position using its radius.
    void Particle::draw(){
        ci::gl::drawSolidCircle( position, radius );
    }
  11. Now with the Particle class complete, we need to begin working on the ParticleSystem class. Move to the ParticleSystem.h file, include the necessary files, and create the ParticleSystem class declaration.
    #pragma once
    
    #include "Particle.h"
    #include <vector>
    
    classParticleSystem{
    public:
    
    };
  12. Let's add a destructor and methods to update and draw our particles. We'll also need to create methods to add and destroy particles and finally a std::vector variable to store the particles in this system. The following is the final class declaration:
    Class ParticleSystem{
    public:
      ~ParticleSystem();
    
      void update();
      void draw();
    
      void addParticle( Particle *particle );
      void destroyParticle( Particle *particle );
    
        std::vector<Particle*> particles;
    
    };
  13. Moving to the ParticleSystem.cpp file, let's begin working on the implementation. The first thing we need to do is include the file with the class declaration.
    #include "ParticleSystem.h"
  14. Now let's implement the methods one by one. In the destructor, we iterate through all the particles and delete them.
    ParticleSystem::~ParticleSystem(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it!= particles.end(); ++it ){
      delete *it;
        }
      particles.clear();
    }
  15. The update method will be used to iterate all the particles and call update on each of them.
    void ParticleSystem::update(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ){
            (*it)->update();
        }
    }
  16. The draw method will iterate all the particles and call draw on each of them.
    void ParticleSystem::draw(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ){
            (*it)->draw();
        }
    }
  17. The addParticle method will insert the particle on the particles container.
    void ParticleSystem::addParticle( Particle *particle ){
      particles.push_back( particle );
    }
  18. Finally, destroyParticle will delete the particle and remove it from the particles' list.

    We'll find the particles' iterator and use it to delete and later remove the object from the container.

    void ParticleSystem::destroyParticle( Particle *particle ){
      std::vector<Particle*>::iterator it = std::find( particles.begin(), particles.end(), particle );
      delete *it;
      particles.erase( it );
    }
  19. With our classes ready, let's go to our application's class and create some particles.

    In our application's class, we need to include the ParticleSystem header file and the necessary header to use random numbers at the top of the source file:

    #include "ParticleSystem.h"
    #include "cinder/Rand.h"
  20. Declare a ParticleSystem object on our class declaration.
    ParticleSystem mParticleSystem;
  21. In the setup method we can create 100 particles with random positions on our window and random radius. We'll define the mass to be the same as the radius as a way to have a relation between size and mass. drag will be set to 9.5.

    Add the following code snippet inside the setup method:

    int numParticle = 100;
      for( int i=0; i<numParticle; i++ ){
      float x = ci::randFloat( 0.0f, getWindowWidth() );
      float y = ci::randFloat( 0.0f, getWindowHeight() );
      float radius = ci::randFloat( 5.0f, 15.0f );
      float mass = radius;radius;
      float drag = 0.95f;
            Particle *particle = new Particle
            ( Vec2f( x, y ), radius, mass, drag );
            mParticleSystem.addParticle( particle );
    }
  22. In the update method, we need to update the particles by calling the update method on mParticleSystem.
    void MyApp::update(){
      mParticleSystem.update();
    }
  23. In the draw method we need to clear the screen, set up the window's matrices, and call the draw method on mParticleSystem.
    void ParticlesApp::draw()
    {
      gl::clear( Color( 0, 0, 0 ) ); 
      gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
      mParticleSystem.draw();
    }
  24. Build and run the application and you will see 100 random circles on screen, as shown in the following screenshot:
    How to do it…

In the next recipes we will learn how to animate the particles in organic and appealing ways.

How it works...

The method described previously uses a popular and versatile Verlet integrator. One of its main characteristics is an implicit approximation of velocity. This is accomplished by calculating, on each update of the simulation, the distance traveled since the last update of the simulation. This allows for greater stability as velocity is implicit to position and there is less chance these will ever get out of sync.

The drag member variable represents resistance to movement and should be a number between 0.0 and 1.0. A value of 0.0 represents such a great resistance that the particle will not be able to move. A value of 1.0 represents absence of resistance and will make the particle move indefinitely. We applied drag in step 7, where we multiplied drag by the velocity:

ci::Vec2f vel = ( position – prevPosition ) * drag;

There's more...

To create a particle system in 3D it is necessary to use a 3D vector instead of a 2D one.

Since Cinder's vector 2D and 3D vector classes have a very similar class structure, we simply need to change position, prevPosition, and forces to be ci::Vec3f objects.

The constructor will also need to take a ci::Vec3f object as an argument instead.

The following is the class declaration with these changes:

class Particle{
public:

    Particle( const ci::Vec3f& position, 
    float radius, float mass, float drag );

    void update();
    void draw();

    ci::Vec3f position, prevPosition;
    ci::Vec3f forces;
    float radius;
    float mass;
    float drag;
};

The draw method should also be changed to allow for 3D drawing; we could, for example, draw a sphere instead of a circle:

void Particle::draw(){
  ci::gl::drawSphere( position, radius );
} 

See also

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

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