Alternativa 3D Series – Tutorial 7 – Sprite3D
by 29 April, 2009 12:30 pm2
A common concept amongst 3D engines is the billboard. A billboard is usually a rectangle that is always orientated to face the camera, presenting the viewer with a face that that is always “front on” regardless of the cameras orientation. More often than not appropriately textured billboards are used in great numbers to represent effects like smoke, rain, snow or fire. This is because billboards require very little processing power: a rectangle is just two triangles, and most modern systems measure triangle throughput in the millions per second. But sometimes you just want to show a 2D object in a 3D world – maybe an icon that hovers over an object or perhaps some text that would be awkward or unpractical to render as a 3D object.
Requirements
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.
Sprite3D
The Alternativa Sprite3D class performs the function of a billboard described above. It is a 2D rectangle that is always orientated to face the camera: you never see a Sprite3D side on. You can assign any material that extends the SpriteMaterial class to a Sprite3D, which with Alternativa 5.5 pretty much means the SpriteMaterial class. Even thought the Sprite3D itself is a rectangle, you can apply a texture with some transparency to display any shape on a Sprite3D. The demo application demonstrates this by displaying a number of circular textures (smiley faces) on the Sprite3D instances.
Another subtle, but important, fact about the Sprite3D class is that it’s geometry is not used to calculate the BSP tree. In simple terms a BSP tree holds all the scene geometry in a format that clearly identifies a polygon as being either in front of or behind any other polygon. It’s an efficient way to sort a 3D scene, and it allows the Alternativa 3D engine to properly render intersecting polygons. However, because the BSP tree is recalculated every time a new polygon is added, removed, moved or rotated you do have to be mindful of the performance impact that modifying 3D objects in the scene can have. Because the Sprite3D is not used in the calculation on the BSP tree it does not incur the same performance cost.
In the demo application each Sprite3D is contained in an instance of the SpriteBounce class.
SpriteBounce.as
package { import alternativa.engine3d.core.Sprite3D; import alternativa.engine3d.materials.SpriteMaterial; import alternativa.types.Point3D; import alternativa.types.Texture; import mx.core.Application; public class BounceSprite extends MeshObject { protected static const BOX_SIZE:Number = 80; protected static const SPEED:Number = 10; protected static const SCALE:Number = 0.2; protected var direction:Point3D = null; public function BounceSprite() { super(); } public function startupBounceSprite(texture:Texture):BounceSprite { var sprite:Sprite3D = new Sprite3D(); // each Sprite3D needs its own SpriteMaterial, so create a new one with the supplied texture sprite.material = new SpriteMaterial(texture, Application.application.sliderAlpha.value, true); // set a random initial position sprite.coords = Point3D.random(-BOX_SIZE, BOX_SIZE, -BOX_SIZE, BOX_SIZE, -BOX_SIZE, BOX_SIZE); // set the initial scale sprite.scaleX = sprite.scaleY = sprite.scaleZ = Application.application.sliderScale.value; // create a random initial direction direction = Point3D.random(-1, 1, -1, 1, -1, 1); direction.normalize(); // let the MeshObject deal with adding the Sprite3D to the scene super.startupModelObject(sprite); return this; } public override function enterFrame(dt:Number):void { super.enterFrame(dt); // find the new point to move to var newPoint:Point3D = direction.clone(); newPoint.multiply(dt * SPEED); newPoint.add(model.coords); // keep the point inside a box whose size is defined by BOX_SIZE // if the point is outside the box, reverse the direction if (newPoint.x > BOX_SIZE) { newPoint.x = BOX_SIZE; direction.x = -1; } else if (newPoint.x < -BOX_SIZE) { newPoint.x = -BOX_SIZE; direction.x = 1; } if (newPoint.y > BOX_SIZE) { newPoint.y = BOX_SIZE; direction.y = -1; } else if (newPoint.y < -BOX_SIZE) { newPoint.y = -BOX_SIZE; direction.y = 1; } if (newPoint.z > BOX_SIZE) { newPoint.z = BOX_SIZE; direction.z = -1; } else if (newPoint.z < -BOX_SIZE) { newPoint.z = -BOX_SIZE; direction.z = 1; } // move the Sprite3D model.coords = newPoint; // update the alpha Sprite3D(model).material.alpha = Application.application.sliderAlpha.value; // update the scale Sprite3D(model).scaleX = Sprite3D(model).scaleY = Sprite3D(model).scaleZ = Application.application.sliderScale.value; } } }
The SpriteBounce class extends the MeshObject class. We create a new function called startupBounceSprite, which holds the code that creates and initialises the Sprite3D. First we create a new instance of the Sprite3D class. The Sprite3D constructor optionally accepts a name parameter, but this has no relevance in our demo so we leave it out. Next we assign a texture to the Sprite3D through the material property. Each Sprite3D needs it’s own instance of a SpriteMaterial class. For an example of how this works take a look at the following code.
var s1:Sprite3D = new Sprite3D(); var s2:Sprite3D = new Sprite3D(); var tex:SpriteTextureMaterial = new SpriteTextureMaterial(someTexture); s1.material = tex; s2.material = tex;
In the example above, s1.material would be null, while s2.material would be equal to tex. The two instances of Sprite3D can not share a SpriteTextureMaterial. To avoid this you would do the following.
var s1:Sprite3D = new Sprite3D(); var s2:Sprite3D = new Sprite3D(); var tex1:SpriteTextureMaterial = new SpriteTextureMaterial(someTexture); var tex2:SpriteTextureMaterial = new SpriteTextureMaterial(someTexture); s1.material = tex1; s2.material = tex2;
Now each Sprite3D has it’s own texture, and will be displayed properly in the scene.
Once it is assigned a texture we position the Sprite3D in the scene via the coords property. We then set the scale of the Sprite3D.
The scale property of the Sprite3D class is the only way to set it’s size. In the demo you can modify the scale using the slider on the left hand side of the screen. The slider only lets you select a value between 0 and 1, but you could set a scale of more than 1 if you wanted to.
With the Sprite3D created and initialised we create a random normalised vector to serve as the direction that the Sprite3D will initially move in. We then make a call to the startupModelObject from the base MeshObject class, supplying the Sprite3D we just created for the startupModelObject function to add to the scene.
The enterFrame function contains the code to move the Sprite3D and and contain it inside an area defined by the static BOX_SIZE property. If the Sprite3D moves outside of this box the direction it is moving in (as defined by the direction property) is reversed. It also updates the scale and transparency of the Sprite3D against the GUI sliders specified in the MXML file.
ApplicationManager.as
package { import alternativa.engine3d.core.Mesh; import alternativa.engine3d.primitives.Plane; import alternativa.types.Point3D; import flash.display.DisplayObject; import mx.core.Application; /** * The ApplicationManager holds all program related logic. */ public class ApplicationManager extends BaseObject { protected static const SPRITE3D_COUNT:int = 20; protected static const CAMERA_MOVEMENT_BOX:Number = 300; protected static const CAMERA_MOVEMENT_BOX_HALF:Number = CAMERA_MOVEMENT_BOX / 2; protected static const CAMERA_SPEED:Number = 100; protected var target:Point3D= null; protected static var instance:ApplicationManager = null; protected var verticalWall:MeshObject = null; protected var horizontalWall:MeshObject = null; public var rotateSpeed:Number = 90; /** * 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 rotating planes updateWalls(); // get a random target for the camera movement target = generateRandomTarget(); // create the BounceSprite objects, assigning a random texture for (var i:int = 0; i < SPRITE3D_COUNT; ++i) { var rand:int = int(Math.random() * 4); switch (rand) { case 0: new BounceSprite().startupBounceSprite(ResourceManager.BlueSmileyTex); break; case 1: new BounceSprite().startupBounceSprite(ResourceManager.RedSmileyTex); break; case 2: new BounceSprite().startupBounceSprite(ResourceManager.GreenSmileyTex); break; case 3: new BounceSprite().startupBounceSprite(ResourceManager.YellowSmileyTex); break; } } return this; } public function updateWalls():void { // if the horizontalWall has not been created, and it is selected, create it if (Application.application.chkGreenWall.selected && horizontalWall == null) { horizontalWall = new MeshObject().startupModelObject(new Plane(175, 175, 4, 4)); Mesh(horizontalWall.model).cloneMaterialToAllSurfaces(ResourceManager.GreenTex); horizontalWall.model.rotationX = Math.PI/2.0; } // if the horizontalWall has been created, and it is not selected, destroy it else if (!Application.application.chkGreenWall.selected && horizontalWall != null) { horizontalWall.shutdown(); horizontalWall = null; } // if the verticalWall has not been created, and it is selected, create it if (Application.application.chkBlueWall.selected && verticalWall == null) { verticalWall = new MeshObject().startupModelObject(new Plane(175, 175, 4, 4)); Mesh(verticalWall.model).cloneMaterialToAllSurfaces(ResourceManager.BlueTex); } // if the verticalWall has been created, and it is not selected, destroy it else if (!Application.application.chkBlueWall.selected && verticalWall != null) { verticalWall.shutdown(); verticalWall = null; } } public override function enterFrame(dt:Number):void { // rotate the planes if (horizontalWall != null) horizontalWall.model.rotationX += rotateSpeed * Math.PI / 180 * dt; if (verticalWall != null) verticalWall.model.rotationY += rotateSpeed * Math.PI / 180 * dt; // move the camera around to a random position, if the option is selected if (Application.application.chkCameraMoves.selected) { 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); // reposition the camera Application.application.engineManager.camera.coords = Point3D.sum(Application.application.engineManager.camera.coords, direction); // look at the centre of the stage Application.application.engineManager.cameraController.lookAt(new Point3D()); } } 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); } } }
We use the ApplicationManager to create several instances of the BounceSprite class, as well as two rotating planes which are used to demonstrate that the Sprite3D‘s are properly depth sorted in the scene. Take a look at the startupApplicationManager function. Here we create the two rotating planes by calling updateWalls, and then get a random point in space for the camera to move to if the Move Camera checkbox has been selected. Next we enter a loop to create the SpriteBounce objects. The Texture that is assigned to the startupBounceSprite function is randomly selected from four Texture‘s created in the ResourceManager class. While each Sprite3D needs it’s own SpriteMaterial assigned to the material property, those SpriteMaterial‘s can share the underlying Texture class. We take advantage of this to save some memory by not creating a new Texture instance for each SpriteMaterial instance.
Take some time now to run the demo application. Pay close attention to the Sprite3D objects as they pass through the rotating planes (you can slow the speed that the planes rotate at using the GUI controls). Do you see how the Sprite3D‘s are either entirely in front or entirely behind the translucent plane? This is because the Sprite3D is only depth sorted in the scene, rather than being part of the BSP tree (which allows other 3D objects to intersect and still be rendered properly). Because the Sprite3D objects are all parallel to each other (they all face the camera) one Sprite3D can not intersect another. You will occasionally see an instance where a Sprite3D should intersect another 3D object (in this case the rotating planes), but is rendered as being completely in front of or behind that intersecting object. However these situations should be rare, and the performance boost from not having to recalculate the BSP tree is worth this minor inconvenience.
Because of their simplicity and the fact that they don’t participate in the construction of the BSP tree, Sprite3D‘s are a very useful way of displaying flat objects in a 3D scene while still maintaining a high frame rate.