Creating 3D Intros with Papervision3D

John Reyes

In this Tut, we will look into creating 3D Intros using Papervision3D. In the process, we will also look into using a real world implementation of the Singleton Pattern. We'll also be pulling together a neat "CustomLoader" class which will allow you to preload and queue images and sounds, then set them all off at once.

All this culminates in a reusable and highly flexible system for creating 3D intros.


Final Result Preview

Let's take a look at the final result we will be working towards; go ahead and click the stage to start the intro:


Step 1: Create a New Project

Open FlashDevelop and click Project>New Project...


Step 2: Set Up

Choose Actionscript 3 > AS3 Project.

For the name of the Project put in Intro3D.

For the location, click and navigate to the folder you would like to save it into. Leave the Create Directory For Project checkbox selected and click OK.

If you want to use Flash CS3+, create a new Flash file and set the width and height to 600x350, set the background color to black, and the frame rate to 30. Name it intro3D.fla and save it anywhere you like.


Step 3: PV3Dassets Folder

For FlashDevelop, open the project directory and copy or drag the PV3Dassets folder from the Source download (linked at the top of the page) into the \bin\ folder.

For Flash, copy or drag the PV3Dassets folder from the Source download into the same folder where you have intro3D.fla.

Also, let's review the contents of this folder. We have 8 images in Portable Network Graphics (PNG) format and 4 MP3s.


Step 4: Install TweenLite & Papervision3D

We're going to use TweenLite for the tweening and Papervision for 3D. Both packages are included with the source download. For more info, you can visit their websites at papervision and tweenlite.

For FlashDevelop, go ahead and copy or drag greensock.swc and the Papervision3D_2.1.932.swc from the Source download into the \lib\ folder for this project.

From FlashDevelop, click View>Project Manager


Step 5: External Libraries

Still in FlashDevelop, click the '+' sign to the left of the lib folder to expand it.

Select both swc files and right-click, choose Add to Library.

For Flash, copy or drag the \com\, \org\, and \nochump\ folders from the Source download into the same folder as your intro3D.fla file.


Step 6: Creating A Singleton CustomLoader Class

Let's start working with the CustomLoader class. We need a Class that will simplify loading and accessing the types of external assets we are using for this project. In addition, we need to be able to access these assets from anywhere within the program. This means only 1 instance of the class needs to exist during the lifetime of the program. A Singleton Pattern is just what we need for this job. For more information about the Pattern, you can go here. Also, Eduardo Gonzalez, a fellow Activetuts+ author, has posted an article about the Singleton Pattern. You can check it out here.

NOTE: If you're using Flash, just follow along to understand this class's responsibility, then refer to Step 17.

For FlashDevelop, click View>Project Manager, right-click the \src\ folder and choose Add>New Class.

The New Actionscript Class window will pop-up. For the Name enter CustomLoader, for the Base Class enter flash.events.EventDispatcher. Click "OK" to finish.

The class extends EventDispatcher to have the ability to dispatch events when it needs to.

Let's now work on the structure of the class so it behaves as a Singleton.

Right below the class declaration, add the following static variables:

private static var _allowed:Boolean;
private static var _customLoader:CustomLoader;
These static variables will make sure the class instantiates only 1 instance of the CustomLoader class and provide access to it.

Step 7: CustomLoader Class Responsibility

Here is the list of what this class should do.

  1. Load external assets (visual and sound assets).
  2. Give out a list of names of all the assets that were loaded.
  3. Inform the listener when all the assets have loaded successfully.
  4. Provide the asset when requested.
  5. If something goes wrong, send out the error.

Add the following lines of code after a space below the _customLoader static variable.

private var _imageArray:Array;
private var _imageURLs:Array;
private var _loadCount:uint;
private var _soundURLs:Array;
private var _soundArray:Array;

The _imageArray array will hold name=value pairs of all the visual assets encased in it's loader. This way, it doesn't matter what type the visual asset is (swf, bitmap), we just refer to it's loader and safe-cast when used. More on this later. The _imageURLs array will hold Strings of the URL that will be passed in every time a visual asset is requested to be loaded. _loadCount will increment by 1 each time a load is successful. The _soundURLs array will hold Strings of the URL passed in each time a sound asset is requested to load. The _soundArray array will hold hold name=value pairs of all the sound assets.

We need 1 public constant to dispatch as a custom event when all loads complete. Above the static variable _allowed declaration, add the following line of code:

public static const ALL_LOADS_COMPLETE:String = 'allLoadsComplete';

That completes the properties of the CustomLoader class. Next up, The constructor method.


Step 8: CustomLoader Constructor Method

This will have a slightly different behavior than a regular constructor. Here, instantiation is prevented unless the _allowed variable is set to true. An Error is also thrown when this method is called inappropriately. On the other hand, if _allowed is set to true, the init() method is then called and an instance is created.

Add the following lines of code inside the constructor:

if (! _allowed)
{
	throw new Error ('This is a Singleton, please use CustomLoader.getInstance ()');
   return;
 }
init ();

The init () method simply instantiates the 4 arrays. Below the constructor, add the following lines of code:

private function init ():void
{
	_imageArray = new Array;
   _imageURLs = new Array;
   _soundURLs = new Array;
   _soundArray = new Array;
}

Step 9: CustomLoader getInstance () Method

This is the only way to create and access the CustomLoader instance. This public static method provides full accessibility. If _customLoader hasn't been assigned an instance yet, _allowed is set to true and the _customLoader variable is assigned a new instance then returned. If _customLoader already has an instance, it is simply returned.

Add the following lines of code right after the init () method:

public static function getInstance ():CustomLoader
{
	if (! _customLoader)
   {
   	_allowed = true;
		_customLoader = new CustomLoader;
       _allowed = false;
   }
   return _customLoader;//happens either way
}

Step 10: CustomLoader Adding Visual and Sound Assets to the Queue

Since we have essentially 2 types of assets to load (visual assets and sound assets), I've created 2 methods to handle loading these 2 types. All they need to do is keep adding URLs to the _imageURLs and _soundURLs arrays. So, as long as you have assets to load, just keep calling the appropriate method and pass in the URL of the asset you want to load and they will get stored inside their corresponding array. The addItem () method adds the URL of the visual asset into the _imageURLs array and the addSound () method adds the URL of the sound asset into _soundURLs array.

Add the following lines of code after the getInstance () method:

public function addItem ($url:String):void
{
	_imageURLs.push ($url);
}

public function addSound ($url:String):void
{
	_soundURLs.push ($url);
}

Step 11: CustomLoader startLoading () Method

We call this public method once we've assigned all the assets we want to load. It first iterates through the _imageURLs array, creates a Loader instance to hold the visual asset then assigns _imageArray a name=value pair. The name is processed through the assignShortName () method which will be created and covered shortly. As I've already mentioned earlier, we use the Loader to avoid the trouble of casting the visual asset's type. You'll see how access the assets when we get to the Intro3D Class. Listeners for the Event.COMPLETE and IOErrorEvent.IO_ERROR are then assigned to listen for when the load completes or fails. Lastly, we load the asset.

