
Building Your Own 3D Particle Generator
Originally published in 2003
In this tutorial, Dan Ebberts explores how you can use expressions in After Effects 5.5 to build a fairly sophisticated 3D particle generator. Our particles will respond to initial velocity, gravity, wind, drag, and much more. Using this particle generator, we’ll look at how you might create explosions, smoke, a fire blast, falling snow, and even a liquid-like flow. We’ll explore how to make each particle re-usable, which will allow us to simulate fountain-like particle streams. This one is quite advanced.
the basics
The basic idea is that we will import a graphic representation of a single particle. We’ll drag it into our comp and make it 3D. We’ll apply our physical simulation expressions to our particle and then duplicate that layer enough times to get a convincing result. We’ll set the expressions up so that each particle (layer) will behave differently. We’ll be applying expressions to many of the particle’s properties, including position, scale, opacity, and rotation.
We’ll start off with a simple explosion and develop more complex expressions from there. It will eventually involve some pretty hairy math and some physics (sorry!) but the good news is that you don’t necessarily have to understand exactly how the expressions work – you just need to know how to modify them to get them to do what you want. In the first part of the tutorial, what I’ll try to do, as much as possible, is to cover the basics of how the expressions work and how you would modify them. Then, towards the end, the stouthearted can follow me through the hairy math and physics.
the strategy
We want each of our particles to have a defined life span that will be random within a certain range. Since we’ll be applying expressions to multiple properties of the particle, we need some way to communicate information about the particle’s life span to all the expressions that need it. We’ll do this by performing the life span calculations in an expression control (if you want to learn more about expression controls, see my tutorial Building the World’s Greatest Cameraman). We’ll then use the expression control as a read-only global variable that can be accessed by all the other expressions applied to our particle.
let’s begin!
Go ahead and open the project file and then open the “explosion – basic” comp. You’ll notice a graphic layer called “star flare” and a null layer, which both have 3D turned on. We’ll be using the position of the null to set the center of the explosion. We’ll refer to the null as the particle “emitter” and later we’ll move the null around to impart “emitter velocity” to the particles. Select the “star flare” layer and type “u” to display all the properties that have expressions applied to them. Also type Ctrl+Shift+t (PC) or Cmd+Shift+t (Mac) to display the effects applied the layer. You’ll see that a “point” control has been applied and renamed “life”. You may be wondering why we’d use a point control instead of a slider control to hold the “life” value. The reason is that later on (when we get to re-usable particles) we’re going to need to pass both “life” and “birth” times so we need a control that can pass two values. Couldn’t we just use two slider controls? No, because the calculations for “birth” and “life” have to be done within the same expression to make it work. In the timeline, click the twirly next to “Point” to reveal the expression applied to the “life” point control:
lmin = 1.5; //minimum particle life
lmax = 2.5; //maximum particle life
seed_random(1,true);
life=random(lmin,lmax);
birth=0;
[birth,life]
This expression will generate a random “life” value for our particle that is between 1.5 and 2.5 seconds. This value will be plugged into the second element of our point control variable. The first element (“birth”) is just set to zero since for this first explosion comp all our particles will be born at time zero. The “seed_random” call just tells After Effects that we want to get the same random value for “life” every time we run the expression (otherwise the “life” value would hop around on every frame – which doesn’t help us here). If you want to learn more about “seed_random”, see my tutorial Generating Random Motion (COW Admin: coming soon). If you wanted to change the life span of the particles, you would just change the “lmin” and “lmax” values.
Click the twirly next to Position to reveal this expression:
vmin=500; //minimum initial velocity
vmax=700; //maximum initial velocity
birth=effect(“life”).param(“Point”)[0];
life=effect(“life”).param(“Point”)[1];
origin=this_comp.layer(“Null 1”).position.value_at_time(birth);
age=time-birth;
seed_random(1,true);
s=random(vmin,vmax);//initial speed
a=degrees_to_radians(random(180)); //angle from vertical
r=degrees_to_radians(random(360)) //rotation around y axis
x=s*Math.sin(a)*Math.cos(r);
y=-s*Math.cos(a);
z=s*Math.sin(a)*Math.sin(r);
v=[x,y,z];
origin + v*time
This is the basic position expression for a “zero g” explosion. Our particle will launch at a random direction in 3D space from the “origin” (which is just the position of the null layer) with an initial speed between 500 and 700 units (which roughly equates to pixels per second). The particle will continue at that speed and direction until it dies out. We haven’t yet factored in wind, gravity, and drag. I’m sure you noticed the three lines that use the JavaScript trig functions to calculate the x, y, and z components of the particle’s velocity vector. This vector is generated from the random values of speed (“s”), launch angle from vertical (“a”) and the rotation relative to the comp’s y axis (“r”). Don’t worry too much about all this vector stuff at this point – like I said before, you don’t have to understand how it works to be able to use it and we’ll cover it in more detail later. The main thing you need to know about this version of the position expression is that you would edit the “vmin” and “vmax” values to change the initial velocity of the particle. Later we’ll add a constraint for the angle from vertical that will allow us to generate a fountain-type spray.
Click the little twirly next to Scale to reveal the expression:
max_scale=50; //maximum scale
s=.2; //scale ramp-up time
life=effect(“life”).param(“Point”)[1];
age=time-effect(“life”).param(“Point”)[0]; //age=time-birth
if (age<s){
x=(age/s)*max_scale;
[x,x,100]
}else{
[max_scale,max_scale,100]
}
All this expression does is to ramp the scale from 0 to the value specified in “max_scale” (50% in this case) over the period defined by “s” (.2 seconds in this case). If you want the particles to reach maximum size faster or slower, you would edit the value for “s”. Note that this value should be less than “lmin” of the “life” expression if you want to be sure that the particle reaches full size before it dies.
Now click the twirly next to Z rotation and you’ll see this expression:
rmax=1080; //maximum rotation
rmin=-1080; //minimum rotation
birth=effect(“life”).param(“Point”)[0];
seed_random(1,true);
r=random(rmin,rmax);
life=effect(“life”).param(“Point”)[1];
age=time-birth;
if(life==0){
0
}else{
r/life*age
}
This expression picks a random value (between –1080 degrees and +1080 degrees) for rotation around the z axis over the life of the particle. Notice that this expression uses the “birth” and “life” values from the point control. Also note the use of the “seed_random” function to make sure that the random rotation value selected remains the same every time the expression is calculated. If you wanted to change how much the particles can rotate, you would edit the “rmax” and “rmin” values.
Finally, click the twirly next to Opacity to reveal this expression:
decay=.75; //particle fade-out time
max_opacity=100; //maximum opacity
life=effect(“life”).param(“Point”)[1];
age=time-effect(“life”).param(“Point”)[0];
if (age>life){
0
}else{
if (age > life-decay){
((life-age)/decay)*max_opacity;
}else{
max_opacity
}
}
This expression handles the fade-out of the particle at the end of its life. “decay” is the time, in seconds, that the fade lasts. You would edit this value if you wanted to change the fade-out time. This value should be less than the “lmin” value of the “life” control. “max_opacity” is, as you might expect, the maximum value of opacity that you want the particle to reach.
That’s it for the expressions for this first explosion. Select the “star flare” layer and duplicate it a bunch of times. I usually just hit Ctrl+d (PC) or Cmd+d (Mac) four or five times, select the original and all the duplicates and then hit Ctrl+d/Cmd+d a bunch more times to generate at least 50 particles. Preview the comp. Naturally, with that many layers, each with five expressions applied, it may take a little while to render the preview. When you apply this technique, you’ll have to balance render time against using enough particles to create a convincing effect. You should end up with a nice 3D explosion where the particles just blast out in all directions without any forces acting on them except their initial velocity.

