1. Code
  2. JavaScript

Manipulating Particle Motion with Stardust Particle Engine – Part 1

Scroll to top
12 min read

Stardust Particle Engine provides two major approaches to freely manipulate particle motion, namely gravitational fields and deflectors. Gravitational fields are vector fields that affect a particle's acceleration, and deflectors manipulate both a particle's position and velocity.

The first part of this tutorial covers the basics of particle motion and gravitational fields. Also, it demonstrates how to create your own custom gravitational fields. The second part focuses on deflectors and how to create custom deflectors.

Prior knowledge of the basic usage of Stardust is required to continue reading this tutorial. If you're not familiar with Stardust, you can check out my previous tutorial on the subject, Shoot out Stars with Stardust Particle Engine, before going on.


Final Result Preview

Let's take a look at the final result we will be working towards. This is an example of a custom vortex gravitational field.


Particle Motion Basics

It's time for some quick flashback on high school physics. Remember basic kinematics? It's all about displacement, which is just a fancier way to say "position," and its relation to time. For the scope of this tutorial, we only need a rather simple grasp of the topic.

Displacement

Displacement means the current position of an object. In this tutorial the "objects" we are mainly dealing with are particles in 2D space. In 2D space, the displacement of an object is represented by a 2D vector.

Velocity

An object's velocity denotes how fast an object's position changes and the direction of the change. The velocity of an object in 2D space is also represented by a 2D vector. The vector's x- and y-components represent the direction of the change of position, and the vector's absolute value denotes the object's speed, i.e. how fast the object's moving.

Acceleration

Acceleration is to velocity as velocity is to displacement. The acceleration of an object denotes how fast an object's velocity changes and the direction of the change. Just like the velocity of an object in 2D space, an object's acceleration is represented by a 2D vector.


Vector Fields

Another thing worth mentioning is the concept of vector fields. You can basically view a vector field as a function that takes a vector as its input and outputs another vector. Gravitational fields are a kind of vector fields that take position vectors as inputs and output acceleration vectors. For example, in physics simulation, usually a uniform gravity pointing downward is applied to all objects; in this case, the gravitational field representing the gravity is a vector field that outputs a constant vector (pointing down), no matter what the input position vectors are.

This is a visualized graph of a vector field. The output vector of a given input vector (1, 2) is (0.5, 0.5), while the output of an input (4, 3) is (-0.5, 0.5).

The Field class in Stardust represents a 2D vector field, and its 3D counterpart, the Field3D class represents a 3D vector field. In this tutorial, we'll only focus on 2D vector fields.

The Field.getMotionData() method takes a Particle2D object, containing a particle's 2D position and velocity information, as parameter. This method returns a MotionData2D object, a 2D vector "value object" that is consisted of x- and y-components. Combined with the Gravity action, a Field object can be used as a gravitational field, whose output is used to manipulate particle velocity.

That's all for our high school physics recap. Now it's time for some real Stardust stuff.


The Gravity Action

As previously mentioned, the Gravity action class makes use of Field objects as gravitational fields to manipulate particle velocity. Here's how you create a uniform vector field, a vector field that returns a constant vector no matter what input is given, and wrap it into a Gravity action.

1
2
//create a uniform vector field pointing downward (remember positive y-coordinate means "down" in Flash)

3
var field:Field = new UniformField(0, 1);
4
5
//create a gravity action

6
var gravity:Gravity = new Gravity();
7
8
//add the field to the gravity action

9
gravity.addField(field);

This Gravity action is now ready to be used in the same way as any other ordinary Stardust actions.

1
2
//add the gravity action to an emitter

3
emitter.addAction(gravity);

Example: Windy Rain

We are going to create a windy rain effect using the Gravity action.


Step 1: Basic Raining Effect

Create a new Flash document, choose a dark color for background, draw a raindrop on the stage, and convert the raindrop to a movie clip symbol, exported for ActionScript with a class name "Raindrop". Delete the raindrop instance on the stage afterward.

