Alternativa 3D Series – Tutorial 3 – Loading Model

by Matthew Casperson 11

A basic function of any 3D engine is to load 3D models. Alternativa has the ability to load 3D models stored in both the 3DS and OBJ formats. In this tutorial we will learn how to load a 3DS model.

In the last tutorial I introduced the ResourceManager class as a location where you can embed images to be used as textures. This has the advantage of keeping all the files and resources for an application in one location. Unfortunately the Alternativa Loader3DS class, which we will use to load and parse a 3DS file, only takes a URL and not an embedded file. This means that the 3DS file will be separate from the main SWF file, and loaded in a separate step.

Drawing a 3D cube and add material

Requirements

Adding to the confusion, the 3DS file is loaded asynchronously, meaning that the actual process of downloading and parsing the 3DS file is done in the background. While this does allow the Flash application to continue executing while the the files are downloaded, in a 3D application that requires the 3D models to be useful, it does mean that we have to introduce some changes to the EngineManager and ResourceManager classes to allow us to only start our application once the required resources have been loaded.

ResourceManager.as

package
{
	import alternativa.engine3d.core.Mesh;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.loaders.Loader3DS;
	import alternativa.engine3d.materials.TextureMaterialPrecision;
	import alternativa.utils.MeshUtils;

	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;

	import mx.core.Application;

	/**
	 * 	ResourceManager is where we embed and create the resources needed by our application
	 */
	public class ResourceManager
	{
		protected static const SERVER_URL:String = "http://alternativatut.sourceforge.net/media/";

		public static var fighter:Mesh = null;
		protected static var fighterLoader:Loader3DS = null;
		protected static var fighterLoaded:Boolean = false;

		public static function get allResourcesLoaded():Boolean
		{
			return fighterLoaded;
		}

		public static function loadResources():void
		{
			fighterLoader = new Loader3DS();
			fighterLoader.smooth = true;
			fighterLoader.precision = TextureMaterialPrecision.HIGH;

			// watch out - case counts on some web servers, but not in windows
			fighterLoader.load(SERVER_URL + "fighter.3DS");
			fighterLoader.addEventListener(Event.COMPLETE, onLoadFighterComplete);
			fighterLoader.addEventListener(IOErrorEvent.IO_ERROR, ioError);
			fighterLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
		}

		protected static function ioError(e:Event):void
		{
			Application.application.lblLoading.text = "Error Loading";
		}

		protected static function securityError(e:Event):void
		{
			Application.application.lblLoading.text = "Error Loading";
		}

		protected static function weldVerticesAndFaces(object:Object3D):void
		{
			if (object != null)
			{
				if (object is Mesh)
				{
					MeshUtils.autoWeldVertices(Mesh(object), 0.01);
					MeshUtils.autoWeldFaces(Mesh(object), 0.01, 0.001);
				}

				// Launching procedure for object's children
				for (var key:* in object.children)
				{
					weldVerticesAndFaces(key);
				}
			}
		}

		protected static function onLoadFighterComplete(e:Event):void
		{
			for (var o:* in fighterLoader.content.children)
			{
				fighter = o;
				weldVerticesAndFaces(fighter);
				break;
			}

			fighterLoaded = true;
		}
	}
}

Lets take a look at the ResourceManager. As you can see the asynchronous process of loading 3D models requires the addition of a few more functions. Like before we declare a public static property for our resource, called fighter here, which the rest of the application will use to access the 3D model information once it is loaded. The fighterLoader property is the 3DSLoader that will do the job of loading and parsing the 3DS file. We also have a boolean called fighterLoaded, which we will use as a signal to the rest of the system that the 3D model has been loaded and is ready to use.

The function allResourcesLoaded simply returns the value of fighterLoaded. If you were to have multiple 3D models being loaded this function would return true only when all the boolean flags that indicate that a model is ready, like fighterLoaded, are true. However since we are only downloading one model this function just returns the value of the one boolean flag.

