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.

class which represents the container So far GLM library is used, but it might change in the future

ParticleSystem holds one ParticleData

holds one Generators and Updaters (stored also in ParticleSystem ) operate on ParticleData

The declaration

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

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