Now we're going to create an AS file for the document class, and an AS file for the rain emitter. I won't explain the code here, since I've already covered the basic usage of Stardust in my previous tutorial. If you're not familiar with Stardust or you need some refreshment, I strongly recommend that you read my previous tutorial before moving on.

This is the document class.

1
2
package {
3
	import flash.display.*;
4
	import flash.events.*;
5
	import idv.cjcat.stardust.common.emitters.*;
6
	import idv.cjcat.stardust.common.renderers.*;
7
	import idv.cjcat.stardust.twoD.renderers.*;
8
9
	public class WindyRain extends Sprite {
10
		
11
		private var emitter:Emitter;
12
		private var renderer:Renderer;
13
		
14
		public function WindyRain() {
15
			emitter = new RainEmitter();
16
			renderer = new DisplayObjectRenderer(this);
17
			renderer.addEmitter(emitter);
18
			
19
			addEventListener(Event.ENTER_FRAME, mainLoop);
20
		}
21
		
22
		private function mainLoop(e:Event):void {
23
			emitter.step();
24
		}
25
	}
26
}

And this is the emitter class used in the document class.

1
2
package {
3
	import idv.cjcat.stardust.common.clocks.*;
4
	import idv.cjcat.stardust.common.initializers.*;
5
	import idv.cjcat.stardust.common.math.*;
6
	import idv.cjcat.stardust.twoD.actions.*;
7
	import idv.cjcat.stardust.twoD.emitters.*;
8
	import idv.cjcat.stardust.twoD.initializers.*;
9
	import idv.cjcat.stardust.twoD.zones.*;
10
	
11
	public class RainEmitter extends Emitter2D {
12
		
13
		public function RainEmitter() {
14
			super(new SteadyClock(1));
15
			
16
			//initializers

17
			addInitializer(new DisplayObjectClass(Raindrop));
18
			addInitializer(new Position(new RectZone(-300, -40, 940, 20)));
19
			addInitializer(new Velocity(new RectZone( -0.5, 2, 1, 3)));
20
			addInitializer(new Mass(new UniformRandom(2, 1)));
21
			addInitializer(new Scale(new UniformRandom(1, 0.2)));
22
			
23
			//actions

24
			addAction(new Move());
25
			addAction(new Oriented(1, 180));
26
			addAction(new DeathZone(new RectZone( -300, -40, 960, 480), true));
27
		}
28
	}
29
}

This is what our current progress looks like.


Step 2: Make it Windy

Now we're going to make the rain effect windy by adding a uniform gravitational field that "pulls" the raindrops to the right. Add the following code in the constructor of the RainEmitter class.

1
2
//create a uniformfield that always returns (0.5, 0)

3
var field:Field = new UniformField(0.5, 0);
4
5
//take particle mass into account

6
field.massless = false;
7
8
//create a gravity action and add the field to it

9
var gravity:Gravity = new Gravity();
10
gravity.addField(field);
11
12
//add the gravity action to the emitter

13
addAction(gravity);

Note that we set the Field.massless property to false, which is true by default. When set to true, this property causes fields to act like ordinary gravitational fields, affecting all objects equally regardless of their mass. However, when the property is set to false, particle mass is taken into account: particles with larger mass are less affected by the field, whereas particles with smaller mass are affected more. That is why we used the Mass initializer in our previous emitter code, to add some randomness into the rain effect.

Test the movie again, and this is what our outcome looks like. Raindrops are now affected by a gravitational field and are all "pulled" to the right.


Example: Turbulence

Now we're going to swap the UniformField object with a BitmapField object, returning vector fields based on a bitmap's color channels, to create a turbulence effect. If you've worked with ActionScript for a while, you might be expecting to use the BitmapData.perlinNoise() method when thinking of turbulence, and that's exactly what we're going to do.

Here's what a sample Perlin noise bitmap looks like. Perlin noise is an excellent algorithm to generate noise for simulating turbulence, water wave, cloud, etc. You can find more information about Perlin noise here.

