Documentation for the Particle System API

By David K. McAllister

davemc@cs.unc.edu

Version 1.20

November 12, 1999

 

The home page of the Particle System API is http://www.cs.unc.edu/~davemc/Particle

Check back occasionally for new versions and other new developments.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The Particle System API may be freely distributed in its original distribution form and used for non-commercial purposes. For commercial licensing, please contact davemc@cs.unc.edu .

Introduction

The Particle System Application Programmer Interface (API) is a set of functions that allow C++ programs to simulate the dynamics of particles. The Particle System API is intended for special effects in interactive and non-interactive graphics applications, not for scientific simulation, although principles of Newtonian physics have been used to implement the particle dynamics where applicable. The style of the API is intended to be similar to that of OpenGL (from Silicon Graphics, Inc. and the OpenGL Architecture Review Board). OpenGL is currently the standard 3D graphics API because of its cleanness, clarity, and ability to represent a variety of graphics hardware while completely abstracting the hardware away from the application programmer.

This API consists of four sets of functions. These are calls that set the current state of the library, calls that act on groups of particles (these functions are called Actions), calls that operate on and manage particle groups, and calls that create and operate on action lists. The documentation is divided into four sections for the four kinds of functions.

Particles

For the purposes of the Particle System API, a particle is not just a very tiny speck. Particles can be drawn as anything - water droplets, birds, tumbleweeds, even people. The Particle System API is generally useful for operating on many similar objects that all move and change according to the same basic rules, no matter what the objects and rules are.

A particle in the abstract sense used by the API is merely an entity with a small set of attributes such as position and color that ultimately dictate the particle's behavior and appearance. Here are the particle attributes: The position, represented as three floats, tells where the particle is. The color is represented as three floats. The velocity consists of three floats telling the direction and speed (velocity vector) of the particle's movement. The size is three floats representing how large the particle is for purposes of drawing. At present, the size attribute does not affect the particle's behavior (under gravity, for example), and should not be interpreted as mass in particle physics equations. The size also does not indicate how close the particle is to other particles or surfaces. It is only a drawing attribute. The age attribute is one float representing the amount of time since the particle's creation. The secondary position, or positionB attribute, specifies another position in space for this particle. It usually represents the particle's initial position, or the particle's destination. See pRestore for a use of positionB.

Particle Groups

A particle group is a system of particles that are all acted on together.

It can be considered a dynamic geometric object.

The dynamics are provided by the particle actions. From the point of view of the graphics system, a particle group is just another model to be drawn. The Particle Groups section of the documentation explains the functions that create and deal with particle groups.

State

As in OpenGL, some calls actually do something and some calls modify the current settings (the current state) that describe the behavior of the calls that do something. Each of the following calls modifies the current state of the Particle System API. Most elements of the current state are used to specify attributes of particles to be created.

Actions

Actions are the core of the Particle System API. They are the functions in the API that directly manipulate particles in particle groups. They perform effects such as gravity, explosions, bouncing, etc. to all particles in the current particle group. A program typically creates and initializes one or more particle groups, then at run time it calls particle actions to animate the particles and finally calls a pDrawGroup function to draw the group of particles onto the screen.

The Particle System API uses a discrete time approximation to all actions. See pTimeStep for information on improving simulation quality by varying the time step, dt.

Action Lists

Action lists are blocks of actions that are applied together to a particle group. They are conceptually similar to scripts or procedures. They can also be thought of as similar to display lists in OpenGL. An action list abstracts the specifics of a particular effect and allows complex effects to be treated as primitives like actions (except that an action list cannot be an action within another action list). Action lists also allow effects to be simulated much more efficiently on certain parallel machines, such as PixelFlow. Also, action lists can be optimized by a particular implementation of the Particle System API. For example, common sequences of actions can be detected and replaced by efficient code that handles all those actions in one pass.

Domains

A Domain is a representation of a region of space. For example, the pSource action uses a domain to describe the volume in which a particle's random initial position will be. The pSink and pBounce actions use domains to describe a volume in space for particles to die when they enter, or to bounce off, respectively. Domains are used as parameters to many functions within the API. See the Domains section of this document for more.

NOTES

Many functions in the Particle System API take arguments with default values. This is a feature of C++ that may be new to many people. For example, in pColor(float r, float g, float b, float alpha = 1.0f), the parameter alpha has a default value of 1.0. This means that pColor can be called with either three parameters or four. When called with four, the parameters are the values for r, g, b and alpha. When called with three parameters, they are the values of r, g, and b. The value of alpha in such a function call is the default value, 1.0. Only parameters starting with the right-hand end of the parameter list can have default values specified. Likewise, when calling functions that have default values for parameters at the end of the list, all values specified will be applied to parameters starting with the left. This means that there is no way to specify a value for a parameter at the end of the list without specifying values for all parameters to its left.

State Setting Calls

This is a description of all calls that set the current state of the Particle System API. With the exception of pTimeStep these state values dictate the properties of particles to be created by pSource or pVertex.

NOTES

When particles are created within a pNewActionList / pEndActionList block, they will receive attributes from the state that was current when the action list was created. When in immediate mode (not creating or calling an action list), particles are created with attributes from the current state.

The initial color domain, modifiable by pColor, is: PDPoint, 1.0, 1.0, 1.0, that is, white. The initial alpha value is 1.0.

The initial velocity domain, modifiable by pVelocity, is: PDPoint, 0.0, 0.0, 0.0, or no velocity.

The initial positionB domain, modifiable by pVertexB, is: PDPoint, 0.0, 0.0, 0.0.

However positionB is initially set to track particle position.

The initial size domain, modifiable by pSize, is: PDPoint, 1.0, 1.0, 1.0, or a unit cube.

