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

Flexible Particle System - Emitter and Generators

5.00/5 (6 votes)
16 May 2014CPOL2 min read 10.9K  
Description of my emitter and generator module for particle system
particle emitter

In our particle system, we already have 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

C++
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 actually be quite a 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:

C++
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 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:

C++
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);

Image 2

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

License

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