Bitmap Fields

You might be wondering, if we're going to use the BitmapData.perlinNoise() method to generate a perlin noise bitmap, how are we going to use this bitmap as a vector field? Well, this is what the BitmapField class is for. It takes a bitmap and converts it to a vector field.

Let's say we have a bitmap field whose X channel is set to red and Y channel is green. This means when the field takes an input vector, say, (2, 3), it looks up to the bitmap's pixel at (2, 3) and the output vector's X component is determined from the pixel's red channel, and the Y component is determined from the green channel. When the components of an input vector are not integers, they are rounded first.

A color channel's value ranges from 0 to 255, 127 being the average. A value less than 127 is regarded as negative by the bitmap field, while a value larger than 127 is taken positive. Zero is the most negative number and 255 is the most positive one. For example, if we have a pixel with a color 0xFF0000 in hexadecimal representation, meaning a red channel with value 255 and green channel with 0, then the bitmap field's output for this pixel would be a vector with X component of a most positive possible number and Y component of a most negative possible number, where this most positive/negative possible number, or maximum number, is specific to the bitmap field. To be more precise, here's the formula of the pixel-to-vector conversion.


Step 1: Basic Flying Arrows

Create a new Flash document. Draw an arrow on the stage, convert it to a symbol named "Arrow" and export it for ActionScript.

Create an AS file for the document class. This is almost the same as the previous example.

1
2
package {
3
	import flash.display.*;
4
	import flash.events.*;
5
	import idv.cjcat.stardust.common.emitters.*;
6
	import idv.cjcat.stardust.common.renderers.*;
7
	import idv.cjcat.stardust.twoD.renderers.*;
8
9
	public class Turbulence extends Sprite {
10
		
11
		private var emitter:Emitter;
12
		private var renderer:Renderer;
13
		
14
		public function Turbulence() {
15
			emitter = new ArrowEmitter();
16
			renderer = new DisplayObjectRenderer(this);
17
			renderer.addEmitter(emitter);
18
			
19
			addEventListener(Event.ENTER_FRAME, mainLoop);
20
		}
21
		
22
		private function mainLoop(e:Event):void {
23
			emitter.step();
24
		}
25
	}
26
}

Create another AS file for our emitter class.

1
2
package {
3
	import idv.cjcat.stardust.common.actions.*;
4
	import idv.cjcat.stardust.common.clocks.*;
5
	import idv.cjcat.stardust.common.initializers.*;
6
	import idv.cjcat.stardust.common.math.*;
7
	import idv.cjcat.stardust.twoD.actions.*;
8
	import idv.cjcat.stardust.twoD.emitters.*;
9
	import idv.cjcat.stardust.twoD.initializers.*;
10
	import idv.cjcat.stardust.twoD.zones.*;
11
	
12
	public class ArrowEmitter extends Emitter2D {
13
		
14
		public function ArrowEmitter() {
15
			super(new SteadyClock(1));
16
			
17
			//initializers

18
			addInitializer(new DisplayObjectClass(Arrow));
19
			addInitializer(new Life(new UniformRandom(50, 10)));
20
			addInitializer(new Position(new SinglePoint(320, 200)));
21
			addInitializer(new Velocity(new LazySectorZone(3, 2)));
22
			addInitializer(new Mass(new UniformRandom(2, 1)));
23
			addInitializer(new Scale(new UniformRandom(1, 0.2)));
24
			
25
			//actions

26
			addAction(new Age());
27
			addAction(new DeathLife());
28
			addAction(new Move());
29
			addAction(new Oriented());
30
			addAction(new ScaleCurve(10, 10));
31
		}
32
	}
33
}

The current progress looks like this.


Step 2: Make it Turbulent

Add the following code that creates a 640-by-480 Perlin noise bitmap data in the emitter constructor. For detailed explanations on each parameter of the BitmapData.perlinNoise() method, you can refer to this documentation. To make it simple, the following code creates a Perlin noise bitmap with "octaves" roughly of the size 50X50, and the noise consists of red and green color channels.