let’s get physical
Now let’s jump into the deep end of the pool and add the physical effects of wind, gravity, drag, and emitter velocity to our position expression. We’ll add all the code now, but we won’t necessarily use all of the new parameters for every example. We’ll go through lots of examples though, until we’ve covered everything. Go ahead and open the “explosion with physics” comp. Select the one of the “star flare” layers and type “p” to display the position property. Click the twirly next to “Position” to reveal our new expression that includes all the physics. This is the new code:
g=100; //gravity
w=0; //wind
wdir=0; //wind direction (0 = from left)
vmin=700; //minimum initial velocity
vmax=1000; //maximum initial velocity
amin=0; //minimum launch angle from vertical
amax=110; //maximum launch angle from vertical
d=4; //drag coeffecient
birth=effect(“life”).param(“Point”)[0];
life=effect(“life”).param(“Point”)[1];
origin=this_comp.layer(“Null 1”).position.value_at_time(birth);
age=time-birth;
seed_random(1,true);
s=random(vmin,vmax);//initial speed
v_e=this_comp.layer(“Null 1”).position.velocity_at_time(birth); //emitter velocity
a=degrees_to_radians(random(amin,amax)); //angle from vertical
r=degrees_to_radians(random(360)) //rotation around y axis
x=s*Math.sin(a)*Math.cos(r);
y=-s*Math.cos(a);
z=s*Math.sin(a)*Math.sin(r);
v=[x,y,z]+v_e;
new_speed=length(v);
unit_v=normalize(v);
if (d>0){
delta_p=new_speed*(1-Math.exp(-d*age))/d;
}else{
delta_p=age*new_speed;
}
delta_w=w*age;
delta_g=g*age*age/2;
wa=degrees_to_radians(wdir);
origin + delta_p*unit_v + [delta_w*Math.cos(wa),0,delta_w*Math.sin(wa)] + [0,delta_g,0]
As you can see, we’ve added code for gravity, wind (which we’re not using in this example), launch angle, drag, and emitter velocity. Don’t worry yet about how it all works. You just need to know that you would modify the first seven parameters to change the behavior of the particles. All of the other expressions are still the same as they were in the “explosion – basic” comp.
Go ahead and preview the comp. You’ll notice that our particles now encounter air resistance (drag) and are affected by gravity. We’ve also restricted the launch angle of the particles to between 0 and 110 degrees from vertical.