Another loop is then iterated through _soundURLs array to handle loading of all sound assets. The process of loading sound assets is very similar to loading visual assets. Within the second loop, a Sound instance is created, assigned as a name=value pair into _soundArray, attached listeners for loading results and then loaded.

Add the following lines of code after the addSound () Method:

public function startLoading ():void
{
	for (var i:uint = 0; i < _imageURLs.length; i++)
	{
		var loader:Loader = new Loader
		_imageArray[assignShortName (_imageURLs[i])] = loader;
		loader.contentLoaderInfo.addEventListener (Event.COMPLETE, onLoadComplete);
		loader.contentLoaderInfo.addEventListener (IOErrorEvent.IO_ERROR, onLoadError);

		loader.load (new URLRequest (_imageURLs[i]));
	}

	for (i = 0; i <
 _soundURLs.length; i++)
	{
		var sound:Sound = new Sound;
		_soundArray[assignShortName (_soundURLs[i])] = sound;
		sound.addEventListener (Event.COMPLETE, onLoadComplete);
		sound.addEventListener (IOErrorEvent.IO_ERROR, onLoadError);

		sound.load (new URLRequest (_soundURLs[i]));
	}
}

Step 12: CustomLoader Extracting a Short Name

Now let's tackle extracting a short name from the URL of the loading asset. Keep in mind that this will only work on assets that are inside a folder that is placed at the same location as the generated swf file. In this particular case, if you're using FlashDevelop, the PV3Dassets folder should be within the \bin\ folder. For Flash, it should be within the same folder as your intro3D.fla file.

This method simply removes the folder name then cuts the file extension. To illustrate, with a url of 'PV3Dassets/dot0.png', 'PV3Dassets' is removed then '.png' is cut-off leaving a short name of 'dot0'.

Add the following lines of code after the startLoading () Method:

private function assignShortName($url:String):String
{
	return $url.split ('/')[1].split('.')[0];
}

Step 13: CustomLoader Dispatching an ALL_LOADS_COMPLETE Event

The onLoadComplete () method is called every time a visual asset or a sound asset successfully loads. All it does is pre-increment _loadCount by 1 each time a load completes then checks to see if the length of both _imageURLs and _soundURLs match _loadCount. If it does, a CustomLoader.ALL_LOADS_COMPLETE event is dispatched.

Add the following lines of code after the assignShortName () Method:

private function onLoadComplete (e:Event):void
{
	if (_imageURLs.length + _soundURLs.length == ++_loadCount)
	{
		dispatchEvent (new Event (ALL_LOADS_COMPLETE));
	}
}

Step 14: CustomLoader Listening for Errors

The onLoadError () method is called whenever the load for a visual or sound asset has failed. With this, you'll know if the program halts due to errors on loading.

Add the following lines of code after the onLoadErrorComplete () Method:

private function onLoadError(e:IOErrorEvent):void
{
	//internal use
	throw new Error (e);
}

Step 15: CustomLoader Accessing Assets for Use

We can start using the assets when a CustomLoader.ALL_LOADS_COMPLETE event has dispatched. To get a visual asset, we call the getItem () method passing in the short name (in String format) as parameter. To get a sound asset, we call the getSound () method also passing in the short name of the sound.

Add the following lines of code after the onLoadError () Method:

public function getItem ($name:String):Loader
{
	return _imageArray[$name];
}

public function getSound ($name:String):Sound
{
	return _soundArray[$name];
}

Step 16: CustomLoader Showing a List of Names

We have 1 last method to add. It's purpose is to return an array of all the names of the assets that are currently saved in the CustomLoader instance's _imageArray and _soundArray arrays.

Add the following lines of code after the getSound () Method:

public function get names ():Array
{
	var tempArray:Array = new Array;
	for (var name:String in _imageArray) tempArray.push (name);
	for (name in _soundArray) tempArray.push (name);

	return tempArray;
}

Step 17: Milestone

Alright! Before moving on for the Main document, double-check to make sure that your CustomLoader class isn't missing anything. Compare it with the complete code below:

Also, if you're using Flash, create a new Class in the same folder with intro3D.fla, name it "CustomLoader" and copy the code below into it.

package
{
	import flash.display.Loader;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.media.Sound;
	import flash.net.URLRequest;

	public class CustomLoader extends EventDispatcher
	{
		public static const ALL_LOADS_COMPLETE:String = 'allLoadsComplete';
		
		private static var _allowed:Boolean;
		private static var _customLoader:CustomLoader;
		
		private var _imageArray:Array;//associative
		private var _imageURLs:Array;
		private var _loadCount:uint;
		private var _soundURLs:Array;
		private var _soundArray:Array;//associative

		public function CustomLoader ()
		{
			if (! _allowed)
			{
				throw new Error ('This is a Singleton, please use CustomLoader.getInstance ()');
				return;
			}
			init ();
		}

		private function init ():void
		{
			_imageArray = new Array;
			_imageURLs = new Array;
			_soundURLs = new Array;
			_soundArray = new Array;
		}

		public static function getInstance ():CustomLoader
		{
			if (! _customLoader)
			{
				_allowed = true;
				_customLoader = new CustomLoader;
				_allowed = false;
			}
		return _customLoader;//happens either way
		}
		
		public function addItem ($url:String):void
		{
			_imageURLs.push ($url);
		}

		public function addSound ($url:String):void
		{
			_soundURLs.push ($url);
		}

		public function startLoading ():void
		{
			for (var i:uint = 0; i < _imageURLs.length; i++)
			{
				var loader:Loader = new Loader
				_imageArray[assignShortName (_imageURLs[i])] = loader;
				loader.contentLoaderInfo.addEventListener (Event.COMPLETE, onLoadComplete);
				loader.contentLoaderInfo.addEventListener (IOErrorEvent.IO_ERROR, onLoadError);
			 
				loader.load (new URLRequest (_imageURLs[i]));
			}
		 
			for (i = 0; i < _soundURLs.length; i++)
			{
				var sound:Sound = new Sound;
				_soundArray[assignShortName (_soundURLs[i])] = sound;
				sound.addEventListener (Event.COMPLETE, onLoadComplete);
				sound.addEventListener (IOErrorEvent.IO_ERROR, onLoadError);
				 
				sound.load (new URLRequest (_soundURLs[i]));
			}
		}

		private function assignShortName($url:String):String
		{
			return $url.split ('/')[1].split('.')[0];
		}
		
		private function onLoadComplete (e:Event):void
		{
			if (_imageURLs.length + _soundURLs.length == ++_loadCount)
		{
			dispatchEvent (new Event (ALL_LOADS_COMPLETE));
		}
		}

		private function onLoadError(e:IOErrorEvent):void
		{
			//internal use
			throw new Error (e);
		}

		public function getItem ($name:String):Loader
		{
			return _imageArray[$name];
		}

		public function getSound ($name:String):Sound
		{
			return _soundArray[$name];
		}

		public function get names ():Array
		{
			var tempArray:Array = new Array;
			for (var name:String in _imageArray) tempArray.push (name);
			for (name in _soundArray) tempArray.push (name);

			return tempArray;
		}
	}
}

Step 18: Setting up the Document Class

