Alternativa 3D Series – Tutorial 8 – Imposters

by Matthew Casperson 2

In the last article we saw what a Sprite3D was, and the performance benefits they have over regular 3D meshes. In this article we will see how Sprite3D‘s can be used to selectively replace 3D meshes, allowing a large number of objects to appear on the screen while still keeping frame rates up.

Requirements

View DemoDownload Source Files

Pre-Requesites

You should have read the first tutorial in this series, as it explains a lot of the underlying code that we build on for this tutorial.

Imposters

Imposters are 3D objects that are rendered using a Sprite3D when they are far enough away from the camera. The idea behind this is that distant objects are too small on the screen to require a full 3D model to represent them. By rendering them as Sprite3D‘s we can reduce the polygon count and therefor increase the frame rate, the number of objects on the screen, or both.

Take a moment to run the demo application. The demo creates a number of imposters to represent the trees in a forest. As you walk around (using the W,S,A,D keys) you’ll see that as you get close to a tree it changes from a flat Sprite3D to a 3D model. In this way any object close to the camera appears as a full 3D object, with as much detail as is required. However the objects in the background are still being represented by Sprite3D‘s, keeping the triangle count down and frame rates up.
Now see what happens when you disable Imposters by removing the tick in the Imposters Enabled checkbox. This forces each tree to be displayed as a 3D model regardless of how far away from the camera it is. On my system the frame rate is halved while the camera is moving, without the distant trees gaining any noticeable increase in visual quality.

Now that we have seen how effective Imposters can be, lets look at the code.

Imposter.as

package
{
	import alternativa.engine3d.core.Mesh;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.core.Sprite3D;
	import alternativa.engine3d.materials.SpriteMaterial;
	import alternativa.types.Point3D;
	import alternativa.types.Texture;

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

	public class Imposter extends BaseObject
	{
	public static var imposterDistance:Number = 750;
	public static var impostersEnabled:Boolean = true;
	public var imposterSprite:Sprite3D = null;
	public var modelElements:ArrayCollection = null;
	public var sourceModelElements:ArrayCollection = null;
	public var showingImposter:Boolean = true;

	public function Imposter()
	{
	    super();
	}

	public function startupImposter(texture:Texture, mesh:ArrayCollection, position:Point3D):Imposter
	{
	    super.startupBaseObject();

	    // keep a reference to the meshes that make up the model
	    // we will only copy and place these meshes if the
	    // camera gets close enough to the Imposter
	    sourceModelElements = mesh;

	    // create the Sprite3D
	    imposterSprite = new Sprite3D();
	    imposterSprite.material = new SpriteMaterial(texture);
	    Application.application.engineManager.scene.root.addChild(imposterSprite);

	    setPosition(position);

	    return this;
	}

	public override function shutdown():void
	{
	    super.shutdown();

	    // remove the Sprite3D from the scene
	    if (showingImposter)
	        Application.application.engineManager.scene.root.removeChild(imposterSprite);
	    // remove the mesh from the scene
	    else
	    {
	        for each (var removeMesh:Mesh in modelElements)
	            Application.application.engineManager.scene.root.removeChild(removeMesh);
	    }

	    if (modelElements != null)
	        modelElements.removeAll();
	}

	public override function enterFrame(dt:Number):void
	{
	    super.enterFrame(dt);

	    // update the mesh coords to match the Sprite3D
	    for each (var mesh:Mesh in modelElements)
	        mesh.coords = imposterSprite.coords;

	    // find out how far away from the camera the object is
	    var distSquared:Number = Point3D.difference(Application.application.engineManager.camera.coords, imposterSprite.coords).lengthSqr;

	    // if we are far enough away, and not already showing the imposter, switch the 3D model for the Sprite3D
	    if ((distSquared >= Math.pow(imposterDistance, 2) && !showingImposter) && impostersEnabled)
	    {
	        // remove the meshes from the scene
	        for each (var removeMesh:Mesh in modelElements)
	            Application.application.engineManager.scene.root.removeChild(removeMesh);

	        // add the Sprite3D to the scene
	        Application.application.engineManager.scene.root.addChild(imposterSprite);

	        showingImposter = true;
	    }
	    // if we close enough, and not showing the imposter, switch the Sprite3D for the 3D model
	    else if ((distSquared < Math.pow(imposterDistance, 2) && showingImposter) || (showingImposter && !impostersEnabled))
	    {
	        // remove the Sprite3D from the scene
	        Application.application.engineManager.scene.root.removeChild(imposterSprite);

	        // if the 3D model has not been seen before clone each
	        // mesh into our local collection
	        if (modelElements == null)
	        {
	            modelElements = new ArrayCollection();
	            for each (var model:Mesh in sourceModelElements)
	            {
	                var myCopy:Object3D = model.clone();
	                myCopy.coords = imposterSprite.coords;
    	            modelElements.addItem(myCopy);
    	            }
        	}

        	// add the meshes to the scene
	        for each (var addMesh:Mesh in modelElements)
                	Application.application.engineManager.scene.root.addChild(addMesh);

        	showingImposter = false;
	    }
	}

	public function setPosition(position:Point3D):void
	{
        	imposterSprite.coords = position;
	}
    }
}

