Previous Topic (Particle Density) Up (Contents) Next Topic (Particle Color By Speed Using Dataflow)

Fading Off Particle Density By Age


Introduction

This tutorial demonstrates various approaches to achieve a density falloff controlled by particle age.

Typically, 3ds Max users would attempt to employ the Particle Age map in a Standard Material to achieve this effect, but Krakatoa doesn't support this map anymore for various reasons. Below, you will find several alternative methods.

You can download the basic 3ds Max 9 scene file which uses a Script Operator.
No additional plug-ins are necessary in order to use this file!

You can download the advanced 3ds Max 9 scene file which uses a Data Operator.
Particle Flow Tools Box #3 plug-in is required in order to use this file!

The Particle Flow

The basic Particle Flow performs the following operations:

  • A Birth Operator emits 100000 particles per second over 200 frames. We will render only 100 of the frames.
  • The particles are positioned in the volume of the icon using a Position Icon operator. The emitter is set to circle with diameter of 15 units.
  • A Force Operator' applies a Wind space warp blowing up.
  • A Delete Operator deletes all particles on frame 100.
  • A Display Operator shows particles as lines.

We will add various other operators during our experiments.

The Original Look

When rendering the basic Particle Flow containing 333333 particles, frame 100 looks like this:

The volumetric density is set to 5.0/-3 and the whole column of particles has a constant density up to the top where the particles die when they reach age of 100 - the result is a top that looks cut off.

Particle Count and Density

Particle Density in Krakatoa depends on both the per-particle Density value in each particle AND the particle count - many particles with low density can accumulate to a volume with high density.

Thus, randomizing the Delete Operator to kill particles at various ages would provide us with a usable gradient!

  • Let's set the Delete Operator to Life Span 50, Variation 50.
  • This means that most particles will live around 50 frames and die around the middle of the column, some lucky ones might reach age of 50+50 = 100 on top, and few unlucky ones might die right after birth at 50-50 = 0:

The result is, as you can see, a gradient with a falloff towards the top.

Changing the Delete event to 80 +/- 20 will localize the falloff in the top portion of the cloud:

Controlling Particle Density Using Box #3 Data Operator

As shown in the tutorials about controlling Color By Speed, using a Data Operator from Orbaz' Box #3 or a Script Operator can be a really powerful way of defining the particle appearance. In addition to support for color per particle, Krakatoa can also read the Script Float Channel and interpret as Particle Density.

In order to calculate a gradient, we need two values - the Particle Age and the Particle Lifespan. While every particle in Particle Flow does have an Age, only particles scheduled to be deleted have a valid Lifespan value. Typically, particles have a lifespan of "forever" and would not be usable for density falloff calculations.

But since we already have a Delete Operator in our flow, our particles can be used in such a function.

The Data Operator Setup

  • We can set the Delete Operator's By Particle Age back to 100 with Variation of 0.
  • We will also need a Krakatoa Options Operator to enable the MXSFloat-->Density option.
  • We add a Data Operator after the Delete Operator and set up the following Data Flow:

  • Here is what this Data Flow does:
    • We have two Input Standard sub-operators:
      • The first one reads the Particle Age of each particle (set up in the Time drop-down list)
      • The second one reads the Particle Lifespan of each particle (also set up in the Time drop-down list)
    • Both values are Time values - unfortunately, time values cannot be used in Divisions. So we convert both values to Real using the mode "Ticks as 1.0".
    • Now we can divide the Age by the Lifespan in a Function sub-operator. The result will be 0.0 when Age is 0 and 1.0 when Age is equal to the Lifespan.
    • Obviously we want exactly the opposite values - newborn particles with Age of 0 should have a value of 1.0, particles about to die should have a value of 0.0. To achieve this, we use another Function sub-operator and subtract the value we got already from the Scalar with value of 1.0.
    • Now we can pipe in the resulting inverted value into an Output Standard sub-operator set to write to the Script Float channel.

Inspecting Channel Values

  • Let's set the Quantity Multiplier in the Viewport to 0.01 so only very few particles are displayed.
  • In order to check the values actually stored in each particle's Float Channel, we can drag a Display Script* operator into our flow - its default settings will display the content of the Scripted Float Channel, exactly what we want!

  • In the viewport, we can now see that particles at the bottom of the system have a value of almost 1.0, while the top particles display a value of almost 0.0.

Rendering

  • Rendering using this setup will give us the following very smooth result:

Controlling Particle Density Using Script Operator

Of course, instead of using a Box #3 Data Operator, we can achieve the same look using a Script Operator writing to the Script Float Channel. The main drawback is, of course, performance - script evaluation is generally much slower than a comparable Data Operator:

  • Pre-rolling to frame 100 takes 31 seconds without Data Operator or Script.
  • Pre-rolling to frame 100 takes 41 seconds with Data Operator.
  • Pre-rolling to frame 100 takes 1117 seconds with a Script Operator (28 times slower!)