For FlashDevelop, open the project manager again (refer to Step 4), expand the \src\ folder and double-click Main.as.

Below the imports and right above the class definition, add the following metadata tag to set up the stage properties.

[SWF (frameRate = 30, width = 600, height = 350, backgroundColor = 0)]

Right below the class declaration, add the following instance variables:

private var _customLoader:CustomLoader;
private var _introArray:Array;
private var _currentIntro:uint;

The _customLoader variable will hold a reference to the CustomLoader instance. The _introArray variable will hold references of all the Intro3D's instantiated later. _currentIntro will increment by 1 every time an Intro3D instance dispatches an Intro3D.INTRO_COMPLETE event which will be used to call on the next Intro3D, add it to the stage and start rendering.

Note: If you're using Flash, jump to Step 19 then come back here.

OK. Let's do some tests with the CustomLoader class. Inside the init()method after where it says "entry point", add the following lines of code:

_customLoader = new CustomLoader;

Hit CTRL + ENTER to run the program.

This should generate you an error stating that you need to use 'CustomLoader.getInstance ()'.

Next, let's try to load 1 visual asset and 1 sound asset. Remove your last input and add these lines of code:

_customLoader = CustomLoader.getInstance ();
_customLoader.addEventListener (CustomLoader.ALL_LOADS_COMPLETE, onLoadComplete);

_customLoader.addItem ('PV3Dassets/dot0.png');//image
_customLoader.addSound ('PV3Dassets/Explosion_hifi.mp3');//sound

_customLoader.startLoading ();

Right below the init () method, add the onLoadComplete ()callback.

private function onLoadComplete (e:Event):void
{
	//remember, all visual assets are saved within their containing Loader which are also DisplayObjects.
	addChild (_customLoader.getItem ('dot0'));

	_customLoader.getSound ('Explosion_hifi').play ();	

	trace (_customLoader.names);
}

Run the program, this time a small dot should be present on the stage along with a sound of an explosion. Also, you'll see an Array of the short names of all the assets loaded into the CustomLoader instance printed on the output panel. CustomLoader works!


Step 19: The Document Class Responsibility

(Flash users: skip down to the second half of this Step.)

Here, we will load all the external assets, setup all 3 Intro3D's and wait for the user to click the stage to start the animation. Listed below are the Main class's responsibilities:

  1. Create a CustomLoader instance and request it to load all the external assets we will use for this application.
  2. Initialize 3 Intro3D instances and store them into _introArray.
  3. Listen for a MouseEvent.CLICK on the stage to trigger the animation.
  4. Play all the Intro3Ds in sequence when the stage is clicked, adding the next one into the stage then removing the one that just finished.

Go ahead and insert the following lines of code from inside the init () right below where it says "entry point".

_customLoader = CustomLoader.getInstance ();
_customLoader.addEventListener (CustomLoader.ALL_LOADS_COMPLETE, onLoadComplete);

for (var i:uint = 0; i < 6; i++)
{
	_customLoader.addItem ('PV3Dassets/splat' + String (i) + '.png');
}
_customLoader.addItem ('PV3Dassets/cross0.png');
_customLoader.addItem ('PV3Dassets/dot0.png');

_customLoader.addSound ('PV3Dassets/Swoosh1.mp3');
_customLoader.addSound ('PV3Dassets/Swoosh3.mp3');
_customLoader.addSound ('PV3Dassets/Explosion_hifi.mp3');
_customLoader.addSound ('PV3Dassets/FastBeat_hifi.mp3');

_customLoader.startLoading ();

After assigning a CustomLoader.ALL_LOADS_COMPLETE listener to the _customLoader instance, we loop 6 times to load all the external images that has a name beginning with "splat". It then loads the other assets individually since they have unique names. It's alway good to name your assets similarly to simplify loading.

Remove all the code inside the onLoadComplete () method. We'll leave it blank for now and start filling it in as we develop the 3 Intro3D classes.

For Flash, create a new class from within the same folder as your intro3D.fla. Name it Main.as and assign as the document class for intro3D.fla file. Copy and paste the code below into the class.

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.media.Sound;
	
	[SWF (frameRate = 30, width = 600, height = 350, backgroundColor = 0)]
    
	public class Main extends Sprite
	{
		private var _customLoader:CustomLoader;
		private var _introArray:Array;
		private var _currentIntro:uint;

		public function Main():void 
		{
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init)
		}

		private function init(e:Event = null):void
		{
			removeEventListener (Event.ADDED_TO_STAGE, init);
			_customLoader = CustomLoader.getInstance ();
			_customLoader.addEventListener (CustomLoader.ALL_LOADS_COMPLETE, onLoadComplete);

			for (var i:uint = 0; i < 6; i++)
			{
				_customLoader.addItem ('PV3Dassets/splat' + String (i) + '.png');
			}
			_customLoader.addItem ('PV3Dassets/cross0.png');
			_customLoader.addItem ('PV3Dassets/dot0.png');

			_customLoader.addSound ('PV3Dassets/Swoosh1.mp3');
			_customLoader.addSound ('PV3Dassets/Swoosh3.mp3');
			_customLoader.addSound ('PV3Dassets/Explosion_hifi.mp3');
			_customLoader.addSound ('PV3Dassets/FastBeat_hifi.mp3');

			_customLoader.startLoading ();
		}

		private function onLoadComplete (e:Event):void 
		{
			//leave this blank for now
		}
	}
}

Save before moving forward.


Step 20: The Intro3D Class Responsibility

The Intro3D class inherits from BasicView which in turn inherits from flash.display.Sprite. BasicView is part of the Papervision3D package. It's a quick setup for basic Papervision project.

We declare protected to the properties and methods that will also be used by the subclasses. Intro3D will be extended by 2 other intros which we will call SubIntro3D1 and SubIntro3D2. When you saw the animation, this is the 1st of the sequence and the other 2 are the subclasses.

Its list of responsibilities are:

  1. Setup the camera, its zoom, target, etc...
  2. Create the background and add it into the scene.
  3. Create particles and add it into the scene.
  4. Add 3D text into the scene.
  5. Animate the camera.
  6. Animate the particles.
  7. Animate the 3D texts.
  8. Inform the listener when the animation has finished.

Create a new class and name it Intro3D;; for its base class, choose org.papervision3d.view.BasicView, and click OK to complete the process. Refer to Step 6 if you're new to FlashDevelop.

Add the following lines of code inside the class declaration before the constructor:

public static const INTRO_COMPLETE:String = 'introComplete';

protected var _backgroundHolder:Sprite;
protected var _customLoader:CustomLoader;
protected var _text3DArray:Array;
protected var _particles:Particles;
protected var _easeOut:Number = .003;
protected var _reachX:Number = .1;
protected var _reachY:Number = .1;
protected var _reachZ:Number = .5;
protected var _xDist:Number = 0;
protected var _yDist:Number = 0;

The INTRO_COMPLETE static constant will be used as a custom event that is dispatched when this intro's animation has finished. The _backgroundHolder will hold the image created for the background. The _customLoader will have a reference of the CustomLoader's instance to load images which will be used for the particles. The _text3DArray will hold all the Text3D instances we will create later. _particles will hold all the particles. The rest will be used to apply a slight control to the camera as the animation plays to give a sense of realtime when the mouse is moved inside the stage. We will look into these variables more when we get to the onRenderTick () method later on.


