Fixing Maya Particle Dynamics

A few days ago at work, I was creating some trails for ############# ######## ### ########## ## ############# (we have NDAs), and I realised that in Maya, if you have some particles with forces and an emitter moving fast enough, they will look choppy. I replicated the problem at home, then spent five minutes with Houdini and got it working there.

Seriously, whoever says Houdini is complicated is lying1. Anyway, back to Maya. This happens because forces only evaluate per frame, no matter if using legacy particles or nParticles, no matter the Nucleus substeps.

The only code-less solution I found is to calculate the simulation more times per frame. We can achieve this by oversampling2 or playing back in slow motion3 —making sure to nCache at the same rate too4. However, this has two major drawbacks: an obvious increase in simulation time, and the possibility that you’ll need to tweak particle masses and field magnitudes to get a similar result to what you wanted, because nDynamics. Don’t. Scale. Well.

So there needs to be another way. My colleague Paul stumbled upon this old post by Sagroth, which touches on this very subject at the end:

(…) So, when dynamics are applied, particles generated in the same frame receive the same modifications and in this case fall down chunked. The solution is to define birthTime value along with generation in expression, so that each trail particle have a smooth increment that blends all chunk with neighboring ones. (…)

In his tutorial he emits the particles in MEL, so some attributes don’t get initialised. But if we use an emitter we can check their birthTime is properly set already. So that’s not the solution: Sagroth gave us half the cake there. To get the other half, we need to dive into his example scene, as Eaclou found out on this CGTalk thread:

(…) You must add a per-particle attribute of the field_magnitude to your trail particle shape, and then connect that attributePP to a ramp with the particle’s age as an input. (The author of tutorial never mentioned this PP field attribute/ramp – but it is the driver of the functionality). (…)

And he’s damn right, that’s what Sagroth did, although at first I couldn’t understand how that field_magnitude thing worked. I learnt two things: that you can connect a user ramp to a per-particle attribute5, and that you can control field attributes per particle, as this Maya User Guide article explains:

(…) Create a per-particle attribute of float or vector type on the nParticleShape with the name fieldName_attributeLongNameOnTheField or fieldName_attributeShortNameOnTheField. The type for the per-particle attribute you use must match the attribute type on the field.
For example, create airField1_magnitude to control the magnitude on airField1. (…)

Did you know that?! That is bonkers, and great news! So what Sagroth did was control the gravity’s magnitude based on each particle’s age, so it would go from 0 to 1 across their lifespan. That, however, is not how forces work in real life: sure, when a constant force is applied on an object it moves faster over time, but Maya’s force fields already take that into account, so we shouldn’t make them stronger the older a particle gets. Eaclou also points that out on the forum thread.

So we need a way of weighing the first kick our particles get from all the force fields during their first frame of existence, and then reset it to full for the rest of the simulation. Fortunately enough, when Maya interpolates particles between the emitter’s last and current positions, it already initialises them with an interpolated age too, as if the particles closer to where the emitter was last frame, had already lived for slightly longer than the ones closer to where the emitter is now. Makes sense?

This means we can tell each particle that, for their first frame, they should have the forces on them multiplied by how many frames old they are —which would range from 0 to 1; and for their second frame on, have the forces affect them completely. Since the age attribute is calculated in seconds, we could multiply that by whatever the framerate in our scene is, or use the currentTimeUnitToFPS MEL command to obtain it. I’ve created an attribute in time1 called fps that has its value set with an expression using that command. Knowing that, our particle expressions would be something like this:

$w = age * time1.fps;
field1_magnitude = $w * field1.magnitude;
field2_magnitude = $w * field2.magnitude;
Runtime before dynamics
if ( floor ( age * time1.fps ) == 1 ) {
  field1_magnitude = field1.magnitude;
  field2_magnitude = field2.magnitude;

The condition is optional, it’s just telling the particles to only reset their field magnitudes on their second frame of existence, and disregard that bit of code afterwards. I just thought it could give better performance on the long run, but don’t really know. The important part is that it works.

Copy this into the Maya Script Editor and run it:

$r=`createNode transform`;
$e=`emitter -pos 5 0 0 -r 1000 -spd 0`;
parent $e $r;
expression -s ($r+".ry = time * 2 * 3.1416;") -uc none;
select -cl;
select -cl;
connectDynamic -em $e -f $g $p;
addAttr -ln ($g[0]+"_magnitude") -dt doubleArray $p;
addAttr -ln fps time1;
expression -s "time1.fps = `currentTimeUnitToFPS`;" -uc none;
dynExpression -c -s ("$w = age * time1.fps;\n"+$g[0]+"_magnitude = $w * "+$g[0]+".magnitude;") $p[0];
dynExpression -rbd -s ("if ( floor ( age * time1.fps ) == 1 ) {\n\t"+$g[0]+"_magnitude = "+$g[0]+".magnitude;\n}") $p[0];

Using this method only has two caveats: first, forget about using Nucleus gravity and wind or nParticle local forces —best to tick Ignore Solver Wind and Ignore Solver Gravity just in case. And second, Conserve, Drag and Damp all work well unless you have emission speed. So it’s best to keep them at 1, 0 and 0 respectively and use the Drag field instead, whose magnitude can be controlled per-particle with our hack.

In conclussion, this is far from ideal, but it does the trick. If only there were a dynamicsWeightPP attribute (don’t try it —it doesn’t work)… Or if only Maya took these things into account already!

  1. Set Jitter Birth Time to Negative and Interpolate Source to any other than None.
  2. ≥2016: FX > nCache > Legacy Cache > Edit Oversampling or Cache Settings… > Over Samples
    ≤2015: Dynamics > Solvers > Edit Oversampling or Cache Settings… > Over Samples
  3. Preferences > Settings > Time Slider > Playback By
  4. ≥2016: FX > nCache > Create > Create New Cache > nObject> Evaluate every
    ≤2015: nDynamics > nCache > Create nCache> Evaluate every
  5. Beyond the scope of this post, but it’s done using an arrayMapper node.

Leave a Reply

Your email address will not be published. Required fields are marked *