mistakes happen
Open and preview the “twinkle explosion” comp. This is an earlier version of the “explosion with physics” comp that had an error in it. It turns out that I had left out the call to seed_random in the expression for the point control. The result is an error where the opacity turns on and off at random after the particle’s life has expired. Since this effect is actually pretty cool, I decided to keep it around. Thus the “mistake” becomes a “feature”. Here’s the erroneous code for the point control:
lmin = 1.5; //minimum particle life
lmax = 2.5; //maximum particle life
life=random(lmin,lmax);
birth=0;
[birth,life]
where there’s smoke…..
Now let’s try our hand at creating some smoke to go with our explosion. From here on, things get a quite a bit easier. To get different particle effects, now we mainly just have to change the parameters that have been established at the beginning of the various expressions. Go ahead and open the “smoke” comp. All I did to create this comp was to duplicate the “explosion with physics” comp and replace the “star flare” image with the “smoke” image. Then I made these modifications to the parameters in the expressions:
Position:
g=10; //gravity
w=80; //wind
Scale:
max_scale=150; //maximum scale
Z-rotation:
rmax=10; //maximum rotation
rmin=-10; //minimum rotation
Opacity:
decay=1.25; //particle fade-out time
max_opacity=50; //maximum opacity
I also changed the blend mode of the layer from “normal” to “screen”.
The parameter changes reflect the way that smoke to behaves differently in response to the different physical forces. The effect of gravity has been greatly reduced. The effect of the wind has been greatly increased. The z-rotation is much less. The opacity of each particle will only reach 50%. You arrive at these values by taking a guess at how the particles should react, and then tweak them until it looks right. Go ahead and preview the comp. Hopefully you’ll notice how the parameter changes have made a big difference in the behavior of the particles.

At this point you might be wondering why we don’t just put all these parameters on slider controls. We could do that and it would certainly make the parameters easier to tweak. The problem is that if you do it, you take on a pretty hefty render hit. It might be worth it, but I like to keep things lean and mean as much as possible, but feel free to try it for yourself if you’d like.
let’s add a little debris to the mix
Open and preview the “debris” comp. This is just a variation of the “explosion with physics” comp where I changed the particle to “specs” and changed the gravity to 80 and the wind to 80. max_scale has be set to 75 and the Z-rotation expression has been modified to just select a random orientation between 180 and –180 degrees. The Opacity expression has been disabled, leaving the opacity at 100%.
putting the parts together
Open and preview the “combo explosion comp”. This comp contains the “twinkle explosion”, “smoke”, and “debris” comps together to demonstrate how the different particle types can work together to create a complex effect. Notice that the “Collapse Transformations” switches have been turned on. This is so that the particles of the three comps will intermingle properly in 3D space.

