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
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
:
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:
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:
and here:
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. CodeProject