Last time, I wrote 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 us a nice possibility to create various ParticleData
configurations. I have not 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 note that the problem arises 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.
Again: the gist is located here: gist.github.com/fenbf/BasicParticles
CodeProject