Tim's Particle System

by Tim Nikias Wenclawiak

Last update: 18.04.04

homepage: www.NoLights.de


Index

Introduction

Basics

- Particles and the required Data

- Groups

- Creating Groups

- Adding Particles

- Running the Simulation

- Displaying Particles

- Animation_Time & Detail-Settings

- Varying frame-rates, Subset-Framing and Resimulation

Macro Overview

Parsys EDK

- Explanation and Example-Dissection

- List of EDK-Macros


Introduction

First of all, the typical stuff

Please make sure that, when you're using these macros, my name and perhaps my homepage is mentioned somewhere in the credits, even if you're using modified versions of the macros. That is mostly more or less a moral issue. For people who actually make money in any way aided by these macros, I'd like to receive a note in which way, and would be happy to have a look at it. I don't expect anyone will actually make much money with these macros, but more with their own ideas, so I don't want to have people think I'd want money for this. I don't.
As a general rule of thumb (and you may easily apply this to works of others as well):
give credit where you feel credit is due.

What is the System designed to do?

This Particle System provides some basic means for animation and simulation of particle-driven effects, e.g. smoke, rain and fire. You define groups of particles which are moved via macros and interact with the environment. Basically, you place a particle and give it a velocity, and the system will take care of moving the particle and letting it rebounce off of objects. There are various details to that process which make it possible to define completely different effects (like slowly dissipating smoke vs falling rain), but still keep everything running inside one system.


Basics

Particles and the required Data

Particles are small objects which are mainly used in huge amounts to generate different kinds of effects which in real-life also require huge amounts of objects. Smoke, for example, is actually made up of millions of small dust- or ash-particles which interact with each other and the environment. Now, a particle system mostly isn't designed to do computationally accurate simulations of such masses of objects, instead, the effects are imitated to a certain extent which makes the visual end-result convincing. In the case of smoke, one mostly only generate a few hundreds of particles which all look like small clouds.

To handle all this data and properly simulate it, a particle system needs to keep track of certain characteristics of a particle, mainly position, velocity and direction. When using vectors, velocity and direction can be combined into a single vector, where the length defines the speed, and the direction is defined by the normalized vector. Many systems also keep track of the age of a particle in order to kill old particles or derive effects from the age (like coloring the particle blue at the beginning, and red near its death). Additionally, this system requires the size of the particle and the amount of samples used to check for object-interactions. These four crucial parts are handed to the system via a one-dimensional array of vectors.

The first vector defines the age, where X defines the current age, Y the maximum age, and Z the moment of birth. The system will check the age of the particle and compare it with the theoretical age of the particle based upon the moment of birth. The missing time between current/saved age and theoretical age is the time that the particle will get simulated.

The second vector defines the size and sampling of the particle. Normally, the system assumes spherical particles. X defines the amount of samples spread onto that spherical hull and Y the radius of the particle. Z defines if a Proximity-Check should be made. This should be set to 1 if a User-Defined Sampling Macro is used. Otherwise, it takes care that once a particle gets closer to a surface than its radius would permit, the particle rebounces off the surface without any effect whatsoever. This is done in case particles, due to precision issues, do get closer to a surface than actually allowed. This may occur for example when a particle is lying on or "hugging" a surface due to outside forces.

The third and fourth vector simply define the position and velocity/direction of the particle.

Additionally, you may increase the size of one-dimensional-array as much as you like and add extra vectors, these won't get touched by the System and may be useful to carry for example color-information. User-Defined Macros for the Group Creation may make use of and modify these values. The reference to Group Creation brings us to the next topic, Groups.

Groups

The System is designed to handle groups. Particles belonging to a group will be influenced by macros and objects that have been associated with that group. Different groups may of course have different macros and objects associated with it. For example, you could define one group, entitled "rain", which uses Gravity and interacts with a heightfield to rain upon, and another group entitled "smoke" which only lets smoke particles rise slowly and increase in size while their density dissipates.

To prepare the particle system, there are several tasks to do: create/set the groups, add particles to a group, run the simulation, and finally, visualize the particles with a macro. The main points are creating a group properly and adding particles, as these two are essential for the "behaviour" and "feel" of the particle-effect. The "Look" is mainly achieved by using proper visualization of the particles, e.g. dust-clouds for smoke and small, spherical balls for water-droplets.

