Tricks with scripted particle events.

December 10th, 2006 by Francois Lord - Viewed 19752 times -




I was recently assigned on a particle recipe that was going to be used by several people on several shots. After some thinking on how I was going to approach the idea, I realized that I needed a few features missing in XSI. In fact, I’ve been wanting those features many times in the past. I decided to take the time to write some scripted events, even though I was on a tight deadline. It turned out it was easier than I thought.

I was required to create a bubble stream behind an underwater torpedo. Do torpedoes really have a stream of bubbles behind them in real life? In movies they do, so the question doesn’t really matter. I wanted to have many small bubbles and a few big bubbles within the same PType. I wanted to have the big bubbles rise faster than the small ones. I also wanted the bubbles to wobble as they were rising, and the wobble to be function of the size and the speed. Finally, I wanted the sprite sequence to begin at a random frame and continue normally as the particle aged. All this is not possible with the actual particle system in XSI 5.11, although they are common requirements in a simulation scene.

The Distribution problem.
Exponential DistributionExponential Fuction In XSI, you can only choose from uniform and Gaussian distribution. This means you can only have a middle value, and an equal probability on each side. For what we need here, we will create a scripted event with the trigger on “Particle Age” at zero. We will name the event “BirthControl“. In the script tab, we set the language to JScript. It’s a shame we can’t use Python, since there are many cool random distributions in the standard library. The ScriptContext will be set to “Per Trigger Particle”. An exponent function will give a random distribution with a high probability at one end and a progressively lower probability towards the other end.

Size = (random number)5 * 0.025 + 0.005
The “* 0.025″ sets the difference in size between the biggest and the smallest bubbles.
The “+ 0.005″ sets the size of the smallest bubble size at 0.005 units. The biggest bubbles will be 0.03 units big.

In JScript, it’s written like this:

1
inParticle.Size = Math.pow(Math.random(), 5) * 0.025 + 0.005;

The object inParticle is given to the script by XSI and contains the current particle that triggered the event.

The Speed from Size problem.
The size of the particles can be determined randomly. The effect of gravity can also be variable among the particles. But we can’t set a relation between the two with the tools in the UI. In fact, in scripting we don’t even have access to the effect of forces per particle. We will need to limit the length of the velocity vector according to the size of each particle. We must create a new scripted event called “PerFrameControl” with the trigger at “Every N Frame” and the value at 1. The ScriptContext will be “Per Particle”.

If a particle’s speed exceeds the product of it’s size and a coefficient, bring it back to that product.

1
2
3
4
5
6
7
speed = inParticle.Velocity;
size = inParticle.Size;
if (speed.Length() > size * 10) {  // if speed is bigger than 10 times the size...
    speed.NormalizeInPlace();      // scale the speed to be 1 and
    speed.ScaleInPlace(size * 10); // scale speed to be 10 times the size.
}
inParticle.Velocity = speed;

This has the advantage that a particle will have no speed at its birth, and slowly accelerate from the effect of gravity. It will continue to accelerate until it reaches the speed it is allowed. Then it will keep this speed forever unless it has to decelerate, which shouldn’t happen in our scene.

The Wobble Problem
There is already some perlin noise in the PType to make the bubbles rise in a natural fashion and we don’t want to lose that. We can’t add a small brownian noise to simulate the wobble and even if we could, we couldn’t link it to the size and speed of the particles. Without going into complicated wobble behavior, we will simply randomize the particle position in X and Z over time.

Generate a vector that has a random value for the X and Z axes, and multiply that value by the size and the speed. Add that vector the the particle’s position.
In the PerFrameControl event, we add:

1
2
3
4
pos = inParticle.Position;
wobble = XSIMath.CreateVector3((Math.random() - 0.5) * size * speed.Length(), 0, (Math.random() - 0.5) * size * speed.Length());
pos.AddInplace(wobble);
inParticle.Position = pos;

The big particles will wobble more than the small ones, and all particles will wobble progressively more as they gain speed.

The Sprite Sequence Problem.
We have a sequence of images of a bubble rising to the surface to apply on the sprite shader of the particles. It is 100 frames long. There are many choices for the behavior of the sprite sequence on the particles, but an important one is missing. We want each particle to choose a random frame in the sequence at its birth, and take the next frame of that sequence at each frame it ages. For this, we need to create a new User Parameter on the PType. We name it “BeginSpriteSeq” and set its type to Integer.

