emit()
function
The PdbEditor executes a pdb shader once for each particle contained in the pdb file(s) specified. Before executing the shader, the particle attributes have been updated to the values for the current pdb particle. Those values are accessed via the getAtt function, and can be modified via the setAtt function. For example
float radius;
getAtt("radiusPP", &radius);
retrieves the value of the particle radius, and places it into the variable radius
. The names of the attributes, such as "radiusPP" in this example, are the ones contained in the Pdb file, and are generally based on the same names given by Maya or any other pdb generating package. Other attribute names commonly used are in the table below:
Attribute  Data Format  Description 
radiusPP  float  radius 
rgbPP  vector  color 
position  vector  3D position of particle center 
velocity  vector  3D velocity of particle 
age  float  age of particle 
id  int  particle id number 
Probably to two most important attributes are the id and position. Because of that, the example editor shading routines below generally begin with the code segment
int id;
getAtt("id", &id);
vector position;
getAtt("position", &position);
The shading technology behind the pdb editor allows for a wide flexibility to manipulate particles, perform mathematical operations, compute vector mathematics, etc. The details can be found in its documentation. To some extent, many of the details can be learned just from following the examples below.
emit()
functionThe shader function emit() performs application specific work within the editor. In the partman application, the emit() call sends the particle attributes the the renderer, which renders a sphere created according to those attributes. In other applications, the emit() call performs other functions.
A simple example editor shader using the emit() call is:
int main()
{
int id;
getAtt("id", &id);
vector position;
getAtt("position", &position);
/* Change the position just for fun */
vector displacement;
displacement[0] = 2.3;
displacement[0] = 0.6777;
displacement[0] = 45;
position = position + displacement;
setAtt("position", position);
/* Now emit the particle */
emit();
return 7;
}
This editor shader will simply shift every particle in the pdb file over by the vector amount (2.3, 0.6777, 45). As each one is shifted, emit() is called to dispose of it.
The reason why emit() is so important is because it may be called any number of times for each pdb "guide" particle. This allows us to turn one pdb particle into many "child" particles. As each child particle is created, the attributes are changed to suit the need, and emit() is called to dispose of it.
Here is a simple example of how to create child particles and emit them:
int main()
{
/* Retrieve guide particle info */
int id;
getAtt("id", &id);
srand48(id);
vector position;
getAtt("position", &position);
float radius;
getAtt("radiusPP", &radius);
/* Determine how many child particles are desired */
float childmax;
childmax = 100;
/* Reduce size of child particles
so that they fit inside guide particle */
float childradius;
childradius = 0.3 * radius / pow(childmax, 0.3333);
setAtt("radiusPP", childradius);
float child;
child = 0;
/* Loop over all children, creating new positions */
vector d;
while( child < childmax )
{
d[0] = drand48()  0.5;
d[1] = drand48()  0.5;
d[2] = drand48()  0.5;
d = radius * d / sqrt( d . d );
d = d + position;
setAtt("position", d);
emit();
child = child + 1;
}
return 7;
}
The figures below show the effect of the child creation process using this code sample.

