A Simple Particle System with ArrayFire

Stefan YurkevitchArrayFire Leave a Comment

It’s the 4th of July today and we’re celebrating at ArrayFire! The 4th of July implies fireworks, and fireworks obviously imply particle systems. Particle systems are a collection of many small images or points that can be rendered to represent some complex behaving object. So before we can launch our fireworks, we will need to create a particle system. The large number of particles in a system lends well to GPU computation. Thankfully, ArrayFire’s easy to use interface will allow us to do this simply and efficiently. First, let’s examine the structure of a typical particle system.

Particle System Structure and Hierarchy

Individual particles in a system typically have a variety of properties that govern their individual behavior. A non-comprehensive list below summarizes some of these:

  • position
  • velocity
  • acceleration
  • lifespan
  • color
  • size
  • rotation

The properties are used to simulate the particles motion and appearance. Together, a collection of particles forms a particle system. The particle system holds additional information about the system as a whole. For example, these properties can include:

  • position of the particle system center of mass
  • total # of particles
  • type of particle system
  • image to draw
  • # of active particles
  • particle system bounding box

The particle system is responsible for keeping track of all of its particles. It simulates and updates their positions. It draws the particles to the screen. It also keeps track of which particles are alive or need to be respawned.
A single scene might have multiple particle systems at once. Fireworks wouldn’t be as exciting if there could only be one explosion at a time, so we’ll also need a particle system manager. The manager will keep track of active particle systems and make sure each one of them is updated at the appropriate time. Particle systems can be added and removed from the manager and the manager will keep track of the systems in memory. These functional requirements create a straightforward blueprint for a small collection of classes and functions required to make a particle system.

Basic classes and data structures

Starting from the top down, let’s create a particle manager:

/*
 * Particle Manager Class
 * keeps track of all living particle systems
 */

class particle_manager
{
public:
    //a vector containing pointers to our active systems
    std::vector systems;

    particle_manager() {}
    
    //function to add a system
    void addSystem(particle_system *sys) {
        systems.push_back(sys);
    }

    //used to update all systems
    //will free particle systems which are inactive
    void update() {
        systems.erase(std::remove_if(systems.begin(), systems.end(),
                    [](particle_system* ps)
                    bool alive = ps->update(); if(!alive) delete ps; return !alive;}), 
                    systems.end());
    }

    //used to render all systems
    void render(af::array &image) {
        for(auto sys : systems)
            sys->render(image);
    }
};

As we planned, the particle manager supports:

  • adding particle systems
  • (automatically) removing particle systems
  • updating particle systems
  • rendering particle systems

Our actual particle system is still undefined. We want the particle system to implement the updating and rendering functions required by the manager while holding data to keep track of the particle system as a whole. We also want our basic particle system class to be generic so we can extend its functionality to match the complexity of any imaginable particle system.

class particle_system
{
public:
    //particle system data
    af::array texture;
    int num_particles;
    int particle_size;
    int system_type;
    float system_pos[2];

    particle_system(int num_particles_) {
        num_particles = num_particles_;
        system_pos[0] = 0; system_pos[1] = 0;
    }

    virtual ~particle_system() {}

    // updates positions, velocities, and lifetime of particles in system
    virtual bool update() {
        return true;
    }
    //renders specific particle system
    virtual void render(af::array &image) = 0;
};

So far, our particle system is actually missing our particles! In a more traditional approach, we would need to dynamically allocate space for our particles as they come alive and free space as they disappear. The frequent allocations would not work well with a GPU approach or with ArrayFire. Instead, we will be managing a data-structure that is known as a pool. Instead of frequently re-allocating space as each particle appears/disappears, we will instead allocate one large array with the maximum number of particles and keep track of what particles need to be updated. The pool approach maps directly to an af:array. This frees up our need to manage memory and puts all of the heavy lifting on ArrayFire. We will add several pools to our particle system to keep track of the particles’ properties:

//particle data
    static const int dims = 2;
    std::vector pos;
    std::vector vel;
    std::vector accel;
    af::array lifetime;
    af::array is_active;

The key array that lets us use a pool approach is the is_active  array. It will be used as a mask array to update only the particles which are active. Now, we can make our generic functions slightly more useful by addressing the newly added particles.

particle_system(int num_particles_) {
    num_particles = num_particles_;
    system_pos[0] = 0; system_pos[1] = 0; system_pos[2] = 0;

    for(int dim=0; dim<dims; ++dim) {
        pos.push_back(af::constant(0, num_particles));
        vel.push_back(af::constant(0, num_particles));
        accel.push_back(af::constant(0, num_particles));
    }
    lifetime = af::constant(0, num_particles);
    is_active = af::constant(0, num_particles);
}
 //offsets the whole particle system
