Table of Contents

Last time I’ve written about problems that we can face when designing a particle container. This post will basically show my current (basic - without any optimizations) implementation. I will also write about possible improvements.

The Series  

Introduction  

Basic design:

  • ParticleData class which represents the container
    • Allocates and manages memory for a given max number of particles
    • Can kill and activate a particle
    • Active particles are in the front of the buffer, stored continuously
    • Each parameter is stored in a separate array. Most of them are 4d vectors
    • No use of std::vectors. The reason: they are very slow in debug mode. Another thing is that I know the max size of elements so managing memory is quite simple. And also I have more control over it.
  • So far GLM library is used, but it might change in the future
  • ParticleSystem holds one ParticleData
  • Generators and Updaters (stored also in ParticleSystem) operate on ParticleData

The declaration  

The gist is located here: gist.github.com/fenbf/BasicParticles

ParticleData class

class ParticleData
{
public:
    std::unique_ptr<glm::vec4[]> m_pos;
    std::unique_ptr<glm::vec4[]> m_col;
    std::unique_ptr<glm::vec4[]> m_startCol;
    std::unique_ptr<glm::vec4[]> m_endCol;
    std::unique_ptr<glm::vec4[]> m_vel;
    std::unique_ptr<glm::vec4[]> m_acc;
    std::unique_ptr<glm::vec4[]> m_time;
    std::unique_ptr<bool[]>  m_alive;

    size_t m_count{ 0 };
    size_t m_countAlive{ 0 };
public:
    explicit ParticleData(size_t maxCount) { generate(maxCount); }
    ~ParticleData() { }

    ParticleData(const ParticleData &) = delete;
    ParticleData &operator=(const ParticleData &) = delete;

    void generate(size_t maxSize);
    void kill(size_t id);
    void wake(size_t id);
    void swapData(size_t a, size_t b);
};

Notes:

  • So far std::unique_ptr are used to hold raw arrays. But this will change, because we will need in the future to allocate aligned memory.

Implementation  

Generation:

void ParticleData::generate(size_t maxSize)
{
    m_count = maxSize;
    m_countAlive = 0;

    m_pos.reset(new glm::vec4[maxSize]);
    m_col.reset(new glm::vec4[maxSize]);
    m_startCol.reset(new glm::vec4[maxSize]);
    m_endCol.reset(new glm::vec4[maxSize]);
    m_vel.reset(new glm::vec4[maxSize]);
    m_acc.reset(new glm::vec4[maxSize]);
    m_time.reset(new glm::vec4[maxSize]);
    m_alive.reset(new bool[maxSize]);
}

Kill:

void ParticleData::kill(size_t id)
{
    if (m_countAlive > 0)
    {
        m_alive[id] = false;
        swapData(id, m_countAlive - 1);
        m_countAlive--;
    }
}

Wake:

void ParticleData::wake(size_t id)
{
    if (m_countAlive < m_count)
    {
        m_alive[id] = true;
        swapData(id, m_countAlive);
        m_countAlive++;
    }
}   

Swap:

void ParticleData::swapData(size_t a, size_t b)
{
    std::swap(m_pos[a], m_pos[b]);
    std::swap(m_col[a], m_col[b]);
    std::swap(m_startCol[a], m_startCol[b]);
    std::swap(m_endCol[a], m_endCol[b]);
    std::swap(m_vel[a], m_vel[b]);
    std::swap(m_acc[a], m_acc[b]);
    std::swap(m_time[a], m_time[b]);
    std::swap(m_alive[a], m_alive[b]);
}

Hints for optimizations:

  • maybe full swap is not needed?
  • maybe those if’s in wake and kill could be removed?

Improvements  

Configurable attributes  

SoA style object gives use a nice possibility to create various ParticleData configurations. I do not have implemented it in current class, but I’ve used it before in some other system.

The simplest idea is to hold a mask of configured params:

ParticleData::mask = Params::Pos | Params::Vel | Params::Acc | Params::Color...

In the constructor memory for only selected param will be allocated.

generate() {
    // ..
    if (mask & Params::Vel)
        allocate ParticleData::vel array
    // ...

The change is also needed in updaters and generators: briefly we will be able to update only active parameters. A lot of if statements would be needed there. But it is doable.

update() {
    // ..
    if (mask & Params::Vel)
        update ParticleData::vel array
    // ...

Please not that the problem arise when one param depends on the other.

Limitations: there is a defined set of parameters, we only can choose a subset.

The second idea (not tested) would be to allow full dynamic configuration. Instead of having named set of available parameters we could store a map of <name, array>. Both name and type of param (vector, scalar, int) would be configurable. This would mean a lot of work, but for some kind of an particle editor this could be a real benefit.

What’s Next  

In the next article I will touch particle generation and update modules.

Read next: Generators & Emitters

Again: the gist is located here: gist.github.com/fenbf/BasicParticles