Click or drag to resize
DigitalRuneHow To: Create a Particle Effector

This article shows how to implement a particle effector that modifies particle parameters using linear interpolation. (Please note: DigitalRune Particles contains a SingleLerpEffector that provides the same functionality.)

Creating a custom particle effector

First, we create a new class that inherits from ParticleEffector. Particle effectors can be added to a particle system, and they are called whenever the particle system is initialized or updated. Here is the empty class and the namespaces that we will need:

C#
using System;
using DigitalRune.Particles;

namespace MyNamespace
{
  public class MyLerpEffector : ParticleEffector
  {
    // TODO: Add code.
  }
}

The particle effector in this example should perform a linear interpolation (LERP) between a start and an end value depending on an interpolation factor. The start values, end values, and interpolation factors are read from particle parameters. The interpolation results are written to another particle parameter.

Particle parameters are identified by their name - a string that must be unique within a particle system. Let's add properties to the particle effector that let the user configure the particle parameters to use:

C#
[ParticleParameter(ParticleParameterUsage.In)]
public string StartParameter { get; set; }

[ParticleParameter(ParticleParameterUsage.In)]
public string EndParameter { get; set; }

[ParticleParameter(ParticleParameterUsage.In)]
public string FactorParameter { get; set; }

[ParticleParameter(ParticleParameterUsage.Out)]
public string ValueParameter { get; set; }

The ParticleParameterAttribute provides meta-information for particle editors and validation. The attributes indicate which parameters are used as input and which are used as output.

Particle effectors should be cloneable. The following two methods must be overridden to support cloning: CreateInstanceCore must return a new instance of the particle effector and CloneCore must copy all important properties:

C#
protected override ParticleEffector CreateInstanceCore()
{
  return new MyLerpEffector();
}

protected override void CloneCore(ParticleEffector source)
{
  base.CloneCore(source);

  var sourceTyped = (MyLerpEffector)source;
  ValueParameter = sourceTyped.ValueParameter;
  StartParameter = sourceTyped.StartParameter;
  EndParameter = sourceTyped.EndParameter;
  FactorParameter = sourceTyped.FactorParameter;
}

The base class ParticleEffector provides several virtual methods, which are automatically called by the particle system. Derived classes can override these methods to change particle parameters, manipulate the particle system, etc.

The particle effector can query the required parameters and cache the references in OnRequeryParameters. This method is called when the particle system is updated for the first time, and then every time the particle parameters change and need to be requeried. OnUninitialize is called when the particle effector is removed from a particle system. In this method all resources and references to external objects should be released.

C#
private IParticleParameter<float> _startParameter;
private IParticleParameter<float> _endParameter;
private IParticleParameter<float> _valueParameter;
private IParticleParameter<float> _factorParameter;          

protected override void OnRequeryParameters()
{
  _valueParameter = ParticleSystem.Parameters.Get<float>(ValueParameter);
  _startParameter = ParticleSystem.Parameters.Get<float>(StartParameter);
  _endParameter = ParticleSystem.Parameters.Get<float>(EndParameter);
  _factorParameter = ParticleSystem.Parameters.Get<float>(FactorParameter);
}

protected override void OnUninitialize()
{
  _valueParameter = null;
  _startParameter = null;
  _endParameter = null;
  _factorParameter = null;
}

The following virtual methods are called whenever the particle system is updated (usually once per frame): OnBeginUpdate, OnUpdateParticles and OnEndUpdate.

OnBeginUpdate is called whenever the particle system starts its update. The method can be used to update any uniform particle parameters. In our example, the output parameter can be uniform:

C#
protected override void OnBeginUpdate(TimeSpan deltaTime)
{
  if (_valueParameter == null || _startParameter == null || _endParameter == null || _factorParameter == null)
    return;

  if (_valueParameter.IsUniform)
  {
    // Value is a uniform parameter.
    var f = _factorParameter.DefaultValue;
    _valueParameter.DefaultValue = (1 - f) * _startParameter.DefaultValue + f * _endParameter.DefaultValue;
  }
}

Please note that the method above aborts if any of the required particle parameters is missing.

Varying parameters are particle parameters that store one value per particle. These types of parameters need to be updated in OnUpdateParticles. The method arguments identify the range of particles that needs to be updated. In our example the output parameter can be a varying parameter, so this case needs to be handled here:

C#
protected override void OnUpdateParticles(TimeSpan deltaTime, int startIndex, int count)
{
  if (_valueParameter == null || _startParameter == null || _endParameter == null || _factorParameter == null)
    return;

  float[] values = _valueParameter.Values;
  if (values == null)
  {
    // Value is a uniform parameter. Uniform parameters are handled in OnBeginUpdate().
    return;
  }

  // Value is a varying parameter.
  for (int i = startIndex; i < startIndex + count; i++)
  {
    float f = _factorParameter.GetValue(i);
    values[i] = (1 - f) * _startParameter.GetValue(i) + f * _endParameter.GetValue(i);
  }      
}