Next we have the loadResources function. It’s here that we create a new instance of the Loader3DS class, initialize it, request that it load the specified 3DS file, and then attach some functions to the various events that the Loader3DS can trigger. Remember that I said that the loading of a 3DS file was asynchronous? The practical implication of this is that after the call to fighterLoader.load(SERVER_URL + “fighter.3DS”), the system moves straight onto the next line of code. The model isn’t actually ready to use until the onLoadFighterComplete is called – onLoadFighterComplete being attached to the Event.COMPLETE event by the line fighterLoader.addEventListener(Event.COMPLETE, onLoadFighterComplete). However once the request has been made to load the 3DS file the system continues on it’s merry way. Unfortunately, because the sole purpose of this application is to display that one 3D model, we can’t actually start our application until this model is available.

This is where the allResourcesLoaded function, and thus the fighterLoaded flag, become important. Take a look at the onLoadFighterComplete function. Once this is called the model is loaded and is available for us to use. We extract the mesh from the fighterLoader.content.children collection (we know in this case that there is only one mesh, so we get the first one and then break the for loop) and then set fighterLoaded to true. At this point allResourcesLoaded will also return true. Keep this in mind as it is important for the changes we make to the EngineManager.

The functions ioError and securityError will be called if the Loader3DS can not load the specified 3DS file. In that event we simply print an error onto a label so the end user knows something went wrong.

The weldVerticesAndFaces function makes use of two functions available through the Alternativa MeshUtils class: autoWeldVertices and autoWeldFaces. The autoWeldVertices takes a look at the vertices that make up the model and combines any that are sufficiently close enough into a single vertex. Similarly the  autoWeldFaces takes any conjoining faces that are sufficiently coplanar (i.e. faces that are basically flat) and melds them into one polygon. The purpose of this is to minimize the number of individual polygons that the Alternativa engine has to render each frame, which improves performance.

EngineManager.as

package
{
	import alternativa.engine3d.controllers.CameraController;
	import alternativa.engine3d.core.Camera3D;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.core.Scene3D;
	import alternativa.engine3d.display.View;
	import alternativa.utils.FPS;

	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;

	import mx.collections.ArrayCollection;
	import mx.core.Application;
	import mx.core.UIComponent;

	/**
	 * 	The EngineManager holds all of the code related to maintaining the Alternativa 3D engine.
	 */
	public class EngineManager extends UIComponent
	{
		public var scene:Scene3D;
		public var view:View;
		public var camera:Camera3D;
		public var cameraController:CameraController;

		// 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;

		protected var applicationManagerStarted:Boolean = false;

		public function EngineManager()
		{
			super();
			addEventListener(Event.ADDED_TO_STAGE, init);
		}

		public function init(e:Event): void
		{
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;

			// Creating scene
			scene = new Scene3D();
			scene.root = new Object3D();
			scene.splitAnalysis = false;

			// Adding camera and view
			camera = new Camera3D();
			camera.x = 100;
			camera.y = -150;
			camera.z = 100;
			scene.root.addChild(camera);

			view = new View();
			addChild(view);
			view.camera = camera;

			// Connecting camera controller
			cameraController = new CameraController(stage);
			cameraController.camera = camera;
			cameraController.setDefaultBindings();
			cameraController.checkCollisions = true;
			cameraController.collisionRadius = 20;
			cameraController.controlsEnabled = false;

			// FPS display launch
			FPS.init(stage);

			stage.addEventListener(Event.RESIZE, onResize);
			stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
			onResize(null);

			// set the initial frame time
			lastFrame = new Date();

			// load the resources
			ResourceManager.loadResources();
		}

		private function onResize(e:Event):void
		{
			view.width = stage.stageWidth;
			view.height = stage.stageHeight;
			Application.application.width = stage.stageWidth;
			Application.application.height = stage.stageHeight;
		}

		protected function onEnterFrame(event:Event):void
		{
			if (ResourceManager.allResourcesLoaded)
			{
				if (!applicationManagerStarted)
				{
					applicationManagerStarted = true;
					// start the application
					ApplicationManager.Instance.startupApplicationManager();
					// remove the loading lable
					Application.application.lblLoading.visible = false;
				}

				// 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);

		    	// User input processing
				cameraController.processInput();
				// Scene calculating
				scene.calculate();
			}
		}

		public function addBaseObject(baseObject:BaseObject):void
		{
			newBaseObjects.addItem(baseObject);
		}

		public function removeBaseObject(baseObject:BaseObject):void
		{
			removedBaseObjects.addItem(baseObject);
		}

		protected 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();
		}
	}
}