born-again particles
Now we’re going to look at making our particles re-usable. That is, after a particle dies, we want it to be reborn with different attributes, as if it were a new particle. This will allow us to generate a continuous stream of particles. To do this we need to modify the expression for our “life” point control. Here’s the new code:
lmin = 1.5; //minimum particle life
lmax = 2.5; //maximum particle life
i=1;
seed_random(i,true);
delay=random(lmax);
birth=delay;
death=delay;
if(time<delay){
[0,0]
}else{
while (time >= death){
i += 1;
seed_random(i,true);
birth=death;
life=random(lmin,lmax);
death += life;
}
[birth,life]
}
This expression has been modified to include a variation of the expressions developed for my “Generating Random Motion” tutorial. There is an initial, random delay before the generation of the first incarnation of the particle. This delay will be between 0 and the maximum particle life. Otherwise, all the “first generation” particles would be generated at the same time (as in an explosion), which is not what we want for a fountain-type effect. In general, this expression works by dividing the time line into random-length “segments” that are between “lmin” and “lmax” in duration. Each segment represents a new life-span for the particle. To allow the regenerated particles to have different attributes than the original, the seed_random() calls in the Position and Z-rotation expressions was changed to this:
seed_random(birth,true);
This causes the particle’s birth time to be used as the seed, which will result in each “life” of the particle being different because the birth times will always be different. The other change that has been made to this comp is to change the “amax” (maximum launch angle from vertical) parameter in the Position expression to 30 so that all particles will be launched in an upward direction. Go ahead and open the “fountain” comp and preview it. You should now see a continuous spray of particles. Note that this comp contains 100 particle layers. If your processor chokes, you may want to delete some layers and try it again.

back to the physics
When we added the code to the Position expression to include physical effects, we added one that we haven’t looked at yet. That effect is the velocity imparted by the “emitter” to the particles. To this point our emitters (null layers) have all been stationary. It makes sense that if we move our emitter through 3D space, the particles that are emitted should have a tendency to follow the emitter until the effects of drag, gravity, and wind send them in other directions. Since we now have a fountain particle spray in our arsenal, this is a good time to look at the emitter velocity property. Open and preview the “moving emitter 1” comp. In this comp the emitter has been keyframed to move in 3D space and the velocity of the emitter is imparted to the particles. I’ve parented a new layer (“spike blob”) to the null so you can see where the emitter is. For contrast, open and preview the “moving emitter 2” comp. This is exactly the same comp, but the code that adds in the emitter velocity has been disabled. You should notice the difference.


more particle fun
OK Now we’re just going to be goofing around. The last four comps that we’re going to look at are just variations on a theme. I’m not going to go into a lot of detail about these, but I invite you to examine the expressions of each comp to see the how the parameters (and sometimes the code) have been modified to achieve a different effect.
Open and preview the “snow” comp. There are several things to note about this comp. If you look at the position expression, you’ll notice that we’ve got a new parameter for emitter width. What we do with this is essentially turn our point emitter into a “line” emitter. In this case we have set the emitter width (“ew”) to 600. When a particle is born, its initial position is within plus or minus half the emitter width (in the x direction) from the position of the null. This allows us to generate new particles anywhere along that line. One other thing that’s different about this comp is the use of multiple particle images. I got the expressions all set up for one particle, duplicated it three times and replaced each of the duplicates with a different snowflake image. Then I selected all four of the snowflake layers and duplicated them a bunch of times.

Open and preview the “fire blast” comp. In this comp wind, gravity, and drag have been set to zero. All particles are launched in the negative z direction (towards the viewer). This one still needs some work, but it has potential.

Now open and preview the “goop” comp. This was an attempt to generate a liquid-type flow. This comp uses the “line emitter” concept of the “snow” comp and the “Tint” effect is used to make each particle a slightly different color. This one is also a work in progress.

Finally, open and preview our last example, the “rings” comp. This comp is similar to “fire blast” in that it launches all its particles in the negative z direction. This comp also has a camera that is keyframed to move from somewhat of a side view to a more front-on view. I included this comp just to show that you can also use these expressions as motion generators for objects that you might not normally think of as “particles”.

