In my previous tutorial Shoot Out Stars with the Stardust Particle Engine, I explained the basic workflow of Stardust. This time, we'll take things further and examine a couple of techniques for creating true 3D particle effects!
Introduction
We'll begin with a demonstration of how to use the Stardust's native 3D engine. Then, I'll show you how to get Stardust to work with Papervision3D; we'll be creating 3D particle effects with Papervision3D's Particles class and DisplayObject3D class.
Previously…
We're going to pick up where we left off in the first tutorial. Last time we created star and circle particles shooting out from a point, growing to a maximum size and then shrinking to nothing, while moving gradually slower over time (called a damping effect). This time, we'll do the same thing, but in 3D. Instead of the particles moving out in a circle, they'll move out in a sphere.
Step 1: Create a New Flash Document
Just as before, first create a new Flash document with dimensions of 640x400, a frame rate of 60fps, and a dark background (I used a dark blue gradient).

Step 2: Draw the Particles
Draw a star and a white circle, then convert them to symbols, separately. These are the two symbols we are going to use as particles later. Name the star symbol "Star" and circle symbol "Circle", exported for ActionScript with the same class names.

(If you're not much of an artist, you can download the source at the top of the page and use my symbols from the library of my FLA.)
Step 3: Create the Pause Button
Click Window > Components to bring up the Components Panel, then drag a Button from the User Interface folder onto the stage. Set the label to "Pause" and name it "pause_btn". We are going to use this button to pause the 3D particle effects, thereby allowing users to spin the camera around in order to get a better taste of the 3D environment.

Step 4: Create the Document Class
Create a new document class and name it StarParticles3D.
package { import flash.display.Sprite; public class StarParticles3D extends Sprite { public function StarParticles() { } } }
Not sure how to use a document class? Read this Quick Tip.
3D Initializers and Actions
The three main packages in Stardust are:
- idv.cjcat.stardust.common: contains general elements for both 2D and 3D particle effects.
- idv.cjcat.stardust.twoD: contains specific elements for 2D particle effects.
- idv.cjcat.stardust.threeD: contains specific elements for 3D particle effects.
In the previous tutorial we used initializers and actions from the common and twoD packages. In this tutorial, we'll still be using elements from the common package, but not from the twoD package. Instead, we will use elements from the threeD package.
The class structure of the threeD package is pretty much the same as in the twoD package, except that the elements have an extra dimension. A 3D element possesses the same name as its 2D counterpart, but its name ends with "3D". For example, the Move3D action in the 3D package updates particle positions in 3D space according to the velocities, just like its 2D counterpart in the 2D package, the Move action.
Step 5: Extend the Emitter
Create a new AS file called StarEmitter.as; inside it, create a new class StarEmitter, which extends the Emitter3D class:
package { import idv.cjcat.stardust.threeD.emitters.Emitter3D; //don't forget to import this! public class StarEmitter extends Emitter3D { public function StarEmitter(clock:Clock) { //pass the clock object to the superclass's constructor super(clock); } } }
Remember the Clock parameter? It's used to control the rate of particle creation. We need to include it in the constructor function, so that we can pass a Clock to it later.
Since we are allowing users to pause the particle effects, we're going to pack all the actions into a single CompositeAction object, which is essentially a group of actions. By deactivating this single composite action, we can "turn off" all the underlying actions. Declare a variable for a composite action in the emitter class. We'll access this variable in the document class, so it needs to be public:
public var pausibleActions:CompositeAction;
Step 6: Declare the Particle Constants
Declare constants that will be used as particle parameters in the emitter class. We already covered the purpose of these constants in the previous tutorial. Most of the names are self-explanatory. These go inside the class but outside the constructor function. Feel free to come back here later and alter the numbers to see the effects.
private static const LIFE_AVG:Number = 30; private static const LIFE_VAR:Number = 10; private static const SCALE_AVG:Number = 1; private static const SCALE_VAR:Number = 0.4; private static const GROWING_TIME:Number = 5; private static const SHRINKING_TIME:Number = 10; private static const SPEED_AVG:Number = 30; private static const SPEED_VAR:Number = 10; private static const OMEGA_AVG:Number = 0; private static const OMEGA_VAR:Number = 5; private static const DAMPING:Number = 0.1;
Step 7: The Switch Initializer for Particle Display Objects
In the previous tutorial I demonstrated how to use the SwitchInitializer to create particles with different display objects. I was using the DisplayObjectClass initializer, which initializes particle appearance with display Objects. That was for 2D particle effects; here we are going to use its 3D counterpart, the DisplayObject3D initializer.
Add the following code to the emitter's constructor function:
//switch initializers for star and circle particles var doc1:DisplayObjectClass3D = new DisplayObjectClass3D(Star); var doc2:DisplayObjectClass3D = new DisplayObjectClass3D(Circle); var si:SwitchInitializer = new SwitchInitializer([doc1, doc2], [1, 1]); addInitializer(si);
Step 8: Add the Remaining Initializers
Same as the previous tutorial; add the other initializers shown below. Note that some of them have similar names to those in the previous tutorial, but end in "3D".
- The Position3D initializer initializes particle positions. Just as its 2D counterpart, the Position initializer, it accepts one constructor parameter, a zone object. However, this time it does not accept a Zone object, which represents a 2D zone; instead, it accepts a Zone3D object, representing a zone in 3D space. The SinglePoint3D class extends the Zone3D class and is the 3D version of the SinglePoint class.
- The same goes for the Velocity3D class. The SphereShell class is essentially the 3D version of the SectorZone class. Here we set the center of the sphere shell to (0, 0, 0), average radius to SPEED_AVG, and radius variation to SPEED_VAR.
- The Rotation3D and Omega3D initializers work exactly the same as their 2D counterparts, Rotation and Omega. However, there is one little difference about the constructor parameters: they accept three Random objects instead of one. That is because now, in 3D space, there are three axes of rotation, so they require three random rotation values for these axes. In this example we are creating "2D billboards" in 3D space (i.e. flat objects), so only the rotation for the Z axis is apparent; that's why the first two parameters, Random objects for the X and Y axes, are assigned with null values.
This code goes in the StarEmitter's constructor function:
addInitializer(new Life(new UniformRandom(LIFE_AVG, LIFE_VAR))); addInitializer(new Scale(new UniformRandom(SCALE_AVG, SCALE_VAR))); addInitializer(new Position3D(new SinglePoint3D())); addInitializer(new Velocity3D(new SphereShell(0, 0, 0, SPEED_AVG, SPEED_VAR))); addInitializer(new Rotation3D(null, null, new UniformRandom(0, 180))); addInitializer(new Omega3D(null, null, new UniformRandom(OMEGA_AVG, OMEGA_VAR)));
Step 9: Add the Actions
Create a composite action, and add some actions to it. Then add this composite action to the emitter; this will make the particles perform the actions. You've seen these actions in the previous tutorial (some of them in 2D version), so I won't explain them all over again. Again, this code goes in the StarEmitter's constructor function:
pausibleActions = new CompositeAction(); pausibleActions.addAction(new Age()); pausibleActions.addAction(new DeathLife()); pausibleActions.addAction(new Move3D()); pausibleActions.addAction(new Spin3D()); pausibleActions.addAction(new Damping3D(DAMPING)); pausibleActions.addAction(new ScaleCurve(GROWING_TIME, SHRINKING_TIME)); addAction(pausibleActions);
Step 10: Back to The Document Class
All right, we're done with the emitter. Now it's time to build our document class.
First, declare constants for the orbiting camera's radius, the camera distance from the origin and the emitter's rate:
private static const CAMERA_RADIUS:Number = 250; private static const PARTICLE_RATE:Number = 0.5;
(As before, consts go inside the class but outside the constructor function.)
Then, declare variables for an emitter, a steady clock and a DisplayObjectRenderer3D (in the same place as the consts):
private var emitter:StarEmitter; private var clock:SteadyClock; private var renderer:DisplayObjectRenderer3D;
In the constructor, initialize the clock, emitter and renderer. Also, set the initial camera's position and direction, making it look at the origin:
//create the clock and emitter clock = new SteadyClock(PARTICLE_RATE); emitter = new StarEmitter(clock); //we can do this because we gave StarEmitter's constructor a clock parameter //create the renderer and its container sprite var container:Sprite = new Sprite(); container.x = 320, container.y = 200; renderer = new DisplayObjectRenderer3D(container); renderer.addEmitter(emitter); //add the container to the stage addChild(container); //add the pause button again so that it is on top of the container addChild(pause_btn); //set the camera's initial position and direction renderer.camera.position.set(0, 0, -CAMERA_RADIUS); renderer.camera.direction.set(0, 0, CAMERA_RADIUS);
Step 11: Program the Pause
Create a handler function in the document class to handle the click event of the pause button:
private function togglePause(e:MouseEvent):void { if (e.target.label == "Pause") { e.target.label = "Resume"; clock.ticksPerCall = 0; //stop the clock emitter.pausibleActions.active = false; //deactivate the emitter's actions } else { e.target.label = "Pause"; clock.ticksPerCall = PARTICLE_RATE; //restart the clock emitter.pausibleActions.active = true; //reactivate the emitter's actions } }
..then register the listener for the pause button, in the constructor function:
pause_btn.addEventListener(MouseEvent.CLICK, togglePause);
Step 12: The Main Loop
Create a handler for the ENTER_FRAME event. This is our main loop. It updates the camera's position by calling the updateCamera() method (which we will code in a minute) and calls the emitter's step() method, which keeps the particle effects running:
private function mainLoop(e:Event):void { updateCamera(); emitter.step(); }
Again, register a listener in the constructor:
addEventListener(Event.ENTER_FRAME, mainLoop);
Step 13: Update the Camera's Position
Now define the updateCamera() method called in the previous step. This is used to move the camera in 3D space depending on the mouse's position. (If you'd like more information on how it works check out this Wikipedia article.)
The magic numbers used to generate theta and phi are just the result of trial and error; feel free to try your own equations.
private function updateCamera():void { var theta:Number = 0.02 * (mouseX - 320); var phi:Number = 0.02 * (mouseY - 200); phi = StardustMath.clamp(phi, -StardustMath.HALF_PI, StardustMath.HALF_PI); var x:Number = CAMERA_RADIUS * Math.cos(theta) * Math.cos(phi); var y:Number = CAMERA_RADIUS * Math.sin(phi); var z:Number = CAMERA_RADIUS * Math.sin(theta) * Math.cos(phi); renderer.camera.position.set(x, y, z); renderer.camera.direction.set(-x, -y, -z); }
Note that I used the StardustMath.clamp() method; this makes sure the phi value is kept between positive and negative half PI.
Milestone: Native Stardust Engine Complete
Okay, we're done! That's all we need to do to get a 3D emitter working with Stardust's native 3D engine. Let's look at the result. You can click the pause button to pause the particle effect, and move the mouse around to orbit the camera:
If you'd like to see the full source code, look in the folder called "01 - Stardust Native 3D Engine" in the Source.
Time for Papervision3D
To switch from Stardust's native 3D engine to Papervision3D is easy. We'll just have to use a different renderer and display object initializer.
(Never used Papervision3D before? Take a look at this beginner's tutorial.)
First we'll use Papervision3D's Particles class. You might not be familiar with this; I'll show you how to use the more common DisplayObject3D class later.
Step 14: Change the Display Object Initializer
Change the following code in the emitter class:
var doc1:DisplayObjectClass3D = new DisplayObjectClass3D(Star); var doc2:DisplayObjectClass3D = new DisplayObjectClass3D(Circle);
to this:
var mat1:MovieParticleMaterial = new MovieParticleMaterial(new Star()); var mat2:MovieParticleMaterial = new MovieParticleMaterial(new Circle()); var doc1:PV3DParticle = new PV3DParticle([mat1]); var doc2:PV3DParticle = new PV3DParticle([mat2]);
As you may already know, the MovieParticleMaterial class allows us to use display objects as particles' appearance in Papervision3D. We create a Star and Circle instance to be used as particle material. The PV3DParticle initializer takes the place of the DisplayObjectClass3D initializer; its constructor accepts an array of parameters, which will all be added to a Particles object.
This is all we have to do regarding the emitter. Next we'll modify the document class.
Step 15: Set Up the Papervision3D Environment
The target container for our renderer is no longer a Sprite object. Instead, we are going to create particles in a Particles object. We'll have to switch the renderer's type from DisplayObjectRenderer3D to PV3DParticleRenderer.
Declare the following variables for Papervision3D-related objects:
private var scene:SceneObject3D; private var particles:Particles; private var camera:Camera3D; private var origin:DisplayObject3D; private var renderEngine:BasicRenderEngine; private var viewport:Viewport3D;
The code in the document class's constructor is now:
initPV3D(); //this is new! clock = new SteadyClock(PARTICLE_RATE); emitter = new StarEmitter(clock); renderer = new PV3DParticleRenderer(particles); //this is new! renderer.addEmitter(emitter); pause_btn.addEventListener(MouseEvent.CLICK, togglePause); addEventListener(Event.ENTER_FRAME, mainLoop);
The initPV3D() method sets up the Papervision3D Environment. Here's the code:
private function initPV3D():void { //create the scene scene = new SceneObject3D(); //create the Particles object particles = new Particles(); //create the camera and initialize its position camera = new Camera3D(); camera.position.x = 0; camera.position.y = 0; camera.position.z = -CAMERA_RADIUS; //create a DO3D representing the origin origin = new DisplayObject3D(); origin.x = origin.y = origin.z = 0; //point the camera to the origin camera.target = origin; scene.addChild(origin); scene.addChild(particles); //create the render engine and viewport renderEngine = new BasicRenderEngine(); viewport = new Viewport3D(640, 400); //add the viewport to the stage addChild(viewport); //add the pause button again so that it is on top of the viewport addChild(pause_btn); }
Step 16: The New Main Loop
Now Stardust only updates the 3D objects' properties; Papervision3D's render engine is taking over the responsibility of rendering. This is what our new main loop looks like:
private function mainLoop(e:Event):void { updateCamera(); emitter.step(); renderEngine.renderScene(scene, camera, viewport); //this is new! }
Step 17: Updating the Camera
Now that we are using Papervision3D's camera, we'll also have to modify the updateCamera() method:
private function updateCamera():void { var theta:Number = 0.02 * (mouseX - 320); var phi:Number = 0.02 * (mouseY - 200); phi = StardustMath.clamp(phi, -StardustMath.HALF_PI, StardustMath.HALF_PI); var x:Number = CAMERA_RADIUS * Math.cos(theta) * Math.cos(phi); var y:Number = -CAMERA_RADIUS * Math.sin(phi); //note this is negative now var z:Number = CAMERA_RADIUS * Math.sin(theta) * Math.cos(phi); camera.x = x; //we update each of the x, y, z properties of PV3D's camera separately camera.y = y; camera.z = z; }
Milestone: Papervision3D with Particles Completed
Okay, we have successfully switched from Stardust's native 3D engine to Papervision3D. Now let's check out the result. Note the pixelation effect on the particles. That is because Papervision3D first draws vector objects into bitmaps before using them as particle materials.
You can find all the source code for this in the "02 - Papervision3D Particles" folder.
Papervision3D's DisplayObject3D Class
So far, we have been working with "2D billboards" - flat objects, like paper. It's possible to create "real" 3D particle objects, like Papervision3D's DisplayObject3D objects. We're just going to have to use another initializer. Now let's get down to the final part of this tutorial. We will create red and blue cube particles.
Step 18: Change the Display Object Initializer, Again
We're going to change the initializer concerning the particle appearance for the last time.
Before that, declare a LightObject3D variable in the emitter class. We are going to use the FlatShadeMaterial for the DisplayObject3D objects, which require a light source. Also, declare the following constants - we'll use these as parameters for the FlastShadeMaterial, and for determining the cubes' sizes:
public var light:LightObject3D; private static const LIGHT_COLOR_1:uint = 0xCC3300; private static const LIGHT_COLOR_2:uint = 0x006699; private static const AMBIENT_COLOR_1:uint = 0x881100; private static const AMBIENT_COLOR_2:uint = 0x002244; private static const CUBE_SIZE:Number = 15;
Now change the following code in the emitter class:
var mat1:MovieParticleMaterial = new MovieParticleMaterial(new Star()); var mat2:MovieParticleMaterial = new MovieParticleMaterial(new Circle()); var doc1:PV3DParticle = new PV3DParticle([mat1]); var doc2:PV3DParticle = new PV3DParticle([mat2]);
to this:
light = new LightObject3D(); var mat1:FlatShadeMaterial = new FlatShadeMaterial(light, LIGHT_COLOR_1, AMBIENT_COLOR_1); var mat2:FlatShadeMaterial = new FlatShadeMaterial(light, LIGHT_COLOR_2, AMBIENT_COLOR_2); var matList1:MaterialsList = new MaterialsList({all:mat1}); var matList2:MaterialsList = new MaterialsList({all:mat2}); var params1:Array = [matList1, CUBE_SIZE, CUBE_SIZE, CUBE_SIZE]; var params2:Array = [matList2, CUBE_SIZE, CUBE_SIZE, CUBE_SIZE]; var doc1:PV3DDisplayObject3DClass = new PV3DDisplayObject3DClass(Cube, params1); var doc2:PV3DDisplayObject3DClass = new PV3DDisplayObject3DClass(Cube, params2);
The new particle appearance will be initialized as red and blue 3D cubes. The first constructor parameter for the PV3DDisplayObject3DClass initializer is the class we wish to instantiate for the particles (so here, it is the Cube class) and the second parameter is an array of constructor parameters for this Cube class.
Step 19: The Three Axes of Rotation
Previously, because we were working with "2D billboards", only the rotation about the Z axis mattered. Now that we are working with true 3D objects, we need to pass three Random object references to the Rotation3D and Omega3D constructors, one for each axis.
Change the following code in the emitter class:
addInitializer(new Rotation3D(null, null, new UniformRandom(0, 180))); addInitializer(new Omega3D(null, null, new UniformRandom(OMEGA_AVG, OMEGA_VAR)));
to this:
var rotationRandom:UniformRandom = new UniformRandom(0, 180); var omegaRandom:UniformRandom = new UniformRandom(OMEGA_AVG, OMEGA_VAR); addInitializer(new Rotation3D(rotationRandom, rotationRandom, rotationRandom)); addInitializer(new Omega3D(omegaRandom, omegaRandom, omegaRandom));
Step 20: Modify the Document Class
This time, instead of using a Particles object as our particle container, we're using a DisplayObject3D as the container. Declare a variable for this container in the document class:
private var container:DisplayObject3D;
Also, we'll need another type of renderer for creating particles in the new container. Change the renderer's type from PV3DParticleRenderer to PV3DDisplayObject3DRenderer. The code in the document class's constructor should now look like this:
initPV3D(); clock = new SteadyClock(PARTICLE_RATE); emitter = new StarEmitter(clock); renderer = new PV3DDisplayObject3DRenderer(container); //this has changed! renderer.addEmitter(emitter); pause_btn.addEventListener(MouseEvent.CLICK, togglePause); addEventListener(Event.ENTER_FRAME, mainLoop);
Step 21: Modify the initPV3D() Method
In the initPV3D() function, we now need to initialize the container variable and add it to the scene. Add these two lines to the end of that function:
container = new DisplayObject3D(); scene.addChild(container);
Step 22: Modify the updateCamera() Method
In the updateCamera() method, we wish to make the light follow the camera, so we'll have an illusion that the light always "shoots" out from our eyes. Change the following code:
camera.x = x; camera.y = y; camera.z = z;
to this:
emitter.light.x = camera.x = x; emitter.light.y = camera.y = y; emitter.light.z = camera.z = z;
Now the light source is always at the same point as the camera.
Milestone: Papervision3D with DisplayObject3D Completed
Yep, we're finally done with this tutorial. No more coding. Let's take a look at our final result, with fancy red and blue 3D cubes!
The source code for this can be found in the "Papervision3D DisplayObject3D" folder.
Conclusion
The workflow for creating 3D particle effects with Stardust is pretty much the same as that for 2D effects. You just choose a different set of initializers, actions, and renderers. Stardust also supports other 3D engines, including ZedBox and ND3D. The usage is almost the same. You'll just have to use a different set of initializers and renderers. You may even extend the Initializer, Action, and Renderer classes to work with whichever 3D engines you like!
Now you've got the basics, why not go back to the consts created in Step 6 and play with them to see the effects?
I hope this tutorial helps you understand Stardust more and makes you more familiar and comfortable with Stardust's workflow. Thanks for reading!
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.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post