Like the MeshObject class, Imposters extends the BaseObject class. This gives us the ability to easily manage the class through the EngineManager, and to update the object every frame. We have two static properties. The imposterDistance property defines how close the Imposter has to be to the camera before the full 3D model is shown. The impostersEnabled property allows the switching between the Sprite3D and 3D model to be enabled or disabled. If impostersEnabled is set to false the Imposters will always render themselves using the 3D model.

In addition to the static properties, there are four more local properties in the Imposter class.

The imposterSprite property represents the Sprite3D that will be displayed when the Imposter is far enough away from the camera.

The showingImposter property is true when the Imposter is displaying the Sprite3D, and false when it is displaying the 3D model.

The sourceModelElements property references a collection of Mesh’s that make up the 3D model. We keep a reference because each of these Mesh’s will have to be cloned before they can be displayed as a separate, individual model, and there is a good chance that some Imposters will never be close enough to the camera to need to be displayed as a 3D model. When the Imposter does need to display itself as a 3D model for the first time, the Mesh’s referenced by the sourceModelElements collection will be cloned and placed in the modelElements property. This happens in the enterFrame function. We can save some memory by only cloning the Mesh’s when they first need to be displayed.

The Imposter class has four functions (aside from the constructor, which does nothing).

The startupImposter takes a Texture, which is applied to the Sprite3D, an ArrayCollection of Mesh’s, which will be used to display the 3D model, and a Point3D, which is used to position the Imposter in the scene. The function itself creates and initialises a new Sprite3D instance, adds it to the scene and then positions it. The collection of Mesh’s is referenced by the sourceModelElements property, ready to be cloned in the enterFrame function when necessary. While the local collection of Mesh’s is initialised when the 3D model first needs to be displayed, every Imposter has it’s Sprite3D initialised by the startupImposter function.

The shutdown function is overridden from BaseObject, and removes the object that is current displayed in the scene (either the collection of Mesh’s or the Sprite3D). It then clears out the modelElements collection, which allows the garbage collector to reclaim that memory.

The enterFrame function is where the switching from Sprite3D to 3D model and back again is done. First the Mesh’s positions are updated to reflect the current position of the Sprite3D (which may have been updated with a call to setPosition). Then the distance of the Imposter from the camera is calculated. If it is far enough away from the camera the Sprite3D is added to the scene and the Mesh’s are removed. If it is close enough to the camera the Sprite3D is removed from the scene. At this point if the modelElements property is null we can safely assume that the Mesh’s referenced by the sourceModelElements have never been cloned for the local use of this Imposter. So we loop through each Mesh in the sourceModelElements collection, clone it, position it, and add it to the modelElements collection. The local copy of the Mesh’s, held in the modelElements collection, are then added to the scene.

Finally the setPosition function sets the position of the Sprite3D. This position will be copied across to the Mesh’s during the next call to enterFrame.

ApplicationManager.as

package
{
	import alternativa.engine3d.controllers.WalkController;
	import alternativa.types.Point3D;

	import mx.core.Application;

	/**
	* The ApplicationManager holds all program related logic.
	*/
	public class ApplicationManager extends BaseObject
	{
	    protected static const NUM_TREES:int = 50;
       	    protected static const FOREST_SIZE:Number = 3000;

	    /// The singelton instance of this class is referenced here
	    protected static var instance:ApplicationManager = null;
	    protected var walk:WalkController = null;

	    /**
	    * returns the singelton instance of the ApplicationManager
	    */
	    public static function get Instance():ApplicationManager
	    {
	        if (instance == null)
	            instance = new ApplicationManager();
	        return instance;
	    }

	    public function ApplicationManager()
	    {
	        super();
	    }

	    /**
	    * Initialise the ApplicationManager
	    */
	    public function startupApplicationManager():ApplicationManager
	    {
	        super.startupBaseObject();

	        // create the walk controller
	        walk = new WalkController(Application.application.stage);
	        walk.object = Application.application.engineManager.camera;
	        walk.setDefaultBindings();
	        walk.checkCollisions = true;
	        walk.flyMode = false;
	        walk.checkCollisions = false;
	        walk.gravity = 0;
	        walk.lookAt(new Point3D(-100, 0, 0));
	        walk.speed = 300;

	        // create the initial group of trees
	        createTrees();

	        return this;
	    }

	    public function createTrees():void
	    {
	        // remove all the existing trees
	        Application.application.engineManager.shutdownAllOfType(Imposter);

	        // create a new forest
	        for (var i:int = 0; i < Application.application.sliderTreeCount.value; ++i)
	        {
	            var position:Point3D = Point3D.random(-FOREST_SIZE, FOREST_SIZE, -FOREST_SIZE, FOREST_SIZE, 0, 0);
	            var imposter:Imposter = new Imposter().startupImposter(ResourceManager.TreeImposterTex, ResourceManager.treeElements, position);
	        }
	    }

	    public override function enterFrame(dt:Number):void
	    {
	        walk.processInput();
	    }
    }
}

The ApplicationManager class is quite simple in this example. The startupApplicationManager function creates a walk controller so the user can move around in the forest (see this tutorial for more information on using the camera controller classes), and the createTrees function simply creates a number of Imposters with the appropriate Texture and Mesh collection to allow them to represent trees (see this tutorial to learn how to load 3DS models).

As you can see by using Imposters to represnet 3D models with Sprite3D‘s at a distance we can easily increase the number of objects that can be displayed on the screen will still retaining a fully 3D look and a good frame rate.

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>