Step 21: The Intro3D Constructor

Add the following lines of code inside the constructor:

_text3DArray = [];
_customLoader = CustomLoader.getInstance ();

addEventListener (Event.ADDED_TO_STAGE, onAddedToStage);

init ();

Here _text3DArray is instantiated. Then _customLoader is assigned a reference to the 1 instance of CustomLoader, this is where the Singleton pattern applies. Notice that as long as the CustomLoader class is accessible, no matter where it's being accessed from, it's still the same 1 instance that responds. We can load all the assets from 1 class and get access for any or all assets from pretty much everywhere else. Next a listener for when an instance of an Intro3D class is added to the stage which will be used to start the animation. The init () is called before the end of the method.


Step 22: The onAddedToStage () Method

Add the code after a space below the constructor method:

private function onAddedToStage (e:Event):void
{
	removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
	addEventListener (Event.REMOVED_FROM_STAGE, onRemovedFromStage);

	startRendering ();

	TweenLite.delayedCall (.2, animateText);
}

The onAddedToStage listener is removed since it will only be used once. We then assign a listener to check when an Intro3D instance is removed from the stage to stop the 3D engine from rendering when it no longer needs to. The startRendering () is then called and the Papervisions3D engine starts. A delayed call to the animateText () method is set for 200 milliseconds to avoid clogging up Flash's pipeline which may cause it to freeze.


Step 23: The init () Method

Called last as soon as an instance of Intro3D is created, this method prepares all the components for the animation before the instance is added to the stage. I'll explain more about each method called as we create them.

Add the lines below right after the onAddedToStage method:

protected function init():void
{
	setUpCamera ();
	createBackground ();
	createParticles ();
	addText ();
}

Step 24: Camera Setup

Add this after the init () method:

protected function setUpCamera ():void
{
	camera.zoom = 50;
	camera.target = null;
}

Anything that applies to the camera should be addressed here. We set the camera's zoom property to 50 and its target to null.


Step 25: Creating the Background

Here we create a vector image having a background color of light grey with a white radial glow around its center. This will be used as the background for the first "scene" of the intro. The _backgroundHolder is instantiated, its graphic property is accessed and drawn a rectangle with a gradient fill. Here's a link for more information if you're unfamiliar with the Graphics.beginGradientFill () method.

Add the following lines of code after the init() method:

protected function createBackground ():void
{
	_backgroundHolder = new Sprite;
	var g:Graphics = _backgroundHolder.graphics;

	var fillType:String = GradientType.RADIAL;
	var colors:Array = [0xFFFFFF, 0xC0C0C0];
	var alphas:Array = [1, 1];
	var ratios:Array = [0x88, 0xFF];
	var matr:Matrix = new Matrix();
	matr.createGradientBox(600, 600, 0, 40, 0);
	var spreadMethod:String = SpreadMethod.PAD;
	g.beginGradientFill(fillType, colors, alphas, ratios, matr, spreadMethod);
	g.drawRect (0, 0, 600, 350);
	
	addChildAt (_backgroundHolder, 0);
}

Step 26: The Particle System

Now comes the biggest method for this class. The createParticles () method.

Add the following lines of code after the createBackground () method:

protected function createParticles
(
$numParticles:Number = 150,
$name:String = 'splat',
$randomColors:Array = null,
$randomX:Array = null,
$randomY:Array = null,
$randomZ:Array = null,
$size:Number = 3
)
:void
{
	var particleArray:Array = new Array;
	for each(var name:String in _customLoader.names)
	{
		if (name.indexOf ($name) > -1)
		{
			particleArray.push (name);
		}
	}
	
	var randomColors:Array = $randomColors == null ? [0x666666, 0x000000, 0x851616] : $randomColors;
	
	var randomX:Array = $randomX == null ? [-5000,5000] : $randomX;

	var randomY:Array = $randomY == null ? [-5000,5000] : $randomY;
	
	var randomZ:Array = $randomZ == null ? [1400, 4000] : $randomZ;
	
	_particles = new Particles;
	scene.addChild (_particles);

	for (var i:uint = 0; i < $numParticles; i++)
	{
		var randomIndex:uint = uint (Math.random () * particleArray.length);
		var loader:Loader = _customLoader.getItem (particleArray[randomIndex]);
		var bitmapData:BitmapData = new BitmapData (loader.width, loader.height, true, 0);
		bitmapData.draw (loader);

		randomIndex = uint (Math.random () * randomColors.length);
		var red:uint = getColorComponent (randomColors[randomIndex]).RED;
		var green:uint = getColorComponent (randomColors[randomIndex]).GREEN;
		var blue:uint = getColorComponent (randomColors[randomIndex]).BLUE;

		var colorTransform:ColorTransform = new ColorTransform (0, 0, 0, 1, red, green, blue);

		bitmapData.colorTransform (bitmapData.rect,  colorTransform);

		var particleMaterial:BitmapParticleMaterial = new BitmapParticleMaterial (bitmapData, 1, loader.width * -.5, loader.height * -.5);
		particleMaterial.smooth = true;

		var particle:Particle = new Particle (particleMaterial, $size, getRandomInRange ( randomX[0], randomX[1]), getRandomInRange ( randomY[0], randomY[1]), getRandomInRange ( randomZ[0], randomZ[1]));
		particle.rotationZ = Math.random () * 360;
		_particles.addParticle (particle);
	}
}

Let's go over the details of what happens in here.

First, as you've noticed, the method already has built-in parameters. We have it this way so that subclasses can just send in its own set of parameters to make changes from this default set up. You'll see when we go over the 2 subclasses later.

The parameters are:

  1. $numParticles - Defaults to 150, this is the number of particles you want to have in the system.
  2. $name - Default is "splat", we pass in the String short name of the image (loaded through CustomLoader earlier) we want to use as Material for the particles.
  3. $randomColors - Defaults to null, an array of colors used for the particles (randomly distributed to all the particles).
  4. $randomX - Defaults to null, 2 dimensional array of minimum and maximum "x" locations for the particles.
  5. $randomY - Defaults to null, same as $randomX but for "y" locations.
  6. $randomZ - Defaults to null, same as $randomX but for "z" locations.
  7. $size - Defaults to 3, this pertains to the scale of each particle compared to it's original image size.

Once this method is called, a local variable particleArray Array is instantiated.Next, it loops through all the names currently stored in the CustomLoader instance. Each time the $name parameter matches a name inside _customLoader.names, it is added into particleArray. Since we're using "splat" as the image, all the names from "splat0" through "splat5" will be collected into particleArray. $randomColors is then checked if it's null or not. If it's null, a local variable named randomColors is assigned [0x666666, 0x000000, 0x851616]. If not, which is the case of the 2 subclasses, then it is assigned to randomColors. The same technique is used for $randomX, randomY, and $randomZ. The _particles is then instantiated and added to the scene. For those of you unsure of what Particles is in PV3D, simply put, it's a container for all the Particle objects. A Particle is a 2 Dimensional image that is constantly facing the camera.

