Table of Contents

In our particle system we have already a basic foundation: the container and the framework. Now we need some modules that can actually wake particles. In this post I will describe the emitter module and generators.

The Series  

Introduction  

Basic design:

  • SRP principle: particle system contains a particle container, list of emitters, list of updaters. It does only basic stuff like initialization, cleanup and manages the update procedure.
  • Generators can generate one or several different attributes for a particle.
  • An emitter holds one or more generators.
  • Updating and killing particles are left to updaters.

The gist is located here: fenbf / BasicParticleGenerators

Emitter loop  

void ParticleEmitter::emit(double dt, ParticleData *p)
{
    const size_t maxNewParticles = static_cast<size_t>(dt*m_emitRate);
    const size_t startId = p->m_countAlive;
    const size_t endId = std::min(startId + maxNewParticles, p->m_count-1);

    for (auto &gen : m_generators)            // << gen loop
        gen->generate(dt, p, startId, endId);

    for (size_t i = startId; i < endId; ++i)  // << wake loop
        p->wake(i);
}

The idea: an emitter should emit a number of particles each frame. The pace of course depends on emit rate. The emitter should generate all needed attributes, but each attribute can be set by a different generator. So we have One to Many relation.

In the gen loop we call generators code. Each generator will set parameters for particles ranging from startId up to endId.

Then in the wake loop we wake selected particles.

Generator  

A generator should now be actually a quite simple module: just take a range of particles and set new values for some parameter. All the ‘complex’ code was handled already by the particle system and the emitter (generator’s parent).

Here is an example of BoxPosGen

class BoxPosGen : public ParticleGenerator
{
public:
    glm::vec4 m_pos{ 0.0 };
    glm::vec4 m_maxStartPosOffset{ 0.0 };
public:
    BoxPosGen() { }

    virtual void generate(double dt, ParticleData *p, 
                          size_t startId, size_t endId) override;
};

void BoxPosGen::generate(double dt, ParticleData *p, size_t startId, size_t endId)
{
    glm::vec4 posMin{ m_pos.x - m_maxStartPosOffset.x, 
                      m_pos.y - m_maxStartPosOffset.y, 
                      m_pos.z - m_maxStartPosOffset.z, 
                      1.0 };
    glm::vec4 posMax{ m_pos.x + m_maxStartPosOffset.x, 
                      m_pos.y + m_maxStartPosOffset.y, 
                      m_pos.z + m_maxStartPosOffset.z, 
                      1.0 };

    for (size_t i = startId; i < endId; ++i)
    {
        p->m_pos[i] = glm::linearRand(posMin, posMax);
    }
}

Thanks to this idea we can have a set of different generators and combine them into various emitters!

Other generators:

  • RoundPosGen - generates particle’s position around the the circle (XY axis only)
  • BasicColorGen - generates start and end color for a particle.
  • BasicVelGen - velocity only, you can set min and max on each axis.
  • SphereVelGen - velocity vector is generated from a sphere around point
  • BasicTimeGen - time generation: between min and max

Example emitter  

Emitter that uses RoundPosGen, BasicColorGen, BasicVelGen and BasicTimeGen:

auto particleEmitter = std::make_shared<ParticleEmitter>();
{
    particleEmitter->m_emitRate = (float)NUM_PARTICLES*0.45f;

    // pos:
    auto posGenerator = std::make_shared<generators::RoundPosGen>();
    posGenerator->m_center = glm::vec4{ 0.0, 0.0, 0.0, 0.0 };
    posGenerator->m_radX = 0.15f;
    posGenerator->m_radY = 0.15f;
    particleEmitter->addGenerator(posGenerator);

    auto colGenerator = std::make_shared<generators::BasicColorGen>();
    colGenerator->m_minStartCol = glm::vec4{ 0.7, 0.0, 0.7, 1.0 };
    colGenerator->m_maxStartCol = glm::vec4{ 1.0, 1.0, 1.0, 1.0 };
    colGenerator->m_minEndCol = glm::vec4{ 0.5, 0.0, 0.6, 0.0 };
    colGenerator->m_maxEndCol = glm::vec4{ 0.7, 0.5, 1.0, 0.0 };
    particleEmitter->addGenerator(colGenerator);

    auto velGenerator = std::make_shared<generators::BasicVelGen>();
    velGenerator->m_minStartVel = glm::vec4{ 0.0f, 0.0f, 0.15f, 0.0f };
    velGenerator->m_maxStartVel = glm::vec4{ 0.0f, 0.0f, 0.45f, 0.0f };
    particleEmitter->addGenerator(velGenerator);

    auto timeGenerator = std::make_shared<generators::BasicTimeGen>();
    timeGenerator->m_minTime = 1.0;
    timeGenerator->m_maxTime = 3.5;
    particleEmitter->addGenerator(timeGenerator);
}
m_system->addEmitter(particleEmitter);


circle particle emitter

Final Notes  

I think that SRP principle helps a lot in this design. The code seems to be simple and straightforward to read. Each module does only one thing.

Another advantage of the system is that we can ’easily’ translate this into a visual editor. You create a system, then add emitter, then fill it with different generators. The whole system can be set up from small blocks.

Are there any disadvantages? You need to understand the whole hierarchy of particle updaters/generators. For a simple system probably that is too much, but over the time such solution should help.

What’s Next  

Generators and emitters are useless when there is no Update mechanism! Next time I will describe such system in my particle ’engine’.

Read next: Updaters

CodeProject