Tracking Particles

Tracing and tracking specific particles produces interesting results, especially when they become meshed later or when a particular object is applied. With this method it is possible to create trails from selected particles and display them either with the help of objects or new particles.

 

Name

ParticleTracker .rfs

Type

Simulation Events

Description

The script records the position data from one or more particles and makes them visible with the help of new particles or "Null" objects. Particles are shifted to a new emitter, "Nulls" are added at the end of the simulation and automatically grouped. 

 

What the script should do:

  • Initialize the needed variables and define the particles to be tracked
  • Loop through the particles and find the ones to track
  • Record the position data and transfer them to A) new particles or B) "Null" nodes
  • Group the objects and rename them, perform final adjustments

 

The entire script is executed with each frame. If you want to track specific particles it is a good idea to use their IDs. An ID is unique for each particle and can be read easily. You can even make the ID visible by activating the "Particle Tooltip" option:

Menu bar > Tools > Particle Tooltip

The only issue with this function is that you have to simulate first before you can see the appropriate ID. A better option is to use a defined volume, instead of a constant particle stream. If you already know which particles you want to track, it is a good idea to store them in a list:

idList = [1,56,145,354,508,722,1032,1195,1482,1648,2000]

 

Now it should not be a problem anymore to go through the particles and compare the current particle’s ID with the list entires. Once the ID was found, you can read out the particle’s position and transfer it to a new particle or an object, maybe a "Null". If you are thinking about new particles, they should be added to a second emitter that is not linked with the other scene elements (= exclusive). Otherwise the particles will be part of the simulation process and may falsify the results. 

If you want to track particles with an object we recommend that you limit the process to a single particle, otherwise you might end up with several thousand new nodes. When using objects, they should be grouped to keep the "Nodes" window clear. The following example shows both methods, but as separate scripts. A good exercise would be to join both methods to one script that is controlled with a GUI. You could, for example, define custom functions for each part and call them through the GUI.

# FramesPost

emitter    = scene.get_PB_Emitter("Square01")
tracker    = scene.get_PB_Emitter("Tracker")
idList     = [1,56,145,354,508,722,1032,1195,1482,1648,2000]
nullVec    = Vector.new(0,0,0)
e_particle = emitter.getFirstParticle()

while (e_particle):
    currentId = e_particle.getId()

    if (currentId in idList):
        pos = e_particle.getPosition()
        tracker.addParticle(pos, nullVec)
        e_particle = e_particle.getNextParticle()

t_particle = tracker.getFirstParticle()

while (t_particle):
    t_particle.freeze()
    t_particle = t_particle.getNextParticle()

 

As you can see, the current position is only extracted when the ID is identified as an entry of “idList”. This helps to save resources, because it is not necessary to get each and every position when just a few selected particles are tracked. It is important to have a look at these apparently small issues, because with lots of particles you will notice an increase in simulation speed.

 

Result of the particle tracker script from different views.

 

The second part is necessary to stop the tracker particles completely. Though a null vector is added to each particle’s velocity, they still have a certain amount of motion energy which could lead to unwanted results. To get rid of this motion, the script goes through the tracker’s particles and freezes them with

t_particle.freeze()

 

The other method uses an object to visualize the particle’s way through the environment. In this case, you can trackjust one particle with a "Null"; but it is also possible to track more than one particle, resulting in many more objects. The created "Nulls" are added to a group automatically and renamed using a certain pattern. Finally, all nodes except the "Nulls" and daemons are made invisible so that only the position trail is visible in the viewport. Additionally the "Nulls" are coloured and reduced in size. This drawing of position markers can either be executed during the simulation or as a post process at the end of the simulation range. Then all markers are drawn from a previously stored position list. This method illustrates how to deal with lists and how to extend them. But that is not all: the script also uses global variables, since some of the stored information has to be transferred to different parts and functions.

The first part is the definition of the particles' position list. This list will later contain the entire recorded position data which will be read out at the end of the simulation. Now it is nothing more than an empty container, but it must be a global variable. In this case it is not possible to initialize the list during the simulation steps, because it would be dumped with each frame losing the previous data.

# SimulationPre

scene.setGlobalVariableValue("posList", [])

 

That is already everything you need. During the simulation the position data are recorded and added to the previously introduced pos_list variable. You also have to specify the desired ID. In this case there is really only one ID tracked. If you want to track more particles another data structure is required to identify the position values and the related particle: a perfect task for a dictionary! The code structure for this section is almost the same as in the example above, but even easier, as you do not have to go through a list object. Here you need just a scalar and of course the global variable "pos_list":

# A. Fetch the global variable and make it local / B. Specify a particle ID

posList     = scene.getGlobalVariableValue("posList")
id_to_track = 30
emitter     = scene.get_PB_Emitter("Circle01")
particle    = emitter.getFirstParticle()

while (particle):
    current_id = particle.getId()

    if (current_id == id_to_track):
        pos = particle.getPosition()
        posList.append(pos)

    particle = particle.getNextParticle()

 # Translate the local variable with the positions into a global variable again