Creating Groups

A group has to consist of a minimum of two Macro calls: ParticleGroup_Begin and ParticleGroup_End. All Macros concerned with the setup and creation of a group should begin with "ParticleGroup" to make their function easily identifiable. The Particle System includes several macros which may be used for the setup of a group, additional macros may (and most probably will) follow. The System also provides a set of macros to easily script your own macros that may be used for the Group-Setup. Details on that process are described later. Once you've begun a group using ParticleGroup_Begin, that Group isn't considered finished until ParticleGroup_End has been called. Also note that Group-Setups may not be stacked inside each other: once a group has begun, it has to be finished before you begin a new one.

Adding Particles

To add a particle to a group, all you have to do is call Particle_Add with two parameters: the name of the group and a one-dimensional array with a minimum of 4 vectors. The vectors are used to define the age, size, position and velocity of a particle. Additional vectors may be added and will get carried along, but the core only needs and uses the first 4 vectors. Macros that have been associated with a group may make use of additional macros, in those cases it is up to the User to provide the proper dimension-size when adding a particle.

Running the Simulation

This is actually the easiest part, since all you have to do is call Parsys_Run(). It will load each group successively and simulate the particles according to the group's setup. The results are then saved to disk.

Displaying Particles

To display the particles, you call Show_Particles with two parameters: the groupname and the name of the Macro which is used to visualize the particles. The macro gets only one parameter: the array that stores the state of the particle.

Animation_Time & Detail-Settings

The Particle System bases its calculations upon a timescale. For easy handling, this timescale is considered to be in seconds. When creating an animation, you can tell the System how long the animation shall be using the Animation_Time-Variable. It then doesn't matter if you render only 10 or 100 frames, though due to precision-issues inherant to computer simulations the end-results may differ.

Another important variable in this context is the Detail-Setting, a vector where X and Y define lower and upper limits for the adaptive timestepping. Z defines the maximum distance a particle may travel before adaptive timestepping kicks in. In this manner, simulation precision is maintained whilst doing larger timesteps when possible. If you don't understand the term "timestep": the Particle System simulates using a simple Forward-Euler approach. This approach uses the following algorithm: get state of object - calculate a given timeframe of object's movement (e.g. one second) - check if something has to happen in between former and new position and/or at the new position - repeat. The timeframe is referred to as timestep. The smaller it is, the more accurate the simulation behaves, while taking longer to calculate. When doing large timesteps, it may easily occur that the system "overlooks" certain events due to insufficient precision. Too small timesteps take forever to simulate and may pose problems themselves when the needed precision isn't supported by the system on which the simulation runs on (in this case, POV-Ray).

Varying frame-rates, Subset-Framing and Resimulation

The Particle System simulates particles over the course of frames and the time passing by between two frames. Precision-issues easily lead to slightly different results with different framerates, which (over time) add up and finally return end-results which may be quiet different from each other. In the end though, it's the final effect that counts, and water will still behave like water and smoke still like smoke. It is quiet impossible though to produce cyclic animations. If this is desired, I can only point you to my non I/O-Particle-System, which can be found on my website. Note though that it is much more limited in its flexibility, especially when considering object-interactions, and is highly calculation excessive.

The system supports POV's subset-framing to a limited extent. When stopping a running simulation, you may re-begin at the last successive frame, aside of that, the system has to assume that its data isn't synchronized with the animation and resets itself, in which case the entire simulation has to begin anew. If the data gets corrupted when stopping the render (this may happen easily when stopping the render during parse-time), the system won't behave as it isn't designed to expect corrupted data. Restarting and/or deleting the data is advised in that case.


Macro Overview

Visualization Macro

[Macroname] (Particledata_Array)

The visualization macro needs to have one parameter. This will be a one-dimensional-array of vectors that has been given to the system when using Add_Particle and gets modified whenever Parsys_Run is called. If the macro requires for example 6 indices, you have to provide them when adding the particle. For easier handling of this problem I've provided ParticleGroup_Datasize, which sets a minimum of indices a particle of a given group must have.