$numParticles is then looped through to create the particles. During each iteration, a local variable named randomIndex of type uint is generated by choosing between 0 & 5. In turn, a local variable named loader of type Loader is assigned the corresponding Loader instance that was saved inside the _imageArray of the single CustomLoader instance. If you need a refresher, go back and visit the CustomLoader class we created earlier. A bitmapData local variable is then created with the height and width of the Loader. It's important to note that the BitmapData's transparent parameter is set to true. The fillColor property is set as Black. If you look at any of the imported images, you'll notice that they are plain white. I have the images in PNG format to preserve transparency while keeping the file size low. The Loader is then drawn into the bitmapData. So far, the image is still colorless. With the help of the ColorTransform class, we can change this bitmapdata's color to the colors of the randomColors array. This is done by using randomIndex again, this time any integer between 0 and randomColors length. With the selected color, a special function called getColorComponent is then utilized to extract its Red, Green, and Blue components. The colorTransform local variable is then istantiated. All the colorMultipliers are assigned "0", the alpha is set to "1", and the colorOffsets are assigned the derived color components. Finally, the bitmapData's colorTransform property is assigned the colorTransform. We pass in bitmapData.rect to apply the colorTransform to the whole image.

The particleMaterial local variable is then created with the bitmapData as the 1st parameter, "1" for its scale, and loader.width * -.5 and loader.height * -.5 to center the material. This is a fix I had to apply since if you leave the default parameters of "0" for the offSet parameters, all the particles that touch the edge of the screen disappear for some reason. The particleMaterial's smooth property is set to true. This prevents the particle from pixelating once it's close to the camera.

Once the particleMaterial is ready, the actual particle is then instantiated. Each particle is created by passing in the particleMaterial, the $size, then the random x, y & z positions. A random rotation is applied to each particle before it is added into _particles.


Step 27: Extracting Color Components

This method takes in a color parameter in hexadecimal format and breaks it down into its 3 components. An object is then created and returned with the components assigned as its RED, GREEN, and BLUE properties.

Add the following lines of code after the createParticles () method:

private function getColorComponent ($color:uint):Object
{
	var colorObject:Object = new Object;
	colorObject.RED = $color >> 16;
	colorObject.GREEN = $color >> 8 & 0xFF;
	colorObject.BLUE = $color & 0xFF;
	
	return colorObject;
}

Step 28: Getting a Random Number Within a Range

Add the following lines of code at the very bottom of the class:

protected function getRandomInRange ($min:Number, $max:Number, $rounded:Boolean = true):Number
{
	if ($rounded) return Math.round (Math.random () * ($max - $min) + $min);
	else return Math.random () * ($max - $min) + $min;
}

(See also this Quick Tip and the discussion in the comments.)


Step 29: Adding 3D Text

This method also has default parameters. Like the createParticles () method, we have it set up this way to easily change the 3D text properties.

Add this after the getColorComponent () method:

protected function addText
(
	$text:String = 'wanna learn \n actionscript?',
	$fillColor:uint = 0x660000,
	$fillAlpha:Number = 1,
	$lineThickness:Number = 1,
	$lineColor:uint = 0x660000,
	$lineAlpha:Number = 1,
	$font3D:Font3D = null,
	$rotationX:Number = 0,
	$rotationY:Number = -45,
	$rotationZ:Number = 5,
	$scale:Number = 2,
	$align:String = 'left',
	$letterSpacing:Number = -2,
	$lineSpacing:Number = -30,
	$x:Number = -550,
	$y:Number = 100,
	$z:Number =-990
):void
{
	var material:Letter3DMaterial = new Letter3DMaterial ($fillColor, $fillAlpha);
	material.lineThickness  = $lineThickness;
	material.lineColor = $lineColor;
	material.lineAlpha = $lineAlpha;

	var font3D:Font3D = $font3D == null ? new HelveticaBold : $font3D;

	var text3D:Text3D = new Text3D ($text, font3D, material);
	text3D.localRotationX = $rotationX;
	text3D.localRotationY = $rotationY;
	text3D.localRotationZ = $rotationZ;

	text3D.x = $x;
	text3D.y = $y;
	text3D.z = $z;

	text3D.scale = $scale;
	text3D.align = $align;
	text3D.letterSpacing = $letterSpacing;
	text3D.lineSpacing = $lineSpacing;

	_text3DArray.push (text3D);
	scene.addChild (text3D);
}

The parameters are:

  1. $text - Defaults to "wanna learn actionscript?", the text for your Text3D.
  2. $fillColor - Defaults to a dark red color, serves as the fill color of the Text3D.
  3. $fillAlpha - Defaults to ".1", sets the alpha transparency of the Text3D's fill.
  4. $lineThickness - Defaults to "1", specifies the thickness of the Text3D's border.
  5. $lineColor - Also defaults to dark red, this is the color of the Text3D's border.
  6. $lineAlpha - Defaults to "1", the alpha transparency of the Text3D's border.
  7. $font3D - Defaults to null which is equivalent to assigning the built-in Font3D HelveticaBold.
  8. $rotationX - Defaults to "0" degrees, this will be the starting localRotationX of the Text3D when added to the scene.
  9. $rotationY - Defaults to "-45" degrees, starting localRotationY of the Text3D.
  10. $rotationZ - Defaults to "5" degrees, starting localRotationZ of the Text3D.
  11. $scale - Defaults to "2", applies to the scale of Text3D.
  12. $align - Defaults to "left", alignment feature Text3D.
  13. $letterSpacing - Defaults to "-2", space between characters.
  14. $lineSpacing - Defaults to "30", space between each line of text if the Text3D is multi-lined (like in our example).
  15. $x - Defaults to "-550", the starting x position when the Text3D is added to the scene.
  16. $y - Defaults to "100", the starting y position when the Text3D is added to the scene.
  17. $z - Defaults to "-990", the starting z position when the Text3D is added to the scene.

This class may seem daunting at first but as soon as you get familiar with it, you'll begin to see just how easy it is to use. Now let's get to know what happens when this method is called.

First, a local variable material of type Letter3DMaterial is instantiated passing in $fillColor and $fillAlpha. The material's thickness, lineColor, and lineAlpha properties are then assigned the appropriate parameter. A local variable font3D of type Font3D is then instantiated with the value passed in from the $font3D parameter, if the value sent in is "null", a new HelveticaBold Font3D is used. Now that both the Font3D and Letter3DMaterial are made, we can instantiate the Text3D. Instantiation is done by passing in $text, font3D, and material as parameters. Once instantiated, rotation is set on its x,y & z axes, then the x, y, & z positions. The last parameters we assign are the scale, align, letterSpacing, and lineSpacing. And that does it! We hold a reference for each Text3D instance inside _text3DArray for access later and add it to the scene.


Step 30: The onRenderTick () Method

Add this after the addText () method:

override protected function onRenderTick (e:Event = null):void
{
	animateParticles ();
	animateCamera ();

	_xDist = mouseX - stage.stageWidth * .5;
	_yDist = mouseY - stage.stageHeight * .5;

	super.onRenderTick (e);
}