scene.setGlobalVariableValue("posList", posList)

 

The position list is finally used at the end of simulation. There, all data will be read out and transferred to a "Null" object – one "Null" for each position. This mode of operation calls for another loop. During this loop the "Nulls" are renamed following a certain pattern to create a uniform notation. Additionally all position markers are added to a group. If you want to delete the trackers and create a new simulation, then it can be done with a single click and you do not have to select and erase hundreds of nodes individually.

First of all it is necessary to make everything invisible. Here, we will leave the daemons still visible. This is not really important, but it is a useful way to illustrate how to filter certain node types. Maybe there will be a situation where you have to separate emitters from the rest of the  nodes in the scene. With this little piece of code it is no problem – please note that all of the following Python snippets have to be inserted under “SimulationPost”:

nodes = scene.getNodes()

for node in nodes:
    if (node.getType() != TYPE_DAEMON):
    node.setParameter("Visible", False)

The loop, listed above, tells RealFlow that only non-daemons should be set to invisible. The "!=" operator means: "If the current node of the list is not (!=) a daemon then hide it to the user."

The next task is to create the group where all "Null" nodes are finally added to. Here a very fast and convenient method is introduced helping you to avoid more complex routines for detecting certain nodes. You will see a little later how easy it is to work with this method. It directly converts a freshly added node, object or whatever into a variable:

nulls = scene.addGroup()
nulls.setName("NullTracker")

 

This easy notation replaces a slightly longer form:

scene.addGroup()
nulls = scene.getGroup("Group01")
nulls.setName("NullTracker")

 

This does not look very sophisticated, but in fact it is! Imagine several simulations with a different group name each time. With this short version you do not have to keep track of the group’s name, because everything is directly stored with the variable. That is very convenient for lots of objects and important when you rename the "Nulls" – which happens in the next step. Before you can add the "Nulls" it is necessary to create a new name for each item. The naming pattern should contain leading 0 characters to allow easy and correct sorting:

Tracker0001 -> Tracker0010 -> Tracker0100 -> Tracker1000+

This kind of naming can be done with a counter and few simple if-conditions which are already familiar from the first example “Placing Object”:

counter = 1

for entry in pos_list:
    if (counter < 10):
        suffix = "000"+str(counter)

    elif (counter >= 10 and counter < 100):
        suffix = "00" + str(counter)

    elif (counter >= 100 and counter < 1000):
        suffix = "0" + str(counter)

    else:
        suffix = counter

    new_name = "Tracker"+str(suffix)

 # Add the Null and convert it into a variable for immediate renaming.

    current_null = scene.addNull()
    current_null.setName(new_name)

    counter += 1

 

This construction allows you to specify your own “file padding”. The str(variable) instruction is used to mix a given string with the content of variables. This does not work with vectors, because they are stored in a hexadecimal format, but here only simple scalars are used. Finally a few basic operations are done to rescale and dye the "Null" nodes and add them to the group. This should be basic stuff now, except adding the new items to the appropriate group. Another new feature that is introduced here is to prevent the "Null" nodes from being exported to disk. Although that is more important for a script version where the "Nulls" are created during the simulation process, it is still worth showing you how to manipulate "Export Central" with Python. The notation is:

current_null.activeExportResource(1, False)

The integer argument is a fixed ID and specifies the file type that is available for object nodes under "Export Central". Following this list you can see that the Boolean data type “False” disables the export function for the individual SD files and the soft body BINs:

  • 1 = Animation (.sd)
  • 2 = Geometry (.obj)
  • 4 = Wetmap texture (*)

 

Here is the complete listing for the last part of the tracking script (please add it to "SimulationPost"):

nodes = scene.getNodes()

for node in nodes:
    if (node.getType() != TYPE_DAEMON):
        node.setParameter("Visible", False)
        nullGroup = scene.addGroup()
        nullGroup.setName("NullTracker")
        posList = scene.getGlobalVariableValue("posList")
        counter = 1

for entry in posList:
    if (counter < 10):
        suffix = "000"+str(counter)

    elif (counter >= 10 and counter < 100):
        suffix = "00" + str(counter)

    elif (counter >= 100 and counter < 1000):
        suffix = "0" + str(counter)

    else:
        suffix = counter
 
    new_name    = "Tracker"+str(suffix) 
    currentNull = scene.addNull()
    currentNull.setName(new_name)
    currentNull.setParameter("Position", entry)
    currentNull.setParameter("Color", Vector.new(180,22,33))
    currentNull.setParameter("Scale", Vector.new(0.2,0.2,0.2))
    currentNull.activeExportResource(1,False)
    currentNull.activeExportResource(3,False)
    nullGroup.add(new_name)
 
    counter += 1

 

The particle tracker script contains a lot of methods and features which are important for your daily scripting tasks. You have to filter out certain node types, define and call global variables, change all kinds of parameters, loop through particles and objects, record data, transfer or manipulate them, set export resources, and rename scene objects. All these fundamental principles are applied within a single script.