Table of Contents
If you have ever played a video game and seen an explosion or smoke trail of some kind, it was most likely created using particle effects. Since I started working with WPF I have often wondered about the many effects that can be created through styling, bitmap effects, storyboards, etc. I decided to use some of these tools in order to implement a basic particle system using WPF.
There is a lot of literature on the net about particle systems, so I will not go into too much detail about them. However, there are some basic elements that everyone should be familiar with. A particle system, in an object oriented language, can simply be described as a class which creates objects called particles and manages their position over time.
In my particle system I created a particle class which defines a position, mass, velocity, force and lifespan. The Particle
class inherits from System.Windows.Controls.Control
so it also contains graphical properties such as Opacity
and Background
.
I wanted the particle system to contain multiple spawn points for particles. So I created the Emitter
class to manage the creation and initial values of particles. An Emitter
is created by the ParticleSystem
class and contains a number of min/max dependency properties for most of the properties found in a Particle
. These min/max dependency properties define a range of possible values (i.e. MinHorizontalVelocity
and MaxHorizontalVelocity
define the range of a particle's velocity in the x-direction).
The Emitter
class inherits from System.Windows.Controls.Control
and provides an abstract base for creating emitters in the system. The class contains three overridable methods which govern the creation of Particles: GenerateParticles
, AddParticles
and UpdateParticles
. The method GenerateParticles
is used to create a number of particles up to MaxParticles
(Note: The property MinParticles
does exist but currently has no function). Each Particle
created is added to the ParticleSystem
through an ObservableCollection
named Particles
. AddParticles
is reserved for any additional processing the developer may need while generating particles for the first time. Finally UpdateParticles
is used to initialize and reinitialize (occurs when a Particle
exceeds its lifespan and is reused) the particles.
Once an Emitter
generates particles it is up to the ParticleSystem
to update their Position
and Velocity
. This is handled via an UpdateRender
event declared in the ParticleSystem
. The event fires every rendering frame and is the ideal place to update the particles.
CompositionTarget.Rendering += UpdateRender;
...
foreach (Particle particle in Particles)
{
if (particle.IsAlive)
{
particle.Force = ComputeForces(particle, time);
Vector v = particle.Velocity;
double vx = v.X + time * (particle.Force.X / particle.Mass);
double vy = v.Y + time * (particle.Force.Y / particle.Mass);
particle.Velocity = new Vector(vx, vy);
if (!particle.IsAnchor)
{
double px = particle.Position.X + time * particle.Velocity.X;
double py = particle.Position.Y + time * particle.Velocity.Y;
particle.Position = new Point(px, py);
}
particle.LifeSpan -= time;
}
}
...
If a Particle
has exceeded its lifespan and has not been reused then the particle's IsAlive
property is set to false
. During each rendering update if the particle's IsAlive
property is true
then the particle is updated in the following order: Force
, Velocity
and Position
(as shown above). All of the forces exerted on a particle at a given time are computed and added to the particle's Force
property. Once that is finished the velocity at a given time is computed and used to update the Position
of the particle. Whenever the particle's Position
is updated, the Top
and Left
Canvas
properties for that particle are also updated. This causes the graphical aspect of the particle to move.
...
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property.Name.Equals("Position") ||
e.Property.Name.Equals("Width") ||
e.Property.Name.Equals("Height"))
{
Canvas.SetLeft(this, Position.X - this.Width / 2d);
Canvas.SetTop(this, Position.Y - this.Height / 2d);
}
...
Since a particle inherits from System.Windows.Controls.Control
it has a Width
and a Height
property. In order to position the particle at its center these properties need to be taken into account. The Particle
class also has an IsAnchor
property. This property is used to denote a particle whose position should not change over time. The render update checks this property and skips the position update accordingly. Since the position is never updated when a particle is anchored, the Canvas Top
and Left
properties are never initialized. To avoid having these properties in an uninitialized state, the Canvas
Top
and Left
properties are updated when the Width
or Height
of a particle changes.
The Force
class was created as an abstract base class to create different kinds of force effects throughout the system. The class exposes a single method named ApplyForce
which is used by the ParticleSystem
to update a particle's Force
property. The ParticleSystem
class contains a collection of Force
objects and some internal forces as well. Each Force
object is iterated over and the ApplyForce
method is called which returns a Vector
. The resulting Vector
along with the internal force vectors are added up and used to set the Particle
object's Force
property.
The two internal forces found in the ParticleSystem
class are Gravity
and Drag
. Gravity
and Drag
are implemented as DependencyProperty
objects. Both of these forces are applied to each Particle
object found in the system's Particles
collection. Gravity
is simply multiplied by the Particle
object's Mass
property. The result is then added to the Particle
object's Force
property. Then the Drag
is multiplied by the Velocity
and subtracted from the Force
property.
double forceX = (particle.Mass * Gravity.X) - (particle.Velocity.X * Drag.X);
double forceY = (particle.Mass * Gravity.Y) - (particle.Velocity.Y * Drag.Y);
...
return new Vector(forceX, forceY);
Another force commonly found in particle systems is a Spring
. A Spring
can be attached to multiple Particle
objects and provides a force between those two objects. Instead of adding the Spring
object to the Forces
collection found in ParticleSystem
I created a separate collection in each Particle
. The Spring
collection is named Connections
and is responsible for maintaining the number of connections made between particles. The Force
for each connection is calculated during the particle system's render update.
double deltaX = particle.Position.X - connectedParticle.Position.X;
double deltaY = particle.Position.Y - connectedParticle.Position.Y;
double deltaVX = particle.Velocity.X - connectedParticle.Velocity.X;
double deltaVY = particle.Velocity.Y - connectedParticle.Velocity.Y;
double fxParticle = -(ks * (Math.Abs(deltaX) - r) + kd *
((deltaVX * deltaX) / Math.Abs(deltaX))) * (deltaX / Math.Abs(deltaX));
double fyParticle = -(ks * (Math.Abs(deltaY) - r) + kd *
((deltaVY * deltaY) / Math.Abs(deltaY))) * (deltaY / Math.Abs(deltaY));
double fxConnectedParticle = -fxParticle;
double fyConnectedParticle = -fyParticle;
The Spring
force can be used to create a mass-spring system. This allows for the creation of a new type of object which I call a Meatball
. A Meatball
is a group of three Particle
objects which are each connected to one another via a Spring
. The following picture shows a representation of a Meatball
.
As any Particle
in the Meatball
moves away from another Particle
, the Spring
between those two particles causes force to be applied, moving the other Particle
. This ensures that all three Particle
objects will move with one another and the Meatball
will remain intact. The effect is a clump of Particle
objects that appear to be stuck together. This allows for effects such as liquids and gels (anything that might flow instead of scatter).
The system contains classes for three types of emitters: PointEmitter
, LineEmitter
and MeatballEmitter
. Like all emitters the PointEmitter
inherits from the Emitter
base class. A PointEmitter
is positioned in a Canvas
based on its X
and Y
dependency properties. Every particle which spawns from the PointEmitter
starts at that position. Additionally, the Emitter
class contains a DependencyProperty
named MinPositionOffset
and another named MaxPositionOffset
which provide an offset range for the particle's Position
. The LineEmitter
is very similar to the PointEmitter
except instead of spawning particles at a specific X
, Y
position, it spawns particles along a line defined by the dependency properties: X1
, Y1
, X2
and Y2
. Unlike the previous two emitters the MeatballEmitter
does not explicitly generate particles. Instead it makes use of the Meatball
class to generate a group of particles forming a mass-spring system. Since a Meatball
contains three Particle
objects the MeatballEmitter
only generates MaxParticle
divided by three Meatball
objects.
Since the particle system was written using C# and WPF, I took advantage of XAML as much as I could. The particle system class was created as an ItemsControl
and given its own look and feel via the generic.xaml file. I used a Canvas
to position the particles and an ItemsControl
to contain them. A snippet from the generic.xaml file can be seen below:
<Style TargetType="{x:Type local:Engine.Controls.ParticleSystem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType=
"{x:Type local:Engine.Controls.ParticleSystem}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<StackPanel Orientation ="Vertical" >
-->
-->
<Grid>
-->
-->
-->
-->
-->
<ItemsControl Background="Transparent"
ItemsSource="{TemplateBinding Particles}"
ItemsPanel="{StaticResource ItemsCanvasTemplate}" />
-->
<Canvas x:Name="ParticleCanvas" Background="Transparent"
IsItemsHost="True" />
</Grid>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
A ParticleSystem
can accept multiple UIElement
objects as children. The Canvas
found in the style shown above acts as the host for these children.
Like the ParticleSystem
class, the Particle
class also has its own style. Every Particle
in the system is displayed using the following style.
<Style TargetType="{x:Type local:Engine.Controls.Particle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Engine.Controls.Particle}">
<Grid>
<Ellipse Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The particles found in this system are drawn as ellipses with a Width
and Height
set by the Emitter
that generates them. The Fill
, Stroke
and StrokeThickness
are each supplied by the Particle
properties Background
, BorderBrush
and BorderThickness
, respectively. A SolidColorBrush
is used to set the Background
property. The Brush
in conjunction with a DependencyProperty
named ColorKeyFrames
(found in the Emitter
base class) facilitates a change in Fill
color over time. The ColorKeyFrames
property is of type ColorKeyFrameCollection
which is simply enough a collection that can be populated with ColorKeyFrame
objects. By setting the Background
property to a SolidColorBrush
and adding the ColorKeyFrame
property to a Storyboard
Timeline
the Background
color can be changed over time. Since the Particle
has a finite lifespan it makes sense to use LifeSpan
as the total length, in seconds, for the Storyboard
. Another Timeline
, managing the particle's Opacity
property, was created and added to the Storyboard
. This controls the particle's transparency over its LifeSpan
.
mStoryboard = new Storyboard();
mStoryboard.FillBehavior = FillBehavior.HoldEnd;
ParallelTimeline pt = new ParallelTimeline(TimeSpan.FromSeconds(0));
DoubleAnimation daOpacity = new DoubleAnimation(StartOpacity, EndOpacity,
new Duration(TimeSpan.FromSeconds(this.LifeSpan)));
Storyboard.SetTargetName(daOpacity, this.Name);
Storyboard.SetTargetProperty
(daOpacity, new PropertyPath(Particle.OpacityProperty));
ColorAnimationUsingKeyFrames daBackground = new ColorAnimationUsingKeyFrames();
daBackground.Duration = new Duration(TimeSpan.FromSeconds(this.LifeSpan));
daBackground.KeyFrames = BackgroundColors;
Storyboard.SetTargetName(daBackground, String.Format("{0}Brush", this.Name));
Storyboard.SetTargetProperty
(daBackground, new PropertyPath(SolidColorBrush.ColorProperty));
pt.Children.Add(daOpacity);
pt.Children.Add(daBackground);
mStoryboard.Children.Add(pt);
The classes being used in the XAML file can be found in the PlayGround.Engine.Controls
, PlayGround.Engine.Emitters
, and PlayGround.Engine.Forces
namespaces. So it is important that a reference to these namespaces be included in SandBox.xaml.
<Window x:Class="PlayGround.SandBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Engine="clr-namespace:PlayGround.Engine.Controls"
xmlns:Emitter="clr-namespace:PlayGround.Engine.Emitters"
xmlns:Force="clr-namespace:PlayGround.Engine.Forces"
Title="SandBox" Height="300" Width="300"
>
<Grid Background="Transparent" >
</Grid>
</Window>
In the code above you can see that I also added a Grid
to contain the ParticleSystem
. In between the Grid
tags is where I added a ParticleSystem
. Each ParticleSystem
contains a collection of Emitter
objects named Emitters
and a collection of Force
objects named Forces
. A ParticleSystem
with a PointEmitter
can be added as follows.
<Grid>
<Engine:ParticleSystem Background="Transparent" x:Name="MyParticleSystem" >
<Engine:ParticleSystem.Emitters>
<Emitter:PointEmitter X="135" Y="25" MaxParticles="140"
MinHorizontalVelocity="-1.0"
MaxHorizontalVelocity="1.0"
MinVerticalVelocity="-13.0" MaxVerticalVelocity="0.0"
MinLifeSpan="1" MaxLifeSpan="4" >
<Emitter:PointEmitter.LinearColorKeyFrames>
<ColorKeyFrameCollection>
<LinearColorKeyFrame Value="Red" KeyTime="0%" />
</ColorKeyFrameCollection>
</Emitter:PointEmitter.LinearColorKeyFrames>
</Emitter:PointEmitter>
</Engine:ParticleSystem.Emitters>
</Engine:ParticleSystem>
</Grid>
Each Emitter
has a MaxParticles
property which determines how many particles should be created. The MaxHorizontalVelocity
and MinHorizontalVelocity
properties manage the range of a Particle
object's Velocity
property in the x-direction. Conversely the MaxVerticalVelocity
and MinVerticalVelocity
properties manage the range of a Particle
object's Velocity
property in the y-direction. The MaxLifeSpan
and MinLifeSpan
properties are responsible for maintaining the longevity of each Particle
.
Setting the horizontal velocity to a number between -1.0 and 1.0 and the vertical velocity between -13.0 and 0.0; results in a stream of particles which move swiftly upwards and very slightly to the left and right. A lifespan of 1 to 4 seconds ensures that Particle
objects will not travel far during their lifecycle and continue to regenerate at a rapid pace. The end effect is a flame-like spread of Particle
objects.
In order to create a better representation of fire, additional colors are added to the ColorKeyFrames
property of the PointEmitter
via a ColorKeyFrameCollection
.
<Engine:PointEmitter X="135" Y="25" MaxParticles="140"
MinHorizontalVelocity="-1.0" MaxHorizontalVelocity="1.0"
MinVerticalVelocity="-13.0" MaxVerticalVelocity="0.0"
MinLifeSpan="1" MaxLifeSpan="4" >
<Engine:PointEmitter.ColorKeyFrames>
<ColorKeyFrameCollection>
<LinearColorKeyFrame Value="Yellow" KeyTime="0%" />
<LinearColorKeyFrame Value="Orange" KeyTime="20%" />
<LinearColorKeyFrame Value="Red" KeyTime="50%" />
<LinearColorKeyFrame Value="Gray" KeyTime="90%" />
</ColorKeyFrameCollection>
</Engine:PointEmitter.ColorKeyFrames>
</Engine:PointEmitter>
In the example above I added four ColorKeyFrame
objects of type LinearColorKeyFrame
. The above example shows the particle's color moving linearly from Yellow
to Orange
to Red
to Gray
reaching each color at a specific percentage of the particle's life.
Combining the change in color with a change to the particle's Opacity
over time adds to the effect considerably. The example below shows the addition of the StartOpacity
and EndOpacity
properties to the PointEmitter
.
<Emitter:PointEmitter X="135" Y="25" MaxParticles="140"
MinHorizontalVelocity="-1.0" MaxHorizontalVelocity="1.0"
MinVerticalVelocity="-13.0" MaxVerticalVelocity="0.0"
StartOpacity="0.3" EndOpacity="0.0"
MinLifeSpan="1" MaxLifeSpan="4" >
<Emitter:PointEmitter.ColorKeyFrames>
<ColorKeyFrameCollection>
<LinearColorKeyFrame Value="Yellow" KeyTime="0%" />
<LinearColorKeyFrame Value="Orange" KeyTime="20%" />
<LinearColorKeyFrame Value="Red" KeyTime="50%" />
<LinearColorKeyFrame Value="Gray" KeyTime="90%" />
</ColorKeyFrameCollection>
</Emitter:PointEmitter.ColorKeyFrames>
</Emitter:PointEmitter>
A PointEmitter
is great for flames and such but in order to get different effects other Emitter
objects are required. A LineEmitter
can change a stream of fire into a full-fledged conflagration. Below is an example of how a LineEmitter
can be used.
<Emitter:LineEmitter X1="4" Y1="0" X2="46" Y2="0" MaxParticles="300"
MinHorizontalVelocity="-1.0" MaxHorizontalVelocity="1.0"
MinVerticalVelocity="-14.0" MaxVerticalVelocity="0.0"
StartOpacity="0.3" EndOpacity="0.0"
MinLifeSpan="1.1" MaxLifeSpan="4.2" >
<Emitter:LineEmitter.ColorKeyFrames>
<ColorKeyFrameCollection>
<LinearColorKeyFrame Value="Yellow" KeyTime="0%" />
<LinearColorKeyFrame Value="Orange" KeyTime="20%" />
<LinearColorKeyFrame Value="Red" KeyTime="50%" />
<LinearColorKeyFrame Value="Gray" KeyTime="90%" />
</ColorKeyFrameCollection>
</Emitter:LineEmitter.ColorKeyFrames>
</Emitter:LineEmitter>
Forces play an important role in the dispersion of particles in a system. The following is an example of how the internal forces, Gravity
and Drag
, are used in the system.
<Engine:ParticleSystem.Gravity>
<Vector X="0" Y="1.3" />
</Engine:ParticleSystem.Gravity>
<Engine:ParticleSystem.Drag>
<Vector X="3.3" Y="0.3" />
</Engine:ParticleSystem.Drag>
This gives the system a downward Gravity
and slows particle velocity over time considerably more in the X-direction than in the Y-direction.
A MeatballEmitter
can take advantage of the Gravity
force as well as the Spring
forces found in a Meatball
to create a goo-like effect.
<Engine:ParticleSystem Width="60" Height="30" Background="Transparent"
x:Name="GooParticleSystem" Grid.Column="1" >
<Engine:ParticleSystem.Emitters>
<Emitter:MeatballEmitter X1="8" Y1="28" X2="48" Y2="28" MaxParticles="90"
MinHorizontalVelocity="0.0"
MaxHorizontalVelocity="0.0"
MinVerticalVelocity="0.0" MaxVerticalVelocity="0.0"
StartOpacity="1.0" EndOpacity="1.0"
MinLifeSpan="40" MaxLifeSpan="40"
MinMass="2.0" MaxMass="10.0"
MinSpringConstant="1.0" MaxSpringConstant="6.0"
MinDampeningConstant="0.01"
MaxDampeningConstant="0.10"
MinRestLength="1.0" MaxRestLength="6.0"
MinParticleWidth="6.0" MaxParticleWidth="6.0"
MinParticleHeight="8.0" MaxParticleHeight="12.0" >
<Emitter:MeatballEmitter.ColorKeyFrames>
<ColorKeyFrameCollection>
<LinearColorKeyFrame Value="Black" KeyTime="0%" /v
</ColorKeyFrameCollection>
</Emitter:MeatballEmitter.ColorKeyFrames>
</Emitter:MeatballEmitter>
</Engine:ParticleSystem.Emitters>
<Engine:ParticleSystem.Gravity>
<Vector X="0" Y="0.4" />
</Engine:ParticleSystem.Gravity>
<Engine:ParticleSystem.Drag>
<Vector X="10.0" Y="0.3" />
</Engine:ParticleSystem.Drag>
<Button Width="60" Height="30" Canvas.Left="0" Canvas.Top="0"
Canvas.ZIndex="100" Click="MyButtonClicked" Content="GOO" />
</Engine:ParticleSystem>
The XAML code above creates a ParticleSystem
with a single MeatballEmitter
. The MeatballEmitter
generates thirty Meatball
objects across the line starting at point 8,28 and ending at point 48,28. The particles have no initial Velocity
(velocity will be generated by Gravity
). The particles are completely opaque (the StartOpacity
and EndOpacity
are both set to 1.0
) and they will each last for forty seconds before being reused. The mass of any particle can range from 2.0 to 10.0. The properties MinSpringConstant/MaxSpringConstant
, MinDampeningConstant/MaxDampeningConstant
and MinRestLength/MaxRestLength
are used by the Spring
forces in each Meatball
. A spring constant between 1.0 and 6.0 will cause a rigid to somewhat flexible springing motion. The dampening constant between 0.01 and 0.10 ensures that the spring will oscillate for a while before coming to rest at a length between 1.0 and 6.0. For the goo-like effect the width of each particle should be constant but the height can vary to provide a drip-like Meatball
. The entire effect is partially hidden behind a button. On the button's click event the system is started or stopped, depending on its previous state.
protected void MyButtonClicked(object sender, EventArgs e)
{
if (!mIsGooRunning)
GooParticleSystem.Start();
else
GooParticleSystem.Stop();
mIsGooRunning = !mIsGooRunning;
}
Particle systems can be powerful tools used to create amazing effects. Unfortunately, using WPF, performance issues significantly reduce the possibilities. Most of the processing is done using software rendering which limits the number of Particle
objects that can be drawn without slowing the system to a crawl. This being the case, the number of particles used for all the effects should range in the low hundreds and the total number of particles in an application should not exceed a thousand particles. For effects which are not persistent such as fire and explosion two hundred to three hundred particles can be adequate. For spring-mass systems which try and simulate effects such as liquids or gels the limited number can handicap the effect.
As I was testing the system I found that not only do the number of Particle
objects matter but also the number of Storyboard
objects. With just two I found the system was slowing down considerably. At one point I toyed around with the idea of having the motion of all the particles controlled by storyboards. After running a few preliminary tests, it was evident that doing so would result in a much slower system. So the system limits itself to one storyboard for each Particle
object which controls both the Opacity
and Background
color for the Particle
.
The adjustment of the Background
color via the Storyboard
Timeline
could have been avoided if WPF allowed for other blending modes besides opacity blending. For instance I could have used additive blending to get the fire effect using a single reddish base color. This would also allow for the goo effect to have multiple colors without changing opacity (resulting in smoky effect). I briefly looked into BitmapEffects
to see if I could implement an additive blending mode. Unfortunately is does not appear that access to the RenderingContext
is available, only access to the UIElement
being affected. So you cannot find what the initial color of the pixel being written to is.
During design I implemented two ways to create Spring
forces. The first gave each particle a Connections
property of type List<Spring>
. When looping through the Force
calculations this List
was iterated over and the results added to the calculation. The other way was to add Spring
objects to a collection in the ParticleSystem
. Then, when calculating the forces, the entire collection was iterated over searching for a match on the current Particle
. The second method was ultimately slower but allowed Spring
objects to be easily shown in the UI of the system. In order to show Spring
objects in the first (and distributed) implementation an ObservableCollection<Spring>
would need to be created containing these objects. Then another ItemsControl
would need to be added to the ParticleSystem
's style using the collection as an ItemSource
.
The particle system turned out to be an interesting exercise in WPF. It helped show me some of the strong and weak points of the technology. I enjoy being able to plan out a particle system in XAML and not having to worry about code but I dislike the speed limitations. Overall the particle system works great for the occasional "button on fire" and "explosions" but further enhancements will need to be made to both this project and WPF in order to create effects produced using other architectures.
The system can easily be enhanced by adding new Emitter
classes. Both a PolygonEmitter
and PathEmitter
would be handy. Additionally, an Emitter
for a more robust mass-spring system would be nice too. Given time I would like to have created a branching mass-spring system, where roots of the system are anchored in place and Particle
objects are generated from each root and attached via springs. Then each child can generate a Particle
object and so forth and so on. I believe something like this would produce a better Goo effect than the one presented in the example.
- Initial Version (October 2007)
- Andrew Witkin. Physically Based Modeling: Particle System Dynamics. SIGGRAPH 2001 Course Notes. 2001.