0 like 0 dislike

I am a bit stuck with a particule system I am trying to make.

I have a script doing a bunch of things and calling an event at the end when a condition it met. Is it possible to call that event only once and completly disable the script or even the particule system?

To be more specific I have kind of a "painting" effect, where particules on a mesh would get discovered on a radius around where I look, and when I look a certain amount of time I want the whole mesh to light up with particules.

When I reach the right amount of time, an event is called, which is just the a new particle layer that has basically no code and just shows up particles on the whole mesh. Basically what I need is to bypass my original "painting" script or as I said, just kill my particle system when my condition is true because I don't want to call the event multiple time and I don't want my effect anymore when my condition is true.

Do you have tips?

Hi Valoon,
coming back for news, did you manage to solve the issue? did the example effect posted below help?
Thanks !

2 Answers

0 like 0 dislike
Hi Valoon,

I was going to say that you could call the kill() function after you triggered your event, with the same condition as the event, to kill the particle, but just realized that might not be the problem at all.

So, just to make sure I follow correctly:

you have a layer "A" that spawns a bunch of particles on a mesh (let's say 1000)
each particle has a particle field that's used as a timer, so that when the particle is in view, the timer is incremented, and possibly reset to zero when the particle goes out of view. is that correct?
when the timer of a particle reaches a predefined limit (for example: 3 seconds), you want to kill ALL the particles of layer "A" and spawn layer "B", which spawns another bunch of particles (let's say 1500), on that same mesh.
Is that correct?

If that's the case, the problem is that from one particle amongst the 1000 particles of layer A, you need to transmit an information to the 999 other particles, that information being "kill yourselves, my timer has reached its limit, I'm going to trigger layer B".

Usually, transmitting information between particles can be done through spatial layers, but in the current case it's somewhat hack-ish.

here's something you could try:

- all layer 'A' particles insert a flag in a spatial layer that says if they're currently visible or not.
- another layer with a single invisible particle queries that spatial layer and takes care of the timer and event.
when it triggers the event, it inserts a value in another spatial layer queried by the particles of layer 'A' to tell them they should die.

If you're not sure how to do this with the short description, don't hesitate, I'll put together an example.

Hope this helps
by Julien (32.9k points)
Hello, thanks for the answer.

No it's not really like that. Basically I have a mesh and I spawn let's say 1k particle on it but they are invisible. Then I have an outside effect on a spatial layer that will reveal them (kinda like a spray) when you "paint" where the mesh is.

I would like that if I am painting the mesh for like 2s already OR if I discovered like 75% of the mesh, every particle that has not been "painted" appears. Then for some performance purposes I'd like to disable the script that was doing the calculations for the painting part.

I hope it's more clear this way.

sorry I missed your comment.
Okay, it's not really important what decides when they should become visible, it can work the same way.

So the particles on the mesh are already doing a spatial query with a query position set to their own position, and a query radius set to the "paint radius", then there's a "paintbrush" particle that gets inserted in that spatial layer and moved around.
when the particles on the mesh get a non-empty result from their spatial query, it means a paintbrush particle is closer than the paint radius, and they can grab its color or whatever and become visible. Right?

I'm throwing together an example effect and will post it here in a second answer.
0 like 0 dislike

So, following up on the first answer and comments, you'll find a .pkkg archive to import in the editor, containing a bunch of example effects here: downloads.popcornfx.com/PaintBrushTest.pkkg

EDIT: I messed up the initial upload, please re-download if you already grabbed it.

I've kept the different iterations, they might be helpful.
You'll find two sets of effects:

  • FX_TestPaintbrush_v***.pkfx : effect that's just paintbrush particles (either one paintbrush particle that you move around to paint for the first effects (v1-v3), and paint spray particles for the last one (v4)).
  • FX_TestPaint_v***.pkfx : effect that spawns paint particles on a mesh, and instantiates the external paintbrush effect in an effect backdrop to test it works as expected. See this as the "test rig" for the whole effect.

Now, the first versions v1 to v3 use a single brush particle setup in localspace that you move around and it "paints" the invisible paint particles that it touches. It seemed simpler to start with that, but in the end not really, so.. I've kept these in if you're curious, but you'll probably just want to skip right to the ones that paint with a spray, which are the v4 versions.

Single localspace particle painting (v1-v3):

Spray painting (v4):

When the amount of paint particles revealed go above 50% of the total, all particles appear.

within the v4 spray version, there are various variations as well, due to some limitations of v1.11 (more on that later)

Also, up until the _v3b version, all the first ones do not perform any kind of switch once everything has been painted, they just focus on the painting and the detection of the paint ratio.

v3b shows a basic approach where each particle triggers an event that spawns a layer in LayerGroups to make the switch, but this produces a massive framerate spike (more on that later), v3c shows an alternate approach using a trail evolver that performs the switch unnoticeably.

Test rig

To test the effects, go in the Backdrops > 3D layers, and select the "PaintBrush" backdrop, then move it around in the viewport, this will reveal the particles on the mesh.

you can also move the instance of the effect around to move the mesh under the paintbrush, by selecting the root "Spawner" folder and moving the instance around in the viewport.

The paint particles layer uses a layer script to automatically adjust the number of particles spawned based on the mesh surface and a "Density" attribute, meaning you'll have less particles for smaller meshes, and more particles for larger meshes. This is clamped to a max value, to avoid getting out of control depending on the mesh you're binding to the attribute sampler in the game engine.

Detecting the paint ratio

The first set of effects with the single-particle brush use two spatial layers, the second set of effects with the spray have to use 3, and they work a bit differently, I'll only cover the second set with the sprays.

The main problem of this effect is to make the thousand of paint particles know when they should all switch to the state "all painted".

We could either:

Option 1: Make them all aware of each other, and have them query the state of all their siblings, so that when more than 'x'% are painted, all of them conclude they should switch themselves to the "painted" state.

Option 2: Use a single intermediate helper particle, that takes care of querying all the paint particles, and decides wether or not they should switch to the full painted state or not. If it decides they should, it writes that information in another spatial layer, that gets queried by the paint particles.

We'll go for option 2 for performance reasons, as it's much faster, even though requiring an additional spatial layer (to give you an idea, for 5000 paint particles, option 1 runs at 20 fps on my machine (i7, 12 HW threads), while option 2 runs at over 600 fps in the editor)

To transmit that information, we can't just make a regular query based on the position of the particles.

For this, we kind of "abuse" spatial layers to store information at a specific location, that has nothing to do with an actual 3D position. For example, we'll say that the boolean flag representing wether or not the particles should switch to the full painted state is going to be stored at coordinates float3(-42, 123, -8) (which represent absolutely nothing, it's just a randomly picked 3D location where we'll store the data we need, think of it as a dog hiding a bone in the garden at some random coordinates. That bone is the information we need, and all our paint particles know they should grab it through a query at coordinates float3(-42, 123, -8).

This location can't be hardcoded in the effect though, because it would then mean that all instances of the paint effect would use the same location to store this information, and they would all switch to the full painted state as soon as one of them switches. This would effectively make the information "global", whereas we'd like it to be "per instance" of the effect.

So, in all example effects up to v4b, the location this data is stored at is exposed as an int3 attribute "_InternalBrushKey", so it's up to you, in the game engine, to feed the same value to the instance of the paintbrush effect and to the instance of the paint effect, so they can properly communicate, and to set different coordinates between different instances so they don't conflict.

the spray effects work slightly differently and their paintbrush FX doesn't need those coordinates anymore, so you only have to set them once for the main paint effect.

effects v4b and v4c show two variants that do not need the attribute at all.

These go through a first invisible dummy particle, whose whole purpose is to generate a random 3D coordinate for the spatial layer key, then to trigger the actual layers of the effect, which will be able to grab the same values by accessing "parent.SpatialLayerKey". However, there are some other limitations. Attribute samplers don't work in v1.11 for effect v4b (see the comments in the effect), but will work in v1.12, and localspace doesn't work in effect v4c, but attribute samplers work.

Dissection of effect "FX_TestPaint_v4.pkfx"


  • PaintCommunication:
    1 particle per instance of the effect. This is the particle that makes the decision wether or not the effect should switch to the fully painted state
  • Paint:
    many particles per instance of the effect. These are the paint particles lying on the mesh, that query the brush particles to know when they should paint themselves, and that switch to the "PaintDone" layer when the effect switches to the fully painted state
  • PaintDone:
    cheap "do nothing" version of the paint particles.

Spatial layers:

  • PaintLookup : Shared with "FX_TestPaintbrush_v4.pkfx", this is where the spray paint particles insert themselves, it's queried by the paint particles on the mesh to know if they're close enough to a brush particle to become painted.
    The insertion location is the actual 3D position of the brush particles
  • PaintComMany : Local to the paint effect, this is a communication layer where the many paint particles insert themselves. It stores their state: wether they are painted or not. They all insert at the same location for a given instance, which is that special "key". It is queried at that location by the single helper particle of the "PaintCommunication" layer with a 'sum' query to count the percentage of painted particles.
  • PaintComFew : Local to the paint effect, this is a communication layer where the single helper particle of the "PaintCommunication" layer will insert the flag telling wether or not the effect should switch to the fully painted state. It is queried by the many paint particles to know if they should make the switch or not

Switching layers once everything's painted

There are two main ways to do this once a paint particle knows it should switch.

First one is creating the "PaintDone" layer as a regular layer in 'LayerGrouns', then, in the original paint particle, do:

    int     shouldPaintAll = ... some condition ...;

This is what effect v3b does.

As mentioned above, it produces a massive framerate spike, because in the same frame, ALL the couple thousand paint particles will trigger the same number of couple thousand layer instantiations, each layer only spawning a single particle. And layer instantiations aren't batched in PopcornFX v1.x (we're adressing this in version 2.0).

To give you a concrete idea, here's a screenshot of the profile of a regular frame of the effect running.
(Horizontal is time, here the update jobs (the yellowish vertical area under the viewport framerate) takes less than 0.5 ms)

And here is the capture of the frame where the transition is made:
(Here the update jobs take more than 16ms, it's all spawning the "PaintDone" layer instances

So, to work around this, we can use a trail evolver instead of manually triggering, as trail evolvers DO batch child spawns, unlike triggering an event to spawn a regular layer.
The idea is to make a trail evolver that will spawn nothing, except on the frame where the transition needs to happen, where for each parent particle, it'll spawn a single child particle.

To do this, we'll need to setup the trail evolver to not be a regular distance-based or time-based trail, but to be entirely custom.

In the trail evolver, switching the "SpawnMetric" property to "Custom" allows to do that. The trail will now look into the field whose name is set in the "CustomSpawnMetricField" property to grab each frame the value it'll use to know how many particles to spawn.

For a distance based trail, that value is the distance the particle has moved during the frame, for a time-based trail, it's the frame time.

So in our case, we'll basically just set this field to 0 all the time, except on the frame where the switch should be made, where we'll set it to 1.
If we setup the "SpawnInterval" property of the trail evolver to 1 as well, this will make the trail spawn a single child particle on that specific frame.

I can't post a screenshot of the profiler for the frame where the switch is made using that method because.. The difference is so unnoticeable that I couldn't spot it / capture it amongst the other regular frames, even after trying to catch it for 5 minutes.

Here are the performance figures when making the switch, ~0.3 ms before switching to full painted mode (all queries and logic still done), down to ~0.045 ms after the switch.

This trail trick is also used in the v4b effect, where I used trails to replace all layers and force them through a single root particle to auto-generate the insertion location.

effect v4c uses triggers but triggering layers in layer groups looses localspace information, so the v4b version uses trails to still be able to use localspace (but, as mentioned earlier, downside is it can't use attribute samplers...)

anyway, I've commented a fair bit of the scripts in all those effects, so you'll have more details there.

I might have skipped explaining something important, so don't hesitate to ask if something's not clear.


by Julien (32.9k points)
Oh ! by the way, I missed a pretty good graphics optimization opportunity in those example effects.
You should definitely select the Size of the paint particles to be zero when they're invisible, this relieves a lot of pressure on overdraw.