Please note, that this method aborts if any of the required particle parameters is missing. It uses the extension method ParticleHelperGetValueT(IParticleParameterT, Int32) to read the particle parameter values because the input parameters can be either uniform or varying parameters.

The code above is all that is needed for our LERP effector. It is important to note that the particle effector can be used to update both, uniform and varying, particle parameters. Uniform particle parameters are updated in OnBeginUpdate. Varying particle parameters are updated in OnUpdateParticles.

To make the particle effector faster, the code in OnUpdateParticles can be extended to use different code paths depending on the particle parameter types (uniform vs. varying) for the most common usages. This additional optimization is implemented in the code below.

The final particle effector code

Here is the full source code of the LERP effector (including optimizations).

C#
using System;
using DigitalRune.Particles;


namespace MyNamespace
{
  public class MyLerpEffector : ParticleEffector
  {
    private IParticleParameter<float> _startParameter;
    private IParticleParameter<float> _endParameter;
    private IParticleParameter<float> _valueParameter;
    private IParticleParameter<float> _factorParameter;


    [ParticleParameter(ParticleParameterUsage.In)]
    public string StartParameter { get; set; }

    [ParticleParameter(ParticleParameterUsage.In)]
    public string EndParameter { get; set; }

    [ParticleParameter(ParticleParameterUsage.In)]
    public string FactorParameter { get; set; }

    [ParticleParameter(ParticleParameterUsage.Out)]
    public string ValueParameter { get; set; }


    protected override ParticleEffector CreateInstanceCore()
    {
      return new MyLerpEffector();
    }

    protected override void CloneCore(ParticleEffector source)
    {
      base.CloneCore(source);

      var sourceTyped = (MyLerpEffector)source;
      ValueParameter = sourceTyped.ValueParameter;
      StartParameter = sourceTyped.StartParameter;
      EndParameter = sourceTyped.EndParameter;
      FactorParameter = sourceTyped.FactorParameter;
    }

    protected override void OnRequeryParameters()
    {
      _valueParameter = ParticleSystem.Parameters.Get<float>(ValueParameter);
      _startParameter = ParticleSystem.Parameters.Get<float>(StartParameter);
      _endParameter = ParticleSystem.Parameters.Get<float>(EndParameter);
      _factorParameter = ParticleSystem.Parameters.Get<float>(FactorParameter);
    }

    protected override void OnUninitialize()
    {
      _valueParameter = null;
      _startParameter = null;
      _endParameter = null;
      _factorParameter = null;
    }

    protected override void OnBeginUpdate(TimeSpan deltaTime)
    {
      if (_valueParameter == null || _startParameter == null || _endParameter == null || _factorParameter == null)
      {
        return;
      }

      if (_valueParameter.IsUniform)
      {
        // Value is a uniform parameter.
        var f = _factorParameter.DefaultValue;
        _valueParameter.DefaultValue = (1 - f) * _startParameter.DefaultValue + f * _endParameter.DefaultValue;
      }
    }

    protected override void OnUpdateParticles(TimeSpan deltaTime, int startIndex, int count)
    {
      if (_valueParameter == null || _startParameter == null || _endParameter == null || _factorParameter == null)
      {
        return;
      }

      float[] values = _valueParameter.Values;
      if (values == null)
      {
        // Value is a uniform parameter. Uniform parameters are handled in OnBeginUpdate().
        return;
      }

      // Value is a varying parameter.

      float[] starts = _startParameter.Values;
      float[] ends = _endParameter.Values;
      float[] factors = _factorParameter.Values;

      if (starts != null && ends != null && factors != null)
      {
        // Optimized case: Start, End, and Factor are varying parameters.
        for (int i = startIndex; i < startIndex + count; i++)
        {
          float f = factors[i];
          values[i] = (1 - f) * starts[i] + f * ends[i];
        }        
      }
      else if (starts == null && ends == null && factors != null)
      {
        // Optimized case: Start and End are uniform parameters, Factor is varying parameter.
        float startValue = _startParameter.DefaultValue;
        float endValue = _endParameter.DefaultValue;
        for (int i = startIndex; i < startIndex + count; i++)
        {
          float f = factors[i];
          values[i] = (1 - f) * startValue + f * endValue;
        }        
      }
      else
      {
        // General case:
        for (int i = startIndex; i < startIndex + count; i++)
        {
          float f = _factorParameter.GetValue(i);
          values[i] = (1 - f) * _startParameter.GetValue(i) + f * _endParameter.GetValue(i);
        }      
      }      
    }
  }
}