Impact-driven Macro

[Macroname] (Particledata_Array, Time_Of_Impact, Pos_Of_Impact, Normal_Of_Surface)

The impact-driven macro requires four parameters. As with the Visualization Macro, the first Parameter is the array of vectors which stores the state of the particle. The other three are pretty self-explanatory. Note that the four parameters are required even if it is used as a Death-Effect-Macro. This is done for consistancy and easy portability of different effects for different purposes. For a Death-Effect though, Pos_Of_Impact and Normal_Of_Surface are <0,0,0> unless called from within the object-interaction routines, in which case an object was responsible for the death of the particle and both those variables are at hand.

Sampling Macro

[Macroname] (ParticleData_1, ParticleData_2, Sample_Number, Max_Samples, Position_Of_Sample, Direction_To_Sample)

The sampling macro may be used in case one wishes to use different sampling techniques for the particle. Normally, a particle is simulated with one sample or a set of samples spread across the forward hemisphere of the particle. If the particle isn't spherical, this might lead to unrealistic results. In order to generate your own samples, the option for a sampling macro was realized. The macro is called with several parameters and should return a vector which is the current position of the sample by declaring Position_Of_Sample with the sample's position. In effect, the sample is returned via parameter modification. The first two parameters are the Particledata-Arrays before and after a given timestep. Sample_Number is a value which runs from 0 to Max_Samples. Additionally, the macro may generate its own direction to trace along, just #declare it inside the macro. If it is left at <0,0,0>, the system will trace along the direction the particle wants to move.

Note that just by itself, the Sampling Macro might not have any great benefits aside that the object-interaction is more accurate to the form of the particle. But coupled with more specific Impact-Macros, it should be possible to realize rigidbody simulations, though this might be pretty complicated with the given confinements of a 1D-Vector-Array for the particle. I did want to have the possibility, but as of yet haven't done anything in that direction so far. It might happen that I realize a plug-in for that at some point.


Group Creation Macros

Basics

The creation of a particle group has to start with ParticleGroup_Begin and end with ParticleGroup_End. The first prepares several variables that will get carried along during the creation macros and keep track of different values, the last finalizes the group and prepares all macros and objects to be used by the simulation. Even a group with no modifiers needs ParticleGroup_Begin and -End. Not doing so can result in faulty behaviour or even stop rendering with an error.

Also note that Macros will take effect on the particle in the order that they have been added to the Group. If, for example, a macro requires an initial velocity of the particle, but velocities are only added AFTER this macro has been processed, it may well happen that the macro doesn't seem to take place. Another example is the ParticleGroup_Damping, which dampens the velocity of a particle. If called somewhere in between other macros, the macros following the Damping-Call won't get dampened until the next timestep.

ParticleGroup_Begin & -End

ParticleGroup_Begin (Name_As_String, Detail-Settings) & ParticleGroup_End ()

ParticleGroup_Begin requires two parameters: the name of the group as string [e.g. ParticleGroup_Begin("smoke")], and the vector for the Detail-Settings of the group. ParticleGroup_End requires no parameter and will just finalize the group that was last begun with ParticleGroup_Begin.

ParticleGroup_Datasize

ParticleGroup_Datasize (Minimum_Size)

Sets a minimum size for the array of vectors attached to a particle. If the array is smaller, it will get increased in size and filled with <0,0,0>.

ParticleGroup_Object

ParticleGroup_Object (Objectname, Rebounce, Damping, FX_Switch, Impact_Macro, Death_Macro)

To add an object to the group for object-interaction, the macro requires several parameters. First, the name of the declared object as a string ("House", not House without quotes). The "Rebounce" is a value that sets the rebounce of the particle when hitting this object. 0 will modify the particle's direction to move along the surface, 1 will rebounce it. Values in between interpolate between those two. The only other value allowed aside of the 0-to-1-range is -1. When used, the particle doesn't rebounce anymore, but still causes a Macro-Call (if defined). "Damping" modifies the velocity of the particle after hitting the object, 0 stops the particle, 1 changes nothing, 2 doubles the velocity. Negative values are allowed only when "Rebounce" is set to -1.