Called at every EnterFrame event, it handles the movement of the camera and _particles. The _xDist and _yDist are also calculated here.


Step 31: Animating the Particles

Add this after the onRenderTick () method:

protected function animateParticles ():void
{
	_particles.z -= 20;
}

This is where you can control all the ways you want the _particles to animate. It just moves the _particles closer at every onRenderTick event, since the camera is pointing along the z-axis. See the paint splats come closer!?


Step 32: The animateCamera () Method

Add this after the animateParticles () method:

protected function animateCamera ():void
{
	camera.x += (_xDist - camera.x * _reachX) * _easeOut;
	camera.y += (_yDist - camera.y * _reachY) * _easeOut;
}

Slightly influenced by mouse movement (try it out in the demo), this gives the scene the real-time feel.


Step 33: Animating the Text3D with TweenLite

Add this below animateCamera ():

protected function animateText ():void
{
	var sound:Sound = _customLoader.getSound ('Swoosh3');
	TweenLite.to 
	(
		_text3DArray[0], 
		1.2,
		{
			z:100,
			localRotationY:-22.5,
			onStart:playSound,
			onStartParams:[sound,0,0,new SoundTransform (3)],
			onComplete:end
		}
	);
}

Triggered 200 milliseconds after the Intro3D instance is added to the stage. Here, with the help of TweenLite, the 3D text is tweened from its starting z position (-990) to 100 within the duration of 1.2 seconds. Also the rotationY is corrected to have it end parallel to the screen.

The rest of the parameters are as follows:

  1. onStart - This is triggered when TweenLite starts this tween. It's used here to call playSound, a method we'll add later for playing sound effects for all Intro3Ds.
  2. onStartParams - An array container for all the parameters we need to pass to the playSound method. I'll go over the details for this method a little later.
  3. onComplete - We pass in a method that needs to be called when this tween ends. It's assigned end (also created and discussed later).

And that's what makes the 3D text move in from behind the camera to the center of the stage.

Step 34: Adding Sound Effects

Add this after the animateText () method:

protected function playSound ($sound:Sound, $startTime:Number = 0, $loops:Number = 0, $soundTransform:SoundTransform = null):void
{
	var soundTransform:SoundTransform = $soundTransform == null ? new SoundTransform () : $soundTransform;
	$sound.play ($startTime, $loops, soundTransform);
}

When called, this method is passed the sound to play, the start time position for the sound, the amount of loops, and a SoundTransform instance. It's a simple and effective control for playing sound.


Step 35: At the End of the Intro

Add this after the playSound () method:

protected function end ($delayInSeconds:Number = 1):void
{
	TweenLite.delayedCall ($delayInSeconds, dispatchEvent, [new Event (Intro3D.INTRO_COMPLETE)]);
}

This waits for 1 second and then dispatches the Intro3D.INTRO_COMPLETE event.


Step 36: When Removed From the Stage

Add this after the checkStatus () method:

private function onRemovedFromStage(e:Event):void
{
	removeEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage);
	stopRendering ();
}

After removal from the stage, It stops PV3D's rendering engine and saves some processing power. This completes the Base class Intro3D.


Step 37: Milestone

Due to the size of the Intro3D class, I've refrained from posting it here. You can compare your code with the Intro3D.as class included with the Source download; we don't need to add any more code to it.


Step 38: Testing Intro3D

Let's go back to the Main class.

Inside the onLoadComplete () method add following lines of code:

var intro:Intro3D = new Intro3D;
intro.addEventListener (Intro3D.INTRO_COMPLETE, onIntroComplete);
addChild (intro);

Add the onIntroComplete ()method below onLoadComplete:

private function onIntroComplete(e:Event):void
{
	removeChild (getChildAt (0));
}

Run the program.

If all went well, you should've seen the animation along with its sound effect.


Step 39: Creating the Subclass SubIntro3D1

Create a new class inside the same folder where you saved Intro3D.as. Refer to Step 6.

For the name, type in "SubIntro3D1", for the base class, have it extend Intro3D.

Inside the package declaration, add the following imports like so:

package
{
	import com.greensock.TweenLite;
	import flash.events.Event;
	import flash.media.Sound;
	import flash.media.SoundTransform;
	import org.papervision3d.core.geom.Particles;
	import org.papervision3d.core.geom.renderables.Particle;
	import org.papervision3d.materials.special.BitmapParticleMaterial;
	import org.papervision3d.materials.special.Letter3DMaterial;
	import org.papervision3d.typography.Font3D;
	import org.papervision3d.typography.Text3D;

	public class SubIntro3D1 extends Intro3D
	{
		public function SubIntro3D1 ()
		{
			super ();
		}
	}
}

Inside the constructor, it calls super (). This is equivalent to calling Intro3D's constructor method, so refer to Step 20.

Now let's test it. Go back to the Main class and change the code from: var intro:Intro3D = new Intro3D; to: var intro:Intro3D = new SubIntro3D1;

Run it and you should see an exact replica of Intro3D's animation. In fact, you can even remove all the imports and it should still work. Also, we won't have to change intro's type declaration since, they will all be Intro3Ds.

Leave the Main class as it is and move forward.


Step 40: Modifying the Setup for SubIntro3D1

In this subclass, we're changing everything. This Intro3D will have the 5 3D texts move in from different corners to express "you've come to the right place!". Also, the particles will instead rotate on its x axis. Let's start by changing the material used as particles and adding more 3D text. If you go back to Intro3D, you'll see that the createParticles () and addText () methods are called from the init () method. To make changes, we need to override this.

Add the following lines of code after the constructor:

override protected function init():void
{
	setUpCamera ();//same
	createBackground ();//same
	createParticles (1000, 'dot0', [0x80FFFF, 0xF7C582, 0xFFFFFF, 0xA40052], null, null, null, 2);

	addText ('you\'ve', 0x00FFFF, 1, 0, 0, 1, new AdelleBasicRgBold, 0, 0, -5, 2, 'right', -2, -30, -2000, 100,   100);
	addText ('come', 	0x00FFFF, 1, 0, 0, 1, new AdelleBasicRgBold, 0, 0, -5, 2, 'right', -2, -30, -450,  1000,  100);
	addText ('to the',  0x00FFFF, 1, 0, 0, 1, new AdelleBasicRgBold, 0, 0, -5, 2, 'right', -2, -30, -1050, -3000, 100);
	addText ('right',   0x00FFFF, 1, 0,	0, 1, new AdelleBasicRgBold, 0, 0, -5, 2, 'right', -2, -30, 2000,  -70,   100);
	addText ('place!',  0x00FFFF, 1, 0,	0, 1, new AdelleBasicRgBold, 0, 0, -5, 2, 'right', -2, -30, -1000, -300,  -2000);
}

We pass in a different set of parameters to the createParticles method to make the change. Then, we call the addText method 5 times to create "you've come to the right place!". This makes the 3D texts independent from each other. See Step 28 for detail of what goes on in this method. I'll cover how to create a custom Font3D - AdelleBasicRgBold we're using here when we finish this class. One thing to note, the x, y, & z positions for the 3D texts we're visually calibrated (took 5 minutes).


Step 41: Changing the Background

