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
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
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:
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:
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:
With this set-up, a variety of algorithms can be brought to bear to spread child particles around the volume of space based on pseudo-dynamical 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.
The figure below is an example of uniform rectangle filling.
|
For filling a sphere, the process is only a little different:
The result is shown in the figure below.
|
To generate the images in this section, the setChildParticleInfo() routine has the form
|
|
|
|
For our algorithm, the new important parameters are:
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:
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:
In the algorithm below, we generate a simple space curve which has constant helicity and curvature. For this, the important data to track is
|
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:
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:
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.0-LevyMu));
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 re-orients the random step so that it moves toward the surface. There is still a random element in each step perpendicular to the normal.
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:
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 near-zero 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 shock-wave 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_maxspread-pulse_minspread) / cosh(pl/pulse_spreadwavethickness);
in ComputeSpread()
with the line
return minspread + (maxspread - minspread) * Heaviside(pl/spreadwavethickness);
where the Heaviside()
function is