using cameras
This is probably a good spot to talk a little about the use of cameras with this particle system. The particles are generated in AE’s 3D space, so moving a camera around the particles can really add a sense of depth to your animation. If you decide to use a moving camera, you have a couple of useful options. If you do nothing except rotate the camera around the emitter, eventually it will become clear that the particles themselves are 2D. This may be the effect you’re after, especially if you have other elements arranged in 3D space that are clearly 2D elements. The other option is to have the particles always oriented towards the camera. You do this through the Layer menu: Layer>Transform>Auto Orient>”Orient Towards Camera”. Depending on the particles, this can provide a convincing illusion that the particles themselves are 3D. Experiment. Have fun!
a word about the particles
The effectiveness of these simulations depends, in large part, on the quality of the particles you use. I encourage you to examine the ones that I’ve used in these examples and to try your hand at making your own and substituting them into one of the comps. For example, try replacing the snowflakes with autumn leaves. Or just invent some weird semi-transparent blob and see what it looks like in the fountain comp. Experiment. Have fun! Also play around with the different blend modes. Remember that you can select all your particles and then when you change the blend mode of one, all of the others will change as well.
a final peek under the hood
Well, the meek among you are free to leave now. As promised, I’m going to get into the physics of the particle motion expression a little before we wrap this up. Let’s take one more look at a typical example of position expression we’ve been using (we’ll use the one from the “moving emitter 1” comp):
g=100; //gravity
w=0; //wind
wdir=0; //wind direction (0 = from left)
vmin=500; //minimum initial velocity
vmax=700; //maximum initial velocity
amin=0; //minimum launch angle from vertical
amax=110; //maximum launch angle from vertical
d=4; //drag coefficient
birth=effect(“life”).param(“Point”)[0];
life=effect(“life”).param(“Point”)[1];
origin=this_comp.layer(“Null 1”).position.value_at_time(birth);
age=time-birth;
seed_random(birth,true);
s=random(vmin,vmax);//initial speed
v_e=this_comp.layer(“Null 1”).position.velocity_at_time(birth); //emitter velocity
a=degrees_to_radians(random(amin,amax)); //angle from vertical
r=degrees_to_radians(random(360)) //rotation around y axis
x=s*Math.sin(a)*Math.cos(r);
y=-s*Math.cos(a);
z=s*Math.sin(a)*Math.sin(r);
v=[x,y,z]+v_e;
new_speed=length(v);
unit_v=normalize(v);
if (d>0){
delta_p=new_speed*(1-Math.exp(-d*age))/d;
}else{
delta_p=age*new_speed;
}
delta_w=w*age;
delta_g=g*age*age/2;
wa=degrees_to_radians(wdir);
origin + delta_p*unit_v + [delta_w*Math.cos(wa),0,delta_w*Math.sin(wa)] + [0,delta_g,0]
I’m no expert on particle systems, but I have seen a few examples of the code used to generate them. The ones that I’ve seen typically do incremental calculations based on how much things have changed since the last calculation cycle. A lot of information about each particle is stored and updated for each cycle of the calculation. In a simple example, the current position, direction, velocity, color, age, and lifespan of the particle might be saved. The calculation looks at these values, as well as the current forces acting on the particles and calculates where each one should be at the next time increment.
We have to approach things a little differently in After Effects. As you may know, (especially if you’ve read my “generating Random Motion” tutorial) it’s not easy to pass information from one cycle (frame) of a calculation to the next. Variables do not survive from one frame to the next. So we have no way to store all this information about the state of the particle at the previous frame. We need a different plan. We do, however, have a few pieces of useful information available to us. From the point control, we have the particle’s birth and life values. Since we have access to the current time, we can calculate the age of the particle. Using the following statement, we can determine where in 3D space particle was born:
origin=this_comp.layer(“Null 1”).position.value_at_time(birth);
Similarly, the initial velocity of the emitter can be obtained with this statement:
v_e=this_comp.layer(“Null 1”).position.velocity_at_time(birth); //emitter velocity
Notice that “v_e” is a vector. It has a magnitude and a direction representing the velocity of the emitter.
The initial speed and launch angle of the particle are determined by these three calls to the random function:
s=random(vmin,vmax);//initial speed
a=degrees_to_radians(random(amin,amax)); //angle from vertical
r=degrees_to_radians(random(360)) //rotation around y axis
To make sure that we always get the same random values throughout the life of the particle, these calls to the random function are preceded by the following call to the “seed_random” function:
seed_random(birth,true);
We use “seed_random” to set the seed of the random number generator to the birth time of the particle. That number will remain the same over the life of the particle, so we’ll always be able to recreate the initial speed and launch angle values.
Armed with this information, a little trigonometry, and a little vector math, we’re ready to tackle this thing. Our plan is to calculate current conditions based on initial conditions and how much time has elapsed since the particle was born, which should give us acceptable results.
OK Our first calculation will be to convert the speed and launch angle of our particle to a velocity vector. We do this by calculating the individual x, y, and z components of the vector using a little trigonometry. These are the statements to calculate x, y, and z:
x=s*Math.sin(a)*Math.cos(r);
y=-s*Math.cos(a);
z=s*Math.sin(a)*Math.sin(r);
You’ll have to take my word for this. I tried to come up with a nifty diagram to show how this is derived, but it needs to be shown in 3D space and the thing gets really cluttered in a hurry. If you know trig, you can muscle through it and figure out what’s going on. Anyway, now we’re ready to combine the initial velocity vector with the emitter’s initial velocity vector to get a composite velocity vector. Here’s the code:
v=[x,y,z]+v_e;
See, vector math isn’t too scary (especially since Adobe changed it with 5.5 to allow the use of the regular math operators, “+” in this case).
Now we’re going to convert this new velocity vector to a magnitude (speed) value and a unit vector (a unit vector is just a vector of length 1):
new_speed=length(v);
unit_v=normalize(v);
What we’re going to do next is calculate the impact that drag has had on the particle over the life of the particle to this point. This is probably the trickiest calculation of the whole project. It turns out that what we have to do is fold the effect of drag into the calculation of the particle’s position at the current frame. We have the magnitude and direction of the particle due to the particle’s initial velocity and the emitter’s initial velocity. We know that air resistance will act to slow the particle down, whichever way the particle is moving. So what we have to do is to integrate (as in calculus – sorry!) the effect of the drag force over time. This results in the following equation:
if (d>0){
delta_p=new_speed*(1-Math.exp(-d*age))/d;
}else{
delta_p=age*new_speed;
}
The result of this calculation (“delta_p”) is the current position of the particle, taking into account only the initial particle velocity, emitter velocity, and the effect of drag. The drag acts to diminish the speed of the particle at an exponential rate.
Next we need to calculate the effect of the wind. This one’s easier. From physics we remember that
distance = velocity * time
So to calculate how far the wind has moved the particle since its birth, we just need to do this:
delta_w=w*age;
The formula for the effect of gravity is a little more complicated, but not too much. Going back to our physics, we remember that distance our particle will move due to the effect of gravity is given by this statement:
delta_g=g*age*age/2;
Now all we have to do is to combine these elements. We have all the magnitudes of the distances moved by the particle due to the various forces. Now we just need to convert them to vectors in the appropriate direction and add them up to get the final position vector. We know that gravity acts in a vertical direction. Wind acts in the direction of the parameter “wdir”. The component due to initial particle velocity, emitter velocity, and drag acts in the direction of the unit vector that we calculated previously. So here’s the final step that combines all the components in the appropriate directions:
origin + delta_p*unit_v + [delta_w*Math.cos(wa),0,delta_w*Math.sin(wa)] + [0,delta_g,0]
Here you can see that the particle’s starting vector (“origin”) is combined with the distance moved due to initial velocity/drag (in the direction of the unit vector), the effect of the wind on the “x” and “z” coordinates, and the effect of gravity in the “y” direction.
Well, that’s it! Are all these formulas really correct? I don’t know. They seem to work well enough to generate useful simulations. I hope you enjoy playing around with them.
Enjoying this tutorial? Sign up for the Creative COW Newsletter!
Sign up for the Creative COW newsletter and get weekly updates on industry news, forum highlights, jobs, inspirational tutorials, tips, burning questions, and more! Receive bulletins from the largest, longest-running community dedicated to supporting professionals working in film, video, and audio.
Enter your email address, and your first and last name below!

Responses