The FX_Switch has three possible values: 0, 1 and 2. 0 switches all impact-based-macros off, 1 uses the Impact_Macro and 2 the Death_Macro. In addition to using the macro, 2 will kill a particle when it hits the objects. Note that the macros will get called in any case, even if the particle already has its own inherant impact- and death-macros. Both Macronames, again, have to be given as strings ("Impact_Effect", not Impact_Effect without quotes).

ParticleGroup_Gravity

ParticleGroup_Gravity (Gravity_Value, Gravity_Direction)

This adds a gravitational pull to particles. The Gravity_Value defines the amount of acceleration in the direction given by Gravity_Direction.

ParticleGroup_Damping

ParticleGroup_Damping (Damping_Modifier)

Modifies the velocity per second by the amount given by Damping_Modifier. Many simulation systems work with damping to gradually reduce velocities/forces in order to keep simulations from "exploding". Damping mostly also leads to an eventual dead-state in which nothing moves anymore, in this case, it can be used to model a crude sort of air-friction.

ParticleGroup_LinearWind

ParticleGroup_LinearWind (Wind_Direction, Wind_Velocity, Friction)

Particles will slowly accelerate to match Direction and Velocity of the wind. "Friction" defines a modifier how fast a particle matches this wind, value may range from 0 to 1. 0 will define vacuum, e.g. there is no friction and a particle may immediately match the speed and direction of the wind. 1 will define solid matter, e.g. the wind is never matched and doesn't even influence the particle. Note that when matching the wind, other forces like gravity may appear to not affect the particle anymore. This is especially true the lower the friction is.

ParticleGroup_TurbWind

ParticleGroup_TurbWind (Lambda, Omega, Octaves, Scale, Pos_Modifier, Vel_Modifier, Friction)