We set this parameter to a random value between 0 and 100, the length of the sequence, on each particle. In the BirthControl event, we add the following line:

1
inParticle.Attributes("BeginSpriteSeq").Value = Math.random() * 100;

Then, we need to tell the particle to choose the next frames over time. In the PerFrameControl event, we add the line:

1
inParticle.SpriteIndex = inParticle.Attributes("BeginSpriteSeq").Value + inParticle.Age;

I know there are ways to fake this in the render tree but the pseudo-random value is not very easy to obtain and it makes a recipe that is more complicated to update from one situation to another. However, contrary to the scripted event solution, it does not increase the simulation time.

The Result.
After all the work, we end up with only two events that solve all the problems encountered with a reasonable simulation time.

BirthControl event
trigger: Particle Age = 0
ScriptContext: Per Trigger Particle

1
2
inParticle.Size = Math.pow( Math.random(), 5 ) * 0.025 + 0.005;
inParticle.Attributes("BeginSpriteSeq").Value = Math.random() * 100;

PerFrameControl event
trigger: Every N Frame = 1
ScriptContext: Per Particle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//speed control
speed = inParticle.Velocity;
size = inParticle.Size;
if (speed.Length() > size * 10) {  // if speed is bigger than 10 times the size...
    speed.NormalizeInPlace();      // scale the speed to be 1 and
    speed.ScaleInPlace(size * 10); // scale speed to be 10 times the size.
}
inParticle.Velocity = speed;
 
//wobble
pos = inParticle.Position;
wobble = XSIMath.CreateVector3((Math.random() - 0.5) * size * speed.Length(), 0, (Math.random() - 0.5) * size * speed.Length());
pos.AddInplace(wobble);
inParticle.Position = pos;
 
//sprite sequence
inParticle.SpriteIndex = inParticle.Attributes("BeginSpriteSeq").Value + inParticle.Age;

Here is a Quicktime movie of the result.
Here is a close-up from the HD version.

In the final version of this recipe, I had to change the script context of the events because I wanted to add a custom PPG on the cloud with some parameters to control the various effects. This required a Dictionary.GetObject() to get the values of those parameters in each event and I didn’t want to evaluate it for each particle. I set the script contexts to Per Cloud, and did some conditional expressions to replace the triggers.

I’m glad we can solve these problems since they occur surprisingly often in production. However, I wish the solutions were more accessible from the UI. Not only would it make it easier for an artist to use them, but also the simulation times might be decreased since it would be part of the internal solver. I hope the good people at Softimage consider those problems in the new particle system they are preparing. The ability to customize the distribution of random values on a parameter is essential to a good simulation package. So is the ability to link random values from one parameter to another.

4 Responses to “Tricks with scripted particle events.”

  1. Marc-Andre says:

    Super François!

  2. tekano says:

    bravo! very informative Mr Lord, thank you indeed for contributing, there are some nuggets in here that hopefully will *not* have to rely on soon \crosses fingers

  3. Vincent says:

    Hey Francois, thanks for sharing your discoveries, much appreciated!
    Last time I tried SpriteIndex (version4.x I think), I had the impression that it was broken. Glad it worked.
    I”m also eager to know if the next gen particle engine comes with an enhanced user or scripting interface… or both! It”s a tough challenge considering that the most intuitive UI can”t hardly contain all possibilities expected from an accomplished particle module. Each time I say that I wish to be proven wrong though. Let”s not forget that particle scripting can be very fast when provided with the right hooks, and new features or plug-ins can be tied up to it afterwards.
    Anyways, those bubbles look great! It”s funny I had to do this recently and came up with similar functions like yours. Oh btw distribution is often hard to script, you can dip into your own fcurve for that but it gets really slow. Sometimes I dream of small ramp widgets; I”d buy a full box and stick them all over each particle attributes.
    Cheers

  4. George R says:

    Thanks for this. I think the best example of a very flexible particle system is PFlow in Max. Basically like the rendertree but for particles, and it’s all fairly easy to understand.