In fact, the generic layout of the child creation editor can be outlined as follows:
/* Define global data structures for guide and child particles */
/* guide particle data */
int guideid;
float guideradius;
vector guideposition;
vector guidecolor;
vector guidevelocity;
float guideage;
/* child particle data */
float childcounter;
float maxchildcount;
float childradius;
vector childposition;
vector childcolor;
/* others... */
/* .... */
/* Define functions to do the work */
void getGuideParticleInfo()
{
getAtt("id", &guideid);
getAtt("radiusPP", &guideradius);
getAtt("position", &guideposition);
getAtt("rgbPP", &guidecolor);
getAtt("velocity", &guidevelocity);
getAtt("age", &guideage);
return;
}
void setChildParticleInfo()
{
/* dependent on application */
/* set child attributes that are fixed */
return;
}
void initChildParticle ()
{
childcounter = 0;
/* if using random numbers, initialize seed
to track with the guide particle */
srand48(guideid);
/* give inital values of changing attributes */
return;
}
int moreChildren ()
{
int flag;
flag = 0;
childcounter = childcounter + 1;
if(childcounter <= maxchildcount) {flag = 1;}
return flag;
}
int setChildParticle ()
{
/* Do something to distinquish this child
from the many others being created. */
return 1; /* return 1 to emit, 0 to not emit */
}
/* Operate main() in a fairly generic way */
int main()
{
/* Retrieve guide particle info of use */
getGuideParticleInfo();
/* Setup and Initialize child parameters */
setChildParticleInfo();
initChildParticle();
/* Loop over all children */
int emitdecision;
while( moreChildren() )
{
emitdecision = setChildParticle();
if(emitdecision==1){emit();}
}
return 7;
}
With this setup, a variety of algorithms can be brought to bear to spread child particles around the volume of space based on pseudodynamical forms, guide particle data, statistics, and any other paradigm. The next section is devoted to providing several methods of filling the setChildParticle() function to achieve various ends. Many of these methods can be used simultaneously or in sequence.
Most of the algorithms in this section for placing child particles are based on some randomizing scheme. This is done in order to achieve complexity in the gross structure of accumulated particle collection. One of the primary issues in taking this approach is the control of the shape and size of the collection after all of the randomization has taken place.
float boxheight, boxwidth, boxlength; /* values set by some criterion */
vector displacement;
int setChildParticle_UniformRectangleFill()
{
displacement[0] = boxheight * (drand48()  0.5);
displacement[1] = boxwidth * (drand48()  0.5);
displacement[2] = boxlength * (drand48()  0.5);
childposition = guideposition + displacement;
setAtt("position", childposition);
return 1;
}
The figure below is an example of uniform rectangle filling.

For filling a sphere, the process is only a little different:
float sphereradius; /* value set by some criterion */
vector displacement;
vector RandomUnitVector()
{
vector ruv;
float theta, phi;
theta = 3.14159265 * drand48();
phi = 2.0 * 3.14159265 * drand48();
ruv[0] = sin(theta) * cos(phi);
ruv[1] = sin(theta) * sin(phi);
ruv[2] = cos(theta);
return ruv;
}
int setChildParticle_UniformSphereFill()
{
displacement = RandomUnitVector();
displacement = sphereradius * displacement * drand48();
childposition = guideposition + displacement;
setAtt("position", childposition);
return 1;
}
The result is shown in the figure below.

To generate the images in this section, the setChildParticleInfo() routine has the form
void setChildParticleInfo()
{
/* dependent on application */
/* set child attributes that are fixed */
childradius = 0.2*guideradius;
setAtt("radiusPP", childradius);
maxchildcount = 100;
sphereradius = guideradius;
boxheight = guideradius;
boxwidth = 1.5 * guideradius;
boxlength = 2 * guideradius;
return;
}