In order to accommodate the asynchronous model loading process we have to make 2 changes to the EngineManager class. The first is in the init function. The last line of the init function used to start the ApplicationManager class. However now this line has been replaced with a call to the ResourceManager loadResources function. By calling this function we are asking the ResourceManager to start downloading the external 3DS file.

The seconds change is to the onEnterFrame function. You will notice now that the entire contents of this function will only run once ResourceManager.allResourcesLoaded returns true. The effect of this is that the render loop doesn’t start until the resources are loaded. Once the resources are loaded we then make one call to ApplicationManager.Instance.startupApplicationManager, allowing the ApplicationManager to startup the application. The applicationManagerStarted flag enables us to make sure that this function is only called once. We also remove a label from the main GUI that says “Loading”.

ApplicationManager.as

package
{
	import alternativa.types.Point3D;

	import mx.core.Application;

	/**
	 * 	The ApplicationManager holds all program related logic.
	 */
	public class ApplicationManager extends BaseObject
	{
		protected static var instance:ApplicationManager = null;
		protected static const CAMERA_MOVEMENT_BOX:Number = 100;
		protected static const CAMERA_MOVEMENT_BOX_HALF:Number = CAMERA_MOVEMENT_BOX / 2;
		protected static const CAMERA_SPEED:Number = 25;
		protected var mesh:MeshObject = null;
		protected var target:Point3D= null; 

		public static function get Instance():ApplicationManager
		{
			if (instance == null)
				instance = new ApplicationManager();
			return instance;
		}

		public function ApplicationManager()
		{
			super();
		}

		public function startupApplicationManager():ApplicationManager
		{
			mesh = new MeshObject().startupModelObject(ResourceManager.fighter);
			super.startupBaseObject();
			target = generateRandomTarget();
			return this;
		}

		public override function enterFrame(dt:Number):void
		{
			var direction:Point3D = Point3D.difference(target, Application.application.engineManager.camera.coords);
			var distToMoveThisFrame:Number = dt * CAMERA_SPEED;

			// don't overshoot the target point
			if (Math.pow(distToMoveThisFrame, 2) >= direction.lengthSqr)
			{
				distToMoveThisFrame = direction.length;
				target = generateRandomTarget();
			}

			direction.normalize();
			direction.multiply(dt * CAMERA_SPEED);
			Application.application.engineManager.camera.coords = Point3D.sum(Application.application.engineManager.camera.coords, direction);
			Application.application.engineManager.cameraController.lookAt(mesh.model.coords);
		}

		protected function generateRandomTarget():Point3D
		{
			// get a random target
			return Point3D.random(-CAMERA_MOVEMENT_BOX, CAMERA_MOVEMENT_BOX, -CAMERA_MOVEMENT_BOX, CAMERA_MOVEMENT_BOX, -CAMERA_MOVEMENT_BOX, CAMERA_MOVEMENT_BOX);
		}
	}
}

With the resources loaded and the ApplicationManager started, it’s business as usual. The ApplicationManager is free to make use of the resources held by the ResourceManager without any consideration for the asynchronous loading process.

It’s worth noting that we have made the ApplicationManager extend the BaseObject class. In the first tutorial I mentioned that occasionally an object without any accompanying on screen representation would need to update itself every frame, which is why the we have the separate BaseObject and MeshObject classes. In this case the ApplicationManager updates every frame, moving the camera around to random points around the 3D model while always looking at it with the CameraController lookAt function.

While loading resources asynchronously can have it’s benefits, in a 3D application you’ll almost always want to have that data available before you start rendering a scene. With the changes we have made here to the EngineManager and ResourceManager you can load and use the resources from other classes like ApplicationManager as if they were loaded synchronously.

Check out the demo here, and download the source here.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>