The Script:

 on ChannelsUsed pCont do
 (
 	 pCont.useAge = true
 	 pCont.useLifespan = true
 	 pCont.useFloat = true
 )
 
 on Init pCont do ( )
 
 on Proceed pCont do 
 (
 	count = pCont.NumParticles()
 	for i in 1 to count do
 	(
 		pCont.particleIndex = i
 		pCont.particleFloat = 1.0 - (pCont.particleAge as float / pCont.particleLifespan as float)
 	)
 )
 
 on Release pCont do ( )
  • Here is what the script does:
    • The ChannelsUsed event handler is called by Particle Flow in the beginning of the evaluation to determine which channels will be required by the script. We enable three channels:
      • pCont.useAge = true enables the particle age channel containing the Time the particle has lived since birth.
      • pCont.useLifespan = true enables the particle lifespan channel containing the Time the particle will live from birth till death.
      • pCont.useFloat = true enables the particle Float channel which is a user channel for storing arbitrary floating point values.
    • The Init event handler is not used this time.
    • The Proceed event handler is called by Particle Flow on each integration step (depending on the Integration Step settings for viewport and render in the Emitter, once or more times per frame). Within this handler, we write our code to set the Density:
      • The user variable count will be set to the number of particles in the Particle Container passed to the handler via the pCont variable (the name of the variable can be anything, but "pCont" sounds like the closest thing to "Particle Container")
      • Next we loop from 1 to the number of particles in the container - the variable i will contain the index of the current particle to work with.
      • Then we set the .particleIndex property of the container to the i variable, making that particle the current one.
      • Now we can write the result of our density calculation into the particleFloat property which gives us access to the Scripted Float channel. The calculation is simply taking the particle age as float, dividing it by the particle lifespan taken as float and subtracting the result from 1.0 to invert the final value to be 1.0 at Age 0 and 0.0 at death when Age is equal to Lifespan.
    • We don't use the Release event handler at all.
  • The result looks identical to the output of the Data Operator version:

Custom Curve Controls For Density Falloff

Other than the Particle Age Map in 3ds Max which provides a basic gradient with 3 colors for 3 Age Points, both the Data Operator approach and the Script Operator method could use a custom function curve to define the falloff over life.

Custom Curve and Data Operator

  • In this Data Flow, we use the X Scale track of a scene object to define the density falloff of the particle system, but the input could be any data you could acquire from the scene, giving you countless ways to make your particles' appearance dependent on other factors.
  • Here is what the above Data Flow does:
    • Just like before, we read the Age and the Lifespan, convert them to Reals and calculate the normalized life time between 0.0 and 1.0.
    • In addition, we multiply this value by 100 because we intend to use the first 100 frames of the scale track of a scene object as the data input.
    • We wire the resulting value between 0.0 and 100.0 through another Convert sub-operator which turns that value into frames.
    • We pipe in this value into the second input (T2) of an Object sub-operator set to read the X Scale value of a scene object. This causes the Object Scale sub-op to read the scale ON THE FRAME entering T2, corresponding to the particle age normalized over the time from frame 0 to frame 100! So if a particle has an Age of 30 and is expected to live 60 frames, its Normalized Age is 0.5 or over 100 frames it is 50, thus it would read the value of the X Scale track of the scene object on frame 50 and use that as its Density!
    • We add a Select Object sub-operator and pick a Box which we animate to scale over 100 frames.
    • From the value of the Box' Scale track which is a Vector we take only the X component using a Vector -> Real Convert sub-operator and finally put the value into the Script Float Channel like before. Since scale of 100% is expressed internally as 1.0, the value of 100% scale will cause a Density of 1.0, the value of 50% will be 0.5 and so on - no need to convert anything.
  • The scale of the Box has the following values:
    • Frame 0: 100%
    • Frame 15: 20%
    • Frame 30: 100%
    • Frame 50: 20%
    • Frame 70: 50%
    • Frame 80: 5%
    • Frame 90: 50%
    • Frame 100: 0%

  • And here is the resulting density distribution based on this custom curve:

Custom Curve and Script Operator

Here is the exact same setup using a Script Operator:

 on ChannelsUsed pCont do
 (
   pCont.useAge = true
   pCont.useLifespan = true
   pCont.useFloat = true
 )
 
 on Init pCont do ( )
 
 on Proceed pCont do 
 (
   count = pCont.NumParticles()
   theController = $Box01.scale.controller
   for i in 1 to count do
   (
     pCont.particleIndex = i
     pCont.particleFloat = at time (100 * (pCont.particleAge as float / pCont.particleLifespan as float)) \
       theController.value.x
   )
 )
 
 on Release pCont do ( )
  • In this case, the only changes to the previous simple gradient example are in the Proceed handler:
    • We grab the Scale Controller of the Box into a user variable (to avoid accessing the object thousands of times inside the loop).
    • In the loop, we calculate the normalized Age by dividing it by the Life Span, multiply it by 100 because we want to read 100 frames from the scale controller, then use this value to set the time context at time and read the .X value of the controller at that frame.
  • The result is identical to the Data Operator's image, just takes longer to calculate - for a quick test, pre-roll to frame 100 with a Particle Birth Rate of 10000.0 instead of 100000.0 took 127 seconds with the Script Operator as opposed to only 11 seconds with the Data Flow Operator which makes the script about 11.5 times slower.

Note that the Data Operator method with curve control takes almost twice as long to calculate the density with Pre-roll to frame 100 and Birth Rate of 100000.0 compared to the simple falloff shown in the earlier example - 70 vs. 41 seconds. This is because reading scene object properties is generally slower than accessing particle properties.