The initial starting age, modifiable by pStartingAge, is 0.0 with standard deviation 0.0.

void pColor(float red, float green, float blue, float alpha = 1.0f)

Specify the color of new particles.

All new particles will have the color red, green, blue, alpha. This is a special case of pColorD.

NOTES

See the documentation of Domains for a discussion of color spaces in the Particle System API.

If rendering is to be done by a method other than the pDrawGroup calls, the particle color does not necessarily need to be used to represent color. It can be interpreted as an arbitrary three-vector.

void pColorD(float alpha, PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Specify the color domain of new particles.

All new particles will have a color chosen randomly from within the specified domain and will have an alpha value (usually used for transparency) of alpha.

NOTES

See the documentation of Domains for an explanation of the other arguments.

void pSize(float size_x, float size_y = 0.0f, float size_z = 0.0f)

Specify the size of new particles.

All new particles will have a size of size. The size values may be negative. This is a special case of pSizeD.

NOTES

The size does not affect any particle dynamics, including acceleration and bouncing. It is merely a triple of rendering attributes, like color, and can be interpreted at the whim of the application programmer (that’s you). In particular, the three components do not need to be used together as three dimensions of the particle’s size. For example, one could be interpreted as radius, another as length, and another as density.

The definition of this call has changed because it is now a special case of pSizeD.

void pSizeD(PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Specify the size domain of new particles.

All new particles will have a size chosen randomly from within the specified domain. The size values may be negative.

NOTES

The size does not affect any particle dynamics, including acceleration and bouncing. It is merely a triple of rendering attributes, like color, and can be interpreted at the whim of the application programmer (that’s you). In particular, the three components do not need to be used together as three dimensions of the particle’s size. For example, one could be interpreted as radius, another as length, and another as density.

See the documentation of Domains for an explanation of the other arguments.

void pStartingAge(float age, float stdev = 0.0f)

Specify the initial age of new particles.

The value age can be positive, zero, or negative. Giving particles different starting ages allows pKillOld to distinguish between which to kill in interesting ways. Setting stdev to a non-zero value will give the particles an initial age with a normal distribution with mean age and standard deviation stdev. When many particles are created at once this allows a few particles to die at each time step, yielding a more natural effect.

void pTimeStep(float newDT)

Specify the time step length.

The Particle System API uses a discrete time approximation to all actions. This means that actions are applied to the particles at a particular instant in time as if the action's effect accumulated over a small time interval, dt, with the world being constant over the interval. The clock is then "ticked" by the length of the interval and the actions can then be reapplied with the particles having their updated values. This is the standard method of doing almost all time-varying simulations in computer science.

How does the time step, dt, relate to the application's frame rate? The easiest method is to apply the actions once per frame. If the application prefers to keep time in terms of seconds, dt can be set to (1 / frames_per_second). But more often, it is easier for a time unit to be one frame instead of one second. In this case, dt should be 1.0, which is the default.

For higher quality, the application can apply particle actions more than once per frame. This provides smoother, more realistic results in many subtle ways. Suppose the application wanted to compute three animation steps for each rendered frame. Set dt to 1/3 its previous value using pTimeStep, then loop three times over all the action code that gets executed per frame, including the calls to pMove. If using action lists, this can be simply a loop over the pCallActionList call. The run-time results should be about the same, but with fewer discrete approximation artifacts. Surprisingly enough, the actions themselves usually take a small percentage of the CPU time, so using a finer step size usually does not impact frame rate very much.

In terms of numerical integration, particle actions can be thought of as the first derivative of unknown functions dictating the particle attributes (such as position) over time. In order to compute the particle attributes these derivative functions must be integrated. Since closed form integration doesn't make sense for most actions, Euler's method is used instead. Euler's method is simply the method just described – the evaluation of the derivative functions at a particular time and then incrementing the current particle values by these derivative results times dt. In Euler's method, the smaller the dt, the more accurate the results.

NOTES

Unlike with other state setting calls, action lists execute using the current dt value set by pTimeStep, rather than the time step value that was current when the action list was created. Making action lists independent of time step size allows the time step to be changed without recompiling the action list.

void pVelocity(float x, float y, float z)

Specify the initial velocity of new particles.

This is shorthand for pVelocityD(PDPoint, x, y, z).

void pVelocityD(PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Specify the initial velocity domain of new particles.

NOTES

See the documentation of Domains for an explanation of the other arguments.

void pVertexB(float x, float y, float z)

Specify the secondary position of new particles.

This is shorthand for pVertexBD(PDPoint, x, y, z).

void pVertexBD(PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Specify the secondary position domain of new particles.

Set the domain from which the positionB of each particle will be randomly chosen when it is created. But if pVertexBTracks is set to true (the default), then the positionB will instead be the same as the initial particle position.

NOTES

See the documentation of Domains for an explanation of the other arguments.

void pVertexBTracks(bool doCopy)

Specify how the secondary position of each new particle is chosen.

Specify how the positionB of each new particle emitted will be chosen. The positionB is the destination position or initial position of particles. See pRestore for a use of positionB.

If doCopy is true then when a particle is created its positionB will be the same as its initial particle position (the default).

If doCopy is false then when a particle is created its positionB will be chosen from the VertexB domain.

Actions

Actions modify the position, color, velocity, size, age, and secondary position and velocity of particles. All actions apply to the current particle group, as set by pCurrentGroup.

Remember that the amount of effect of an action call depends on the time step size, dt, as set by pTimeStep. See pTimeStep for an explanation of time steps.

NOTES

Some functions have parameters with a default value of P_EPS. P_EPS is a very small floating point constant that is most often used as the default value of the epsilon parameter to actions whose influence on a particle is relative to the inverse square of its distance from something. If that distance is very small, the amount of influence approaches infinity. Since all actions are computed using Euler's method, this can cause unsatisfying results in which particles are accelerated way too much. So this epsilon parameter is added to the distance before taking its inverse square, thus keeping the acceleration within reasonable limits. By varying epsilon, you specify what is reasonable. Larger epsilon make particles accelerate less.

void pAvoid(float magnitude, float epsilon, float look_ahead, PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Steer particles away from a domain of space.

Particles are tested to see whether they will pass from being outside the specified domain to being inside it within look_ahead time units from now if the next pMove action were to occur now. magnitude tells how drastically the particle velocities are modified to avoid the obstacle at each time step. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

The specific direction and amount of turn is dependent on the kind of domain being avoided.

NOTES

At present the only domains for which pAvoid is implemented are PDSphere, PDRectangle, PDTriangle, PDDisc and PDPlane.

void pBounce(float friction, float resilience, float cutoff, PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Bounce particles off a domain of space.

Particles are tested to see whether they will pass from being outside the specified domain to being inside it if the next pMove action were to occur now. If they would pass through the surface of the domain, they are instead bounced off it. That is, their velocity vector is decomposed into components normal to the surface and tangent to the surface. The direction of the normal component is reversed, and the components are recomposed into a new velocity heading away from the surface.

The normal component is multiplied by the resilience parameter and the tangential component, if its magnitude is greater than cutoff, is multiplied by (1 - the friction parameter) when being composed into the new velocity vector.

The cutoff parameter can allow particles to glide smoothly along a surface without sticking.

NOTES

Since particles are tested to see whether they would pass through the domain if pMove were called now, it is best to have pBounce be the last action that modifies a particle's velocity before calling pMove. Also, actions such as pRandomDisplace and pVortex that modify a particle's position directly, rather than modifying its velocity vector, may yield unsatisfying results when used with pBounce.

At present the only domains for which pBounce is implemented are PDSphere, PDRectangle, DTriangle, PDDisc and PDPlane. For spheres, the particle is always forced out of the sphere. For planes, triangles and discs, the particles bounce off either side of the surface. For rectangles, particles bounce off either side of the diamond-shaped patch whose corners are o, o+u, o+u+v, and o+v. See the documentation on domains for an explanation.

See the documentation of Domains for an explanation of the other arguments.

pBounce doesn't work correctly with small time step sizes for particle sliding along a surface. The friction and resilience parameters should not be scaled by dt, since a bounce happens instantaneously. On the other hand, they should be scaled by dt because particles sliding along a surface will hit more often if dt is smaller. If you have any suggestions, let me know.

 

void pCopyVertexB(bool copy_pos = true, bool copy_vel = false)

Set the secondary position from current position.

If copy_pos is true, sets the positionB of each particle in the current particle group to be equal to the current position of that particle. This makes each particle "remember" this position so it can later return to it using pRestore. If copy_vel is true, sets the velocityB of each particle in the current particle group to be equal to the current velocity of that particle. This is useful for computing the orientation of the particle when rendering it using pDrawGroupl.

void pDamping(float damping_x, float damping_y, float damping_z, float vlow = 0.0f, float vhigh = P_MAXFLOAT)

Simulate air by slowing down particle velocities.

If a particle's velocity magnitude is within vlow and vhigh, then multiply each component of the velocity by the respective damping constant. Typically, the three components of damping will have the same value.

NOTES

There are no bounds on the damping constants. Thus, by giving values greater than 1.0 they may be used to speed up particles in stead of slow them down.

void pExplosion(float center_x, float center_y, float center_z, float velocity, float magnitude, float stdev, float epsilon = P_EPS, float age = 0.0f)

An Explosion.

Causes an explosion by accelerating all particles away from the center. Particles are accelerated away from the center by an amount proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

The shock wave of the explosion has a gaussian magnitude. The center of the gaussian travels outward from the center at the specified velocity. So at a given time step, particles at a distance (velocity * age) from center will receive the most acceleration, and particles not at the peak of the shock wave will receive a lesser acceleration.

 The shock wave has a standard deviation of stdev, which is the sharpness or broadness of the strength of the wave.

age is used to calculate the radius of the shock wave. For pExplosion calls in action lists, age is the initial age of the explosion. It is incremented by dt after each call. For immediate mode, age is the current age of the explosion, and it is up to the application to increment the age parameter for each call to pExlosion.

NOTE

Since the current radius of the shock wave is given by (velocity * age) you can set up a standing wave by setting velocity to the desired radius of the wave and age to 1.0. This only works in immediate mode.

void pFollow(float magnitude = 1.0f, float epsilon = P_EPS, float max_radius = P_MAXFLOAT)

Accelerate toward the next particle in the group.

This allows snaky effects where the particles follow each other. Each particle is accelerated toward the next particle in the group by an amount proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

max_radius defines the sphere of influence of this action. No particle further than max_radius from its predecessor is affected.

NOTES

The pFollow action does not affect the last particle in the group. This allows controlled effects where the last particle in the group is killed after each time step and replaced by a new particle at a slightly different position. See pKillOld to learn how to kill the last particle in the group after each step.

void pGravitate(float magnitude = 1.0f, float epsilon = P_EPS, float max_radius = P_MAXFLOAT)

Accelerate each particle toward each other particle.

Each particle is accelerated toward each other particle in the group by an amount proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

max_radius defines the sphere of influence of this action. No particle further than max_radius from another particle is affected.

NOTES

This action is more computationally intensive than the others are because each particle is affected by each other particle.

void pGravity(float dir_x, float dir_y, float dir_z)

Accelerate particles in the given direction.

The gravity acceleration vector is simply added to the velocity vector of each particle at each time step. The magnitude of the gravity vector is the acceleration due to gravity. For example, pGravity(0, 0, -9.8) specifies gravity in the negative Z direction.

void pJet(float center_x, float center_y, float center_z, float magnitude = 1.0f, float epsilon = P_EPS, float max_radius = P_MAXFLOAT)

Accelerate particles that are near the center of the jet.

For each particle, chooses an acceleration vector from the domain and applies it to the particle's velocity. The amount of acceleration applied is directly proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

The domain from which acceleration vectors are chosen is the current velocity domain.

max_radius defines the sphere of influence of this action. No particle further than max_radius from the center is affected.

void pKillOld(float age_limit, bool kill_less_than = false)

Remove old particles.

Removes all particles older than age_limit. But if kill_less_than is true, it instead removes all particles newer than age_limit. age_limit is not clamped, so negative values are ok. This can be used in conjunction with pStartingAge(-n) to create and then kill a particular set of particles.

NOTES

In order to kill a particular particle, set pStartingAge to a number that will never be a typical age for any other particle in the group, for example -10.0. Then emit the particle using pSource or pVertex. Then do the rest of the particle actions and finally call pKillOld(-8.0, true) to kill the special particle because it is the only one with an age less than -8.0.

void pMatchVelocity(float magnitude = 1.0f, float epsilon = P_EPS, float max_radius = P_MAXFLOAT)

Modify each particle’s velocity to be similar to that of its neighbors.

Each particle is accelerated toward the weighted mean of the velocities of other particles in the group by an amount proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2 to the other particles. But when r is small, the acceleration would be infinite, so epsilon is always added to r. Using an epsilon similar in magnitude to magnitude can increase the range of influence of nearby particles on this particle.

NOTES

This action is more computationally intensive than the others are because each particle is affected by each other particle.

void pMove()

Move particle positions based on velocities.

This action actually updates the particle positions by adding the current velocity to the current position. This is typically the last particle action performed in an iteration of a particle simulation, and typically only occurs once per action list.

The velocity is multiplied by the time step length, dt, before being added to the position. This implements Euler's method of numerical integration with a constant, but specifiable step size. See pTimeStep for more on varying the step size.

void pOrbitLine(float p_x, float p_y, float p_z, float axis_x, float axis_y, float axis_z, float magnitude = 1.0f, float epsilon = P_EPS, float max_radius = P_MAXFLOAT)

Accelerate toward the closest point on the given line.

p and axis define an infinite line, where p can be any point on the line and axis is any vector parallel to the line. For each particle, this action computes the vector to the closest point on the line, and accelerates the particle in the vector direction. The amount of acceleration applied is directly proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

max_radius defines the infinite cylinder of influence of this action. No particle further than max_radius from the line is affected.

void pOrbitPoint(float center_x, float center_y, float center_z, float magnitude = 1.0f, float epsilon = P_EPS, float max_radius = P_MAXFLOAT)

Accelerate toward the given center point.

For each particle, this action computes the vector to the center point, and accelerates the particle in the vector direction. The amount of acceleration applied is directly proportional to magnitude. As with most acceleration actions, the amount of acceleration falls off inversely with r2. But when r is small, the acceleration would be infinite, so epsilon is always added to r.

max_radius defines the sphere of influence of this action. No particle further than max_radius from the center is affected.

void pRandomAccel(PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Accelerate particles in random directions.

For each particle, chooses an acceleration vector from the specified domain and adds it to the particle's velocity.

Reducing the time step, dt, will make a higher probability of being near the original velocity after unit time. Smaller dt approach a normal distribution of velocity vectors instead of a square wave distribution.

NOTES

See the documentation of Domains for an explanation of the other arguments.

void pRandomDisplace(PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Immediately replace position with a position from the domain.

For each particle, chooses a displacement vector from the specified domain and adds it to the particle's position.

Reducing the time step, dt, will make a higher probability of being near the original position after unit time. Smaller dt approach a normal distribution of particle positions instead of a square wave distribution.

NOTES

Since this action moves particle positions, unsatisfying results may occur when used with the pAvoid or pBounce actions. In particular, particles may be displaced to the opposite side of the surface without bouncing off it.

See the documentation of Domains for an explanation of the other arguments.

void pRandomVelocity(PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Immediately replace velocity with a velocity from the domain.

For each particle, sets the particle's velocity vector to a random vector in the specified domain.

This function is not affected by dt. If you can think of an appropriate way for this to vary with dt, let me know.

NOTES

See the documentation of Domains for an explanation of the other arguments.

void pRestore(float time_left)

Over time, restore particles to their secondary positions.

Computes a new velocity for each particle that will make the particle arrive at its positionB at the specified amount of time in the future. The curved path that the particles take is a parametric quadratic. Once the specified amount of time has passed, the particles are clamped to their positionB and their velocities are set to 0 to freeze them in place.

If pRestore is called in immediate mode, it is the application's responsibility to decrease time_left by dt on each call. When in an action list, time_left gets decremented automatically.

The positionB attribute of each particle is typically the particle's position when it was created, or it can be specified within a domain. This is controlled by pVertexBTracks, pVertexB, and pVertexBD. The positionB can be set at any time to the particle's current position using the pCopyVertexB action.

NOTES

pRestore(0) is the opposite of pCopyVertexB – it sets each particle's position to be equal to its positionB. However, this has the side effect of setting each particle's velocity to 0.

void pSink(bool kill_inside, PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Kill particles with positions on wrong side of the specified domain.

If kill_inside is true, deletes all particles inside the given domain. If kill_inside is false, deletes all particles outside the given domain.

NOTES

See the documentation of Domains for an explanation of the other arguments.

void pSinkVelocity(bool kill_inside, PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Kill particles with velocities on wrong side of the specified domain.

If kill_inside is true, deletes all particles whose velocity vectors are inside the given domain. If kill_inside is false, deletes all particles whose velocity vectors are outside the given domain. This allows particles to die when they turn around, get too fast or too slow, etc.

NOTES

This action replaces the deprecated pKillSlow by using a sphere domain centered at the origin and a radius equal to the cutoff velocity.

See the documentation of Domains for an explanation of the other arguments.

void pSource(float particle_rate, PDomainEnum dtype, float a0 = 0.0f, float a1 = 0.0f, float a2 = 0.0f, float a3 = 0.0f, float a4 = 0.0f, float a5 = 0.0f, float a6 = 0.0f, float a7 = 0.0f, float a8 = 0.0f)

Add particles in the specified domain.

Adds new particles to the current particle group. The particle positions are chosen from the given domain. The particle colors, sizes, initial ages, velocities, and secondary positions are chosen according to their current domains. See pColor, pColorD, pSize, pStartingAge, pVelocity, pVelocityD, pVertexB, pVertexBD, and pVertexBTracks.

When pSource is called within an action list, the particle attribute domains used are those that were current when the pSource command was called within the pNewActionList / pEndActionList block instead of when pCallActionList is called. Note that this is unlike OpenGL.

particle_rate is the number of particles to add per unit time. If particle_rate / dt is a fraction then pSource adjusts the number of particles to add during this time step so that the average number added per unit time is particle_rate.

NOTES

If too few particles seem to be added each frame, it is probably because the particle group is already full. If this is bad, you can grow the group using pSetMaxParticles.

See the documentation of Domains for an explanation of the other arguments.

void pSpeedLimit(float min_speed, float max_speed = P_MAXFLOAT)

Clamp each particle’s speed to the given min and max.

Computes each particle’s speed (the magnitude of its velocity vector) and if it is less than min_speed or greater than max_speed it is clamped to those bounds, while preserving the velocity vector’s direction.

NOTES

The vector [0,0,0] is an exception because it has no direction. Such vectors are not modified by pSpeedLimit.

void pTargetColor(float color_x, float color_y, float color_z, float alpha, float scale)

Change color of all particles toward the specified color.

Modifies the color and alpha of each particle to be scale percent of the way closer to the specified color and alpha. scale is multiplied by dt before scaling the sizes. Thus, using smaller dt causes a slightly faster approach to the target color.

NOTES

This action makes all colors tend toward the specified, uniform color. Future versions will have more actions that modify color. Please send me suggestions (perhaps with sample implementations).

The value of scale will usually be very small (less than 0.01) to yield a gradual transition.

void pTargetSize(float size_x, float size_y, float size_z, float scale_x = 0.0, float scale_y = 0.0, float scale_z = 0.0)

Change sizes of all particles toward the specified size.

Modifies the size of each particle to be scale percent of the way closer to the specified size triple. This makes sizes grow asymptotically closer to the given size. scale is multiplied by dt before scaling the sizes. Thus, using smaller dt causes a slightly faster approach to the target size. The separate scales for each component allow only selected components to be scaled.

NOTES

This action makes all sizes tend toward the specified, uniform size. Future versions will have more actions that modify size. Please send me suggestions (perhaps with sample implementations).

The value of scale will usually be very small (less than 0.01) to yield a gradual transition.

void pTargetVelocity(float vel_x, float vel_y, float vel_z, float scale)

Change velocity of all particles toward the specified velocity.

Modifies the velocity of each particle to be scale percent of the way closer to the specified velocity triple. This makes velocities grow asymptotically closer to the given velocity. scale is multiplied by dt before scaling the velocities. Thus, using smaller dt causes a slightly faster approach to the target velocity.

NOTES

This action makes all velocities tend toward the specified, uniform velocity.

The value of scale will usually be very small (less than 0.01) to yield a gradual transition.

void pVertex(float x, float y, float z)

Add a single particle at the specified location.

When called within a pNewActionList / pEndActionList block, this action is a shorthand for:

pSource(1, PDPoint, x, y, z).

However, when called in immediate mode, this action uses a slightly faster method to add a single particle to the current particle group. Also, when in immediate mode, exactly one particle will be added per call, instead of an average of 1 / dt particles being added. Particle attributes are chosen according to their current domains, as with pSource.

NOTES

This call is patterned after the glVertex calls. It is useful for creating a particle group with exactly specified initial positions. For example, you can specify a geometrical model using pVertex calls, and then explode or deform it.

void pVortex(float center_x, float center_y, float center_z, float axis_x, float axis_y, float axis_z, float magnitude = 1.0f, float epsilion = P_EPS, float max_radius = P_MAXFLOAT)

Swirl particles around a vortex.

center and axis define an infinite line, where center represents the tip of the vortex and axis is a vector along the line, the length of which is irrelevant. As with most acceleration actions, the amount of acceleration falls off inversely with r2 to the center. But when r is small, the acceleration would be infinite, so epsilon is always added to r. Using an epsilon similar in magnitude to magnitude can increase the range of influence of the vortex. max_radius defines the sphere of influence of this action. No particle further than max_radius from the vortex center is affected.

NOTES

 pVortex immediately displaces particle positions, unlike most actions which affect particle velocities, so unsatisfying results may occur when used with the pAvoid or pBounce actions. In particular, particles may be displaced to the opposite side of the surface without bouncing off it.

pVortex currently does not pull the particles up or down along the axis like a tornado. This will be saved for a future release. If you can suggest an implementation, feel free to send it to me.

Particles can be attracted toward the axis of the vortex using pOrbitLine.

Particle Group Calls

A particle group is first created using pGenParticleGroups, which will return the identifying number of the generated particle group. Unless otherwise specified, all other commands operate on the current particle group. You specify which group is current using pCurrentGroup. The maximum number of particles in the group is specified using pSetMaxParticles. The particle group is then acted upon using the functions listed in the Actions. Some actions will add particles to the particle group, and others will modify the particles in other ways. Typically, a series of actions will be applied to each particle group once (or more) per rendered frame. The particles are then actually drawn. This is similar to the process of rendering a display list in OpenGL, and should be done at the same stage of the application's execution as drawing geometry. To draw a particle group in OpenGL, the application calls one of the pDrawGroup functions. Alternatively, the application can get a copy of the particle data using pGetParticles and use it for other rendering or processing methods. When a particle group is no longer needed, it is deleted using pDeleteParticleGroups.

void pCopyGroup(int p_group_num, int index = 0, int copy_count = P_MAXINT)

Copy particles from the specified group into the current group.

Copy particles from the specified particle group, p_group_num, to the current particle group. Only copy_count particles, starting with number index are copied. Of course, the number of particles actually copied is bounded by the available space in the current particle group, and the number of particles actually in the source particle group. The particles are added in order to the end of the current group. index is the index of the first particle in the source particle group to be copied.

void pCurrentGroup(int p_group_num)

Change which group is current.

Makes p_group_num be the current particle group to which all actions and commands apply.

void pDeleteParticleGroups(int p_group_num, int p_group_count)

Delete one or more consecutive particle groups.

Deletes p_group_count particle groups, with p_group_num being the particle group number of the first one. The groups must be numbered sequentially, and must all exist. This removes the specified particle groups from existence. It does not merely change the number of existing particles or the maximum size of the group.

void pDrawGroupl(int dlist, bool const_size = false, bool const_color = false, bool const_rotation = false)

Draw each particle as a model using OpenGL display lists.

Calls the given OpenGL display list, dlist, once for each particle in the particle group. The display list typically contains only geometry (glBegin / glEnd blocks). Before the display list is drawn for each particle, the OpenGL state is changed. First, the display list is translated to the particle's position. Next, if const_size is false, the matrix stack is modified to scale the display list by the particle's size. Then, if const_rotation is false, the matrix is rotated so that the display list's positive X axis points in the direction of the particle's velocity vector. Finally, if const_color is false, the OpenGL current color is set to the particle's color. If const_color is true, then the OpenGL current color is set only once before drawing any particles. If const_size is true, then no scaling is ever done. If const_rotation is true then no rotation is ever done. This is useful for symmetrical models such as spheres.

NOTES

The OpenGL current color is left in an undefined state following a call to pDrawGroupl.

This command operates on the OpenGL matrix stack, but does not change the matrix mode. It is the application's responsibility to ensure that the matrix mode is correct, usually by calling glMatrixMode(GL_MODELVIEW) first.

Models containing multiple colors can be drawn by constructing a display list that first draws the geometry that will be in the particle color, then changes the color (by calling glColor), then draws geometry in the new color. For example, this could be used to draw balloons of many different colors that each have a white string.

Another way to draw particles that allows greater flexibility than pDrawGroupl is for the application to get the particle data using pGetParticles, and then draw it using any desired method.

void pDrawGroupp(int primitive, bool const_size = false, bool const_color = false)

Draw a particle group using OpenGL primitives.

This is the fastest OpenGL-based method of drawing particles. The exact results depend on the OpenGL primitive type specified. When primitive equals GL_POINTS or any value other than GL_LINES, each particle becomes a single vertex in an OpenGL glBegin / glEnd block. For GL_LINES, each particle becomes a line specified by two vertices - the particle's position and the particle's position minus velocity, yielding a line in the direction that the particle is travelling.

NOTES

The OpenGL current color is left in an undefined state following a call to pDrawGroupp.

At present, GL_LINES is the only primitive handled as a special case. If you have suggestions for sensible, general ways to handle other OpenGL primitives, please tell me.

See also pDrawGroupl.

int pGenParticleGroups(int p_group_count = 1, int max_particles = 0)

Create particle groups, each with max_particles allocated.

Generates p_group_count new particle groups and returns the particle group number of the first one. The groups are numbered sequentially, beginning with the number returned. Each particle group is set to have at most max_particles particles. Call pSetMaxParticles to change this.

Particle group numbers of groups that have been deleted (using pDeleteParticleGroups) may be reused by pGenParticleGroups.

int pGetGroupCount()

Returns the number of particles existing in the current group.

What the summary says.

int pGetParticles(int index, int count, float *position = NULL, float *color = NULL, float *vel = NULL, float *size = NULL, float *age = NULL)

Copy particles from the current group to application memory.

Copies at most count particles beginning with the index-th particle in the current particle group into memory already allocated by the application. Three floats are returned for the position of each particle, representing its x,y,z location. Four floats are returned for the color of each particle, representing its R,G,B,A color. Three floats are returned for the velocity of each particle, representing its dx,dy,dz direction vector. Three floats are returned for the size of each particle, representing whatever the application wants them to. One float is returned for the age of each particle.

pGetParticles returns the number of particles copied to application memory. Of course, the number of particles actually returned is bounded by count and by the number of particles actually in the particle group minus index.

If verts, color, vel, size or age is NULL then no position, color, velocity, size or age data, respectively, will be returned. index and count must be at least 0 and less than the number of particles. index + count must be less than the number of particles.

NOTES

The following code gets the position of all particles:

float *ppos = new float[pGetGroupCount() * 3];

int num_ret = pGetParticles(0, MAXINT, ppos);

As with all arrays in C, the index of the first particle is zero.

int pSetMaxParticles(int max_count)

Change the maximum number of particles in the current group.

If necessary, this will delete particles from the end of the particle group, but no other particles will be deleted.

Action List Calls

These calls create and operate on action lists, which are scripts of many actions to be applied together as a block to the current particle group. An action list is first created using pGenActionLists, and is then defined by calling particle action functions between a call to pNewActionList and a call to pEndActionList. Once the action list is created, it is run via pCallActionList. Thus, an action list is sort of a higher-level action. Complex behaviors can be stored in an action list and then called later, even as part of another action list. Action lists cannot be edited. They can only be created or destroyed. To destroy an action list, call pDeleteActionLists.

NOTES

When particles are created within a pNewActionList / pEndActionList block, they will receive attributes from the state that was current when the action list was created. When in immediate mode (not creating or calling an action list), particles are created with attributes from the current state.

The time step length, dt, uses the value that is current when pCallActionList is executed, not the value of dt when the action list was created. This allows dt to be modified without recompiling action lists. Maybe this isn't a good idea. If it should be the other way in the future, let me know.

void pCallActionList(int action_list_num)

Apply the specified action list to the current particle group.

Call the action functions as specified when this action list was created with pNewActionList. The actions are executed with the state elements values in effect when the action list was created, except the current global value of dt is used, not the value of dt when the list was created.

pCallActionList is the only function other than actions that can be stored in an action list. This allows action lists to become atomic operations in more complex action lists. When calling pCallActionList during the creation of a new action list, action_list_num does not need to indicate an existing action list.

NOTES

It is an error for action_list_num to not indicate an existing (generated) action list.

void pDeleteActionLists(int action_list_num, int action_list_count = 1)

Delete one or more consecutive action lists.

Deletes action_list_count action lists, with action_list_num being the list number of the first one. The lists must be numbered sequentially, and must all exist. This removes the specified action lists from existence.

void pEndActionList()

End the creation of a new action list.

int pGenActionLists(int action_list_count = 1)

Generate a block of empty action lists.

All list numbers are in sequential order starting with the first list. Returns the action list number of the first allocated list. Valid action list numbers are non-negative.

 

void pNewActionList(int action_list_num)

Begin the creation of the specified action list.

The action_list_num must have already been generated using pGenActionLists. Most calls other than actions cannot be made between a call to pNewActionList and the corresponding call to pEndActionList.

NOTES

If called on an action list that has previously been defined, the previous contents of the action list are destroyed and the action list will be created anew. This is as with glNewList in OpenGL.

Domains

A Domain is a representation of a region of space. For example, the pSource action uses a domain to describe the volume in which a particle will be created. A random point within the domain is chosen as the initial position of the particle. The pAvoid, pSink and pBounce actions use domains to describe a volume in space for particles to steer around, die when they enter, or bounce off, respectively.

Domains are also used to describe velocities. Picture the velocity vector as having its tail at the origin and its tip being in the domain.

Finally, domains can be used to describe colors. For drawing with OpenGL, the full color space is 0.0 -> 1.0 in the red, green, and blue axes. For example, the domain PDLine, 1, 0, 0, 1, 1, 0 will choose points on a line between red and yellow. Points outside the 0.0 -> 1.0 range will not be clamped, but eventually will be clamped deep within the OpenGL pipeline.

Since data from particle systems can be used by more than just the OpenGL renderer, the floating point triple used for color can mean different things to different consumers of the data. For example, if a software renderer used colors on the range 0 -> 255, the domain used to choose the colors can be on that range. The color space does not even need to be thought of as RGB, but will be for use in OpenGL.

Several types of domains can be specified. The two basic abstract operations on a domain are Generate, which returns a random point in the domain, and Within, which tells whether a given point is within the domain. Functions such as pSource that take a domain as an argument take it in the form of a PDomainEnum, followed by nine floats. The PDomainEnum is one of the symbolic constants listed below, such as PDPoint. The nine floats mean different things for each type of domain, as described below. Not all domains require all nine floats. You only need to specify the first few values that are relevant to that domain type. The rest default to 0.0 and will be ignored.

PDPoint x, y, z

This domain is a single point.

Generate always returns this point. Within always returns false.

PDLine  x1, y1, z1, x2, y2, z2

These are the endpoints of a line segment.

Generate returns a random point on this segment. Within always returns false.

PDTriangle                x0, y0, z0, x1, y1, z1, x2, y2, z2

These are the vertices of a triangle. The triangle can be used to define an arbitrary geometrical model for particles to bounce off, or generate particles on its surface (and explode them), etc.

Generate returns a random point in the triangle. Within always returns false. [This must eventually change so we can sink particles that enter/exit a model. Suggestions?]

PDPlane                ox, oy, oz, nx, ny, nz

The point o is a point on the plane. n is the normal vector of the plane. It need not be unit length. If you have a plane in a,b,c,d form remember that n = [a,b,c] and you can compute a suitable point o as o = -n*d. The normal will get normalized, so it need not already be normalized.

Generate returns the point o.

 Within returns true if the point is in the positive half-space of the plane (in the plane or on the side that n points to).

PDRectangle                ox, oy, oz, ux, uy, uz, vx, vy, vz

The point o is a point on the plane. u and v are (non-parallel) basis vectors in the plane. They don't need to be normal or orthogonal.

Generate returns a random point in the diamond-shaped patch whose corners are o, o+u, o+u+v, and o+v. Within returns true if the point is in the positive half-space of the plane (in the plane or on the side that the normal (u cross v) points to).

PDBox   x1, y1, z1, x2, y2, z2

These are the minima and maxima of an axis-aligned box. It doesn't matter which of each coordinate is min and which is max.

Generate returns a random point in this box. Within returns true if the point is in the box.

PDSphere                ox, oy, oz, radius1, radius2 = 0.0

The point o is the center of the sphere. radius1 is the outer radius of the shell and radius2 is the inner radius.

Generate returns a random point in the thick shell at a distance between radius1 to radius2 from point o. If radius2 is 0, then it is the whole sphere. Within returns true if the point lies within the thick shell at a distance between radius1 to radius2 from point o.

PDCylinder                x1, y1, z1, x2, y2, z2, radius1, radius2 = 0.0

The two points are the endpoints of the axis of the cylinder. radius1 is the outer radius, and radius2 is the inner radius for a cylindrical shell. radius2 = 0 for a solid cylinder with no empty space in the middle.

Generate returns a random point in the cylindrical shell. Within returns true if the point is within the cylindrical shell.

PDCone                 x1, y1, z1, x2, y2, z2, radius1, radius2 = 0.0

The first point is the apex of the cone and the second is the other endpoint of the axis of the cone. radius1 is the radius of the base of the cone. radius2 is the radius of the base of a cone to subtract from the first cone to create a conical shell. This is similar to the cylindrical shell, which can be thought of as a large cylinder with a smaller cylinder subtracted from the middle. Both cones share the same apex and axis, which implies that the thickness of the conical shell tapers to 0 at the apex. radius2 = 0 for a solid cone with no empty space in the middle.

Generate returns a random point in the conical shell. Within returns true if the point is within the conical shell.

PDBlob                  x, y, z, stdev

The point x, y, z is the center of a normal probability density of standard deviation stdev. The density is radially symmetrical. The blob domain allows for some very natural-looking effects because there is no sharp, artificial-looking boundary at the edge of the domain.

Generate returns a point with normal probability density. Within has a probability of returning true equal to the probability density at the specified point.

PDDisc                  x, y, z, nx, ny, nz, radius1, radius2 = 0.0

The point x, y, z is the center of a disc in the plane with normal nx, ny, nz. The disc has outer radius radius1 and inner radius radius2. The normal will get normalized, so it need not already be normalized.

Generate returns a point inside the disc. Within returns false.

Appendix

Acknowledgements

Code for some of the particle actions and several concepts regarding the structure of the API are thanks to Jonathan P. Leech, ljp@sgi.com. See also:

Jonathan Leech and Russell M. Taylor II, "Interactive Modeling Using Particle Systems", Proceedings of the 2nd Conference on Discrete Element Methods, MIT, spring 1993, pp. 105-116.

Thanks to Mark B. Allen of NASA Ames Research Center for finding bugs, making suggestions and implementing the particle length attribute.

Thanks to Jason Pratt of CMU Stage 3 research group (makers of ALICE) for adding the PDTriangle domain. This is a powerful feature that should have been there the whole time.

Recent Changes

New in version 1.20.

·         Added a gaussian splat drawing mode to pspray. You can draw either as a triangle or a quad with a gaussian alpha texture.

·         Added a PDDisc domain for generating and bouncing.

·         Split the PDPlane domain into PDPlane, for infinite planes and half-spaces; and PDRectangle, for rhombus-shaped planar patches.

·         Added pTargetVelocity to make all particles tend toward a given velocity. This is a component of Boids.

·         Added pMatchVelocity to match velocity to near neighbors.

·         Added pAvoid to steer to avoid the given domain.

·         Added pSpeedLimit.

·         Removed pKillSlow. Instead use pSinkVelocity.

·         Fixed a bug in the Within logic for PDBlob so now it’s right.

·         Demonstrated flocking behavior using a Boids demo.

·         Changed pGetParticles so that verts can be NULL. It now returns the number of particles it’s copying. This way you can just tell it the size of your array instead of having to call pGetGroupCount.

·         Changed the particle size scalar to be a domain. This will allow greater expression of particle shape. Orientation becomes more important. Added pSizeD to specify it. Changed pGetParticles so that it returns three floats per particle for size.

·         For Windows, made it a DLL instead of a static library.

·         For Windows, ships with a DLL compiled with the Intel Vtune compiler (a bit faster).

·         Made pspray choose a demo randomly on startup.

·         Made pStartingAge take an optional parameter for the standard deviation of random starting ages, centered at age.

·         Replaced the lifetime parameter of explosions with stdev and made the shock wave be gaussian instead of square. This will fix the ugly stratification effects in explosions of very dense particle clouds.

·         Updated the documentation.

·         Changed pCallActionList to be storable in action lists, as the docs said.

·         Added a trivial app. for learning the API.

·         Made a rocket demo. It has the rockets in one particle group and their sparks in another, generated at the rocket positions.

·         When re-using an action list that was already generated, it now properly deletes the previous actions.

To Do

·         Add a TOC listing all functions.

·         Change pDraw* const_ to apply_, reverse the sense of it, and make it so if false, color isn’t touched.

·         Compound domains (so you can define a whole model as a single domain instead of a zillion triangle domains). This may be the answer for obstacle avoidance.

·         Const_size isn’t used in pDrawGroupp.

·         For the pspray demo, have it randomly change modes occasionally.

·         Have a secondary color for each particle.

·         How can you add user-defined particle attributes? For now, pSize is the best we can do.

·         Make a screensaver.

·         Make color and size be generic attributes with generic attribute operators.

·         pDensityColor - color is f (density)

·         pPositionColor - color is f (position)

·         pRandomColor - random domain is added to color

·         pVelocityColor - color is f( velocity)

·         pVelocitySize - size is f(velocity)

·         Test multiple particle groups better.

·         Think about changing dt to not be a global value. I don’t think I’ll do it.

·         Think about removing pDraw* from the API since they are OpenGL dependent.

·         Change pVortex to accelerate the particles instead of displacing them. Tried it. Hard.