For our algorithm, the new important parameters are:
/* cauliflower distribution */
float cauliflowerscale;
int nbcauliflowerclumps, nbcauliflowerrecursions;
The parameter cauliflowerscale
controls the relative size of spheres in each recursion level. At each level, nbcauliflowerclumps
spheres are placed on the surface of each sphere from the previous level. Finally, the total number of recursion levels is nbcauliflowerrecursions
.
These three parameters are used to recursively create the cauliflower. The approach is:
void putbumps(vector gpos, float gradius, int nb, float scale, int rec)
{
int p, trackrec;
float theta, phi, radius;
vector pos;
/* track the number of recursions */
trackrec = rec1;
/* loop over particles to clump onto surface of parent */
p = 0;
while(p < nb)
{
/* place the particle on the surface of its parent */
pos = RandomUnitVector();
pos = gpos + gradius * pos;
setAtt("position", pos);
/* give the particle a reduced size */
radius = gradius * scale;
setAtt("radiusPP", radius);
emit();
/* now recurse to put particles on the surface of this particle */
if(trackrec > 0){putbumps(pos, radius, nb, scale, trackrec);}
p = p + 1;
}
return;
}
int setChildParticle_Cauliflower()
{
emit(); /* emit guide particle */
/* recursively places and emits particles */
putbumps(guideposition, childradius, nbcauliflowerclumps,
cauliflowerscale, nbcauliflowerrecursions);
return 0; /* dont emit because emission occured during recursion */
}
Several new things are going on in this algorith. First, because of the recursion, all of the emit() calls take place inside setChildParticle_Cauliflower(), and its return value is zero so that no additional emit() calls are made. The second new technique is recursion. With each recursive call to putbumps()
, the next level of spheres are placed, with a sphere radius that is larger or smaller than the previous level by a factor of cauliflowerscale
. By setting this parameter less than one, the bumps get smaller and smaller. In the example images above, the values used were:
/* cauliflower placement */
cauliflowerscale = 0.5;
nbcauliflowerclumps = 20;
nbcauliflowerrecursions = 3;
In the algorithm below, we generate a simple space curve which has constant helicity and curvature. For this, the important data to track is
/* Constant helicity and curvature space curve */
float pathlength, dpath, helicity, curvature;
vector Tangent, Normal, Binormal, T, N, B, G;
/* Space Curve Routines */
void ComputeSpaceCurveGamma()
{
float hh;
hh = 1.0/sqrt(1.0 + helicity*helicity);
G = (helicity * B  T ) * hh;
return;
}
vector ComputeSpaceCurvePosition()
{
vector Position;
float cl, sl, theta;
theta = pathlength * curvature;
sl = sin(theta);
cl = cos(theta);
float hh;
hh = 1.0/sqrt(1.0 + helicity*helicity);
Position = T * theta;
Position = Position  N * cl * hh;
Position = Position + G * (theta  sl) * hh;
Position = Position / curvature;
return Position;
}
void ComputeSpaceCurveTangent()
{
float cl, sl, theta;
theta = pathlength * curvature;
sl = sin(theta);
cl = cos(theta);
float hh;
hh = 1.0/sqrt(1.0 + helicity*helicity);
Tangent = T;
Tangent = Tangent + N * sl * hh;
Tangent = Tangent + G * (1.0cl) * hh;
return;
}
void ComputeSpaceCurveNormal()
{
float cl, sl, theta;
theta = pathlength * curvature;
sl = sin(theta);
cl = cos(theta);
float hh;
hh = 1.0/sqrt(1.0 + helicity*helicity);
Normal = N * cl + G * sl;
return;
}
void ComputeSpaceCurveBinormal()
{
float cl, sl, theta;
theta = pathlength * curvature;
sl = sin(theta);
cl = cos(theta);
float hh;
hh = 1.0/sqrt(1.0 + helicity*helicity);
Binormal = B  ( N * sl + G * (1.0cl) ) * helicity * hh;
return;
}

The figure below was generated from the most basic random walk method. To accomplish this, the particles are very small (0.005 times the guide particle radius), and lots of them are used (20000 per guide particle). When even more particles are used in the next image (200,000 per guide particle, over 15 million total), you can clearly see that the random walk marches all over space.




The random walk process uses the following code:
vector RandomStep()
{
vector ruv;
ruv[0] = drand48()  0.5;
ruv[1] = drand48()  0.5;
ruv[2] = drand48()  0.5;
return ruv;
}
int setChildParticle_BasicRandomWalk()
{
childposition = childposition + walkstep * RandomStep();
setAtt("position", childposition);
return 1;
}
The routine RandomStep()
generates a vector with components lying in the range (0.5,0.5). The current child particle is placed at a position that is randomly displaced from the previous particle, with the distance of the displacement in the range (0,sqrt(3)/2 walkstep).
There are no bounds on the size or length of random walk achievable. Because of the randomness in the direction and size of the steps however, there is a theoretical estimate of the approximate range the random walk will occupy. Based on the statistics of the random numbers being used, the root mean square step sizes is S = walkstep/sqrt(24)
. After maxchildcount
steps, the range of the random walk should be roughly (S sqrt(maxchildcount))
.
int setChildParticle_CorrelatedRandomWalk()
{
walk = walk * mixin + walkstep * RandomStep() * mixout;
childposition = childposition + walk;
setAtt("position", childposition);
return 1;
}
The parameters mixin
and mixout
are mix the previous value of the random step with the new one, and so 0 < mixin
< 1, and 0 < mixout
< 1, with mixin
+ mixout
= 1. The special case mixin
= 0 is the uncorrelated basic random walk, while at the other extreme mixin
= 1 produces purley straight lines.
The range in between uncorrelated random walk and straight line posseses a wide range of behaviors that we will try to organize in this section.
The first thing to sort out is what the impact of correlation is. The figure below shows that clearly: correlation turns the path that the particles are laid out on into a "smooth" curve with unpredictable twists and turns. As the mixin
parameter approaches 1, the number of kinks and turns become fewer. The example below for example, using 50000 particles per guide particle, with short spacing between them. The mixin
is 0.9999. Thats right, there are four 9's to the right of the decimal point.

