— Spock
In 1982, William T. Reeves, a researcher at Lucasfilm Ltd., was working on the film Star Trek II: The Wrath of Khan. Much of the movie revolves around the Genesis Device, a torpedo that when shot at a barren, lifeless planet has the ability to reorganize matter and create a habitable world for colonization. During the sequence, a wall of fire ripples over the planet while it is being “terraformed.” The term particle system, an incredibly common and useful technique in computer graphics, was coined in the creation of this particular effect.
“A particle system is a collection of many many minute particles that together represent a fuzzy object. Over a period of time, particles are generated into a system, move and change from within the system, and die from the system.”
—William Reeves, "Particle Systems—A Technique for Modeling a Class of Fuzzy Objects," ACM Transactions on Graphics 2:2 (April 1983), 92.
Since the early 1980s, particle systems have been used in countless video games, animations, digital art pieces, and installations to model various irregular types of natural phenomena, such as fire, smoke, waterfalls, fog, grass, bubbles, and so on.
This chapter will be dedicated to looking at implementation strategies for coding a particle system. How do we organize our code? Where do we store information related to individual particles versus information related to the system as a whole? The examples we’ll look at will focus on managing the data associated with a particle system. They’ll use simple shapes for the particles and apply only the most basic behaviors (such as gravity). However, by using this framework and building in more interesting ways to render the particles and compute behaviors, you can achieve a variety of effects.
We’ve defined a particle system to be a collection of independent objects, often represented by a simple shape or dot. Why does this matter? Certainly, the prospect of modeling some of the phenomena we listed (explosions!) is attractive and potentially useful. But really, there’s an even better reason for us to concern ourselves with particle systems. If we want to get anywhere in this nature of code life, we’re going to need to work with systems of many things. We’re going to want to look at balls bouncing, birds flocking, ecosystems evolving, all sorts of things in plural.
Just about every chapter after this one is going to need to deal with a list of objects. Yes, we’ve done this with an array in some of our first vector and forces examples. But we need to go where no array has gone before.
First, we’re going to want to deal with flexible quantities of elements. Sometimes we’ll have zero things, sometimes one thing, sometimes ten things, and sometimes ten thousand things. Second, we’re going to want to take a more sophisticated object-oriented approach. In previous chapters, we have written classes to handle objects and their interactions. In Unity, specifically for particle systems, we use an Interface approach that handles all of the modifications we want to make to our particle systems. Critically, this means we cannot directly modify individual particles. Instead, we pass information through the interface which then handles one to hundreds of thousands of particles.
The goal here is to be able to write a main program that looks like the following:
public class Chapter4Fig1 : MonoBehaviour { particleSystemFigure1 psf1; Vector3 particleSystemLocation; public Vector3 velocity; public float lifeTime; public float startSpeed; int maxParticles; // Start is called before the first frame update void Start() { //Let's just have one particle maxParticles = 1; psf1 = new particleSystemFigure1(particleSystemLocation, startSpeed, velocity, lifeTime, maxParticles); } }
a single particle flying over the screen. Getting used to creating Unity scenes with multiple classes, classes that keep lists of instances of other classes, and objects that use interfaces will prove very useful as we get to more advanced chapters in this book.
Though it may seem obvious to you, I’d also like to point out that there are typical implementations of particle systems, and that’s where we will begin in this chapter. However, the fact that the particles in this chapter look or behave a certain way should not limit your imagination. Just because particle systems tend to look sparkly, fly forward, and fall with gravity doesn’t mean that those are the characteristics yours should have.
The focus here is really just how to keep track of a system of many elements. What those elements do and how those elements look is up to you.
Before we can get rolling on the system itself, we have to take a look at the ParticleSystem component and interface built-in to Unity. The Particle System component has many properties, and for convenience, the Inspector organizes them into collapsible sections called “modules”. These modules are documented in separate pages. See documentation on Particle System Modules to learn about each one. We will be accessing these same modules programmatically in our scripts.
The first module we will be accessing is the Main module. We access it through the particle system component. From there we can add and modify many of the common variables we’ve addressed in previous chapters. Properties like velocity and location.Typical particle systems involve something called an emitter. The emitter is the source of the particles and controls the initial settings for the particles, location, velocity, etc. An emitter might emit a single burst of particles, or a continuous stream of particles, or both. The point is that for a typical implementation such as this, a particle is born at the emitter but does not live forever.
If you look at the above code, you’ll see that there are some new properties we are adding: a start speed, lifetime, and max number of particles. Unlike in previous examples, we need to give our particles a starting speed as a float. This start speed can be understood as an initial pop, an immediate addition of velocity added to the particle as soon as it is instantiated. The max number of particles is self-explanatory. As you might expect, the max number of particles you choose to have in the scene will impact performance. Lastly, the Lifetime of a particle dictates how long, in milliseconds, it will be rendered and exist in the scene. The longer the Lifetime, the more memory each particle will use. Balancing particle lifetime and the max particles is necessary to achieve optimal performance.
public class particleSystemFigure1 { //We need to create a GameObject to hold the ParticleSystem component GameObject particleSystemGameObject; //This is the ParticleSystem component but we'll need to access everything through the .main property //This is because ParticleSystems in Unity are interfaces and not independent objects ParticleSystem particleSystemComponent; public particleSystemFigure1(Vector3 particleSystemLocation, float startSpeed, Vector3 velocity, float lifeTime, int maxParticles) { //Create the GameObject in the constructor particleSystemGameObject = new GameObject(); //Move the GameObject to the right position particleSystemGameObject.transform.position = particleSystemLocation; //Add the particle system particleSystemComponent = particleSystemGameObject.AddComponent(); }
So, okay, we’ve accessed the Main module of our particle system. Now let’s look at the Velocity Over Lifetime module. There will be plenty of time where you’ll want particles to speed up, accelerate, or slow down. In previous chapters, we have added an acceleration Vector2 or 3 to a velocity Vector2 or 3 to increase the velocity of an object over time. In this instance, the Particle System interface will handle acceleration for us.
Just like we accessed the Main module, we need to access our Velocity Over Lifetime module in the same way. However, unlike Main we have to enable this one, and all of the others. The Main module is the only one that does not need to be enabled. After that, we need to say how the particles will react to other objects in the scene, whether or not they will be impacted by parent objects(local) or all objects in world space. Let’s have them work locally since our particle system does not have a parent in the hierarchy.
//Now we need to gather the interfaces of our ParticleSystem //The main interface covers general properties var main = particleSystemComponent.main; //In the Main Interface we'll sat the initial start LifeTime (how long a single particle will live) //And, of course, we'll set our Max Particles main.startLifetime = lifeTime; main.startSpeed = startSpeed; main.maxParticles = maxParticles;
Now, to create an organic look in particle systems the particles need to have a little randomness. To achieve this, we can use Unity’s Animation Curves which allow us to set key frames. These key frames mark either, when a change should occur over a particle’s lifetime; or what the extremes of a range should be for randomness related to a property. To use these curves, the particle system component has a property for a MinMaxCurve. We can also provide float values ot the numers that are constant.
Looking at the example below, we’ll set the curves for the velocity. The minimum, will be a negative velocity multiplied by the velocity itself. The max, for our x-property will the velocity we pass into our method; for our y-property, the velocity will be a negative version because we want our particles to move downward. In future examples, we’ll use gravity to do this. Now that we’ve calculated these, we can go ahead and set this MinMax curve for each of the floats in our Vector3. Unity requires us to assign a value to the z-property even when we do not use it.
public void velocityModule(Vector3 velocity) { //The velocityOverLifetime inferface controls the velocity of individual particles var velocityOverLifetime = particleSystemComponent.velocityOverLifetime; //First we need to enable the Velocity Over Lifetime Interface; velocityOverLifetime.enabled = true; velocityOverLifetime.space = ParticleSystemSimulationSpace.Local; //We then to create a MinMaxCurves which will manage the change in velocity a ParticleSystem.MinMaxCurve minMaxCurveX = new ParticleSystem.MinMaxCurve(-velocity.x * velocity.x, velocity.x); ParticleSystem.MinMaxCurve minMaxCurveY = new ParticleSystem.MinMaxCurve(-velocity.y * velocity.y, -velocity.y); velocityOverLifetime.x = minMaxCurveX; velocityOverLifetime.y = minMaxCurveY; //Even though we are not using Z, Unity needs us to otherwise it will throw an error. //This is a bug in 2019. velocityOverLifetime.z = minMaxCurveY; }
To add color, we need to access and then enable the Color Over Lifetime module. We’ll put this in a method, but it works the same way as the Velocity Over Time module. We’ve included a standard particle material in Resources to use. However, you could use any number of methods for creating your own material and modifying its color. Since we want to create a fadeout effect, wherein the particle becomes more transparent as it dies, we need to use a gradient. While the syntax for these gradients is different than Animation Curves, they’re similar.
public void colorModule() { //The colorOverLifetime interfaces manages the color of the objects over their lifetime. var colorOverLifetime = particleSystemComponent.colorOverLifetime; //While we are here, let's add a material to our particles ParticleSystemRenderer r = particleSystemGameObject.GetComponent(); //There a few different ways to do this, but we've created a material that is based on the default particle shader r.material = Resources.Load ("particleMaterial"); }
First, we create a new gradient object. This gradient object holds an array of GradientColorKeys. These GradientColorKeys specify two pieces of information: one, the color and two, when the change occurs. These changes occur on a scale from 0 to 1. So, for example, we’ll set our first GradientColorKey to be white at the 0 marker on the scale. Then red at the .5 marker on the scale. Lastly, blue at the 1 marker. This will cause the particle to be white at the beginning of its lifetime, red in the middle, and blue at the end.
//To have the particle become transparent we need to access the colorOverLifetime Interface colorOverLifetime.enabled = true; Gradient grad = new Gradient(); //This gradient key lets us choose points on a gradient that represent different RGBA or Unity.Color values. //These gradient values exist in an array grad.SetKeys(new GradientColorKey[] { new GradientColorKey(Color.white, 0.0f) , new GradientColorKey(Color.red, 0.5f), new GradientColorKey(Color.blue, 1.0f) }, new GradientAlphaKey[] { new GradientAlphaKey(1.0f, 0.0f), new GradientAlphaKey(0f, 2.0f) }); //Set the color to the gradient we created above colorOverLifetime.color = grad;
We can do the same thing the alpha value of our material as well. We refer to that as a GradientAlphaKey. We set it in a similar way, but the first value is the opacity. A zero opacity means it is invisible; a 1 means it is opaque and non-transparent. The second value is the same.
We’ll keep it basic for this first figure. We only want the color to be white and for its transparency to increase as it reaches the end of its lifetime. Once we set up our GradientColorKey[] array, we assign it to the material’s color value.
Example 4.1: A Single Particle
Rewrite the example so that 1000 particles are being emitted.
Add three different colors, with different alpha values, to the particles based on their lifetime.
Now that we have a single particle system rocking and rolling. Let’s add some interactivity.
Let’s review for a moment where we are. We know how to talk about an individual Particle object. We also know how to talk about a system of Particle objects, and this we call a “particle system.” And we’ve defined a particle system as a collection of independent objects. But isn’t a particle system itself an object? If that’s the case (which it is), there’s no reason why we couldn’t also have a collection of many particle systems, i.e. a system of systems.
This line of thinking could of course take us even further, and you might lock yourself in a basement for days sketching out a diagram of a system of systems of systems of systems of systems of systems. Of systems. After all, this is how the world works. An organ is a system of cells, a human body is a system of organs, a neighborhood is a system of human bodies, a city is a system of neighborhoods, and so on and so forth. While this is an interesting road to travel down, it’s a bit beyond where we need to be right now. It is, however, quite useful to know how to write a Unity scene that keeps track of many particle systems, each of which keep track of many particles. Let’s take the following scenario.
Whenever the mouse is pressed, a new ParticleSystem object is created. We then have a choice. We can child these particle systems to existing systems or we can have them be independent. If we child a particle system to another in the hierarchy, the systems will influence one another.
In this example, if you left-click, the ParticleSystem object will not have a parent. If you right-click, the ParticleSystem will be a child of the last ParticleSystem.
public class Chapter4Fig2 : MonoBehaviour { Vector3 origin; particleSystemFigure2 psf2; // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { origin = Camera.main.ScreenToWorldPoint(Input.mousePosition); createParticleSystem(origin); } else if (Input.GetMouseButtonDown(1)) { origin = Camera.main.ScreenToWorldPoint(Input.mousePosition); createParticleSystem(origin, psf2.particleSystemGameObject); } } void createParticleSystem(Vector3 origin) { Vector3 particleSystemLocation = new Vector3(origin.x, origin.y, -10); Vector3 velocity = new Vector3(1f, 3f, 0f); float lifeTime = 5f; float startSpeed = Random.Range(-3f, 0f); int maxParticles = 1000; psf2 = new particleSystemFigure2(particleSystemLocation, startSpeed, velocity, lifeTime, maxParticles); } //We create an overloaded method and pass along the parent GameObject void createParticleSystem(Vector3 origin, GameObject parentParticleSystem) { Vector3 particleSystemLocation = new Vector3(origin.x, origin.y, -10); Vector3 velocity = new Vector3(1f, 3f, 0f); float lifeTime = 5f; float startSpeed = Random.Range(-3f, 0f); int maxParticles = 1000; //Create the child particle system particleSystemFigure2 child = new particleSystemFigure2(particleSystemLocation, startSpeed, velocity, lifeTime, maxParticles); //Now let's go ahead and give the child a parent particle system child.particleSystemGameObject.transform.SetParent(psf2.particleSystemGameObject.transform); //Now let's turn on the inherit velocity module child.inheritVelocityModule(); } }
To make sure these particles systems interact with one anothwe need to turn on the Inherit Velocity Module.
public void inheritVelocityModule() { var inheritVelocityModule = particleSystemComponent.inheritVelocity; inheritVelocityModule.enabled = true; //Let's change the color of the child material so it is easier to see ParticleSystemRenderer r = particleSystemGameObject.GetComponent(); r.material.color = Color.red; //Now let's grab the current velocity from the parent particleSystem inheritVelocityModule.mode = ParticleSystemInheritVelocityMode.Current; //And add a multiplier so they move faster //We can use a curve, like above in velocity, or just a general multiplier. inheritVelocityModule.curveMultiplier = 100; }
Create a particle system that particles floating upward and another with particles floating downward. Can you get the particlesystms to inherit properties from one another?
So, particles are cool and all but what if we wanted to add trails like a comet or create waving grasses? We call those trails. The Trail module has a number of fantastic options worth exploring. Trails can exist as individual particles or as ribbons. Ribbons utilize a Line Renderer to draw lines between the particles or an initial origin point.
First things first, we need to decide how many of the particles will get their own trails. Not every particle is cool enough to have one. In the Trails Module this property is referred to as “ratio”. Setting the ration to 1 means that 100% of the particles will have their own trail. Next, we need to decide if a trail will die when its associated particle does. If, for example, you want to create ribbons that looked like kelp, with particles that look like fish passing through, you would not have the ribbons die with the particles. The Kelp would still be waving after the fish have moved on. We can then set the number of ribbons.
public void trailModule() { //The Trails Modules interface manages the color of individual particle trails var trailsModule = particleSystemComponent.trails; trailsModule.enabled = true; //This is how many particles will receive the trails. Setting it to 1 means they all will. trailsModule.ratio = .5f; //Next we want the trail to die when the particle dies trailsModule.dieWithParticles = true; //And we want these trails to act like a ribbon trailsModule.mode = ParticleSystemTrailMode.Ribbon; //We also want a few of these ribbons trailsModule.ribbonCount = 10; //Lastly, we want the trails to all connect to an origin point to create a many-legs effect trailsModule.attachRibbonsToTransform = true; //Let's add some color by having the trail shift between two different color values Gradient grad = new Gradient(); //This gradient key lets us choose points on a gradient that represent different RGBA or Unity.Color values. //These gradient values exist in an array grad.SetKeys(new GradientColorKey[] { new GradientColorKey(Color.green, 0.0f)}, new GradientAlphaKey[] { new GradientAlphaKey(1.0f, 0.0f), new GradientAlphaKey(1f, 2.0f) }); //Set the color to the gradient we created above trailsModule.colorOverLifetime = grad; //While we are here, let's add a material to our particle trails ParticleSystemRenderer r = particleSystemGameObject.GetComponent(); //There a few different ways to do this, but we've created a material that is based on the default particle shader r.trailMaterial = Resources.Load ("particleMaterial"); }
Next, we can go ahead and add color like we did before. Keep in mind that both the particles and the ribbons need their own material.
Try to get the Ribbon Size to increase with its lifetime.
Create trails that change color over time.
Create trails made of multiple particles that create tails like a dotted line.
Remember how much fun we had with Perlin Noise in the Introduction and Chapter 1? Well, the ParticleSystem Interface has its own Noise module! This greatly simplifies the process of implementing Noise across an entire particle system. First, we choose how strong we want the turbulence of the noise to be. The stronger the turbulence, the more extreme the randomness. Next, we need to transition between these points and have the particles smoothly move into those new positions as the Noise changes. To do this, we set the ParticleSystemNoiseQuality to high, medium, or low. High provides the best transition.
public void noiseModule() { //The Noise Module manages the distortion and randomness of the particles var noiseModule = particleSystemComponent.noise; //To have the particle become transparent we need to access the colorOverLifetime Interface noiseModule.enabled = true; //The strength of the turbulence noiseModule.strength = 1.0f; //High quality settings simulate smoother transitions noiseModule.quality = ParticleSystemNoiseQuality.High; //Next we can have the noise scroll which adds even more randomness. We can set it to a constant or curve. noiseModule.scrollSpeed = 4; //We can even add some noise to the scale of each particle noiseModule.sizeAmount = 3; }
Since our particles are moving, we can have them move against the noise. This increases randomness but also results in a greater sense of the organic. We can then choose how fast this noise moves and how much it should impact each particle.
Example 4.4: Noise in the System
Change the scale of the noise by adding layers through the properties octaveCount, octaveMultiplier, and octaveScale.
Adding gravity to our Particle System is quite simple. We set, the gravityModifer property to 1. Not very exciting. Let’s add some rotation to the particles in this system too. There are two modules we are going to enable: RotationBySpeed Module and RotationOverLifetime Module.
rotationOverLifetime(rotationVelocity); rotationBySpeed(rotationVelocity);
We can have rotation occur along one axis or all three. For this example, let’s have RotationBySpeed occur on the Y-axis. We’ll have RotationOverLifetime occur on the X-axis and Y-axis.
public void rotationOverLifetime(Vector3 rotationVelocity) { var rotationOverLifetime = particleSystemComponent.rotationOverLifetime; rotationOverLifetime.enabled = true; //we'll now go ahead and rotate 360-degrees on the x and y axes. rotationOverLifetime.separateAxes = true; //Now let's pass on the floats in our Vector3 rotationOverLifetime.x = rotationVelocity.x; rotationOverLifetime.y = rotationVelocity.y; rotationOverLifetime.z = rotationVelocity.z; }
Now, for a bit more fun, let’s have the gravityModifier randomly change every two seconds and have that impact the rotations.
public void gravityModifier() { //The main interface covers general properties var main = particleSystemComponent.main; //We'll dramatically move gravity around to create a pulsing fountain effect main.gravityModifierMultiplier = Random.Range(-6, 10); }
Now, we'll turn to the other module, RotationBySpeed. This works in the same way as the RotationOverLifeTime module. After we enable it though, we'll have the particles rotate on the Z-axis based on their speed. Since we added the gravity modifier, we should see these rotations shift. Notice that the gravityModifierMultiplier impacts the entire particle system.
public void rotationBySpeed(Vector3 rotationVelocity) { var rotationBySpeed = particleSystemComponent.rotationBySpeed; rotationBySpeed.enabled = true; //we'll now go ahead and rotate 360-degrees on the x and y axes. rotationBySpeed.separateAxes = true; //Now let's pass on the floats in our Vector3 //This time let's pass the Y into the Z rotationBySpeed.x = 0f; rotationBySpeed.y = 0f; rotationBySpeed.z = rotationVelocity.y; }Example 4.5: Gravity and Rotation
Modify gravity modifiers over time to change a different aspect of the Particle System behavior.
Collision is another Module. Particle collisions can be really helpful for water running over rocks, clouds parting around mountains, and smoke spreading across a ceiling. When used atmospherically, collisions can add immersive depth to your environments.
Once we’ve enabled the Module, we can choose what kind of objects the particles will collide against. We can choose planes or the World geometry. The latter is every 3D mesh in the Unity scene. Let’s go ahead and add planes in this example.
//Add our collission planes so we can access them later this.collisionPlane1 = collisionPlane1; this.collisionPlane2 = collisionPlane2; collisionModule(collisionPlane1, collisionPlane2);
We then have to choose a collision mode. We have the option between 2D and 3D, so let’s choose 3D for greater effect. Next, we can use the SetPlane() method to add individual planes already in the scene or we can create them on the fly.
public void collisionModule(GameObject plane1, GameObject plane2) { var collisionModule = particleSystemComponent.collision; collisionModule.enabled = true; //Now we can collide with items in world space or planes. collisionModule.type = ParticleSystemCollisionType.Planes; collisionModule.mode = ParticleSystemCollisionMode.Collision3D; //Since we have no world, we'll add two planes for the particles to collide against collisionModule.SetPlane(0, plane1.transform); collisionModule.SetPlane(1, plane2.transform); //The collision detection on the planes is quite large and can be reduced }Example 4.6: Particle Collisions
Create a world of object and instantiate a particle system that reacts to world space.
In Chapter 2, we created attractor and repellor objects. We can do the same with ParticleSystems but it is a bit more complicated. Remember, everything is done through an interface. We can’t just call AddForce() on each particle’s Rigidbody. Instead, we have to use a different method straight from science fiction—force fields.
Force Fields, rather a ParticleSystemForceField, is a component that exerts forces on particle systems. One way to think of them is as an invisible shield around a ship. As space dust passes by the ship, the force field pushes it the particles away from the ship as it moves. We see similar things in nature in surf moving alongside a boat or when we turn on a high-powered blower to scatter leaves.
To begin though, let’s create our repeller object. We’ll follow the same procedure in Chapter 2.
public class repeller { public Rigidbody body; private GameObject gameObject; public ParticleSystemForceField repelField; float repellerStrength = 10; public repeller() { // Create the components required for the mover gameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere); gameObject.transform.position = new Vector3(0, -6f, 0); body = gameObject.AddComponent(); gameObject.GetComponent ().enabled = false; Object.Destroy(gameObject.GetComponent ()); //We need to create a new material for WebGL Renderer r = body.GetComponent (); r.material = new Material(Shader.Find("Diffuse")); r.material.color = Color.red; //Turn off gravity for this object body.useGravity = false;
This time, we’ll add our Forcefield as a component to the repeller object.
//Now let's add our Particle System Force Field Component repelField = gameObject.AddComponent();
Next, we can choose a shape for the Forcefield. While we can’t make it a mesh, we can use primitives such as a sphere, hemisphere, cylinder and box.
repelField.shape = ParticleSystemForceFieldShape.Sphere;
Forcefield’s are natural attractors. They use gravity to attract objects to them. While this is all well and good, we need a repeller object. So, we need to pass a negative force to the gravity property. This will push the particles away from the repeller instead of drawing them to it. Lastly, we give a range to the forcefield that achieves our intended behavior for the particles.
//To repel the object away. We make the strength negative because unity doesn't differentiate the direction of a gravitational force //in the way we are doing it repelField.gravity = -repellerStrength; //We set a range for the field repelField.endRange = 2 * repelField.transform.localScale.x;
Let’s go back to our particle system. We now need to let it know that it will be impacted by external forces. We’ll do this with the ExternalForces module. We’ll follow the same procedure as the modules. After that, it is as simple as calling the AddInfluence() method from the module.
public void externalForceModule() { //Now we need to turn on the External Forces Module var externalForces = particleSystemComponent.externalForces; externalForces.enabled = true; //And now we just add the influential force of the repeller externalForces.AddInfluence(repeller); }
Example 4.7: Repelling Particle Systems
Create a particle system in is reacting two to separate force fields.
Step 4 Exercise:
Take your creature from Step 3 and build a system of creatures. How can they interact with each other? Can you use inheritance and polymorphism to create a variety of creatures, derived from the same code base? Develop a methodology for how they compete for resources (for example, food). Can you track a creature’s “health” much like we tracked a particle’s lifespan, removing creatures when appropriate? What rules can you incorporate to control how creatures are born?
(Also, you might consider using a particle system itself in the design of a creature. What happens if your emitter is tied to the creature’s location?)