1
2
//create a Perlin noise bitmap data

3
var noise:BitmapData = new BitmapData(640, 400);
4
noise.perlinNoise(50, 50, 1, 0, true, true, 1 | 2);

Next, create a BitmapField object and assign the bitmap data to it through the update() method. Then the rest of the code concerning the Gravity action is exactly the same as the previous example.

1
2
//create a uniformfield that always returns (0.5, 0)

3
var field:BitmapField = new BitmapField();
4
field.channelX = 1; //set X channel to red

5
field.channelY = 2; //set Y channel to green

6
field.max = 1; //set the max vector component absolute value

7
field.update(noise); //update the field with the noise bitmap

8
9
//take particle mass into account

10
field.massless = false;
11
12
//create a gravity action and add the field to it

13
var gravity:Gravity = new Gravity();
14
gravity.addField(field);
15
16
//add the gravity action to the emitter

17
addAction(gravity);

Now test the movie again, and you shall see our flying arrows are now experiencing turbulence.


Custom Fields

We've used the UniformField and BitmapField provided by Stardust, and now we're going to create our own custom fields by extending the Field class and overriding the getMotionData2D() method.

The getMotionData2D takes a Particle2D parameter as input, which contains the position vector information of the particle, and that's the input to the field. The method returns a MotionData2D object, representing the output of the field. That's all you need to know to create a custom field. Let's create a custom vortex field.

Below is the visualized graph of our vortex field. It's pretty self-explanatory why it's called a vortex field.

Here's the formula for our vortex field.

And the vortex field class is as simple as the code below. We've made use of the Vec2D class to do all the dirty work of rotating a vector by 90 degree clockwise and setting the vector's absolute value. Then, we dump the vector's x- and y-components into a MotionData2D object, which is of the object type to be returned.

1
2
package {
3
	import idv.cjcat.stardust.twoD.fields.*;
4
	import idv.cjcat.stardust.twoD.geom.*;
5
	import idv.cjcat.stardust.twoD.particles.*;
6
	
7
	public class VortexField extends Field {
8
		
9
		public var centerX:Number;
10
		public var centerY:Number;
11
		public var strength:Number;
12
		
13
		public function VortexField(centerX:Number = 0, centerY:Number = 0, strength:Number = 1) {
14
			this.centerX = centerX;
15
			this.centerY = centerY;
16
			this.strength = strength;
17
		}
18
		
19
		override protected function calculateMotionData2D(particle:Particle2D):MotionData2D {
20
			var dx:Number = particle.x - centerX;
21
			var dy:Number = particle.y - centerY;
22
			var vec:Vec2D = new Vec2D(dx, dy);
23
			vec.length = strength;
24
			vec.rotateThis(90);
25
			
26
			return new MotionData2D(vec.x, vec.y);
27
		}
28
	}
29
}

Now that we have our custom field, let's test it out in the following example.


Example: Vortex

This example serves as a test drive for our vortex vector field. It's as simple as swapping a field with a new one. Change the following code from the previous example from this

1
2
//create a uniformfield that always returns (0.5, 0)

3
var field:BitmapField = new BitmapField();
4
field.channelX = 1; //set X channel to red

5
field.channelY = 2; //set Y channel to green

6
field.max = 1; //set the max vecotr length

7
field.update(noise); //update the field with the noise bitmap

to this

1
2
//create a vortex field centered at (320, 200) with strength 1

3
var field:VortexField = new VortexField();
4
field.centerX = 320;
5
field.centerY = 200;
6
field.strength = 1;

And we're done. You may test the movie and see the vortex effect. Sweet!


Conclusion

You have seen how to use the Gravity action and Field class to affect particle velocity. And you've learned how to create your own custom vector fields to be used as gravitational fields. The next part of this tutorial will show you how to make use of deflectors to manipulate both particle position and velocity at once.

Thank you very much for reading!

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.