mixin = 0.9999. Despite the very high correlation, there is considerable structure in each path. There are 50000 child particles for each guide particle.

To give you some idea of the importance of 0.9999 versus 0.999 for example, the image below was produced with mixin
= 0.999. There is a substantial difference between the two.

mixin = 0.999. There are 50000 child particles for each guide particle.

mixin
= 0.99.

mixin = 0.99.

int setChildParticle_SphericalRandomWalk()
{
childposition = childposition + walkstep * RandomUnitVector();
setAtt("position", childposition);
return 1;
}
So, in the spherical random walk, successive steps are a distance exacly walkstep
apart, whereas in the basic cartesian random walk, the distance between steps can be as little as 0 and as much as walkstep
. This has the effect of leaving the spherical random walk volume less dense in the center, as shown in the example below.


Just as with the cartesian random walk, we can introduce correlation in the walk. This is accomplished this way:
int setChildParticle_CorrelatedSphericalRandomWalk()
{
walk = walk * mixin + walkstep * RandomUnitVector() * mixout;
childposition = childposition + walk;
setAtt("position", childposition);
return 1;
}
We have introduced the mixin
and mixout
parameters that same as in the basic cartesian case. An example of correlated spherical walk is below:

mixin = 0.999. 

LevyMu = 2.2 and LevyMin = 0.03 * guideradius . There was no correlation in the steps, and there were 2000 child particles (i.e. Levy steps) per guide particle. 
vector RandomLevyStep()
{
vector ruv;
ruv = RandomUnitVector();
float step;
step = LevyMin * pow(drand48(), 1.0/(1.0LevyMu));
ruv = step * ruv;
return ruv;
}
int setChildParticle_RandomLevyWalk()
{
childposition = childposition + RandomLevyStep();
setAtt("position", childposition);
return 1;
}
Before proceeding, the convention chosen here for implicit surfaces is that the implicit function has a value of zero on the surface, has a positive value inside the surface, and a negative value outside the surface.
First, we need a method of computing the relevant information about the implicit surface. For this problem, we need routines that can retrieve two pieces of information:
getISFunction()
and getISNormal()
. For the simplest case of a spherical implicit surface, these can take the form
float getISFunction(vector r)
{
/* simple circle case */
vector test;
test = r  positionIS;
float returnvalue;
returnvalue = 1.0  (test.test)/(radiusIS*radiusIS);
return returnvalue;
}
int getISSign(vector r)
{
/* > 0 => inside surface; < 0 => outside surface */
float ISFunctionvalue;
ISFunctionvalue = getISFunction(r);
int returnvalue;
returnvalue = 0;
if(ISFunctionvalue > 0){ returnvalue = 1; }
if(ISFunctionvalue < 0){ returnvalue = 1; }
return returnvalue;
}
vector getISNormal(vector r)
{
/* simple circle case */
vector test;
test = r  positionIS;
test = test / sqrt(test . test);
return test;
}
We have also added the routine getISSign()
which uses getISFunction()
to decide if a point in space is inside or outside the implicit surface. While we have built functions for spherical implicit surfaces, the process described here is applicable for any implicit surface.
The random walk illustrated in setChildParticle_CorrelatedTraverseIS()
below is altered at each step to that is preferentially moves along the normal toward the surface of the Implicit Surface. At any step, the walk determines whether the current child position is inside or outside the surface, then reorients the random step so that it moves toward the surface. There is still a random element in each step perpendicular to the normal.
int setChildParticle_CorrelatedTraverseIS()
{
float ISsign;
vector ISnormal;
ISnormal = getISNormal(childposition);
/* ISsign > 0 => inside; ISsign < 0 => outside */
ISsign = getISSign(childposition);
/* step in a random direction perpendicular to IS */
walk = RandomUnitVector();
walk = walk  (walk . ISnormal) * ISnormal;
walk = walk / sqrt(walk . walk);
walkIS = walkIS * mixin + stepIS * (ISsign * ISnormal + walk) * mixout;
childposition = childposition + walkIS;
setAtt("position", childposition);
return 1;
}
The two examples results below demonstrate that the random walk is bound to the implicit surface.

mixin = 0.0. 

