Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / performance

Flexible Particle System - Updaters

4.67/5 (2 votes)
8 Jun 2014CPOL3 min read 12.7K  
Flexible Particle System - Updaters

In the previous particle post, the particle generation system was introduced. But after a new particle is created, we need to have a way to update its parameters. This time, we will take a look at updaters - those are the classes that, actually, make things moving and living.

 

The Series

Introduction

Updaters also follow SRP principle. They are used only to update particle's parameters and finally decide if the particle is alive or not. We could also go further and create 'killers' - that would kill particles, but probably it would be too exaggerated design.

The gist is located here: fenbf / BasicParticleUpdaters

The Updater Interface

C++
class ParticleUpdater
{
public:
    ParticleUpdater() { }
    virtual ~ParticleUpdater() { }

    virtual void update(double dt, ParticleData *p) = 0;
};

Updater gets delta time and all the particle data. It iterates through alive particles and does some things. The class is quite 'broad' and gives a lot of possibilities. Someone might even point out that it gives too many options. But at this time, I do not think we should restrict this behaviour.

Ideally, an updater should focus only on one set of params. For instance, EulerUpdater or ColorUpdater.

Particle Updaters Implementation

Let's have a look at EulerUpdater.

Here is an example of BoxPosGen:

C++
class EulerUpdater : public ParticleUpdater
{
public:
    glm::vec4 m_globalAcceleration{ 0.0f };
public:
    virtual void update(double dt, ParticleData *p) override;
};

void EulerUpdater::update(double dt, ParticleData *p)
{
    const glm::vec4 globalA{ dt * m_globalAcceleration.x, 
                             dt * m_globalAcceleration.y, 
                             dt * m_globalAcceleration.z, 
                             0.0 };
    const float localDT = (float)dt;

    const unsigned int endId = p->m_countAlive;
    for (size_t i = 0; i < endId; ++i)
        p->m_acc[i] += globalA;

    for (size_t i = 0; i < endId; ++i)
        p->m_vel[i] += localDT * p->m_acc[i];

    for (size_t i = 0; i < endId; ++i)
        p->m_pos[i] += localDT * p->m_vel[i];
}

Pretty simple! As with generators, we can mix different updaters to create the desired effect. In my old particle system, I would usually have one huge 'updater' (although the whole system was totally different). Then, when I wanted to have a slightly modified effect, I needed to copy and paste common code again and again. This was definitely not a best pattern! You might treat this like an antipattern :)

Other updaters:

  • FloorUpdater - can bounce particle off the floor.
  • AttractorUpdater - attractors in a gravity system.
  • BasicColorUpdater - generate current particle color based on time and min and max color.
  • PosColorUpdater - current color comes from position.
  • VelColorUpdater - current color comes from velocity.
  • BasicTimeUpdater - measures the time of life of a particle. It kills a particle if its time is over.

Example Updater Composition

For 'floor effect', I use the following code:

C++
auto timeUpdater = std::make_shared<particles::updaters::BasicTimeUpdater>();
m_system->addUpdater(timeUpdater);

auto colorUpdater = std::make_shared<particles::updaters::BasicColorUpdater>();
m_system->addUpdater(colorUpdater);

m_eulerUpdater = std::make_shared<particles::updaters::EulerUpdater>();
m_eulerUpdater->m_globalAcceleration = glm::vec4{ 0.0, -15.0, 0.0, 0.0 };
m_system->addUpdater(m_eulerUpdater);

m_floorUpdater = std::make_shared<particles::updaters::FloorUpdater>();
m_system->addUpdater(m_floorUpdater);

You can see it here in action - from 39 sec:

<iframe width="560" height="315" src="//www.youtube.com/embed/Zl7FWFsIJqA" frameborder="0" allowfullscreen></iframe>

Cache Usage

Mixing different updaters is a great thing of course. But please notice that it is also quite efficient. Since we use SOA container, each updater uses cache in a smart way.

For instance, ColorUpdater uses only three arrays: currentColor, startColor and endColor. During the computation, the processor cache will be filled with only those three arrays. Remember that CPU does not read individual bytes from the memory - it reads whole cache lines - usually 64bytes.

On the other hand, if we had AOS container each particle would be 'huge' - one object contains all the parameters. Color updater would use only three fields. So all in all, cache would be used quite ineffectively because it would have to store fields that are not involved in the update process.

Look here:

Three arrays of params

and here:

Single particle array

In the second option, cache stores also members that are not used during the update process.

The problem: Of course, our solution is not ideal! Sometimes, you might have some advanced effect that uses all parameters of a particle. For instance, all parameters are used to compute final color. In this case, cache will try to load all the params (from AOS) and performance can go down... but I will describe this later when we move to the optimization part.

Please share any doubts about this design!

What's Next

We have all the systems for particle creation, update and the storage... but what about rendering? Next time, I will describe current, but actually simple, rendering system for particles.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)