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
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->generate(dt, p, startId, endId);
for (size_t i = startId; i < endId; ++i) 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
:
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
:
auto particleEmitter = std::make_shared<ParticleEmitter>();
{
particleEmitter->m_emitRate = (float)NUM_PARTICLES*0.45f;
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 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'.
CodeProject