mixin = 0.99. 
setChildParticle_CorrelatedFillIS()
looks like:
int setChildParticle_CorrelatedFillIS()
{
float ISsign;
/* ISsign > 0 => inside; ISsign < 0 => outside */
ISsign = getISSign(childposition);
/* step in a random direction perpendicular to IS */
walk = RandomUnitVector();
if(ISsign < 0) /* Outside: redirect inside */
{
vector ISnormal;
ISnormal = getISNormal(childposition);
walk = walk  (walk . ISnormal) * ISnormal;
walk = walk / sqrt(walk . walk);
walkIS = walkIS * mixin + stepIS * (ISsign * ISnormal + walk) * mixout;
}
else /* Inside: let it go */
{
walkIS = walkIS * mixin + stepIS * walk;
}
childposition = childposition + walkIS;
setAtt("position", childposition);
return 1;
}
As you can see, the difference is that now if the particle is inside, the random walk is unaltered.
The examples below demonstrate filling for various random walk correlations.

mixin = 0.0. 

mixin = 0.5. 

mixin = 0.9. 

mixin = 0.95. 

mixin = 0.99. 
$frame
provided by the pdbEditor, the tag can be positions along the walk in a time dependent way.
An example of this is the following code:
float cosh(float arg)
{
float b;
b = exp(arg);
return 0.5*(b + 1.0/b);
}
float ComputeSpread()
{
float pl;
/* compute relative centroid of pulse based on speed and time */
pl = pathlength  pulse_spreadspeed * $frame;
/* use cosh function for a onoff behavior */
return pulse_minspread + (pulse_maxspreadpulse_minspread) / cosh(pl/pulse_spreadwavethickness);
}
int setChildParticle_Pulse()
{
/* Start out similar to a space curve */
childposition = guideposition + ComputeSpaceCurvePosition();
ComputeSpaceCurveNormal();
ComputeSpaceCurveBinormal();
ComputeSpaceCurveTangent();
pathlength = pathlength + dpath;
/* Now build a cloud around that position with a thickness */
float spread, angle;
spread = ComputeSpread();
int cloudsize, cloudcount;
cloudsize = pulse_cloudsize * (spread/pulse_minspread);
cloudcount = 0;
while(cloudcount < cloudsize)
{
displacement = RandomStep();
/* extract and scale component along tangent */
pulse_position = childposition + dpath * (displacement.Tangent)*Tangent;
/* extract and scale component perp to tangent */
displacement = displacement  (displacement.Tangent) * Tangent;
pulse_position = pulse_position + spread * displacement;
setAtt("position", pulse_position);
emit();
cloudcount = cloudcount + 1;
}
return 0;
}
What does this code do? The first few lines of setChildParticle_Pulse()
just set up the walk as a SpaceCurve, exact as in the previous Curves in space section. Then a routine called ComputeSpread()
is invoked. The number that comes from this is used in two ways. First, a set of "grandchild" particles are created, and the number of them depends on the spread
value  the higher the spread, the more particles. Each grandchild is created as part of an ordinary random placement, but the placement is confined to the disk perpendicular to the space curve at that point. The distance that the placement extends perpendicular to the curve is controlled by the value of spread
also. Effectively, spread
controls the local thickness of the curve in space.
Now examine how spread
is computed. The computation involves the inverse of the cosh()
function, which is plotted below:

ComputeSpread()
routine returns the value pulse_minspread
for all points on a walk except when pl = pathlength  pulse_spreadspeed * $frame
nears a value of zero, where it smoothly changes to the value pulse_maxspread
. But that nearzero point is a funciton of frame number, so over a sequence of frames, the segment of a walk with ComputeSpread()
different from pulse_minspread
moves farther out on the walk, giving the illusion of pulse propagation. This image below illustrates a frame of pulses in a set of space curves.


This effect can be modified so that instead of producing a limited region of a pulse, a shockwave style effect can be produced. In this effect, once ComputeSpread()
has smoothly changed from the value pulse_minspread
to the value pulse_maxspread
, it remain there. This is accomplished by the line
return pulse_minspread + (pulse_maxspreadpulse_minspread) / cosh(pl/pulse_spreadwavethickness);
in ComputeSpread()
with the line
return minspread + (maxspread  minspread) * Heaviside(pl/spreadwavethickness);
where the Heaviside()
function is
float Heaviside(float arg)
{
float b;
b = exp(arg);
return b/(1.0 + b);
}