Introduction
Away3D is a powerful Flash 3D engine which started life as a spin off from the Papervision Flash 3D engine. Since then, it has taken on a life of its own, and is currently one of the few 3D engines available for Flash that can make use of the new features of Flash Player 10.
In this tutorial, I will be showing you how to get a basic Away3D program up and running. This will be the first in a series of tutorials, and as such, we will lay down a lot of foundation here that we can build on. This means that while the outcome will be quite simple, we will be creating a few classes as a result.
Background
This code is used as the basis for a series of tutorials showing off the Away3D engine.
Using the code
The program itself will be conceptually split into two areas: the "engine" and the "application". The reason for this is that there will be a great deal of code that is used to create, run, and cleanup the Away3D "engine", and for the most part, this code will be common amongst the tutorials. On the other hand, the "application" will change significantly between tutorials. By keeping the code that makes up these two areas separate, we can define a reusable base that will be used with all the coming tutorials, and it will also help isolate the code that achieves the result of the tutorial from the boilerplate code that runs the Away3D engine.
The first class we need to create is the EngineManager
. As the name suggests, this class will deal with creating, running, and destroying the Away3D engine.
EngineManager.as
package
{
import away3d.cameras.Camera3D;
import away3d.containers.View3D;
import away3d.core.render.Renderer;
import mx.collections.ArrayCollection;
import mx.core.UIComponent;
import mx.core.Application;
import flash.events.*;
public class EngineManager extends UIComponent
{
public static const version:String = "1.0.0";
protected static const MEASURED_MIN_WIDTH:int = 25;
protected static const MEASURED_MIN_HEIGHT:int = 25;
protected static const MEASURED_WIDTH:int = 100;
protected static const MEASURED_HEIGHT:int = 100;
internal var view:View3D = null;
internal var cam:Camera3D = null;
protected var baseObjects:ArrayCollection = new ArrayCollection();
protected var newBaseObjects:ArrayCollection = new ArrayCollection();
protected var removedBaseObjects:ArrayCollection = new ArrayCollection();
protected var lastFrame:Date;
protected var applicationManager:ApplicationManager = null;
protected var myResourceManager:ResourceManager = null;
protected var addedToStage:Boolean = false;
protected var propertiesDirty:Boolean = false;
internal function get MyResourceManager():ResourceManager
{
return myResourceManager;
}
protected function propertyChanged():void
{
propertiesDirty = true;
invalidateProperties();
invalidateDisplayList();
}
public function EngineManager()
{
super();
}
override protected function measure():void
{
super.measure();
this.measuredMinWidth = MEASURED_MIN_WIDTH;
this.measuredMinHeight = MEASURED_MIN_HEIGHT;
this.measuredHeight = MEASURED_HEIGHT;
this.measuredWidth = MEASURED_WIDTH;
}
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if (view != null)
{
view.x = this.height/2;
view.y = this.width/2;
}
}
override protected function commitProperties():void
{
super.commitProperties();
if (propertiesDirty)
{
propertiesDirty = false;
if (addedToStage)
{
applicationManager.shutdown();
applicationManager.startupApplicationManager();
}
}
}
override protected function createChildren():void
{
super.createChildren();
addEventListener(Event.ADDED_TO_STAGE, createChildrenEx);
addEventListener(Event.REMOVED_FROM_STAGE, shutdown);
}
protected function shutdown(event:Event):void
{
if (applicationManager != null)
applicationManager.shutdown();
shutdownAll();
this.removeChild(view);
applicationManager = null;
addedToStage = false;
view = null;
cam = null;
}
protected function createChildrenEx(event:Event):void
{
if (!addedToStage)
{
cam = new Camera3D();
view = new View3D({x:250,y:150,camera:cam});
view.renderer = Renderer.BASIC;
addChild(view);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
lastFrame = new Date();
myResourceManager = new ResourceManager();
myResourceManager.loadResources();
applicationManager =
new ApplicationManager(this).startupApplicationManager();
addedToStage = true;
}
}
protected function onEnterFrame(event:Event):void
{
var thisFrame:Date = new Date();
var seconds:Number = (thisFrame.getTime() -
lastFrame.getTime())/1000.0;
lastFrame = thisFrame;
removeDeletedBaseObjects();
insertNewBaseObjects();
for each (var baseObject:BaseObject in baseObjects)
baseObject.enterFrame(seconds);
view.render();
}
public function addBaseObject(baseObject:BaseObject):void
{
newBaseObjects.addItem(baseObject);
}
public function removeBaseObject(baseObject:BaseObject):void
{
removedBaseObjects.addItem(baseObject);
}
public function shutdownAll():void
{
for each (var baseObject:BaseObject in baseObjects)
{
var found:Boolean = false;
for each (var removedObject:BaseObject in removedBaseObjects)
{
if (removedObject == baseObject)
{
found = true;
break;
}
}
if (!found)
baseObject.shutdown();
}
}
protected function insertNewBaseObjects():void
{
for each (var baseObject:BaseObject in newBaseObjects)
baseObjects.addItem(baseObject);
newBaseObjects.removeAll();
}
protected function removeDeletedBaseObjects():void
{
for each (var removedObject:BaseObject in removedBaseObjects)
{
var i:int = 0;
for (i = 0; i < baseObjects.length; ++i)
{
if (baseObjects.getItemAt(i) == removedObject)
{
baseObjects.removeItemAt(i);
break;
}
}
}
removedBaseObjects.removeAll();
}
}
}
The EngineManager
involves quite a bit of code. The reason for this is that we are creating the EngineManager
as a Flash component (see the Adobe documentation here). By extending the UIComponent
class and overriding a number of key functions, we can create a class that can be dropped onto a Flash or Flex application by someone with no knowledge of the underlying code.
One of the functions of the EngineManager
will be to allow any class extending the BaseObject
class (more on that later) to update itself before a frame is rendered to the screen. For this, we need to maintain a collection of BaseObject
s, which is where the baseObjects
, newBaseObjects
, and removedBaseObjects
properties come in. The baseObjects
property holds all the currently active BaseObject
s, while newBaseObjects
and removedBaseObjects
contain BaseObject
s that were just added or removed from the system.
The reason why we don't just add and remove objects from baseObjects
directly is that it is almost always a bad idea to modify a collection while you are lopping over it.
for each (var baseObject:BaseObject in baseObjects)
baseObject.enterFrame(seconds);
See how we loop over the baseObjects
collection and call enterFrame
on each BaseObject
. If we added and removed an instance of a BaseObject
class from baseObjects
directly, and the BaseObject
we called enterFrame
on was to create a new BaseObject
or remove itself from the system during this call, we would find ourselves with an inconsistent collection. You might find the foreach
loop ends up skipping a record, or visits a record twice. In fact, a lot of programming languages expressly forbid this sort of collection modification during a loop by throwing an exception, and even in those languages that don't, it's best avoided.
Once you understand the reason for maintaining separate collections for newly added and removed BaseObject
s, you can explain a lot of the code in the EngineManager
class. The addBaseObject
and removeBaseObject
functions add a BaseObject
to the newBaseObjects
and removedBaseObjects
collections, respectively, while the insertNewBaseObjects
and removeDeletedBaseObjects
functions are called (from the onEnterFrame
function) to synchronise the main baseObjects
collection with the added and removed BaseObject
s.
EngineManager
contains six functions that are used in the lifecycle of a Flash/Flex component. Four of these are detailed in the Adobe documentation, and I'll run through them here.
The measure
function is used to define the minimum and default size of the control. This is pretty straightforward as all we need to do is assign four values to the underlying UIComponent
measuredMinWidth
, measuredMinHeight
, measuredHeight
, and measuredWidth
properties.
The updateDisplayList
function is called to size and position the children of the control. In this case, our only child element is the Away3D engine, specifically the View3D object. All we do here is resize the view property to reflect the changes in the size of the EngineManager
control.
The commitProperties
function is called to allow the control to apply any property changes that have been made. The idea behind this is that properties can and will be changed in any order, but may have to be applied or processed in a specific order. Even though EngineManager
doesn't expose any properties that can be changed, the code here is set to re-initialise the ApplicationManager
(more on that class later), which in effect restarts the application.
The createChildren
function is called when the control is expected to create any of its children. As mentioned before, the only child of the EngineManager
control is the Away3D engine; however, we don't create the engine just yet. The Away3D engine makes numerous references to the stage
property, which is null
until the ADDED_TO_STAGE
event has been triggered. So, we attach the createChildrenEx
function to this event, and create the Away3D engine then. Likewise, we use the REMOVED_FROM_STAGE
to call shutdown, which will clean up the Away3D engine.
The updateDisplayList
, commitProperties
, createChildren
, and measure
functions all have functionality defined by the UIComponent
class. There are two more functions, shutdown
and createChildrenEx
, that also play an important role.
As noted above, the createChildrenEx
function is where the Away3D engine actually starts up. For this simple example, we only need two Away3D classes: Camera3D
and View3D
. The Camera3D
class, which we assign to the cam
property, is the camera through which we look into the Away3D world. The View3D
class, which we assign to the view
property, takes care of rendering the 3D world onto your 2D monitor. The actual code for initialising the Away3D engine isn't more than a few lines. We create a new instance of the Camera3D
class, and then a new instance of the View3D
class. We then define which sort of renderer we want (the basic one in this case), and add the View3D
as a child element of the EngineManager
control.
In addition to initialising the Away3D engine, the createChildrenEx
function also takes create of creating a new ResourceManager
and ApplicationManager
, and attaching the onEnterFrame
function to the ENTER_FRAME
event.
The shutdown
function is used to clean up the Away3D engine. "Cleaning up" essentially means running through the createChildrenEx
in reverse, removing children where they have been added, and setting to null
the properties that had been initialised.
Finally, in the onEnterFrame
function, we manage our render loop. It's here that we determine how much time has passed sine the last frame was rendered, synchronise the baseObjects
collection, call enterFrame
on all of our BaseObject
s, and then finally render the frame to the screen with view.render()
.
BaseObject.as
package
{
internal class BaseObject
{
protected var engineManager:EngineManager = null;
public function BaseObject(engineManager:EngineManager)
{
this.engineManager = engineManager;
}
public function startupBaseObject():void
{
engineManager.addBaseObject(this);
}
public function shutdown():void
{
engineManager.removeBaseObject(this);
}
public function enterFrame(dt:Number):void
{
}
}
}
The sole purpose of the BaseObject
class is to allow an extending class to update itself when enterFrame
is called. The startupBaseObject
and shutdown
functions are called to add the BaseObject
to the EngineManagers
collection and remove it. Then, we have the enterFrame
function, which is empty. The enterFrame
function is expected to be overridden by extending classes.
MeshObject.as
package
{
import away3d.core.base.Object3D;
import away3d.core.utils.Init;
import away3d.loaders.Collada;
import mx.controls.Alert;
import flash.events.Event;
internal class MeshObject extends BaseObject
{
public var model:Object3D = null;
public function MeshObject(engineManager:EngineManager)
{
super(engineManager);
}
public function startupColladaModelObject(collada:XML, init:Init):MeshObject
{
super.startupBaseObject();
if (collada != null)
model = Collada.parse(collada, init);
engineManager.view.scene.addChild(model);
return this;
}
public override function shutdown():void
{
super.shutdown();
if (model != null)
engineManager.view.scene.removeChild(model);
model = null;
}
}
}
The MeshObject
class extends BaseObject
and adds the ability for an object to have a 3D mesh representation on the screen. It includes a function called startupColladaModelObject
which takes a Collada XML document, loads it as a mesh, textures it, and adds the result to the Away3D scene.
RotatingModel.as
package
{
import away3d.core.utils.Init;
public class RotatingMesh extends MeshObject
{
public function RotatingMesh(engineManager:EngineManager)
{
super(engineManager);
}
public function startupRotatingMesh(collada:XML, init:Init):RotatingMesh
{
super.startupColladaModelObject(collada, init);
return this;
}
public override function enterFrame(dt:Number):void
{
this.model.rotationY += 90 * dt;
}
}
}
The RotatingModel
class is an example of how you would use the MeshObject
(and therefore the BaseObject
) class. RotatingModel
extends MeshObject
, and then overrides the enterFrame
function to rotate the model around by a small amount every frame. As you can see, there is very little effort involved to have the RotatingModel
load a model, add it to the scene, and then perform updates every frame: the majority of the work has been taken care of, thanks to the MeshObject
and BaseObject
classes.
ResourceManager.as
package
{
import away3d.materials.BitmapMaterial;
import flash.utils.ByteArray;
public class ResourceManager
{
[Embed (source="../media/fighter1.dae",
mimeType="application/octet-stream")]
public static const Fighter1:Class;
public var Fighter1XML:XML = null;
[Embed (source="../media/sf-02.jpg")]
public static const SF02:Class;
public var SF02_Tex:BitmapMaterial= null;
public function ResourceManager()
{
}
public function loadResources():void
{
Fighter1XML = ConvertToXML(Fighter1);
SF02_Tex = new BitmapMaterial(new SF02().bitmapData);
}
protected function ConvertToXML(data:Class):XML
{
var byteArray:ByteArray = new data() as ByteArray;
return new XML(byteArray.readUTFBytes(byteArray.length))
}
}
}
The ResourceManager
is used as an area to hold any resources used by the application. One of the problems you will face as a developer is the Flash security sand box, where local resources can't be loaded from a SWF located on a web server, and web resources can't be loaded from a local SWF (not without some mucking around anyway). The ResourceManager
makes use of resource embedding through the Embed
tag, which essentially takes a file on your development PC and embeds the data into the final SWF file. This makes it easy to distribute the resulting Flash SWF file because all the data is included in one file, and it overcomes any security issues when loading resources.
As you can see, we embed two files. The fighter1.dae file is the Collada mesh that will be displayed on the screen, and the sf-02.jpg file will be used to texture the mesh.
The loadResources
function is used to load the resources. The ConvertToXML
function takes the embedded fighter1.dae file, which is embedded as a ByteArray
, and converts it back into an XML object.
ApplicationManager.as
package
{
import away3d.core.math.Number3D;
import away3d.core.utils.Init;
public class ApplicationManager extends BaseObject
{
protected var mesh:MeshObject = null;
public function ApplicationManager(engineManager:EngineManager)
{
super(engineManager);
}
public function startupApplicationManager():ApplicationManager
{
super.startupBaseObject();
mesh = new RotatingMesh(engineManager).startupRotatingMesh(
engineManager.MyResourceManager.Fighter1XML,
new Init({material:engineManager.MyResourceManager.SF02_Tex}));
mesh.model.moveTo(100, -100, 2000);
return this;
}
public override function shutdown():void
{
super.shutdown();
}
public override function enterFrame(dt:Number):void
{
}
}
}
The ApplicationManager
class defines the code that makes use of all the other classes we have created to actually produce the desired outcome. In this case, the desired outcome is quite simple: we just want to create an instance of the RotatingMesh
class. Because of the work we put into the previous classes, the only thing ApplicationManager
has to do is create a new instance of the RotatingMesh
, initialise it with a call to startupRotatingMesh
, and reposition it slightly on the screen.
GettingStarted.mxml
="1.0"="utf-8"
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
xmlns:ns1="*"
width="600"
height="400">
<ns1:EngineManager x="0" y="0"
width="100%" height="100%"
id="engineManager"/>
</mx:Application>
Finally, we have the GettingStarted.mxml file. As you can see, we add the EngineManager
like it was just another control like a button or a textbox. Because we have implemented the nessessary functions to make EngineManager
a Flex component, this is all the code that is required.
We have covered a lot of code here for such a simple program, but creating this initial framework does save a lot of time later on, so it is worth the extra initial effort.
Check out the online demo here.
History
- 23 August 2009 - Initial post.