If we don't override this method, the background will be the same as Intro3D's. Let's just use the stage's background.

Add the following lines of code after the init() method:

override protected function createBackground ():void {/*do nothing*/}

Here, we do nothing. And since we're overriding Intro3D's createBackground () method, it doesn't get applied.


Step 42: Changing the Particles' Animation

Ok, refer back to Step 30. For Intro3D, the particles move in towards the camera. For this SubIntro3D1, this takes care of making the particles roll on their x axes.

Add the following lines of code after the createBackground () method:

override protected function animateParticles ():void {_particles.localRotationX += .3;}

Step 43: Animating the 5 Text3Ds in the Scene

The 3D texts start to move in to their proper positions in sequence. We assign a SoundTransform instance with a volume value of 3 since the sound effect we are using here has less volume than the others. The last tween also triggers the end method.

Add the following lines of code after the animateParticles () method:

override protected function animateText():void
{
	var sound:Sound = _customLoader.getSound ('Swoosh1');
	
	TweenLite.to (_text3DArray[0], .4, {x:-1000, y:100, z:100, delay: 0, onStart:playSound, onStartParams:[sound,0,0,new SoundTransform (3)]});
	TweenLite.to (_text3DArray[1], .4, {x:-450, y:150, z:100, delay: .2, onStart:playSound, onStartParams:[sound,0,0,new SoundTransform (3)]});
	TweenLite.to (_text3DArray[2], .4, {x:-1050, y:-120, z:100, delay: .4, onStart:playSound, onStartParams:[sound,0,0,new SoundTransform (3)]});
	TweenLite.to (_text3DArray[3], .4, {x:-520, y:-70, z:100, delay: .6, onStart:playSound, onStartParams:[sound,0,0,new SoundTransform (3)]});
	TweenLite.to (_text3DArray[4], .4, {x:-1000, y:-300, z:100, delay: .8, onStart:playSound, onStartParams:[sound,0,0,new SoundTransform (3)], onComplete:end});
}

That sums up the SubIntro3D1 class.


Step 44: Milestone

Review your class and compare it with the SubIntro3D.as class also included with the Source download.

Before running a test, go through Step 45.

When you run the test. You'll see the 2nd part of the animation from the preview at the top of the page. We're now 2/3rds done with the project.


Step 45: Font3D Creation

If you want to skip this, just grab the accompanying AdelleBasicRgBold.as actionscript class file from the Source download and put it into the same folder you've been using all along and run the test.

Mathieu Badimon has an excellent tool for creating Font3D. You can find out more about this and FIVe3D (a vector based 3D engine) here. I've also included a zip package for his "make typography file" with the Source download.

