Writing New Behaviour Ops

NukeX 6.3 has a particle system. It is possible to hook into this and write your own plug-ins that operate on particles.

The base class of interest is DD::Image::ParticleBehaviour in <DDImage/ParticleOp.h>. Subclasses should implement the following function:

virtual void applyBehaviour( const ParticleContext& context, ParticleSystem* ps)

This should apply the operation to the particles in ParticleSystem.

A simple example is as follows:

virtual void applyBehaviour( const ParticleContext& context, ParticleSystem* ps)
{
   for (unsigned i = 0; i < ps->numParticles(); i++) {
      if ( conditionsApply( ps, i ) ) {
        applyAcceleration(ps, i, context, Vector3(0, -0.1, 0));
      }
   }
}

This applies a consistent downwards acceleration to all particles. The conditionsApply function checks to see whether the given particle is operated upon. It is important to call this, as the ParticleMerge node relies on it. Apart from checking which branch the particle was emitted on for ParticleMerge, it also checks whether or not the particle matches various criteria set on knobs. These are the conditionKnobs (which allow execution to be made conditional on min/max age, channels, etc.) and the domainKnobs (which allow execution to be confined to a certain region). To add these knobs call the addDomainKnobs and addConditionKnobs functions from your knobs() call, like so:

 void knobs(Knob_Callback f)
 {
   addConditionsKnobs(f);
   addDomainKnobs(f);
}

It is possible to access properties of particles from within the applyBehaviour function by functions on the ParticleBehaviour. For example, the mass of the particle is retrieved with:

const float& particleMass(unsigned idx) const;

and can be set by:

float& particleMass(unsigned idx);

The properties that exist are as follows:

Type Name Description
Vector3 initialPosition The position the particle was created at
Vector3 position The position the particle is at now
Vector3 velocity The velocity of the particle, expressed in units/frame
Vector3 size The size of the particle, expressed in units
Quarternion orientation  
Vector3 rotationAxis  
float rotationAngle  
float rotationVelocity  
float mass The mass of the particle
float life The maximum lifetime of the particle (in frames)
float expirationChance The chance that this particle dies each frame (for halflife)
float t The number of frames (possibly fractional) this particle has been alive
float startTime The time this particle was emitted
int id A unique ID for the particle
ParticleChannelSet channels The channel sets the particle belongs to
int pathMask The pathMasks the particle is active for (internal)
bool active Whether the particle currently exists or not
Op* representation The Iop or GeoOp to use to as the particle for rendering

All of these are set by an applyBehaviour implementation, although it would not make sense to set initialPosition/t/dtOffset/startTime/id/pathMask/active/representation.

applyBehaviour is given a ParticleContext which tells it how long to run the effect for. For example, a force applied for 0.5 frames ought to only result in half the acceleration that it would if it were run for 1 frame. This is used to implement sub-frame stepping. A couple of helper functions are provided on ParticleBehaviour:

void applyAcceleration(ParticleSystem* ps, unsigned idx, const ParticleContext& ctx, Vector3 accel);
void applyForce(ParticleSystem* ps, unsigned idx, const ParticleContext& ctx, Vector3 force);

These take into account the ctx.dt(), and also whether or not the particle was only just emitted in the last frame, and if so, whether only part of that force should be applied. applyForce() takes into account the mass, dividing the force by the mass to get the acceleration. For example, if you’re writing a plug-in that reduces the particle’s mass slightly each frame, then you need to take ctx.dt() into account too. If, on the other hand, you are writing a plug-in that sets the particle’s color to a different value depending upon its value t, there is no need to take into account ctx.dt().

Improving Particle Performance

Since Nuke 11.3v1, the Particle system API has been expanded to allow you to write more efficient Ops.

The ParticleSystem class has a new set of accessor methods which return pointers to attribute arrays. Using these is much faster than calling the individual methods for each particle. Instead of:

for ( int i = 0; i < ps->numParticles(); i++ )
  do_something_with( particlePosition(i) );

it’s better to do this:

auto particlePosition = ps->particlePosition();
for ( int i = 0; i < ps->numParticles(); i++ )
  do_something_with( particlePosition[i] );

Even better, if your code is thread safe, is to use the new ParallelFor API to multithread your particle system:

auto particlePosition = ps->particlePosition();
ParallelFor(ps->numParticles(),
  [&](int i) {
    do_something_with( particlePosition[i] );
  }
)

This will use Nuke’s rendering threads to spread the work.

Table Of Contents

Previous topic

Particles

Next topic

Deep