06 June 2014

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, makes things moving and living.

The Series


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

class ParticleUpdater
    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 much 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

class EulerUpdater : public ParticleUpdater
    glm::vec4 m_globalAcceleration{ 0.0f };
    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 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:

auto timeUpdater = std::make_shared<particles::updaters::BasicTimeUpdater>();

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

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

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

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

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 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.

Read next: Renderer

Interested in new blog posts and occasional updates? Please sign up for my free newsletter.

Copyright Bartlomiej Filipek, 2016, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.