0 like 0 dislike
How to emit a bursts of particles intermittently when using "Infinite" mode?

1 Answer

1 like 0 dislike

Two main ways to do this (which are identical to what you'd do with non-infinite layers actually)

1- Layer flux-function

  1. Right-click on your layer, select "New flux function"
  2. Change the newly added flux-function curve to match the rate of intermittent bursts you want

By default, when using non-infinite layers, this flux curve will be stretched to cover the whole layer duration.
so cursor "0.0" of the curve will be the emitter start, cursor "1.0" will be at the end of the emitter's life, cursor 0.5 at half the layer duration, etc..
You can tile the flux-function by using the "FluxFunctionTiledRelativeDuration" property in the layer, which defaults to 1

For infinite layers, the flux function loops every second. If you set the "FluxFunctionTiledRelativeDuration" to, say, 5, it'll make the flux function stretched over a period of 5 seconds.

2- Layer scripts

Second method, that gives you more control, and allows you to have random burst instead of a predefined emission pattern as with flux functions:

  1. Right-click on your layer, select "New layer script"
  2. The layer script is evaluated every frame before spawning particles. Here, you can write to the "Flux" property of the layer :
    function void    Run()
        float    timeCoherentRand = noise(Age*2);
        Flux = max(0, remap(timeCoherentRand, -1,1, -0.3,1));

Here, the "noise" function is evaluated with the age of the layer, giving a smooth noise signal that varies with time. (you can also use "scene.Time" if you prefer)
Then, we remap it from -1,1 (which is the range of the values returned by the "noise" functions), to -0.3,1, then cut off the negative parts with max(0, x);
This will give an emission rate that's at 0 roughly 100 * (0.3 / (0.3 + 1)) = 23% of the time.
Remapping to -0.5,1 will give a 0-emission rate 33% of the time, keeping it at -1,1 will up it to 50% of the time.
if you never want the emitter emission rate to fall to 0 particles/second, you can change the remap :

Flux = max(0, remap(timeCoherentRand, -1,1, 0,1));

I advise you to keep the max(0, x) no matter what, even if in theory the value set into Flux will never go below zero, it costs virtually nothing and is an additional security.

3- Layer scripts, more complex flux

You can then build on 2/ and using the layer script, have more complex/interesting patterns, for example:

function void    Run()
    float    octave0 = noise(Age);
    float    octave1 = noise(Age * 2.3);
    float    octave2 = noise(Age * 4.0);
    float    timeCoherentRand = octave0 + octave1 * 0.5 + octave2 * 0.25;
    Flux = max(0, remap(timeCoherentRand, -1.75,1.75, -0.5,1));

This is the same idea as before, but with a 3-octave noise instead of 1.

You can also combine this to a predefined flux curve. The layer will combine the two fluxes together.

If for some reason you don't want to use the flux curve, or want to combine different flux curves based on an effect attribute or whatever, you can do that as well.

The simple naive method is to just sample the curve using the current time:

function void    Run()
    // compute a random time-varying flux:
    float    octave0 = noise(Age);
    float    octave1 = noise(Age * 2.3);
    float    octave2 = noise(Age * 4.0);
    float    timeCoherentRand = octave0 + octave1 * 0.5 + octave2 * 0.25;
    Flux = 0.5 * max(0, remap(timeCoherentRand, -1.75,1.75, -0.5,1));
    // combine with a user-defined flux curve:
    float    predefinedFluxA = MyFluxCurve1.sample(frac(Age*0.25));
    float    predefinedFluxB = MyFluxCurve1.sample(frac(Age*1));
    float    predefinedFlux = lerp(predefinedFluxA, predefinedFluxB, MyFluxControlAttribute);
    Flux += predefinedFlux;

The 'frac' function returns the fractional part of its argument. frac(x) is equivalent to doing x % 1.0
This is used to wrap the curve cursor and keep it in the [0,1] range, as the curve sampler will clamp the cursor they are given to [0,1], and won't wrap it, so we have to wrap it manually.

Here, the "MyFluxControlAttribute" is used to dynamically lerp between two flux curve repeat rates per emitter (which you can't do using the regular flux function)

However, just using "sample" on the curves will lead to poor dt-dependent approximations, as mentioned on this page: http://wiki.popcornfx.com/index.php/Particle_tips_FluxFactorExpression#Side-Notes

Here's how you'd do it "properly", minimizing the framerate-dependent discretization artefacts:

function void    Run()
    // compute a random time-varying flux:
    float    octave0 = noise(Age);
    float    octave1 = noise(Age * 2.3);
    float    octave2 = noise(Age * 4.0);
    float    timeCoherentRand = octave0 + octave1 * 0.5 + octave2 * 0.25;
    Flux = 0.5 * max(0, remap(timeCoherentRand, -1.75,1.75, -0.5,1));
    // combine with a user-defined flux curve:
    float    tA00 = frac((Age-dt)*0.25);
    float    tA01 = frac(Age*0.25);
    int      hasWrappedA = tA00 > tA01;
    float    tA10 = select(tA01, 1, hasWrappedA);
    float    tA11 = select(tA01, 0, hasWrappedA);
    float    predefinedFluxA = (MyFluxCurve1.integrate(tA00, tA10) +
                                MyFluxCurve1.integrate(tA01, tA11)) / dt;
    float    tB00 = frac((Age-dt)*1);
    float    tB01 = frac(Age*1);
    int      hasWrappedB = tB00 > tB01;
    float    tB10 = select(tB01, 1, hasWrappedB);
    float    tB11 = select(tB01, 0, hasWrappedB);
    float    predefinedFluxB = (MyFluxCurve1.integrate(tB00, tB10) +
                                MyFluxCurve1.integrate(tB01, tB11)) / dt;
    float    predefinedFlux = lerp(predefinedFluxA, predefinedFluxB, MyFluxControlAttribute);
    Flux += predefinedFlux;

Yeah, better to use the builtin "flux function" feature if you can :)

All the added complexity comes from the fact that the "integrate" function of the curve sampler doesn't support tiling the curve, so it saturates the cursor.
All those selects are here to handle the case where, after wrapping, the cursor of the previous frame (Age-dt) is at the end of the curve, and the cursor of the current frame (Age), has wrapped around and is at the start of the curve, so we end up having to split the integration in two separate ranges: one from (Age-dt) to 1.0, and one from 0.0 to Age.

Anyway, that's pretty advanced usage of the curve sampler, you probably won't need this, but I figured you might as well be interested by the full gory details, and can always discard the info if you don't need it :)

Hope this helps,

by Julien (33.5k points)
Thank you very much.
I applied the first solution.
It is enough to solve my problem.