The Turbulenced Wind bases its directions on the turb_vect-function, the same function that turbulences textures, pigments etc. Lambda, Omega and Octaves are the typical variables used to control the "turbulenciness". The direction of the wind, as driven by the turbulence, slowly changes throughout the three dimensions, but is relatively random when just inspecting two further apart positions. In order to increase/decrease this effect, Scale may be used. Additionally, as the turbulence is based upon the current position of the particle, Pos_Modifier may be used to "translate" the turbulenced field.Vel_Modifiert modifies the velocity of the wind. Due to the pseudo-random nature, the velocity given by the turbulenced field varies greatly. If the effect is too much or less, the Vel_Modifier takes care of that. Finally, Friction defines how fast a particle will adapt to the speed, where 0 defines vacuum, e.g. a particle will immediately adapt to the wind, and 1 is solid matter (wind won't affect the particle). The lower the value is, the less other forces affect the particle.

ParticleGroup_FXImpact & ParticleGroup_FXDeath

[Either Macro] (FX_Macroname)

Both Macros connect a certain event with a macro, one with an impact on an object, the other with the death of the particle. Both require a declared Macro, of which the name is given as a parameter (as a String, e.g. "Impact_Effect", not Impact_Effect without quotes). The Macros must have the amount of parameters as stated above at Impact-driven Macro. Note that even if an object defines Impact- or Death-Macro, then both will be called: the macro connected to the object AND the macro connected to the particle.

ParticleGroup_Sampler

ParticleGroup_Sampler (Sampling_Macro)

Attaches a macro to a particle which is used instead of generating its own samples for the particle. The specs for the Sampling Macro can be found here.


Adding Particles

Add_Particle

Add_Particle (Groupname, Particledata)

With Groupname you define the group into which the particle is placed. The Particledata is a one-dimensional-array which has to follow a set of rules which are defined above.


Simulation Macro

Parsys_Run

Parsys_Run ()

This Macro simply traverses the active particles and simulates their behaviour according to their group (the process of tracking and storing the particles is quiet tedious and too much to explain here). The particles get loaded, modified/simulated and then saved back to disk.


Displaying Macro

Show_Particles

Show_Particles (Groupname, Macroname)

The particles of the given group will be visualized by the given macro (which has to comply to the specs of the Visualization Macro). In effect, the Macro will be called once per particle with the particle's data-array. If the group contains no particles, there's of course nothing to be visualized.


Particle I/O Macros

Particles_Get

Particles_Get (Groupname)

Once called, this macro will return a one-dimensional array. In it are stored one-dimensional-arrays, each one being the data of a single particle. Accessing the data of a particle is thus much like accessing a 2D-Array, but Povray won't issue a warning in case only one dimension is given. This is because in its essence, the array is 1D, with arrays as input. Referencing only the first dimension, e.g. Array[0] will return the 1D-Array of a particle's data. As this makes variable lengths of the second dimension possible, a special Save- and Load-Macro is supplied. Note that when calling Particles_Get before Parsys_Run, it will return the state of the particles before the simulation of this frame, and when calling it afterwards, the particles are in the state after the simulation of this frame. It might be easier to imagine that before is much like "beginning of frame" and after is much like "end of frame".

Particles_Check

Particles_Check (Array from Particles_Get)

This macro checks if the Array returned by Particles_Get or loaded by Particles_Load is actually a valid particle-array or merely an empty hull. It returns a true/false.

Particles_Save & Particles_Load

Particles_Save (Array, Filename) & Particles_Load (Filename)

Saves/loads the array returned by Particles_Get. Particles_Save will save the array into the specified file, Particles_Load will load from the specified file and return the array.

Particles_ResetAge

Particles_ResetAge (Array, Moment_Of_Birth, Delta_Birth)

When retrieving/loading the particles, their age and moment of birth are of course set to their original values. Thus, just introducing them back into the system would just send identical twins into the systems. When calling Particles_ResetAge, you define a moment of birth with the respective parameter. Additionally, you may provide Delta_Birth, which defines a maximum for a random value that will get added to the moment of birth, but gets randomized. The current age of a particle is automatically set to 0.

Particles_Add

Particles_Add (Groupname, Array)

Much like Add_Particle, this macro will put the array of particles into the system and the defined group.

Particles_Show

Particles_Show (Array, Macroname)

Similiar to Show_Particles, this macro will actually take the array retrieved by Particles_Get and visualize it with the defined Macro.


Parsys EDK

Explanation and Example-Dissection

Additionally to the basic effects provided with the basic release of the Particle System is an Effects-Development-Kit. This is a set of Macros which make it easy to create custom macros for the groups. To understand their functionality and get a grasp of their limitations, here's an insight into the inner workings of the Group-Creation:

Once a group is begun with ParticleGroup_Begin, several files are prepared which are then finalized by ParticleGroup_End. One important file is "xxx.tmp", where "xxx" is the name of the group: it stores a series of POV-Ray commands like declarations and macro-calls. Essentially, that file is included for every particle of that group, and the declarations and macros modify the data of the particle. The EDK allows Usermade-Macros to be integrated into this file. Let's have a closer look at ParticleGroup_Damping for learning purposes:

#macro ParticleGroup_Damping(_DampingAmount)

ParsysEDK_AddMacro("_VectorParticle_Damping")
ParsysEDK_AddParameterValue(_DampingAmount)
ParsysEDK_CloseMacro()
ParsysEDK_AddLineFeed()

#end

As we can clearly see, the ParsysEDK-Macros have a sortiment of commands. "AddMacro" most probably tells the System to add another macro of which the name is supplied as a parameter. Note that every macro which is to be used by the EDK needs at least two parameters: ParticleData, which is the 1D-Array of the particle's data, and TimeStep, which is the timestep that currently needs to be calculated. Since Damping requires a modifier, we add another parameter, since we can simply use a value (and not a variable, e.g. "Damping_Value" declared globally in the scene) we use AddParameterValue. We close the macro using CloseMacro and add a line feed just for a tidyness.

Now, let's take a look at the actual Dampening-Macro, because obviously, ParticleGroup_Damping only prepares the Macro, but isn't the macro itself.

#macro _VectorParticle_Damping(_Data,_TStep,_DampingAmount)

#local _tmp_Data = _Data;
#local _tmp_Data[3] = _tmp_Data[3]-VNormalize(_tmp_Data[3]) * (1-_DampingAmount) * _TStep;
#declare _Data = _tmp_Data;

#end

Now, we can see some actual POV-SDL taking effect. The first two parameters, as explained above, are the particle's data and the timestep, in this macro just named _Data and _TStep. The underscore isn't normally used in front of variable-names and hence should be safe to be used inside a macro in order not to overwrite or access other, global variables. The third parameter is the Damping-Modifier which we added in ParticleGroup_Damping. If we now take a look at the macro itself we can see how we create a local copy of the particle-data, modify it, and then replace the original data with the new data by declaring it. This is referred to as "returning via parameters" and can be looked up in the POV-Ray Docs as one of the two methods to return values from a macro.

In this case, we modify the fourth index of the array (remember, arrays count from 0 onward), which is the velocity-vector. We subtract an amount which is in direct relation to the timestep and the damping-modifier. It is most crucial that macros take the timestep into account, as this may greatly differ during the simulation. The macro isn't called only once per particle, but may be called several times, depending on the Detail-Settings. If the effect of the macro wouldn't be dependant on the timestep, it's effect on a particle would differ depending on how detailed the calculation is. In this macro, timestep modifies the amount that is subtracted from the original velocity. In effect, the damping will affect a particle by the given modifier every second, because only when the timestep is 1 (one second) will the entire dampening-amount get subtracted.

When building your own effects, there are few guidelines to follow in order not to screw up the Particle System's inner workings. From the basic four data-vectors it is advised to only modify velocity, though position is allowed as well. Do note though that the position will further get modified by adding the velocity to it by an amount defined by the timestep (e.g. the entire velocity-vector if timestep is 1, as the velocity defines the speed in seconds). Modifying the age and/or the size of a particle may produce unwanted side-effects like dissapearing and/or timewarping particles. Generally, it is advised to only modify the indices from 2 upwards, as 0 and 1 are the age- and size-vectors. Additionally, note that the basic macros only require the fundamental 4 indices, and you may simply build upon the system by using a larger array for a particle and using the added vectors for new functionality. In such a case, it is advised to make the index variable, e.g. by declaring a global-variable which tells the macro which index to use for its modification. Otherwise, it might be difficult to combine different effects when all of them make use of, for example, the fifth index, especially when they all make different use of that vector.


ParsysEDK_AddMacro

ParsysEDK_AddMacro (Macroname)

The only parameter needed for this is the name of the macro as a string. It will add the beginning of the macro along with two minimum parameters, ParticleData and Timestep to the file. Note that when declaring your macro, the naming of the parameters doesn't matter as long as it is consistant within the macro. But since the local variables used by the Particle System are named _tmp_Data and _tmp_TStep, these shouldn't be used.

ParsysEDK_AddParameterValue, ParsysEDK_AddParameterString, ParsysEDK_AddParameterVariable

[Either Macro] (Value/String/Variable)

Will add another parameter to the macro. Note that "String" and "Variable" both are strings with quotes, but ParsysEDK_AddParameterVariable won't save the quotes in order for POV-Ray to understand the parameter as the globally declared variable.

ParsysEDK_CloseMacro

ParsysEDK_CloseMacro ()

Places the closing bracket of a macro and thus finalizes it.

ParsysEDK_AddDeclarationValue, ParsysEDK_AddDeclarationString, ParsysEDK_AddDeclarationVariable

[Either Macro] (Name_Of_Variable, Value_Of_Variable)

These macros use the Name_Of_Variable (a string) and use it for declaring it. The value assigned is Value_Of_Variable. Much like the AddParameter-Bunch, Value just has to be value, whereas String and Variable are both strings, but Variable doesn't save the quotes. In effect, the result is much like this:

#declare Name_Of_Variable = Value_Of_Variable;

ParsysEDK_AddLineFeed

ParsysEDK_AddLineFeed()

Just adds a simple linefeed to the file in case you want to separate your macros visually from the others. Not really useful unless for debugging purposes.

ParsysEDK_AddSDL

ParsysEDK_AddSDL (POV-SDL as String)

In case you want to add more complicated scripts into the file (which isn't advised but may be needed), this macro will simply write the given string to the file as POV-SDL. Note the complications that arise when using quotes inside of strings (e.g. "this is \"quoted\", got it?") and linefeeds. If the String is too long and POV issues a warning, just use this macro twice instead of just once. It doesn't add any default linefeeds.