Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Flash

Getting Started with Away3D

5.00/5 (1 vote)
23 Aug 2009CPOL9 min read 26.9K   316  
See how to create a simple framework for your 3D Flash applications using Away3D.

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;
        
        // Away3D view
        internal var view:View3D = null;
        // Away3D camera
        internal var cam:Camera3D = null;        
        // a collection of the BaseObjects 
        protected var baseObjects:ArrayCollection = new ArrayCollection();
        // a collection where new BaseObjects are placed, to avoid adding items 
        // to baseObjects while in the baseObjects collection while it is in a loop
        protected var newBaseObjects:ArrayCollection = new ArrayCollection();
        // a collection where removed BaseObjects are placed, to avoid removing items 
        // to baseObjects while in the baseObjects collection while it is in a loop
        protected var removedBaseObjects:ArrayCollection = new ArrayCollection();
        // the last frame time 
        protected var lastFrame:Date;
        // the application manager
        protected var applicationManager:ApplicationManager = null;
        // the resource manager
        protected var myResourceManager:ResourceManager = null;
        // true when we have added the Away3D controls
        protected var addedToStage:Boolean = false;
        // true if some properties have been modifed
        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();
            
            // set a bunch of predefined sizes
            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)
            {
                // resize the viewport to match the new settings
                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);
                
                // set the initial frame time
                lastFrame = new Date();        
                
                // load any resources
                myResourceManager = new ResourceManager();
                myResourceManager.loadResources();
                
                // start the application manager
                applicationManager = 
                  new ApplicationManager(this).startupApplicationManager();
                
                addedToStage = true;
            }
        }
                
        protected function onEnterFrame(event:Event):void 
        {
            // Calculate the time since the last frame
            var thisFrame:Date = new Date();
            var seconds:Number = (thisFrame.getTime() - 
                                     lastFrame.getTime())/1000.0;
            lastFrame = thisFrame;
            
            // sync the baseObjects collection with
            // any BaseObjects created or removed during the 
            // render loop
            removeDeletedBaseObjects();
            insertNewBaseObjects();
            
            // allow each BaseObject to update itself
            for each (var baseObject:BaseObject in baseObjects)
                baseObject.enterFrame(seconds);
            
            // render the scene
            view.render();
        }
        
        public function addBaseObject(baseObject:BaseObject):void
        {
            newBaseObjects.addItem(baseObject);
        }
        
        public function removeBaseObject(baseObject:BaseObject):void
        {
            removedBaseObjects.addItem(baseObject);
        }
        
        public function shutdownAll():void
        {
            // don't dispose objects twice
            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 BaseObjects, which is where the baseObjects, newBaseObjects, and removedBaseObjects properties come in. The baseObjects property holds all the currently active BaseObjects, while newBaseObjects and removedBaseObjects contain BaseObjects 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.

// allow each BaseObject to update itself
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 BaseObjects, 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 BaseObjects.

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 BaseObjects, 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
XML
<?xml version="1.0" encoding="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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)