Here are the steps for installation for Vista/Windows7 users:

  1. Type"appdata" into Window's search bar, click it when found.
  2. Navigate to "\Local\Adobe\YOUR_FLASH_VERSION_HERE\en_US\Configuration\WindowSWF".
  3. Once you're inside the WindowsSWF folder, extract the Make typography file.swf in it.
  4. Install the AdelleBasic_bold.otf font (it's in the Source download).
  5. Open Flash, create a new file and name it vText. Save it into the same folder where you have intro3D.fla.
  6. Click the Window panel from within Flash, then click Other Panels, from there you can click Make a new typography file.
  7. Once it's open, select Adelle Basic Rg, click the Bold icon, leave the rest as they are. The file name should now list as "AdelleBasicRgBold.as".
  8. Hit generate. Once created, the file should be inside the same folder with the rest of the classes we've been working on.
  9. Open the AdelleBasicRgBold.as class and have the class extend Font3D, also since we are not using packages for this project, remove the package name from the top of the document. Be careful not to remove the "package" declaration though.
  10. Go to the bottom of the class before the closing bracket and paste the following 3 methods before saving and closing the file. If you run into trouble, compare the class with the 1 I included with the Source download.
override public function get motifs():Object
{
	if(!__initialized)initialize();
	return __motifs;
}

override public function get widths():Object
{
	if(!__initialized)initialize();
	return __widths;
}

override public function get height():Number
{
	if(!__initialized)initialize();
	return __heights;
}

That's it! Consult the instructions.pdf included with the FIVe3D_make_typography_v2.0.zip if you're not using Vista/Windows7.

Step 46: The 2nd Subclass - SubIntro3D2

Overall, this class will only be a little bit larger than SubIntro3D1. We have a few changes with its animation so we're adding a couple of new methods.

Create a new class and name it "SubIntro3D2"; for its base class, extend Intro3D.

Add the imports and also call super () in the constructor just like you did with SubIntro3D1:

package 
{
	import com.greensock.easing.Linear;
	import com.greensock.easing.Strong;
	import com.greensock.TweenLite;
	import flash.display.GradientType;
	import flash.display.Graphics;
	import flash.display.SpreadMethod;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.filters.BevelFilter;
	import flash.geom.Matrix;
	import flash.media.Sound;
	import flash.media.SoundTransform;
	import org.papervision3d.materials.special.Letter3DMaterial;
	import org.papervision3d.typography.Font3D;
	import org.papervision3d.typography.Text3D;
	import org.papervision3d.view.layer.ViewportLayer;
	
	public class SubIntro3D2 extends Intro3D
	{
		public function SubIntro3D2 () 
		{
			super ();
		}
	}
}

Again, let's test. Go back to the Main class and change the code: var intro:Intro3D = new SubIntro3D1; to: var intro:Intro3D = new SubIntro3D2;

When you run the program, SubIntro3D2 replicates Intro3D. If you want SubIntro3D2 to replicate SubIntro3D1, go back to the class declaration and have SubIntro3D2 extend SubIntro3D1. If you test it this time, the animation should now look like SubIntro3D1's animation. Either way will work. But since we're making SubIntro3D2 also look very different from the other two, we'll again override the same methods and even add some new ones. What we want to have SubIntro3D2 to do is have the 3D text "active tuts" fly in from the back of the scene similar to Intro3D. Except without the rotationY having to change. Then once "active tuts" reaches is z destination, we want the camera to focus on it for 2.5 seconds then dash to the right to reveal "mad skills!".


Step 47: Modifying the Setup for SubIntro3D2

Alright, we're changing the text, its properties, and the material for the particles. So we go to the init () method.

Add this below the constructor:

override protected function init():void
{
	setUpCamera ();
	createBackground ();
	createParticles (500, 'cross', [0xD56A00, 0xBE1410, 0xFFFFFF], null,null,null, 1);
	
	addText ('active tuts', 0xFFFFFF, 1, 1, 0xE87400, 1, new LubalGraphBdBTBold, 0, 0, 0, 2, 'right', -2, -30, -2000, 0, -1100);
	addText ('mad skills!', 0x80FF00, 1, 0, 0x80FF00, 1, new LubalGraphBdBTBold, 0, 0, 0, 2, 'right', -2, -30, 2100, -1100, 1000);

	embossText ();
}

By this time, you should be familiar with this method. Everything's the same until we start plugging in different sets of parameters for the createParticles and addText methods. We call addText only twice this time to add 2 3D texts into the scene. We then add embossText. This method will take care of embossing the 3D texts. Did you notice this effect in the last of the 3 intros when you first saw the preview? That takes care of the initial setup.


Step 48: Adding Functionality to setUpCamera

Here, super.setUpCamera is called first to set the camera's zoom to 50 and its target to null. It then moves the camera to an x position of -1500. With this adjustment, the audience will have a nice 3D visual experience when the camera dashes to the right to look at "mad skills!" 3D text.

Add this after the init () method:

override protected function setUpCamera():void
{
	super.setUpCamera();
	camera.x = -1500;
}

Step 49: Adding Background for SubIntro3D2

You guys are probably wondering why this method didn't get set up with parameters. It's to not limit background creation by using internal graphics. You can choose to use imported images as well.

Add this after the init () method:

override protected function createBackground ():void
{
	_backgroundHolder = new Sprite;
	var g:Graphics = _backgroundHolder.graphics;
	
	var fillType:String = GradientType.RADIAL;
	var colors:Array = [0x9C3301, 0x820000];
	var alphas:Array = [1, 1];
	var ratios:Array = [0x00, 0xFF];
	var matr:Matrix = new Matrix();
	matr.createGradientBox(500, 500, 0, 100, 0);
	var spreadMethod:String = SpreadMethod.PAD;
	
	g.beginGradientFill(fillType, colors, alphas, ratios, matr, spreadMethod);
	g.drawRect (0, 0, 600, 350);

	addChildAt (_backgroundHolder, 0);
}

Step 50: Embossing Text3D

Embossing the 3D texts directly will have no effect, you need to apply it to the ViewportLayer that holds the 3D text. To do this, you access the layer by using the viewport.getChildLayer method and pass in the 3D text as show below.

Add this after the init () method:

protected function embossText ():void
{
	for each (var text3D:Text3D in _text3DArray)
	{
		var layer:ViewportLayer = viewport.getChildLayer (text3D);
		layer.filters = [new BevelFilter (1)];
	}
}

Step 51: Animation Sequence 1

The "active tuts" 3D text moves in from the back of the scene. TweenLite moves the 3D text from -1100 to 1000 in 1.5 seconds. A sound is also played as soon as the tween starts. Once the tween completes, it calls focus.

Add this after the embossText () method:

override protected function animateText ():void
{
	var sound:Sound = _customLoader.getSound ('Swoosh1');
	TweenLite.to (_text3DArray[0], 1.5, { z:1000, onStart:playSound, onStartParams:[sound,0,0,new SoundTransform (5)], onComplete:focus});
}

Step 52: Animation Sequence 2 and 3

This is the part where the camera zooms in to look at the "active tuts" 3D text. We pass in an "x" position of -1550 since we set "active tuts" 3D text's "x" position to -2000. For the ease, we use Linear.easeNone for a steady approach which takes place within 100 milliseconds. An explosion sound is also played here. When the sound plays, its start position is set at 400 milliseconds so the sound is half-way done. Another tween is then assigned for the camera. This will take care of moving the camera to the right to look at "mad skills!" 3D text. It moves the camera from an "x" position of -1550 to 2500 and "y" position of 0 to -1000 in 1 second. The 2 tweens prevent overriding each other by setting their overwrite properties to false. This tween takes place after 2.5 seconds when this method is called and when it finished, it calls end passing in 4 seconds before dispatching an Intro3D.INTRO_COMPLETE event.

Add this after the animateText () method:

private function focus ():void
{
	var sound1:Sound = _customLoader.getSound ('Explosion_hifi');
	TweenLite.to (camera, .1, { x: -1550, y:0, z: 0, ease:Linear.easeNone, overwrite:false, onStart:playSound, onStartParams:[sound1,400,0,new SoundTransform (1)]} );

	var sound2:Sound = _customLoader.getSound ('Swoosh3');
	TweenLite.to (camera, 1, { x:2500, y: -1000, ease:Strong.easeInOut, overwrite:false, onStart:playSound, onStartParams:[sound2, 0, 0, new SoundTransform (3)], delay: 2.5, onComplete:end, onCompleteParams:[4]} );
}

Step 53: Animating the Particles

Finally, the last method for this subclass!

We just want the particles to move in towards the camera at a much slower pace.

Add this after everything else:

override protected function animateParticles ():void {_particles.z -= 2;}

Step 54: Milestone

Before testing, check your code against the SubIntro3D2.as with the Source download. Also, make sure to drag the LubalGraphBdBTBold.as from the Source download into the same folder as all the classes we've made.

Note: Unfortunately, the LubalGraphBdBTBold.as only has the characters that spell "active tuts" and "mad skills!". You won't be able to use it for anything else due to its licensing restrictions.

Run the program and watch the animation.


Step 55: Putting Things Together

Go back to the Main document.

Modify the onLoadComplete method as shown below:

private function onLoadComplete (e:Event):void
{
	_introArray = [];
	var intro:Intro3D = new Intro3D;
	intro.addEventListener (Intro3D.INTRO_COMPLETE, onIntroComplete);
	var intro1:Intro3D = new SubIntro3D1;
	intro1.addEventListener (Intro3D.INTRO_COMPLETE, onIntroComplete);
	var intro2:Intro3D = new SubIntro3D2;
	intro2.addEventListener (Intro3D.INTRO_COMPLETE, onIntroComplete);

	_introArray.push (intro);
	_introArray.push (intro1);
	_introArray.push (intro2);

	stage.addEventListener (MouseEvent.CLICK, onStageClick);
}

All 3 Intro3Ds are instantiated and assigned a listener for Intro3D.INTRO_COMPLETE. We keep a reference for each Intro3D instance into the _introArray array for access later. It then listens for the user to click the stage to run the animation.


Step 56: Starting the Intro

Once the stage is clicked, we remove it's listener to prevent the Intros from playing repeatedly. The 1st of the Intro3D inside the _introArray is added to the stage. Remember how we set the Event.ADDED_TO_STAGE to start PV3D's rendering engine and animate the 3D text? The music is also triggered to play here.

Add the code after onLoadComplete:

private function onStageClick(e:MouseEvent):void
{
	stage.removeEventListener (MouseEvent.CLICK, onStageClick);
	addChild (_introArray[0]);

	var sound:Sound = _customLoader.getSound ('FastBeat_hifi');
	sound.play ();
}

Step 57: When an Intro Ends

Each time an Intro3D ends, _currentIntro pre-increments by 1. The _introArray is then checked to see if there's an Intro3D inside it at _currentIntro's value. If there's an Intro3D, it is added to the stage. Then the finished Intro3D is removed either way.

Modify the onIntroComlete method as shown below:

private function onIntroComplete(e:Event):void
{
	var intro:Intro3D = _introArray [++_currentIntro];
	if (intro != null) addChild (intro);
	
	removeChild (getChildAt (0));
}

Run it and enjoy the movie!!

You certainly deserve it after all that code!!! =)


Conclusion

You now have the basic knowledge of using the Intro3D class. Remember not to modify it. Instead, subclass it and change anything you want from there. It should serve as a quick setup for 3D animation.

You can also add videos in between intros to improve on it some more.

As always, for any comments, suggestions, or concerns, please post a note in the comments section.

Thanks for Reading!