Saturday, July 25, 2009

Away3D Programming Tutorials

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.
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
If you click the link above you can see that there is actually 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. Take a look at line 158. 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 for each 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 execption, and even in those languages that don't its 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 6 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 straight forward as all we need to do is assign 4 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 define 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 it actually started 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 the have been added, and setting to null 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
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
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
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
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
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
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, and download the source code here.