Example Helical Particles Around a Curve 1
In this example, we look at a more challenging particle expression application: making particles travel in a precise helix around an arbitrary NURBS curve.
An important first step in developing an approach to solving a complex problem using expressions or MEL is to try to come up with the most complete possible list of solutions. Usually, there are several ways to solve a problem in Maya, but only one or two efficient ways, and since particle expressions can be extremely slow to execute, you must keep efficiency in mind when developing a particle application.
Brainstorming Possible Solutions
The following list, almost certainly not comprehensive, suggests ways to approach creating a helix of particles that wind around a curve. Some may not be practical, but it's worthwhile to write them out in any case.
- Using the Curve Flow feature under Maya's Dynamics menu to force the particles to move along a motion path. This requires creating a helical secondary curve around the original curve.
- Using the curve as a goal object and animating the goal offset to force a helical motion around the goal object.
- Developing a particle expression that manipulates position directly based on the particle's age.
At first, it may seem that using curve flow or animating goal offsets might be mathematically simpler than using a direct particle expression for position. Animating goal offsets, though, requires calculating the direction and distance of the offset away from the curve, which is probably just as complicated as directly calculating a particle's position based on its age.
Using curve flow requires making a helical curve that wraps around the original curve. Not only does this raise the same mathematical questions as the other two approaches, it also relies on the Curve Flow feature, which can be very slow when it operates on a complex curve path with many control vertices (CVs).
Thus, directly manipulating the particle position with a particle expression is probably the easiest approach.
A First Attempt at the Particle Expression
- Start by creating a curve and an emitter (Figure 4.17). Since we will be setting the particle's position directly in our expression, it probably does not matter where the emitter is located. Make sure that the curve has at least one inflection point, a point where it goes from convex to concave or vice versa.
- If necessary, rename the emitter emitter1 and the curve curvei before proceeding.
- Set your timeline to run from frame 1 to frame 400.
- Play the animation until particles appear; then drag over them to select the particle object.
- Figure 4.17 An emitter and a curve.
- Hold down the right mouse button on the particle object, and choose particle1 . . . from the menu to open the Attribute Editor.
- Scroll down to the Per Particle (Array) Attributes tab, and then right-click on position. Choose Runtime Expression after Dynamics . . . from the menu.
- Enter the following expression:
float $scaleOffset = 0.2;
float $pos[] = ^pointOnCurve -pr (age) -p curve]/; float $tan[] = ^pointOnCurve -pr (age) -nt curve]/; float $norm[] = ^pointOnCurve -pr (age) -nn curve]/;
vector $posvec = <<$pos[0], $pos[1], $pos[2]>>; vector $tanvec = <<$tan[0], $tan[1], $tan[2]>>; vector $normvec = <<$norm[0], $norm[1], $norm[2]>>;
vector $norm2vec = cross($tanvec, $normvec);
position = $posvec + ($scaleOffset *$normvec* cos(age*20)) +
8. Click Create.
Now rewind and play back the resulting animation (Figure 4.18). Depending on the curve that you created, you may see a few glitches in the particle motion. Ignore them for now.
How this expression works The first line, float $scaleOffset = 0.2;
defines a floating-point variable that we leave unchanged throughout the expression. This variable defines how far the helix is from the original curve, as we'll see later on. Changing this value will move the helix of particles closer to the curve or farther away.
Immediately following the definition of $scaleOffset are three statements that execute the MEL function called pointOnCurve to get information about curve1. Let's examine the first of these for a moment:
This statement resembles many of our previous definitions of variables, except that $pos is declared with square brackets â–¡. These brackets establish that $pos is a variable that will contain an array, or a series of values. This is necessary because the pointOnCurve MEL command creates an array that contains its result. We'll see how to access the contents of an array later on in the expression.
- Figure 4.18 A first helix.
The parameters of a curve, just like the U and V parameters on a NURBS surface or mesh, define a particular point on the curve. This script uses the particle's age to find the location on the curve whose parameter has the same value as the particle's age. As a particle gets older, then each time we call pointOnCurve for that particle, it returns a position farther down the curve.
The backticks vv surround a MEL command that creates a result that we want to capture in a variable. In this case, the pointOnCurve command examines curvel and returns with values that describe its geometry at a particular point. -pr (age) tells the command to look at the point along the curve that is defined by the parameter corresponding to this particular particle's age (which the "age" particle attribute contains, measured in seconds), and -p tells the command to return the position of the point on the curve. Finally, the curvel at the end indicates which curve to examine.
You can see which options a particular command can accept by looking at its page in the MEL Command Reference, available from Maya's Help menu. In Chapter 5, we'll look at MEL commands in more detail.
Since this MEL command finds a position in three-dimensional space along the curve, the pointOnCurve command generates a result that has three components, X, Y, and Z. Unlike the particle attributes we've seen, such as position and velocity, pointOnCurve returns these values not in the form of a vector but as an array, which is a list of values (in this instance) in X, Y, and Z order.
Next, we use the pointOnCurve command to extract more information about our curve at the point corresponding to the particle's age, as follows:
float $tan[] = ^pointOnCurve -pr (age) -nt curvel^;
float $norm[] = ^pointOnCurve -pr (age) -nn curve!;
The first pointOnCurve command uses the -nt parameter, which returns the normalized tangent on the curve. The X, Y, and Z values that are returned define both a direction and a distance. Requesting a "normalized" tangent ensures that the distance is 1; this saves us some effort, as otherwise we'd have to use the unit vector function to normalize the tangent by hand.
The second command uses the -nn parameter to get the normalized normal of the curve. The normal to a curve defines a perpendicular direction to the curve that points in the convex direction of the curve's curvature. Asking for it to be normalized, again, ensures that the normal has a length of 1.
Since we wish to calculate a position for the point, and the position per-particle attribute is a vector, we must now convert the three arrays $pos[], $tan[], and $norm[] to vectors:
vector $posvec = <<$pos[0], $pos[1], $pos[2]>>;
vector $tanvec = <<$tan[0], $tan[1], $tan[2]>>;
vector $normvec = <<$norm[0], $norm[1], $norm[2]>>;
Now we have three vectors: a position along the curve, a tangent vector pointing along the curve at that point, and a normal vector pointing outward from the curve at that point.
To get the helical motion, we first place the particle at the position on the curve corresponding to the particle's age. Then, we move the particle away from the curve along the normal by an amount that varies with a function that oscillates, either sine or cosine. Thus, as the particle travels down the curve, it will oscillate back and forth along the normal. Finally, we'll apply another oscillation along the vector that is perpendicular to the normal we were given and the tangent vector.
Since this vector is also a normal to the curve, we'll call it $norm2vec. We can find it using the cross operator described above. Since the existing tangent and the normal vectors both have lengths of 1 and are perpendicular to each other, the cross operator will give us a vector that's perpendicular to both with a length of 1 as well:
vector $norm2vec = cross($tanvec, $normvec);
Finally, we construct the expression for position according to our plan:
position = $posvec +($scaleOffset *$normvec * cos(age*20))+
This expression first places the particle at a particular point on the curve ($posvec), and then moves it a distance of ($scaleOffset * cos(age * 20)) along Snormvec. $scaleOffset defines how far the particle will move away from the curve in that direction, and cos(age * 20) oscillates between -1 to 1. Changing the value by which we multiply the particle's age (in this case, 20) changes how fast the particle oscillates.
Finally, the expression moves the particle (SscaleOffset * sin(age*20)) along $norm2vec. By oscillating with a cosine curve in one direction and a sine curve in the perpendicular direction, we get circular motion around the curve.
Refining the Solution
The above expression works fairly well to make the particles move in a helix around the curve, and for some applications it might be just fine. However, it has a couple of problems.
First, rewind and run the simulation until the helix of particles has passed the inflection point in the curve. Then, move in close with the camera (Figure 4.19).
At the inflection point, you can see that the particles jump discontinu-ously from one side of the curve to the other.
Next, examine the volume immediately around the emitter. A few of the particles seem to be clustered there, rather than at the appropriate point on the curve (Figure 4.20).
- Figure 4.19 A break in the helix.
Since we're determining where to put each particle based on its age, it would be helpful to see the ages of each particle in the cluster around the emitter. The Numeric particle render type gives us an easy way to do this, as follows:
- Make sure you have the particle object selected, and set the Particle Render Type in the Channel Box to Numeric.
- Once again, open the Attribute Editor by right-clicking on the particle object and selecting particlel. . . from the menu that appears.
- Open the Render Attributes tab if necessary (Figure 4.21). It should be the second tab above the Per Particle (Array) Attributes tab.
- Click the Add Attributes For [ Current Render Type ] button.
- At this point, a couple of new attributes should appear, including Attribute Name. In the Attribute Name field, type age and press Enter (Figure 4.22).
- Close the Attribute Editor window.
Upon examining the cluster of points around the emitter, we can see that their ages are all less than 1/24, which means they are all in their first frame of existence (Figure 4.23). (If you've let the simulation run far enough that particles reach the end of the curve, you will also see a number of particles exactly at 0, 0, 0.)
The points clustered around the emitter whose ages are less than one frame duration are in the wrong place because, once again, we've assumed that setting a runtime expression will place them correctly on their first
- Figure 4.20 Errant particles at the origin.
Figure 4.21 The Render Attributes tab.
Figure 4.21 The Render Attributes tab.
frame of existence. Fixing this problem requires setting the same expression for both the runtime and creation expressions.
To clean up the particles that are old enough to have reached the end of the curve, setting the particle lifetime parameter appropriately will prevent their ending up at the origin. Solving the problem of the particles'
11 t mission nanaom stream see as \ Render Attributes r Depth Soil_
Particle Render Type | Numeric
Add Attributes For Cuirent Render Type
Attribute Name [age
r Selected Only jJ Render Stats
Figure 4.22 Adding the age attribute.
- Figure 4.23 Particles labeled with their ages.
discontinuous jump at the inflection point, though, is trickier. The underlying cause is that using the pointOnCurve function to find a curve's normal gives a result that always points toward the convex side of the curve. Unfortunately, at an inflection point, such a normal flips by 180 degrees to point toward the new convex side. Figure 4.24 shows a view looking down the axis of the helix to see the 180-degree jump.
Fixing this problem requires a different approach to choosing a normal to the curve. Start over with a new scene to look at, such an approach, as follows:
- Figure 4.24 Axial break in helix at inflection point.
- Choose New Scene, and select No when asked whether you want to save.
- Create a new curve called curvel with at least one inflection point.
- Make sure curvel is selected, and then open the Script Editor and type the following:
offsetCurve -d 0.1 -n curvelguide
4. Press Enter.
Figure 4.25 shows the result of the above expression. Note the second curve running along your curvel.
What the offsetCurve command has done is produce another curve called curvelguide that is a constant distance from the original curve, always in the same direction. The -d 0.l parameter specifies that the curve should be 0.1 units from the original. If you see glitches at points where the original curve bends sharply, making the distance smaller should clean them up.
By using pointOnCurve to find the point on curvel that matches a given parameter, and by using pointOnCurve again to find the matching point on
- Figure 4.25 Result of offsetCurve command.
curvelguide, we can use the difference of those two points to find a direction that will be our normal. Unlike the normal we get by calling pointOnCurve with the -nn parameter, this normal will always change continuously as the particles travel along curvel.
Finally, there is one other change we can make to our expression that should not change its result but will make it run much faster. Remember that if there is a node that exists to calculate a particular value and also a MEL command for the same purpose, in many cases using the node will be faster.
One of the options for pointOnCurve is -ch on (for "construction history on"). What this does is create a node of type pointOnCurvelnfo that has attributes for its inputs and outputs. By creating such a node once for each curve and by using setAttr to set the parameter and getAttr to get the value, the expression will execute much faster. Why? Because the MEL command pointOnCurve creates and destroys one such node every time it runs, which is once per particle per frame. By eliminating this creation and destruction of nodes, the expression will run much faster.
1. In the Script Editor, type the following and press Enter:
pointOnCurve -ch on -pr 0 curvel; pointOnCurve -ch on -pr 0 curvelguide;
You'll notice that each command responds with a result like
// Result: pointOnCurvelnfol This is the name of the pointOnCurvelnfo node that the command created.
- Create a new emitter called emitterl.
- Select the particle object; use the Runtime Expressions After Dynamics option, and lauch the Expression Editor. Now enter the following position expression as both the runtime and creation expressions for the particle object:
float $scaleOffset = 0.2;
setAttr pointOnCurvelnfol.parameter (age); setAttr pointOnCurveInfo2.parameter (age);
float $pos[] = ^getAttr pointOnCurvelnfol.position^; float $tan[] = ^getAttr pointOnCurvelnfol.normalizedTangent^; float $normEnd[] = ^getAttr pointOnCurveInfo2.position~; float $norm[];
$norm[0] = $normEnd[0] -$pos[0]; $norm[l] = $normEnd[l] -$pos[l]; $norm[2] = $normEnd[2] -$pos[2];
vector $posvec = <<$pos[0], $pos[l], $pos[2]>>;
vector $tanvec = <<$tan[0], $tan[l], $tan[2]>>;
vector $normvec = unit(<<$norm[0], $norm[l], $norm[2]>>);
vector $norm2vec = cross($tanvec, $normvec);
position = $posvec +($scaleOffset*$normvec*cos(age*20)) +
($scaleOffset * $norm2vec * sin(age*20));
- Find the maximum parameter of your curve by selecting curvel, opening the Attribute Editor, and looking at the Min Max Value boxes under the shape node's NURBS Curve History tab. The Max Value is what you want to set as the particle life span, because it's the age at which a particle will hit the curve's maximum value (Figure 4.26).
- Select the particle object; change the Lifespan Mode to Constant in the Channel Box, and set Lifespan (not Lifespan Random) to the curve parameter's maximum value (Figure 4.27).
- Rewind and play back the animation. As shown in Figure 4.28, you'll see that the first few particles are almost, but not quite, in the right place.
As it turns out, this is due to the particles' initial speed being 1, not 0. Select the emitter, and set Speed to 0 in the Channel Box.
7. Rewind and play back the animation again. The speed is much improved, and the glitches we noted above are gone.
- Figure 4.26 Minimum and maximum parameters for curve1.
Geometry Space Geometry Lo ount From Hisloiy on
Geometry Space Particle Loc Goaf Smoothness 3
Cache Data oil
Trace Depth 10 tide Render Type Points
_Lifespan ft_j
INPUTS timel
Figure 4.27 Setting Lifespan value.
8. Finally, save your scene; select the particle object; make sure the Dynamics menu set is selected, and choose Solvers > Create Particle Disk Cache. The time slider will run through the entire time range, and when finished you should be able to scrub the time slider and play forward and backward.
Note that in this example we've deliberately violated one of the particle expression tips mentioned earlier—that it's often best to avoid accessing per-object attributes in a particle expression. In this instance, because our particles were calculating their position using a complex geometric relationship to a curve object, there was little choice. Also important to
- Figure 4.28 A glitch resulting from a nonzero initial velocity.
note is that using the pointOnCurvelnfo MEL command, as with many
MEL commands one might wish to use in an expression, not only broke this rule but quietly created and destroyed nodes behind the scenes.
Post a comment