Recently working with ribbons I’ve stumbled upon a problem that led me to a pretty deep investigation of how Niagara emitters and Attribute Readers work and I think it’s worth sharing.
The Crime
Let’s say we have a Source
emitter that spawns a random amount of particles at random points in time and we have another emitter which should generate ribbons following those particles. The one important restriction: ribbons should start precisely at the particle’s spawn location. For example imagine a bullet flying from the muzzle and for some reason we want the ribbon to start from a muzzle location.
Gray squares at the bottom illustrate spawn locations
The obvious approach is to use an attribute reader in the Ribbon
emitter. The process would be to:
- Read the
NumParticles
from theSource
emitter (which should return the total amount of particles alive in theSource
emitter for the current frame) - Spawn the same amount of ribbon particles
- Read the attributes from the source particles and be done
Except we will get gaps between the spawn location of the particle and the start of a ribbon, which might be fine in some cases but it’s completely unacceptable in others.
To illustrate this I have the following simple setup.
We start with the Source
emitter. First we create a SpawnThisFrame
attribute in which we calculate the amount of particles that should be spawned at the current frame. Here we spawn one or two particles with 10% probability. Then we pass this attribute to the Spawn Per Frame
module. I disabled Interpolated Spawning
as I want particles to be at their spawn positions in their first frames, so I can sample them from the Ribbon
emitter and start ribbons also from the spawn locations. Require Persistent IDs
is enabled so we can later read it from the Ribbon
emitter and use it for setting RibbonID
.
At spawn we put particles on random discrete positions:
The rest of the emitter is just the usual simulation, nothing special, so I’m not showing it here.
In the real-world case the number of particles to spawn and their starting positions may come from some gameplay logic via user-parameters, instead of being just random.
The Ribbon
emitter does exactly as I described before: reads the NumParticles
from the Source
, spawns the same amount of particles and reads the attributes from source particles. I added Sprite Renderer so we can see every ribbon particle.
I’ve also illustrated the spawn locations with SpawnPoints
emitter which simply places sprites to those spawn locations. The logic for Position
is the same as in Source
emitter except for randomization.
And here they are: gaps between spawn locations and ribbons.
The Criminal
The reason for such behavior is the NumParticles attribute.
For CPU simulations it actually contains the amount of particles alive for the previous frame and not for the current as one would expect. For GPU simulations the situation is even “worse” and this attribute may contain the amount of particles spawned since the last time the emitter was able to fetch the actual amount of particles alive from the GPU, which might be several frames ago. And for some reason it still doesn’t account for the particles spawned at the current frame. So using this parameter to decide how many particles to spawn for the Ribbon
emitter we are always lagging behind:
- Frame 1:
Source
emitter spawns 1 particle at spawn location P0.
NumParticles
is 0.
Thus theRibbon
emitter doesn’t spawn any particles. - Frame 2:
Let’s saySource
emitter doesn’t spawn particles, but the first particle is already moved to a new position P1 after the ParticleUpdate stage.
NumParticles
is 1.
TheRibbon
emitter spawns 1 particle and reads the position P1 from the source particle - and this position already differs from the spawn location P0, hence the gap.
The Solution
How can we solve this?
One option would be using Events instead of attribute reader as they don’t rely on the NumParticles
attribute and don’t have such problems. However this constrains us to using only CPU emitters which is not great so let’s consider another approach.
While we don’t know how many source particles are alive in the current frame (some of them might have died) we do know how many particles there were in the previous frame and how many particles we spawned in this frame. However the information about how many particles we want to spawn in this frame in our current setup resides in the Source
emitter and we can’t read emitter attributes with an emitter-level attribute reader.
So one solution is to move the calculation of the SpawnThisFrame
attribute to System level so it will be available in the Ribbon
emitter. And if this information is passed by user-parameters then it’s already available for all the emitters and we are already good.
Then in the Ribbon
emitter we spawn ParticleCount
the number of particles that is equal to NumParticles
from the Source
emitter (which will give us at least the amount of particles alive in the previous frame) plus SpawnThisFrame
the number of particles that should be spawned in the current frame from the System attribute or user-parameter. This amount may be bigger than the actual number of particles alive in the Source
emitter (some particle might have died in the current frame but we don’t know about it yet), so when reading the attributes from the Source
emitter we should check if the read is valid and kill the particle if it’s not.
And voilà! The problem is solved.
The Investigation
For those of you who want to verify this behavior or are just curious here are some more details from my investigation.
First, to check what is going on with NumParticles
we can write this attribute to some other emitter attribute (let’s call it ParticleCount
for example) and visualize it via the Niagara Debugger. This is the most simple emitter that spawns ParticlesToSpawn
amount of particles every frame. And ParticleToSpawn
increments by one every frame just to get something changing.
If we pause the debugger and refresh the system we will see that at the first frame we have one particle but the ParticleCount
is zero. Step to the next frame and we will see that now we have three particles, but the ParticleCount
is one. In the next frame there will be six particles and ParticleCount
is three and so on.
I’ve also found this function GetNumParticles
in Unreal’s source code NiagaraEmitterInstance.cpp
, with comments explaining why we get TotalSpawnedParticles
in the NumParticles
for the GPU emitter.
If you have any other solutions or thoughts on this - I will be happy to hear them