void move_system(float x, float y) {
    system_pos[0] += x;
    system_pos[1] += y;
    pos[0] += x;
    pos[1] += y;
}
// updates positions, velocities, and lifetime of particles in system
virtual bool update() {
    if(af::where(is_active > 0).elements() > 0 ) {
        for(int i=0; i<pos.size(); ++i) {
            pos[i](is_active>0) += vel[i](is_active>0);
            vel[i](is_active>0) += accel[i](is_active>0);
            lifetime += 1;
        }
        return true;
    }
    return false;
}

First, we initialize af::arrays which represent the particles in the constructor. Then we add a method to move the particle system as a whole. The update function shows the key use of the is_active  array where the is_active array is used to index into the positions of the active particles and update only those particles.

Constructing a specific particle system

Now that we have these generic classes we can attempt to make something more concrete, like an explosion! To do this, we will inherit from our basic particle system and mainly implement the rendering function specific to our firework explosion.

class explosion_system : public particle_system
{
public:
    float expl_speed;
    explosion_system(int num_particles_, float expl_speed_) 
    : particle_system::particle_system(num_particles_)
    {
        particle_size = 5;
        expl_speed = expl_speed_;

        is_active = af::constant(1, num_particles);

        pos[0]   = af::constant(system_pos[0], num_particles);
        pos[1]   = af::constant(system_pos[1], num_particles);
        vel[0]   = af::randn(num_particles) * expl_speed;
        vel[1]   = af::randn(num_particles) * expl_speed;
        accel[0] = af::constant(0.f, num_particles);
        accel[1] = af::constant(0.09f, num_particles);

        system_type = EXPLOSION_SYSTEM;
    }

    bool update() {
        //kill particles after 250 iterations
        if(af::where(lifetime > 250).elements() > 0 ) {
            is_active(lifetime > 250) = 0;
        }
        //limit the top speed of our particles
        if(af::where(vel[1] > 3.f).elements() > 0) {
            vel[0](vel[1] > 3.f) *= 0.9f;
            vel[1](vel[1] > 3.f) *= 0.9f;
        }
        return particle_system::update();
    }

    void render(af::array &image) {
        int image_width = image.dims(0);
        int image_height = image.dims(1);
        static const af::array particle = af::constant(1, particle_size, particle_size);
        af::array ids = ids_from_pos(image_width, image_height, pos);
        if(af::where(is_active > 0).elements() > 0 ) {
            ids = ids(is_active>0);
            af::array temp = af::constant(0, image.dims());
            temp(ids) = 5/lifetime(is_active>0);
            image += af::convolve(temp, particle);
        }
    }
};

In this class, we start by setting up the initial conditions of our explosion particle system. This uses the af::constant and af::randn functions to set the particles’ positions and speeds. We set all of the pooled particles to be active from the very beginning and specify the type of our system in case we need to modify it from the manager.
Our update function keeps track of the lifetime of our particles and also limits their maximum speed.
The render function is fairly similar to the update function with regards to the use of the is_active array. Once again, this allows drawing only the active particles. To make our fireworks fade away, we vary the intensity of each particle with respect to its lifetime.

Now that we have a decent particle system and manager, let’s set up the framework code and use our shiny new classes.

#include "arrayfire.h"
#include "particle_system.h"

using namespace af;
using namespace std;

static const int width = 768, height = 768;
const float explosion_speed = 1.1f;
const int num_explosion_particles = 400;

int main(int argc, char *argv[])
{
    try {
        //set up an arrayfire window with a color map
        af::Window myWindow(width, height, "Particle Engine using ArrayFire");
        myWindow.setColorMap(AF_COLORMAP_HEAT);

        int frame_count = 0;
        af::array image = af::constant(0, width, height);

        //create our particle manager
        particle_manager particles;

        while(!myWindow.close()) {
            //launch new firework every 200 frames
            if(frame_count % 200 == 0) {
                explosion_system *explosion = new 
                                  explosion_system(num_explosion_particles, explosion_speed);
                explosion -> move_system(width/2, height/2);
                particles.addSystem(explosion);
            }

            //render image with the particle manager
            particles.render(image);
            //and display it to the window then clear the image
            myWindow.image(image);
            image = af::constant(0, image.dims());

            //simulate all particle systems
            particles.update();
            frame_count++;
        }
    } catch (af::exception& e) {
        fprintf(stderr, "%s\n", e.what());
        throw;
    }
    return 0;
}

And Boom! You should have a working particle system.
optimized
In our full code, we added one more particle system which shows additional ways the particle manager can be used to manage particle systems. Check it out at this github repository.

Leave a Reply

Your email address will not be published. Required fields are marked *