-->

Not to be missed! Check out:

Currently weighing in at under 25K, Away3D Lite can be used in projects with the most stringent bandwidth restrictions. No problem for use in banners, widgets, thumbnails… anything where filesize is a priority. But the biggest single feature offered by the new engine is it’s speed. Current tests clock framerates up to 4 times faster than the standard Away3D library. And with more frames-per-second comes the potential for more polygons, more accessible content on slower machines, and more processing power left for other areas of a Flash application.

Away3D Lite is not meant as a replacement for the standard Away3D libraries. Because of it’s use of the native 3D features in the Flash 10 Player, Away3D Lite is Flash 10 only, so the development of Away3D 2.5 & 3.5 will continue as usual. These engines are more of a workhorse for many different purposes and with many different features, while Away3D Lite offers the choice to be fast and small at the sacrifice of features. This is not to say that you can’t do anything with Away3D Lite! The list of main supported features in 1.0 includes:

  • 3DS, MD2, Collada & Metasequoia loaders
  • Bones animation
  • Viewport clipping
  • 3d mouse events
  • All standard primitive types
  • All standard camera types
  • All standard material types
  • Template classes for quick and easy setup

Plus, you will already know how to use Away3D Lite if you’ve used Away3D! Everything is where you’d expect, and there have only been very minor changes made to some property names, which in time will most likely percolate back through the various engines.

Fantastic work from the Away3D team - congratulations to everyone involved!

Read more, and be sure to check out the demos showing how fast it really is (believe me, if its speedy 3D rendering you’re after, you won’t do better than this!), at away3d.com

Great news! Away3D 2.3 has been released, featuring amongst others:

  • Frustum and nearfield culling, as well as object culling for standard clipping
  • Normalmaps tools to help with their generation and/or manipulation
  • BezierPatch for geometry generation allowing the creation of bezier surfaces programatically
  • Improved Awaybuilder with a custom parser for Maya scenes which can dramatically speed up 3D workflow
  • Interchangeable camera lenses to allow for different types of projection
  • All values allowed for stage properties “align” and “scaleMode”
  • Improved memory management
  • Improved extrusion tools
  • ROLL_OVER/ROLL_OUT events added to MouseEvent3D
  • Billboard mesh objects for fast 2d sprites

If you haven’t checked it out already, then now is the time to do so!

Congrats to the whole Away3D team!

-->
Monday, December 15th, 2008

First steps in Away3D : Part 7 - Movie and video materials

Having looked at the different types of texture mapped and coloured materials with different lighting aspects, I want to discuss briefly a couple of more interactive and dynamic materials. These don’t necessarily add realism to a scene but can be very useful for 3D website development to provide a richer user experience. To do this we use the MovieMaterial and VideoMaterial. If you’re interested here is an equivalent tutorial for movie materials in Papervision3D.

Previous articles summary :

This article will be looking at two materials available in Away3D: the MovieMaterial for rendering Flash movies onto a surface and VideoMaterial to show Flash video streams on an object (using .flv files). As well as being able to show another Flash animation, mouse events are mapped to the MovieMaterial allowing it to be interactive, even in a 3D environment.

This article is taking advantage of the very latest types of materials available with Away3D: to be able to compile the examples and develop your own you may need to use the repository (SVN) version of Away3D. If you don’t have away3d.materials.VideoMaterial available in your Away3D source then this is the case. Here’s an article on downloading and installing Away3D from SNV if you need help.

To illustrate these different materials we’re going to create three simple planes rotated and translated to form three sides of a cube. Two of these will be rendered using MovieMaterials and the third with a Flash video stream using a VideoMaterial. One of the MovieMaterial planes will be interactive and I’ll show how a external Flash movie can be embedded in the compiled animation.

So let’s look at the code. For this example we will actually have three ActionScript classes: the main Away3D class and two additional classes to be used for the individual MovieMaterial cube faces. The latter two will be shown at the end of the article just for completeness.

package {   import away3d.cameras.HoverCamera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.base.Object3D;   import away3d.core.render.Renderer;   import away3d.events.MouseEvent3D;   import away3d.materials.MovieMaterial;   import away3d.materials.VideoMaterial;   import away3d.primitives.Plane;     import caurina.transitions.Tweener;     import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.filters.BlurFilter;     [SWF(backgroundColor="#222222")]     public class Example007 extends Sprite {     private var videoURL:String = "http://www.tartiflop.com/away3d/FirstSteps/AmIWrong.flv";     [Embed(source="/../assets/DrawTool.swf")]     private var DrawToolEmbedded:Class;     private var scene:Scene3D;     private var camera:HoverCamera3D;     private var view:View3D;     private var planeGroup:ObjectContainer3D;         private var doRotation:Boolean = true;         public function Example007() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Initialise Away3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     /**     * Initialise all 3D components.     */     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new HoverCamera3D({zoom:25, focus:30, distance:200});       camera.yfactor = 1;             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the view to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             // ensure that the z-order is calculated correctly       view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }     /**     * Create the objects of the scene     */     private function createScene():void {       // Video material using a flash streaming video URL       var frontMaterial:VideoMaterial = new VideoMaterial({file:videoURL});             // Movie material from an embedded flash animation       var topMaterial:MovieMaterial = new MovieMaterial(new DrawToolEmbedded(), {lockW:320, lockH:240, interactive:true, smooth:true, precision:5});             // Movie material from another class       var leftMaterial:MovieMaterial = new MovieMaterial(new Pong(), {smooth:true, precision:5});       // Create three planes with different material, blurred by default       // and position them them to create tree sides of a cube       var topPlane:Plane = new Plane({material:topMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       topPlane.rotationY = -180;       topPlane.y = 50;       topPlane.filters.push(new BlurFilter(8, 8));             var leftPlane:Plane = new Plane({material:leftMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       leftPlane.rotationZ = -90;       leftPlane.rotationY = -90;       leftPlane.x = 50;       leftPlane.filters.push(new BlurFilter(8, 8));             var frontPlane:Plane = new Plane({material:frontMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       frontPlane.rotationX = -90;       frontPlane.rotationZ = 180;       frontPlane.z = 50;       frontPlane.filters.push(new BlurFilter(8, 8));             // Create an object container to group the sides of the cube       planeGroup = new ObjectContainer3D();       scene.addChild(planeGroup);       planeGroup.addChild(topPlane);       planeGroup.addChild(leftPlane);       planeGroup.addChild(frontPlane);                   // Add mouse listeners to each plane for mouse down, over and out events       topPlane.addOnMouseDown(onMouseClickOnObject);       leftPlane.addOnMouseDown(onMouseClickOnObject);       frontPlane.addOnMouseDown(onMouseClickOnObject);       topPlane.addOnMouseOver(onMouseOverObject);       leftPlane.addOnMouseOver(onMouseOverObject);       frontPlane.addOnMouseOver(onMouseOverObject);       topPlane.addOnMouseOut(onMouseLeavesObject);       leftPlane.addOnMouseOut(onMouseLeavesObject);       frontPlane.addOnMouseOut(onMouseLeavesObject);           }         /**     * Frame-enter event handler     */     private function loop(event:Event):void {             // update camera position       updateCamera();       camera.hover();             // Render the 3D scene       view.render();     }     /**     * Update the camera position from mouse positions     */     private function updateCamera():void {       if (doRotation) {         camera.targetpanangle = (stage.stageWidth - stage.mouseX) / stage.stageWidth * 90;         camera.targettiltangle = (stage.stageHeight - stage.mouseY) / stage.stageHeight * 70       }     }         /**     * Event listener for mouse click on plane. Makes the camera look     * directly at the plane and move closer to it.     */     private function onMouseClickOnObject(event:MouseEvent3D):void {       var object:Object3D = event.object;             doRotation = false;       // Calculate angles necessary for camera            var theta:Number = Math.atan2(object.x, object.z);       var len:Number = Math.sqrt(object.x*object.x + object.z*object.z);       var phi:Number = Math.atan2(object.y, len);       // rotate camera position       camera.targetpanangle = theta * 180 / Math.PI;       camera.targettiltangle = phi * 180 / Math.PI;       // move camera towards plane       Tweener.addTween(camera, {distance:150, time:0.5, transition:"easeOutSine"});     }        /**     * Event listener for mouse over plane. Removes the blur filter.     */        private function onMouseOverObject(event:MouseEvent3D):void {       var object:Object3D = event.object;             object.filters = new Array();     }            /**     * Event listener for mouse out of plane. Adds blur filter and moves     * camera away from plane if it isn't already.     */     private function onMouseLeavesObject(event:MouseEvent3D):void {       var object:Object3D = event.object;       object.filters.push(new BlurFilter(8, 8));       Tweener.addTween(camera, {distance:200, time:0.5, transition:"easeOutSine"});       doRotation = true;     }     /**     * Resize the scene when the stage resizes     */      private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   }   }

This (along with the other ActionScript classes shown below) produces a cube that rotates as the mouse moves. One face shows a Flash video stream (Etienne de Crécy : Am I Wrong), another is an interactive drawable surface and the third a simple Pong simulation. Each face is blurred until the mouse enters it. Clicking on a face moves the camera directly above it and moves towards it. Moving the mouse outside of a face blurs the face again and the cube regains its original size. You can see the finished result by clicking on the image below.

As usual, the code for Example007 follows the same style as shown in the previous articles of this series. Lets look at what has changed.

The constructor and initialisation of Away3D elements is virtually identical to before so not worth looking at here. Lets move onto the scene creation (createScene())and see how we use these new materials. To start off with the materials are created.

      // Video material using a flash streaming video URL       var frontMaterial:VideoMaterial = new VideoMaterial({file:videoURL});             // Movie material from an embedded flash animation       var topMaterial:MovieMaterial = new MovieMaterial(new DrawToolEmbedded(), {lockW:320, lockH:240, interactive:true, smooth:true, precision:5});             // Movie material from another class       var leftMaterial:MovieMaterial = new MovieMaterial(new Pong(), {smooth:true, precision:5});

As you can see, the VideoMaterial is very easy to create - simply pass the URL of the Flash video stream in the initialisation parameters and its ready! Compared to Papervision3D this is much easier (in fact, the internet connection and creation of the video stream is encapsulated in the VideoMaterial class). If you want the video to loop you can set the loop parameter to true, also sent in the initialisation parameters array. If you want to pause/play the video then you can access the netStream object directly from the material to control the playback.

The MovieMaterial is similarly easy to create. The constructor takes a Sprite object which will be mapped to the surface. I show two cases here: one where the object is an instantiation of a class in the same project, another where we embed a previously compiled Flash movie. Other than the smooth and precision parameters as we discussed in the texture mapping tutorial, we can indicate that the material should be interactive by specifying the interactive parameter to be true. Similarly we can force a size of the Sprite by giving the lockW and lockH parameters being the width and height - note that these have no relation to the dimensions of the object to which the material is mapped.

These materials are used just like all the other materials presented in this series of articles: simply create an object and pass the material to it.

      // Create three planes with different material, blurred by default       // and position them them to create tree sides of a cube       var topPlane:Plane = new Plane({material:topMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       topPlane.rotationY = -180;       topPlane.y = 50;       topPlane.filters.push(new BlurFilter(8, 8));             var leftPlane:Plane = new Plane({material:leftMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       leftPlane.rotationZ = -90;       leftPlane.rotationY = -90;       leftPlane.x = 50;       leftPlane.filters.push(new BlurFilter(8, 8));             var frontPlane:Plane = new Plane({material:frontMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       frontPlane.rotationX = -90;       frontPlane.rotationZ = 180;       frontPlane.z = 50;       frontPlane.filters.push(new BlurFilter(8, 8));             // Create an object container to group the sides of the cube       planeGroup = new ObjectContainer3D();       scene.addChild(planeGroup);       planeGroup.addChild(topPlane);       planeGroup.addChild(leftPlane);       planeGroup.addChild(frontPlane);

As you can see, three planes are created, as shown in previous articles, and a different material passed to each one. These planes are then rotated and translated to form three faces of a cube. I also added a BlurFilter just to show how adding effects to Away3D objects is very simple as well.

Moving onto the mouse event listeners, for this example I don’t have a stage mouse listener to rotate the scene, only listeners for the MouseEvent3D events. Here, for each face, I add a mouse down, mouse over and mouse up listener that are triggered only when the mouse interacts with a specific 3D object.

      // Add mouse listeners to each plane for mouse down, over and out events       topPlane.addOnMouseDown(onMouseClickOnObject);       leftPlane.addOnMouseDown(onMouseClickOnObject);       frontPlane.addOnMouseDown(onMouseClickOnObject);       topPlane.addOnMouseOver(onMouseOverObject);       leftPlane.addOnMouseOver(onMouseOverObject);       frontPlane.addOnMouseOver(onMouseOverObject);       topPlane.addOnMouseOut(onMouseLeavesObject);       leftPlane.addOnMouseOut(onMouseLeavesObject);       frontPlane.addOnMouseOut(onMouseLeavesObject);

Looking first at the mouse down listener, the objective is to make the camera look directly at a cube face.

    /**     * Event listener for mouse click on plane. Makes the camera look     * directly at the plane and move closer to it.     */     private function onMouseClickOnObject(event:MouseEvent3D):void {       var object:Object3D = event.object;             doRotation = false;       // Calculate angles necessary for camera            var theta:Number = Math.atan2(object.x, object.z);       var len:Number = Math.sqrt(object.x*object.x + object.z*object.z);       var phi:Number = Math.atan2(object.y, len);       // rotate camera position       camera.targetpanangle = theta * 180 / Math.PI;       camera.targettiltangle = phi * 180 / Math.PI;       // move camera towards plane       Tweener.addTween(camera, {distance:150, time:0.5, transition:"easeOutSine"});     }

From the object location we can calculate a camera tilt and pan angle. Using the properties targetpanangle and targettiltangle the camera moves gently towards the desired position. To move the camera towards the object I’ve added a Tweener call (as we looked at in scene interaction). In this function we also turn off the automatic camera movement (the camera follows the mouse otherwise as shown below).

Moving on to the mouse over event listener, here we simply remove the BlurFilter making the face come into focus.

    /**     * Event listener for mouse over plane. Removes the blur filter.     */        private function onMouseOverObject(event:MouseEvent3D):void {       var object:Object3D = event.object;             object.filters = new Array();     }

Finally for the listeners, the mouse out event listener adds the BlurFilter, ensures that the camera moves freely with the mouse movement again and executes another Tweener call to take the camera back to its original distance.

    /**     * Event listener for mouse out of plane. Adds blur filter and moves     * camera away from plane if it isn't already.     */     private function onMouseLeavesObject(event:MouseEvent3D):void {       var object:Object3D = event.object;       object.filters.push(new BlurFilter(8, 8));       Tweener.addTween(camera, {distance:200, time:0.5, transition:"easeOutSine"});       doRotation = true;     }

That leaves us with just the event loop and the camera update function. You’ll notice that the update loop is very similar to before - there is no rotation applied to the scene objects this time but we do update the camera position.

To move the camera automatically, the relative mouse position on the screen is simply converted into pan and tilt angles and applied to the camera target angles as shown below.

    /**     * Update the camera position from mouse positions     */     private function updateCamera():void {       if (doRotation) {         camera.targetpanangle = (stage.stageWidth - stage.mouseX) / stage.stageWidth * 90;         camera.targettiltangle = (stage.stageHeight - stage.mouseY) / stage.stageHeight * 70       }     }

Note that if the user has clicked on a cube face then the doRotation boolean is false allowing the user to interact more easily with the different movie or video stream material.

And that’s all there is to it! As you’ll see, mouse events are mapped effectively to the embedded Flash animations, even in 3D, and the streaming of video is very clean and very simple to implement. Hopefully this has provided a useful introduction to these types of materials, don’t hesitate to look into the code itself to understand them more. As always comments, questions and suggestions are very welcome too!

As promised, just for completeness, you’ll find the source for the drawing tool movie and the automated Pong game below - going into the detail of these is out of the scope of this article !

DrawTool.as :

package {   import flash.display.Sprite;   import flash.events.MouseEvent;   import flash.text.TextField;   import flash.text.TextFieldAutoSize;   import flash.text.TextFormat;     public class DrawTool extends Sprite {     private var isDrawing:Boolean = false;     private var sprite:Sprite;     public function DrawTool() {       // create a drawing surface       sprite = new Sprite();       sprite.graphics.beginFill(0xEEEEEE);       sprite.graphics.moveTo(0, 0);       sprite.graphics.lineTo(320, 0);       sprite.graphics.lineTo(320, 240);       sprite.graphics.lineTo(0, 240);       sprite.graphics.endFill();       addChild(sprite);             // create text and format       var textFormat:TextFormat = new TextFormat();       textFormat.size = 30;       textFormat.font = "Arial";             var text:TextField = new TextField();       text.x = 50;       text.y = 100;       text.textColor = 0x222222;       text.text = "click to draw!";       text.setTextFormat(textFormat);       text.autoSize = TextFieldAutoSize.LEFT;       text.selectable = false;       addChild(text);             // listen to mouse events       this.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       this.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       this.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);           }         /**     * Event listener for mouse down event. Starts drawing circles.     */     private function onMouseDown(event:MouseEvent):void {       isDrawing = true;       drawCircle(this.mouseX, this.mouseY);     }         /**     * Event listener for mouse up event. Stops drawing circles.     */     private function onMouseUp(event:MouseEvent):void {       isDrawing = false;     }         /**     * Event listener for mouse move event. Draws a circle.     */     private function onMouseMove(event:MouseEvent):void {       if (isDrawing) {         drawCircle(this.mouseX, this.mouseY);       }     }     /**     * Function to draw a circle.     */     private function drawCircle(x:int, y:int):void {       sprite.graphics.beginFill(Math.random() * 0xFFFFFF, 0.5);       sprite.graphics.drawCircle(x, y, 5);       sprite.graphics.endFill();     }       } }

Pong.as :

package {   import flash.display.Sprite;   import flash.events.Event;     [SWF(backgroundColor="#000000")]   /**   * Simple computerised Pong copy.   */   public class Pong extends Sprite {     private static const COURT_WIDTH:Number = 320;     private static const COURT_HEIGHT:Number = 240;     private static const BALL_WIDTH:Number = 5;     private static const BAT_WIDTH:Number = 5;     private static const BAT_HEIGHT:Number = 30;     private static const COLOUR:Number = 0xDDDDDD;     private var player1:Sprite;     private var player2:Sprite;     private var ball:Sprite;         private var ballSpeedX:Number;     private var ballSpeedY:Number;     private var activePlayer:int;     private var playerIsMoving:Boolean = false;     private var playerSpeed:Number;     private var playerDestination:Number;     public function Pong() {       createCourt();       addPlayer1();       addPlayer2();       addBall();             this.addEventListener(Event.ENTER_FRAME, onFrameEnter);     }     /**     * Creates the court with net     */     private function createCourt():void {       var background:Sprite = new Sprite();       background.graphics.beginFill(0x000000);       background.graphics.moveTo(0, 0);       background.graphics.lineTo(COURT_WIDTH, 0);       background.graphics.lineTo(COURT_WIDTH, COURT_HEIGHT);       background.graphics.lineTo(0, COURT_HEIGHT);       background.graphics.endFill();       addChild(background);       var net:Sprite = new Sprite();       var nPoints:Number = 32;       var pointHeight:Number = (COURT_HEIGHT / nPoints);       var drawHeight:Number = pointHeight * 0.6;       var drawWidth:Number = drawHeight / 2;       // Create dashed net       for (var i:Number = 0; i < nPoints; i++) {         var x:Number = COURT_WIDTH / 2 - drawWidth / 2;         var y:Number = i*pointHeight;         net.graphics.beginFill(COLOUR);         net.graphics.moveTo(x, y);         net.graphics.lineTo(x+drawWidth, y);         net.graphics.lineTo(x+drawWidth, y+drawHeight);         net.graphics.lineTo(x, y+drawHeight);         net.graphics.endFill();       }                  addChild(net);     }     /**     * Add left-hand player     */     private function addPlayer1():void {       player1 = new Sprite();       createBat(player1);       player1.x = 20;       player1.y = (COURT_HEIGHT / 2) - (BAT_HEIGHT / 2);             addChild(player1);     }         /**     * Add right-hand player     */     private function addPlayer2():void {       player2 = new Sprite();       createBat(player2);       player2.x = COURT_WIDTH - 20 - BAT_WIDTH;       player2.y = (COURT_HEIGHT / 2) - (BAT_HEIGHT / 2);             addChild(player2);     }         /**     * Create a bat     */     private function createBat(player:Sprite):void {       player.graphics.beginFill(COLOUR);       player.graphics.moveTo(0, 0);       player.graphics.lineTo(BAT_WIDTH, 0);       player.graphics.lineTo(BAT_WIDTH, BAT_HEIGHT);       player.graphics.lineTo(0, BAT_HEIGHT);       player.graphics.endFill();     }         /**     * Add ball to scene and initialise speeds     */     private function addBall():void {       ball = new Sprite();             ball.graphics.beginFill(COLOUR);       ball.graphics.moveTo(0, 0);       ball.graphics.lineTo(BALL_WIDTH, 0);       ball.graphics.lineTo(BALL_WIDTH, BALL_WIDTH);       ball.graphics.lineTo(0, BALL_WIDTH);       ball.graphics.endFill();             ball.x = 0;       ball.y = 20;       addChild(ball);             ballSpeedX = 6;       ballSpeedY = 5;       activePlayer = 1;     }         /**     * Called at every frame     */     private function onFrameEnter(event:Event):void {       // update ball position       updateBall();             // update player position       updateActivePlayer();             // detect hits       hitTest();     }         /**     * Updates the ball position taking into account court dimensions     */     private function updateBall():void {       ball.x = ball.x + ballSpeedX;       ball.y = ball.y + ballSpeedY;             // Detect if ball escapes a player       if (ball.x > COURT_WIDTH - BALL_WIDTH) {         ball.x = 0;         ball.y = COURT_HEIGHT / 2;                 ballSpeedY = Math.random() * 10;       } else if (ball.x < 0) {         ball.x = COURT_WIDTH - BALL_WIDTH;         ball.y = COURT_HEIGHT / 2;                 ballSpeedY = -Math.random() * 10;       }       // Detect wall hits: invert ball y-direction and initiate player position calculation       if (ball.y < 0) {         ball.y = 0;         ballSpeedY = -ballSpeedY;         playerIsMoving = false;       } else if (ball.y > COURT_HEIGHT - BALL_WIDTH) {         ball.y = COURT_HEIGHT - BALL_WIDTH;         ballSpeedY = -ballSpeedY;         playerIsMoving = false;       }     }     /**     * Updates player position or calculates new position     */     private function updateActivePlayer():void {       var player:Sprite;       var calculatedTime:Number;             // calculate time for ball to reach player       if (activePlayer == 0) {         player = player1;         calculatedTime = Math.abs(player.x + BAT_WIDTH - ball.x) / Math.abs(ballSpeedX);       } else {         player = player2;         calculatedTime = Math.abs(player.x - (ball.x + BALL_WIDTH)) / Math.abs(ballSpeedX);       }             if (playerIsMoving) {         // update player position         player.y = player.y + playerSpeed;                 // ensure that player does not leave the court         if (player.y > COURT_HEIGHT - BAT_HEIGHT) {           player.y = COURT_HEIGHT - BAT_HEIGHT;           playerIsMoving = false;         } else if (player.y < 0) {           player.y = 0;           playerIsMoving = false;         }                 // determine if player is more or less in position         if (Math.abs(player.y - playerDestination) < BAT_HEIGHT / 4) {           playerIsMoving = false;         }               } else {         // calculate expected ball position         var calculatedY:Number = ball.y + ballSpeedY * calculatedTime;         var random:Boolean = false;                 if (calculatedY < 0 || calculatedY > COURT_HEIGHT) {           calculatedY = Math.random() * COURT_HEIGHT;           random = true;         }                 // calculate desired player position with a random element         playerDestination = calculatedY - (BAT_HEIGHT / 2) + (0.4 * (Math.random() - 0.5) * BAT_HEIGHT);         if (playerDestination < 0) {           playerDestination = 0;         } else if (playerDestination > COURT_HEIGHT - BAT_HEIGHT) {           playerDestination = COURT_HEIGHT - BAT_HEIGHT;         }                 // Calculate a random speed for player         if (player.y < playerDestination) {           playerSpeed = 5 + Math.random() * 10;                   } else {           playerSpeed = -5 + Math.random() * -10;         }                 // increase speed if player can't work out ball position         if (random) {           playerSpeed = playerSpeed * 2;         }                 // only move player if really necessary (+ stupidity estimate)         if (Math.abs(playerDestination - player.y) > BAT_HEIGHT / 2) {           playerIsMoving = true;         }       }           }         /**     * Determine if ball hits a bat     */     private function hitTest():void {       var player:Sprite;       var check:Boolean = false;             // check to see if ball is at same x position as bat       if (activePlayer == 0) {         player = player1;         if (ball.x > player1.x && ball.x < player1.x + BAT_WIDTH) {           check = true;         }       } else {         player = player2;         if (ball.x + BALL_WIDTH > player2.x && ball.x + BALL_WIDTH < player2.x + BAT_WIDTH) {           check = true;         }       }       if (check) {         // verify y position         if ((ball.y + BALL_WIDTH <= player.y + BAT_HEIGHT) && (ball.y >= player.y)) {                     // hit, change player           activePlayer = 1 - activePlayer;                     // reverse ball direction           ballSpeedX = -ballSpeedX;           // calculate new ball speed in y                    var batPosition:Number = (ball.y + (BALL_WIDTH / 2)) - (player.y + (BAT_HEIGHT / 2));           ballSpeedY = batPosition / BAT_HEIGHT * 10 * (1 + Math.random() * 0.1);         }       }     }       } }
Sunday, December 7th, 2008

First steps in Away3D : Part 6 - Normal mapping and environment mapping

Back again after a break longer than predicted! In this tutorial I’ll show how we can add more stunning visual effects to objects by applying normal maps (to give the impression that an object is composed of many more triangles from the rendered shading) and environment maps (for objects to appear shiny and reflecting the surrounding environment). An equivalent tutorial for Papervision3D can be found at my post on texture mapping with lighting, bump mapping and environment mapping.

Previous articles summary :

Normal maps - or Dot3 bump maps - can be seen as something of an evolution from standard bump maps. Bump maps, as used in Papervision3D, contain a single value describing a displacement perpendicular to a surface. Normal maps, however, are more detailed because they contain three axis elements, effectively describing the normal to a specific point on a surface, replacing the triangle normal entirely. Whereas a bump map would be rendered the same for any point of view, a normal map is rendered differently depending on the viewer’s position: hence giving a much better impression of realism.

Normal maps are mapped in much the same way as a texture map, using the same uv values as for a texture. The bitmap data used in a normal map is split into the three components red, green and blue, each one giving a representation of the normal vector components in x, y and z respectively. So, normal values between -1 and 1 are discretised to an integer value between 0 and 255.

Normal maps greatly increase the performance of rendering a 3D object since the normals are given directly rather than requiring a calculation from triangles. So, a complex model requiring thousands of triangles can be reduced to, say, a few hundred (a low-poly model) with a normal map being derived from the original model. You can check out wikipedia for more information on normal mapping. For an example of a low-poly model using a normal map generated from a more complex one, check out this demo of normal mapping at Away3D.

Environment mapping, or reflection mapping, is a technique used to map a surrounding environment onto an object giving the impression that the object has a mirrored surface. The technique is much more efficient than ray tracing and, even though it is not exact, still produces realistic effects. Again, wikipedia contains some useful information on environment mapping.

In this tutorial I’m going to show normal mapping on a cube and a sphere and environment mapping on a sphere. Following from the last post on Lighting and Shading, we’re going to be using another couple of CompositeMaterials: namely the Dot3BitmapMaterial and the EnviroBitmapMaterial.

It should be noted that while normal mapping can produce a more realistic effect than bump mapping, it can be more complex to implement. Since a normal map replaces the normal to a surface we cannot use the same map on an object that has many faces (unless the normal map has been created specifically for example as shown in the Away3D demo above). This is a problem for example for Primitives. A Cube for example has six faces with six discrete principal normals - if we use the same normal map for each surface then each face will have the same normal data and hence be rendered identically, rather than each face being independent. Currently we therefore have to provide six independent normal maps.

My method for this example however is to create a cube using six individual planes using the same normal map and rotating and translating them: the normal map data is then rendered correctly for each face.

I also had a problem retrieving normal maps from the web for use in this tutorial. To help me I created a small utility that takes bump (displacement) map data and converts this into normal map data for either a plane or a sphere. You can try this out yourselves by checking out my article on displacement map to normal map conversion. With this utility you are able to specify the direction of the displacement (for a plane) or the direction against which the angle phi is calculated for a sphere.

So, lets get on with the code! Below you’ll see the next example in this series, producing three rotating texture-mapped objects each one rendered with either an environment map or normal map.

package {   import away3d.cameras.HoverCamera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.render.Renderer;   import away3d.core.utils.Cast;   import away3d.lights.DirectionalLight3D;   import away3d.materials.Dot3BitmapMaterial;   import away3d.materials.EnviroBitmapMaterial;   import away3d.primitives.Plane;   import away3d.primitives.Sphere;     import flash.display.Bitmap;   import flash.display.BitmapData;   import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;     [SWF(backgroundColor="#000000")]     public class Example006 extends Sprite {     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/normalMap.png")] private var NormalImage:Class;     private var normalBitmap:Bitmap = new NormalImage();     [Embed(source="/../assets/asteroidNormal.png")] private var SphereNormalImage:Class;     private var sphereNormalBitmap:Bitmap = new SphereNormalImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();     [Embed(source="/../assets/envMap.png")] private var EnvironmentImage:Class;     private var envBitmap:Bitmap = new EnvironmentImage();     private static const ORBITAL_RADIUS:Number = 150;     private static const CAMERA_ORBIT:Number = 600;     private var scene:Scene3D;     private var camera:HoverCamera3D;     private var view:View3D;       private var planeGroup:ObjectContainer3D;     private var normalSphere:Sphere;     private var envSphere:Sphere;     private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var lastPanAngle:Number = 60;     private var lastTiltAngle:Number = -60;         public function Example006() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Initialise Away3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     /**     * Initialise all 3D components.     */     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new HoverCamera3D({zoom:25, focus:30, distance:600});       camera.targetpanangle = camera.panangle = -10;       camera.targettiltangle = camera.tiltangle = 20;       camera.yfactor = 1;             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the view to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             // ensure that the z-order is calculated correctly       view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }     /**     * Create the objects and lighting of the scene     */     private function createScene():void {       // Create 3 different materials: two normal mapped ones (planar and spherical) and an       // environment mapped one.       var normalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(Cast.bitmap(away3DBitmap), Cast.bitmap(normalBitmap), {smooth:true, precision:5});       var envMapMaterial:EnviroBitmapMaterial = new EnviroBitmapMaterial(Cast.bitmap(checkerBitmap), Cast.bitmap(envBitmap), {smooth:true, precision:5});       var sphereNormalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(new BitmapData(1, 1, false, 0x666666), Cast.bitmap(sphereNormalBitmap), {smooth:true, precision:5});       // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 10000;       light.z = 50000;       light.y = 50000;       scene.addChild(light);       // Create six with the same (normal mapped) material and position them them to create a cube       var topPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       topPlane.y = 50;       var leftPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       leftPlane.rotationZ = 90;       leftPlane.x = -50;       var frontPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       frontPlane.rotationX = 90;       frontPlane.z = -50;       var bottomPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       bottomPlane.rotationX = 180;       bottomPlane.y = -50;       var rightPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       rightPlane.rotationZ = -90;       rightPlane.x = 50;       var backPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       backPlane.rotationX = -90;       backPlane.z = 50;             // Create an object container to group the sides of the cube       planeGroup = new ObjectContainer3D();       scene.addChild(planeGroup);       planeGroup.addChild(topPlane);       planeGroup.addChild(leftPlane);       planeGroup.addChild(frontPlane);       planeGroup.addChild(bottomPlane);       planeGroup.addChild(rightPlane);       planeGroup.addChild(backPlane);       planeGroup.x = -100;       planeGroup.z = 100;       // Create a sphere with normal-mapped material       normalSphere = new Sphere({material:sphereNormalMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       normalSphere.x = 100;       normalSphere.z = 100;       scene.addChild(normalSphere);       // Create a sphere with environment-mapped material       envSphere = new Sphere({material:envMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       envSphere.z = -90;       scene.addChild(envSphere);     }         /**     * Frame-enter event handler     */     private function loop(event:Event):void {             // rotate the objects       planeGroup.rotationY += 2;       normalSphere.rotationY += 2;       envSphere.rotationY += 2;       // update camera position       updateCamera();       camera.hover();       // Render the 3D scene       view.render();     }     /**     * Update the camera position from mouse movements     */     private function updateCamera():void {       if (doRotation) {         camera.targetpanangle = 0.5 * (stage.mouseX - lastMouseX) + lastPanAngle;         camera.targettiltangle = 0.5 * (stage.mouseY - lastMouseY) + lastTiltAngle;       }     }         /**     * Mouse down listener for camera rotation     */     private function onMouseDown(event:MouseEvent):void {       lastPanAngle = camera.targetpanangle;       lastTiltAngle = camera.targettiltangle;       lastMouseX = stage.mouseX;       lastMouseY = stage.mouseY;       doRotation = true;       stage.addEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }         /**     * Mouse up listener for camera rotation     */     private function onMouseUp(event:MouseEvent):void {       doRotation = false;       stage.removeEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }     /**     * Mouse stage leave listener for camera rotation     */     private function onStageMouseLeave(event:Event):void {       doRotation = false;       stage.removeEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }         /**     * Resize the scene when the stage resizes     */      private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   }     }

If you want to use the same images as in this example then away3D.png and checker.jpg you’ll find in the previous post on texture mapping in Away3D. The new images are shown below.

The first two images (the normal maps) were created using the utility described above. The first one from a bump map I used for a Papervision3D tutorial on bump mapping, the second one comes from a bump map I found on the internet at gamedev.net. The environment map comes from and old OpenGL example I discovered on my hard disk… if you search for “opengl sphere map” in Google you’ll find plenty of instances!

Once its all compiled you should see three rotating objects with either normal and environment mapped data. Click on the image below to see the Flash movie. You can click anywhere and move the mouse to rotate the scene.

So, lets go into more detail on each part of the code. There’s nothing too complicated here but a few changes from the last time…

The first change comes from the initialisation of the 3D basics of the scene, specifically with the camera.

    private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new HoverCamera3D({zoom:25, focus:30, distance:600});       camera.targetpanangle = camera.panangle = -10;       camera.targettiltangle = camera.tiltangle = 20;       camera.yfactor = 1;             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the view to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             // ensure that the z-order is calculated correctly       view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }

I’ve now chosen to use a HoverCamera3D. This simplifies the movement and positioning of the camera. You’ll remember that previously I used the basic Camera3D class and had to calculate specific values of x, y and z as well as redirect the camera towards the origin. Much of this is included in the HoverCamera3D class. By specifying targettiltangle and targetpanangle the camera will calculate a trajectory necessary to move the camera to a new position and keep it focused on a particular target. To maintain the orbital radius of the camera we set the yfactor to 1 otherwise a more elliptical orbit is calculated.

You’ll see towards the end of the code how we perform an interaction with the camera. If we look at the loop function, called on every frame-enter event we can see how the camera position is updated.

    private function loop(event:Event):void {             // rotate the objects       planeGroup.rotationY += 2;       normalSphere.rotationY += 2;       envSphere.rotationY += 2;       // update camera position       updateCamera();       camera.hover();       // Render the 3D scene       view.render();     }

The object rotation is self evident so we’ll ignore that. The updateCamera function recalculates the camera position from mouse position. More importantly the camera.hover() call makes the camera move gradually towards the target tilt and pan angles giving a smoother animation of the camera than in previous examples.

For completeness, the updateCamera function and the mouseDown event listener are shown below.

    private function updateCamera():void {       if (doRotation) {         camera.targetpanangle = 0.5 * (stage.mouseX - lastMouseX) + lastPanAngle;         camera.targettiltangle = 0.5 * (stage.mouseY - lastMouseY) + lastTiltAngle;       }     }         private function onMouseDown(event:MouseEvent):void {       lastPanAngle = camera.targetpanangle;       lastTiltAngle = camera.targettiltangle;       lastMouseX = stage.mouseX;       lastMouseY = stage.mouseY;       doRotation = true;       stage.addEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }

As you can see, the target angles are updated in the updateCamera function, taking into account the current mouse position. The onMouseDown function performs the necessary initialisation of pan and tilt angles. It also adds an event listener to detect when the mouse leaves the stage: this is useful because if the mouse button is released outside the stage, the onMouseUp listener is not called and we can obtain a state in which the scene is continuously rotated even if the mouse button is up.

So, on to more interesting elements of the code: normal mapped and environment mapped materials!

All the interesting stuff happens in createScene where the materials and objects are created.

    private function createScene():void {       // Create 3 different materials: two normal mapped ones (planar and spherical) and an       // environment mapped one.       var normalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(Cast.bitmap(away3DBitmap), Cast.bitmap(normalBitmap), {smooth:true, precision:5});       var envMapMaterial:EnviroBitmapMaterial = new EnviroBitmapMaterial(Cast.bitmap(checkerBitmap), Cast.bitmap(envBitmap), {smooth:true, precision:5});       var sphereNormalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(new BitmapData(1, 1, false, 0x666666), Cast.bitmap(sphereNormalBitmap), {smooth:true, precision:5});       // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 10000;       light.z = 50000;       light.y = 50000;       scene.addChild(light);       // Create six with the same (normal mapped) material and position them them to create a cube       var topPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       topPlane.y = 50;       var leftPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       leftPlane.rotationZ = 90;       leftPlane.x = -50;       var frontPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       frontPlane.rotationX = 90;       frontPlane.z = -50;       var bottomPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       bottomPlane.rotationX = 180;       bottomPlane.y = -50;       var rightPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       rightPlane.rotationZ = -90;       rightPlane.x = 50;       var backPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       backPlane.rotationX = -90;       backPlane.z = 50;             // Create an object container to group the sides of the cube       planeGroup = new ObjectContainer3D();       scene.addChild(planeGroup);       planeGroup.addChild(topPlane);       planeGroup.addChild(leftPlane);       planeGroup.addChild(frontPlane);       planeGroup.addChild(bottomPlane);       planeGroup.addChild(rightPlane);       planeGroup.addChild(backPlane);       planeGroup.x = -100;       planeGroup.z = 100;       // Create a sphere with normal-mapped material       normalSphere = new Sphere({material:sphereNormalMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       normalSphere.x = 100;       normalSphere.z = 100;       scene.addChild(normalSphere);       // Create a sphere with environment-mapped material       envSphere = new Sphere({material:envMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       envSphere.z = -90;       scene.addChild(envSphere);     }

To start off with the three different materials are created. Firstly a Dot3BitmapMaterial taking the Away3D texture and a random normal map. I’ve chosen to smooth the material and have increased the precision of rendering to 5 pixels. The second material uses the EnviroBitmapMaterial using the checkered texture and the OpenGl sphere-map for the environment data, again smoothed as before. Incidentally, the shininess of the material can be modified by specifying the reflectiveness parameter in the initialisation parameters, taking a value between 0 and 1 (for most reflective). The final Dot3BitmapMaterial material, rather than using a bitmap file, creates a plain grey bitmap. The normal map comes from the asteroid spherical normal map.

We add a DirectionalLight3D to the scene to provide uniform lighting, independent of light distance. It should be noted that the Dot3BitmapMaterial will not currently work with a PointLight3D. Also, the EnviroBitmapMaterial is independent of the source: the environment map provides the equivalent shading.

For the cube we create six Planes, each using the same material. These are then rotated and translated to form a cube. They are finally added to a ObjectContainer3D so that we can rotate all of them together in the loop function. Note as well that, as before, each object needs to have its own canvas which is specified within the initialisation parameters.

The two spheres are then created: one using an environment map, the other with normal mapped data. These are then positioned and added to the scene, again with independent canvases.

And that’s all there is to it! As you’ll see, the normal map produces fantastic results and really adds a feeling of depth to the object surface. The environment map really gives the impression that the object is very shiny. These effects are very simple to produce in Away3D but be aware too that added realism comes at a cost - the frame rate is bound to be slower than without these effects - so be warned!

Anyway, I hope this has been a useful addition to basic lighting and shading. As always, let me know if you have any comments or questions!

Next article:

-->
Friday, December 5th, 2008

Displacement map to normal map converter

I’m currently looking at the normal mapping materials that are available in Away3D and became aware that to do a decent test it’d be nice to be able create normal maps myself. Previously, when looking at lighting effects in Papervision3D, I created my own bump map from random data very easily. I’ve tried doing the same for a normal map but haven’t found a decent utility to help me do that… so, I decided to write one myself.

Assuming that the displacement map image is applied to a single surface, the normal to the surface can be calculated quite easily by taking the gradients in the x and y directions and calculating the cross product of the two (see this description from Wolfram MathWorld on surface normals for example).

Using the bitmap data (assuming it is greyscale), we can calculate the gradients using finite difference calculations using the pixel data as a height (displacement) and the pixel position as the x and y coordinates (again, Wolfram MathWorld on finite differences provides a useful source of information). The normals can then be calculated at each individual pixel and converted into red, green and blue data.

The Flex application shown below reads displacement map data, calculates the normals, and creates normal map data. You can check it out by clicking on the image below or visiting http://www.tartiflop.com/disp2norm/.

Note that this requires Flash Player 10 because it uses some of the new features available: namely reading and writing to the local file system and the use of the Vector3D class.

If you’re interested you can check out the source as well.

Two additional features are available:

Update !

The converter can now create normal maps for spherical objects too!

Simply select the Spherical radio button and the normal map will be produced for a sphere. It assumes that the displacement map data has also been created for a sphere. The axis from which phi is calculated can be chosen using the combo. For example, in Away3D, this is the y axis for the Sphere primitive.

For info, the normal is calculated differently (and more simply): from the pixel indices, equivalent to spherical positions, we can calculate Cartesian positions. These can be converted into vectors and hence the cross product of vectors running across the pixel (in latitude and longitude directions), taking into account the displacement map data, gives us directly the normal to the sphere… hope that makes some kind of sense!

If you have any comments or questions then please let me know!

Wednesday, November 19th, 2008

First steps in Away3D : Part 5 - Lighting and shading

Having obtained an interactive, texture mapped scene I’d now like to a bit more realism into it by adding a light source and shading the objects. The price of course for having a more realistic scene is at the cost of reduced performance as you’ll find many more calculations are necessary to render the scene. You can find the equivalent article for Papervision3D in my post on First steps in Papervision3D : Part 4 - lighting and shading, however I find that there is quite a difference for Away3D, at least compared to the other tutorials in this series.

Previous articles summary :

Lighting in Away3D can be split into three basic components:

The first difference I notice compared to Papervision3D is that we can specify more than one light source. This of course increases the cpu load but in certain cases may produce a more realistic rendering of a scene.

There are three main types of light sources:

In terms of shaders we have the following posibilities:

The above shaders, with the exception of the EnviroShader assume that the light source is a DirectionalLight3D.

We then have a choice of CompositeMaterials that uses a combination of different shaders and base materials as shown in the following list:

The above materials all produce smoothly shaded objects which naturally has a price in terms of performance. If we want to have objects that are more simply shaded then we can use a CenterLightingMaterial. This type of material produces the equivalent of flat-shaded materials in Papervision3D. We have in this category two choices:

In the example code for this article I’m going to look at two of these materials: the PhongBitmapMaterial and the WhiteShadingBitmapMaterial. These two materials are very simple to use and provide a lot of control over the lighting effects on the material. One advantage here, personally speaking, over Papervision3D is the ability to mix ambient, diffuse and specular lighting all together which is necessary to produce real Phong reflection. Papervision3D provides materials for ambient plus diffuse and ambient plus specular but not all three together. This obviously is quicker to render but is less realistic when drawing shiny materials for example. Mixing all three is still possible but requires more effort as I showed in my post on phong reflection with Papervision3D.

For the light source I’m simply going to use a DirectionalLight3D which, as I mentioned before, correctly renders these materials. Another aspect that I like with Away3D is that the light source itself can be given a number of characteristics. Specifically we can give it a colour and define relative strengths of ambient, diffuse and specular shading, none of which are possible with Papervision3D. Being able to specify a light colour allows us to create new effects with materials of different colours. Specifying the ambient level directly with the light source, for example, also eliminates the need to specify the ambient light with every material created, as is the case with Papervision3D.

Away3D also allows us to create more than one light source and have a material rendered appropriately, which is not possible in Papervision3D. This, again, is obviously a drain on computational resources, but, in certain cases, this can open up new possibilities for rendering a more realistic scene.

I guess what I like most is that, conceptually, Away3D allows us to disassociate the light source from the material meaning that we can create a specific material without thinking about the environment and lighting in which it will be placed.

Anyway, enough of the theory and personal opinions, lets move onto the code. I’m using exactly the same scene as for the previous example only this time adding a DirectionalLight3D and replacing the BitmapMaterial with PhongBitmapMaterial and WhiteShadingBitmapMaterial.

package {   import away3d.cameras.Camera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.base.Object3D;   import away3d.core.math.Number3D;   import away3d.core.render.Renderer;   import away3d.core.utils.Cast;   import away3d.events.MouseEvent3D;   import away3d.lights.DirectionalLight3D;   import away3d.materials.PhongBitmapMaterial;   import away3d.materials.WhiteShadingBitmapMaterial;   import away3d.primitives.Cube;   import away3d.primitives.Cylinder;   import away3d.primitives.Sphere;   import away3d.primitives.Torus;     import caurina.transitions.Tweener;     import flash.display.Bitmap;   import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;     [SWF(backgroundColor="#000000")]     public class Example005 extends Sprite {     [Embed(source="/../assets/earth.jpg")] private var EarthImage:Class;     private var earthBitmap:Bitmap = new EarthImage();     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();     private static const ORBITAL_RADIUS:Number = 150;     private static const CAMERA_ORBIT:Number = 600;     private var scene:Scene3D;     private var camera:Camera3D;     private var view:View3D;       private var group:ObjectContainer3D;     private var sphere:Sphere;     private var cube:Cube;     private var centerCube:Cube;     private var cylinder:Cylinder;     private var torus:Torus;       private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var cameraPitch:Number = 60;     private var cameraYaw:Number = -60;         public function Example005() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:25, focus:30});       setCameraPosition();             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera, renderer:Renderer.CORRECT_Z_ORDER});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             //view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }     private function createScene():void {       // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 100;       light.z = 500;       light.y = 500;       scene.addChild(light);       // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);           // Create a new sphere object using a very shiny phong-shaded bitmap material representing the earth       var earthMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(earthBitmap));       earthMaterial.shininess = 100;       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       sphere.ownCanvas = true;       group.addChild(sphere);       // Create a new cube object using a tiled, phong-shaded bitmap material       var tiledAway3DMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(away3DBitmap), {repeat:true, scaleX:.5, scaleY:.5});       cube = new Cube({material:tiledAway3DMaterial, width:75, height:75, depth:75});       cube.z = -ORBITAL_RADIUS;       cube.ownCanvas = true;       group.addChild(cube);         // Create a cylinder mapping the earth data again       cylinder = new Cylinder({material:earthMaterial, radius:25, height:100, segmentsW:16});       cylinder.x = -ORBITAL_RADIUS;       cylinder.ownCanvas = true;       group.addChild(cylinder);         // Create a torus object and use a checkered, flat-shaded (from white light) bitmap material       var checkerBitmapMaterial:WhiteShadingBitmapMaterial = new WhiteShadingBitmapMaterial(Cast.bitmap(checkerBitmap));       torus = new Torus({material:checkerBitmapMaterial, radius:40, tube:10, segmentsT:8, segmentsR:16});       torus.z = ORBITAL_RADIUS;       torus.ownCanvas = true;       group.addChild(torus);         // Create a new cube object using a smoothed, precise, phong-shaded, mat (not shiny) bitmap material       var away3DMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(away3DBitmap), {smooth:true, precision:2});       away3DMaterial.shininess = 0;       centerCube = new Cube({material:away3DMaterial, width:75, height:75, depth:75});       centerCube.ownCanvas = true;       group.addChild(centerCube);       // add mouse listeners to all the 3D objects       sphere.addOnMouseDown(onMouseDownOnObject);       cube.addOnMouseDown(onMouseDownOnObject);       cylinder.addOnMouseDown(onMouseDownOnObject);       torus.addOnMouseDown(onMouseDownOnObject);       centerCube.addOnMouseDown(onMouseDownOnObject);     }         private function loop(event:Event):void {             // rotate the group of objects       group.yaw(2);           // rotate the objects       sphere.yaw(-4);       cube.yaw(-4);       cylinder.yaw(-4);       torus.yaw(-4);       // update the camera position       updateCamera();       // Render the 3D scene       view.render();     }     // updates the camera position     private function updateCamera():void {             // If the mouse button has been clicked then update the camera position            if (doRotation) {                 // convert the change in mouse position into a change in camera angle         var dPitch:Number = (mouseY - lastMouseY) / 2;         var dYaw:Number = (mouseX - lastMouseX) / 2;                 // update the camera angles         cameraPitch -= dPitch;         cameraYaw -= dYaw;         // limit the pitch of the camera         if (cameraPitch <= 0) {           cameraPitch = 0.1;         } else if (cameraPitch >= 180) {           cameraPitch = 179.9;         }               // reset the last mouse position         lastMouseX = mouseX;         lastMouseY = mouseY;                 // reposition the camera         setCameraPosition();       }           }     // sets the camera position given pitch and yaw angles     private function setCameraPosition():void {       camera.y = CAMERA_ORBIT * Math.cos(cameraPitch * Math.PI / 180);       camera.x = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.cos(cameraYaw * Math.PI / 180);       camera.z = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.sin(cameraYaw * Math.PI / 180);             // keep the camera looking at the origin       camera.lookAt(new Number3D(0, 0, 0));     }     // called when mouse down on stage     private function onMouseDown(event:MouseEvent):void {       doRotation = true;       lastMouseX = event.stageX;       lastMouseY = event.stageY;     }     // called when mouse up on stage     private function onMouseUp(event:MouseEvent):void {       doRotation = false;     }       // called when mouse down on a 3D object     private function onMouseDownOnObject(event:MouseEvent3D):void {       var object:Object3D = event.object;       Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });     }         // called when a tween created in onMouseDownOnObject has terminated     private function goBack(object:Object3D):void {       Tweener.addTween(object, {y:0, time:2, transition:"easeOutBounce"});     }         // called when the window is resized     private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   }     }

When compiled you should get the same result as below (click on the image). As before, the scene is interactive so you can click on the background and move the mouse to move the camera and click on an object to make it bounce.

Since the code is so similar to the previous example I’m going to concentrate solely on the function createScene - this is the only function that has been modified.

In this example we use a simple white light source and see four different types of materials rendered: one that is very shiny, another that has a mat texture, one that takes default lighting parameters (being quite shiny) and another that is flat shaded.

Starting with the light source, we use a DirectionLight3D for which we specify its position and lighting properties.

      // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 100;       light.z = 500;       light.y = 500;       scene.addChild(light);

As you can see the constructor takes an array of initialisation parameters in which we set the colour to white and give it ambient, diffuse and specular factors (being between 0 and 1).

The first object that we create is the rotating globe. Here I wanted the object to appear to be very shiny so modify its shininess.

      // Create a new sphere object using a very shiny phong-shaded bitmap material representing the earth       var earthMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(earthBitmap));       earthMaterial.shininess = 100;       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       sphere.ownCanvas = true;       group.addChild(sphere);

First of all, the PhongBitmapMaterial takes bitmap data for the texture mapping, as is the case for an ordinary, unshaded BitmapMaterial. Next we modify the shininess property (by default this is set at 20). This property, on the rendered scene, in effect modifies the extent of the specular lighting. The bright white dot that we see on the globe is reduced by increasing this property as if the light source is reflected off a more concentrated region of the material.

An important point to note is the line

      sphere.ownCanvas = true;

Without this line the scene is not correctly rendered as objects remain partially visible when they pass behind others.

Moving on to the tiled material object, I wanted to show that the initialisation parameters that we send to a material are still taken into account for the base material. The PhongBitmapMaterial is composed of a TransformBitmapMaterial (as well as the shaders) as we discussed in the previous example. The initialisation parameters {repeat:true, scaleX:.5, scaleY:.5} are passed directly to this element when it is constructed and therefore the texture map is still tiled. This object uses the default shading parameters of the PhongBitmapMaterial and is therefore appears less shiny than the globe.

The cylinder uses the same material as the globe just to illustrate that we can use the same material more than once.

Next, for the torus, we implement flat shading by using a WhiteShadingBitmapMaterial. As its name indicates this assumes a white light source. You can test this by experimenting with the light source colour. You’ll notice that all the other objects correctly reflect the change in light colour but this object remains white and red. This material is obviously less complex in terms of necessary calculations and therefore provides better rendering performance. However, we see that smoothly shaded materials, as with the other objects, allow us to reduce the number of rendered triangles to produce more rounded objects.

Finally, the cube in the center is rendered with shininess set to zero. Only diffuse and ambient lighting remain in this case. This is useful for rendering mat materials but unfortunately has no performance benefits. We can see however that there is a difference in the visible result for the two cubes.

So thats it for this tutorial! I hope this has given a useful overview of lighting and shading in Away3D. Of course I’ve not gone into detail for all the different types of lights and shaders - you’ll discover a lot by experimenting with this example, for example by changing the materials, the type of lighting, its properties and its position. I’ve purposely ignored the Dot3BitmapMaterial and EnviroBitmapMaterial for this post because I felt that it was important to look at simple lighting and shading first - they should appear in the next article. But for now, at least, I hope this provides a useful starting point - comments and suggestions, as always, are very welcome!

Next article:

Monday, November 17th, 2008

First steps in Away3D : Part 4 - Scene interaction

Having created a scene last time with texture mapped objects, I’d now like to add some interaction so that we can rotate the camera around the scene and make the 3D objects react to mouse events. As you’ll see this is very straightforward with Away3D as each triangle drawn on the screen individually detects mouse events. For an equivalent tutorial in Papervision3D, check out First steps in Papervision3D : Part 5.

Previous articles summary :

For the scene interaction we’ll be looking at two different types of events. Firstly, the standard flash MouseEvent that we’ll use to rotate the camera when the user clicks on the stage and moves the mouse. Secondly we’ll look at the Away3D-specific event: the MouseEvent3D. This event is created for similar mouse actions on 3D objects but more importantly it contains the associated 3D object. To add MouseEvent3D listeners, all Object3D objects have the following functions for different mouse interactions:

Another new feature for this tutorial is to add object animation using an external library: Tweener.

Without having to calculate and specify the change in positions (for example) of objects within the code, a tween does all the hard work for us. A tween works on a particular property of an object and at each frame of an animation modifies this property in a particular manner. You’ll find more information on tweens on the web, for example at wikipedia.

Tweener is an opensource library, compatible with Actionscript2 and Actionscript3 as well as JavaScript and HaXE. To install it in your Eclipse/Flex Builder 3 environment simply create a new Flex Library Project, download the sources from the Tweener homepage and extract them into the src directory. The source should be automatically compiled and you can either link your own ActionScript projects to this project or directly used the compiled library. If you need help on compiling new flex library projects in eclipse then you should find useful information on my post for installing and compiling Away3D in eclipse.

Lets take a look at the code. I’m following directly from the example in the last post but am simply adding the new mouse listeners.

package {   import away3d.cameras.Camera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.base.Object3D;   import away3d.core.math.Number3D;   import away3d.core.utils.Cast;   import away3d.events.MouseEvent3D;   import away3d.materials.BitmapMaterial;   import away3d.materials.TransformBitmapMaterial;   import away3d.primitives.Cube;   import away3d.primitives.Cylinder;   import away3d.primitives.Sphere;   import away3d.primitives.Torus;     import caurina.transitions.Tweener;     import flash.display.Bitmap;   import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;     [SWF(backgroundColor="#000000")]     public class Example004 extends Sprite {     [Embed(source="/../assets/earth.jpg")] private var EarthImage:Class;     private var earthBitmap:Bitmap = new EarthImage();     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();     private static const ORBITAL_RADIUS:Number = 150;     private static const CAMERA_ORBIT:Number = 600;     private var scene:Scene3D;     private var camera:Camera3D;     private var view:View3D;       private var group:ObjectContainer3D;     private var sphere:Sphere;     private var cube:Cube;     private var centerCube:Cube;     private var cylinder:Cylinder;     private var torus:Torus;       private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var cameraPitch:Number = 60;     private var cameraYaw:Number = -60;         public function Example004() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:25, focus:30});       setCameraPosition();             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;       addChild(view);     }     private function createScene():void {       // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);           // Create a new sphere object using a bitmap material representing the earth       var earthMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(earthBitmap));       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       group.addChild(sphere);       // Create a new cube object using a tiled bitmap material       var tiledAway3DMaterial:TransformBitmapMaterial = new TransformBitmapMaterial(Cast.bitmap(away3DBitmap), {repeat:true, scaleX:.5, scaleY:.5});       cube = new Cube({material:tiledAway3DMaterial, width:75, height:75, depth:75});       cube.z = -ORBITAL_RADIUS;       group.addChild(cube);         // Create a cylinder mapping the earth data again       cylinder = new Cylinder({material:earthMaterial, radius:25, height:100, segmentsW:16});       cylinder.x = -ORBITAL_RADIUS;       group.addChild(cylinder);         // Create a torus object and use a checkered bitmap material       var checkerBitmapMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(checkerBitmap));       torus = new Torus({material:checkerBitmapMaterial, radius:40, tube:10, segmentsT:8, segmentsR:16});       torus.z = ORBITAL_RADIUS;       group.addChild(torus);         // Create a new cube object using a smoothed, precise bitmap material       var away3DMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(away3DBitmap), {smooth:true, precision:2});       centerCube = new Cube({material:away3DMaterial, width:75, height:75, depth:75});       group.addChild(centerCube);       // add mouse listeners to all the 3D objects       sphere.addOnMouseDown(onMouseDownOnObject);       cube.addOnMouseDown(onMouseDownOnObject);       cylinder.addOnMouseDown(onMouseDownOnObject);       torus.addOnMouseDown(onMouseDownOnObject);       centerCube.addOnMouseDown(onMouseDownOnObject);     }         private function loop(event:Event):void {             // rotate the group of objects       group.yaw(2);           // rotate the objects       sphere.yaw(-4);       cube.yaw(-4);       cylinder.yaw(-4);       torus.yaw(-4);       // update the camera position       updateCamera();       // Render the 3D scene       view.render();     }     // updates the camera position     private function updateCamera():void {             // If the mouse button has been clicked then update the camera position            if (doRotation) {                 // convert the change in mouse position into a change in camera angle         var dPitch:Number = (mouseY - lastMouseY) / 2;         var dYaw:Number = (mouseX - lastMouseX) / 2;                 // update the camera angles         cameraPitch -= dPitch;         cameraYaw -= dYaw;         // limit the pitch of the camera         if (cameraPitch <= 0) {           cameraPitch = 0.1;         } else if (cameraPitch >= 180) {           cameraPitch = 179.9;         }               // reset the last mouse position         lastMouseX = mouseX;         lastMouseY = mouseY;                 // reposition the camera         setCameraPosition();       }           }     // sets the camera position given pitch and yaw angles     private function setCameraPosition():void {       camera.y = CAMERA_ORBIT * Math.cos(cameraPitch * Math.PI / 180);       camera.x = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.cos(cameraYaw * Math.PI / 180);       camera.z = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.sin(cameraYaw * Math.PI / 180);             // keep the camera looking at the origin       camera.lookAt(new Number3D(0, 0, 0));     }     // called when mouse down on stage     private function onMouseDown(event:MouseEvent):void {       doRotation = true;       lastMouseX = event.stageX;       lastMouseY = event.stageY;     }     // called when mouse up on stage     private function onMouseUp(event:MouseEvent):void {       doRotation = false;     }       // called when mouse down on a 3D object     private function onMouseDownOnObject(event:MouseEvent3D):void {       var object:Object3D = event.object;       Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });     }         // called when a tween created in onMouseDownOnObject has terminated     private function goBack(object:Object3D):void {       Tweener.addTween(object, {y:0, time:2, transition:"easeOutBounce"});     }         // called when the window is resized     private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   }     }

The example embeds the same images as the last time so if you need them you can find them in my last post. Don’t forget that you’ll need to link to the Tweener library for this to compile correctly. The resulting Flash movie should be as below - click on the image below to load it. Now when you click on the background and move the mouse you should be able to rotate the scene (effectively moving the camera) and clicking on each object should make them bounce.

First of all, lets have a look how the we add the standard Flash MouseEvent to rotate the camera.

Starting with the constructor we add two listeners.

      // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);

When a mouse click is detected on the stage the function onMouseDown is called. When the button is released onMouseUp is called. These functions indicate when the camera movement should start and stop.

    // called when mouse down on stage     private function onMouseDown(event:MouseEvent):void {       doRotation = true;       lastMouseX = event.stageX;       lastMouseY = event.stageY;     }     // called when mouse up on stage     private function onMouseUp(event:MouseEvent):void {       doRotation = false;     }

To start the camera movement we set the boolean doRotation to true and to stop it we set it to false. The amount of rotation depends on how far the mouse moves during a single frame so we store the position of the mouse at every frame as well. The camera is then updated during the loop function

    private function loop(event:Event):void {             // rotate the group of objects       group.yaw(2);           // rotate the objects       sphere.yaw(-4);       cube.yaw(-4);       cylinder.yaw(-4);       torus.yaw(-4);       // update the camera position       updateCamera();       // Render the 3D scene       view.render();     }

Other than the call to updateCamera the rendering remains identical to the last example. The camera position is then calculated as follows.

    // updates the camera position     private function updateCamera():void {             // If the mouse button has been clicked then update the camera position            if (doRotation) {                 // convert the change in mouse position into a change in camera angle         var dPitch:Number = (mouseY - lastMouseY) / 2;         var dYaw:Number = (mouseX - lastMouseX) / 2;                 // update the camera angles         cameraPitch -= dPitch;         cameraYaw -= dYaw;         // limit the pitch of the camera         if (cameraPitch <= 0) {           cameraPitch = 0.1;         } else if (cameraPitch >= 180) {           cameraPitch = 179.9;         }               // reset the last mouse position         lastMouseX = mouseX;         lastMouseY = mouseY;                 // reposition the camera         setCameraPosition();       }           }

From the change in mouse position we calculate (arbitrarily) a change in angle from the y axis (pitch) and similarly around the y axis (yaw). The pitch is kept within limits to avoid the camera from going over the poles and hence changing the up direction of the camera. Once we’ve calculated the new values of pitch and yaw for the camera position we can convert these angles into x, y and z positions. This is done in setCameraPosition.

    // sets the camera position given pitch and yaw angles     private function setCameraPosition():void {       camera.y = CAMERA_ORBIT * Math.cos(cameraPitch * Math.PI / 180);       camera.x = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.cos(cameraYaw * Math.PI / 180);       camera.z = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.sin(cameraYaw * Math.PI / 180);             // keep the camera looking at the origin       camera.lookAt(new Number3D(0, 0, 0));     }

Note that we keep the camera at all times looking at the origin.

Moving on to the interaction with the 3D objects using the MouseEvent3D event, for this example I’m just going to look at the events when the mouse is clicked over an object. You’ll see that adding the listeners is very simple: at the end of the createScene a listener is added to each object.

      // add mouse listeners to all the 3D objects       sphere.addOnMouseDown(onMouseDownOnObject);       cube.addOnMouseDown(onMouseDownOnObject);       cylinder.addOnMouseDown(onMouseDownOnObject);       torus.addOnMouseDown(onMouseDownOnObject);       centerCube.addOnMouseDown(onMouseDownOnObject);

The rest of createScene is identical to the previous example. Now, whenever a user clicks on these objects, the function onMouseDownOnObject is called.

    // called when mouse down on a 3D object     private function onMouseDownOnObject(event:MouseEvent3D):void {       var object:Object3D = event.object;       Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });     }

The listener is called with the MouseEvent3D passed as an argument. From this we can easily obtain the Object3D for which the event occurred. We then invoke Tweener to perform an animation on the object.

Tweener contains static methods that allow us to modify object properties. In this case we simply modify the y position of the object so that it goes to a value of 200 in 1 second using the easeOutSine transition function. I’d recommend looking at the Tweener documentation on the different transition functions. The onComplete argument allows us to put the sphere back to its original position when the tween has completed.

    // called when a tween created in onMouseDownOnObject has terminated     private function goBack(object:Object3D):void {       Tweener.addTween(object, {y:0, time:2, transition:"easeOutBounce"});     }

This then uses another tween with an easeOutBounce transition to move the sphere back to the x-z plane.

And that’s all there is to it! With Tweener you can modify a number of different properties in parallel. For example you can change y and scale the object at the same time. The interface provided by Away3D is in any case very easy to use. Making a comparison to Papervision3D I find it a bit easier: with Papervision3D we need to say that the say that each individual material is interactive, as well as the View itself. This is surely to produce some performance optimisation. Other than this the interaction with objects is very similar for both engines and, I hope you’ll find, is straightforward to implement!

Next article:

Sunday, November 16th, 2008

First steps in Away3D : Part 3 - Texture mapping

In this post I’d like to show how to add textures to 3D objects rather than using plain colours which provides much more realistic (or at least interesting) rendering. You’ll find that with Away3D the concepts are very similar to those used in other 3D engines - for a comparison and some texture mapping references check out my equivalent tutorial for texture mapping in Papervision3D. I’m going to discuss texture mapping rather early on in this series of articles (earlier than for the Papervision3D series) simply because you’ll find that when come on to adding lighting and shading it opens up a much richer set of possibilities. But, I’m sure you’ll find that adding textures is a very simple process - at least, I hope so!

Previous articles summary :

Adding textures to objects in Away3D (as with most, if not all, 3D engines) is done by mapping coordinates on bitmap data to triangle vertices, known as texture mapping. More specifically, a technique known as uv mapping is used where u and v are the coordinates of the bitmap data which are then passed to the renderer for every triangle vertex. The following diagram tries to explain this for a pyramid. The uv coordinates are totally invented but you can see that for a single triangle the uv coordinates for every vertex are mapped onto the bitmap data (having a maximum u being equal to 1, and similarly for the maximum v) and the resulting triangle from the bitmap used to render the pyramid face.

In this example you’ll see that a number of different primitives are used (a cube, sphere, torus and cylinder). Each triangle used to produce these 3D objects has the uv coordinates for each vertex calculated. You can see in the link above for uv mapping the calculation necessary for a sphere. As you’ll see below, the image used to give texture to the sphere has been pre-calculated so that once mapped it produces a realistic image of the earth.

Ok, so on with the example code. As usual this is built on the previous example and there are not too many differences. We’ll go into more detail on what’s changed later. The code produces five primitives, all rotating about the origin and each one rotating about its individual y-axis.

package {   import away3d.cameras.Camera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.math.Number3D;   import away3d.core.utils.Cast;   import away3d.materials.BitmapMaterial;   import away3d.materials.TransformBitmapMaterial;   import away3d.primitives.Cube;   import away3d.primitives.Cylinder;   import away3d.primitives.Sphere;   import away3d.primitives.Torus;     import flash.display.Bitmap;   import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;     [SWF(backgroundColor="#000000")]     public class Example003 extends Sprite {     [Embed(source="/../assets/earth.jpg")] private var EarthImage:Class;     private var earthBitmap:Bitmap = new EarthImage();     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();     private static const ORBITAL_RADIUS:Number = 150;     private var scene:Scene3D;     private var camera:Camera3D;     private var view:View3D;       private var group:ObjectContainer3D;     private var sphere:Sphere;     private var cube:Cube;     private var centerCube:Cube;     private var cylinder:Cylinder;     private var torus:Torus;         public function Example003() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:25, focus:30, x:-200, y:400, z:-400});       camera.lookAt(new Number3D(0, 0, 0));             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;       addChild(view);     }     private function createScene():void {       // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);           // Create a new sphere object using a bitmap material representing the earth       var earthMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(earthBitmap));       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       group.addChild(sphere);       // Create a new cube object using a tiled bitmap material       var tiledAway3DMaterial:TransformBitmapMaterial = new TransformBitmapMaterial(Cast.bitmap(away3DBitmap), {repeat:true, scaleX:.5, scaleY:.5});       cube = new Cube({material:tiledAway3DMaterial, width:75, height:75, depth:75});       cube.z = -ORBITAL_RADIUS;       group.addChild(cube);         // Create a cylinder mapping the earth data again       cylinder = new Cylinder({material:earthMaterial, radius:25, height:100, segmentsW:16});       cylinder.x = -ORBITAL_RADIUS;       group.addChild(cylinder);         // Create a torus object and use a checkered bitmap material       var checkerBitmapMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(checkerBitmap));       torus = new Torus({material:checkerBitmapMaterial, radius:40, tube:10, segmentsT:8, segmentsR:16});       torus.z = ORBITAL_RADIUS;       group.addChild(torus);         // Create a new cube object using a smoothed, precise bitmap material       var away3DMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(away3DBitmap), {smooth:true, precision:2});       centerCube = new Cube({material:away3DMaterial, width:75, height:75, depth:75});       group.addChild(centerCube);     }         private function loop(event:Event):void {             // rotate the group of objects       group.yaw(2);           // rotate the objects       sphere.yaw(-4);       cube.yaw(-4);       cylinder.yaw(-4);       torus.yaw(-4);       // Render the 3D scene       view.render();     }     private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   } }

Copy the code above and compile it within Flex Builder 3 or Eclipse. The images I used are shown below - click on them and save them if you want to use the same ones.

Click on the image below to see the resulting flash movie. The images are embedded in the movie making it larger than the previous examples so it could take a couple of seconds to load.

The code follows the same style as the previous examples: initialisation of 3D elements, creation of scene and rendering loop. As usual I’ll just concentrate on what’s new.

Not really related to Away3D but important for projects (in Flex) using images is the ability to embed images in the compiled movie. As you can see the images are embedded in the class definition.

    [Embed(source="/../assets/earth.jpg")] private var EarthImage:Class;     private var earthBitmap:Bitmap = new EarthImage();     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();

Using the Embed meta-tag we can add binary object to the compiled movie. These can then be converted into usable graphic elements, as shown above for a Bitmap. As with the same example in Papervision3D, I’m not going to go into detail of embedding objects here, rather I’ll leave you with these couple of references that are very useful. The first one is at at assertTrue and the second is from the Flex livedocs.

You’ll note that the source is “../assets”. This is because I have my assets folder at the same level as the src folder - when the Actionscript is compiled, the root folder is the src folder. If you want to compile the above source then simply create an assets folder and put the image files in it.

Now that we have embedded the images and obtained bitmap data we can start to create texture mapped materials. This is done in createScene where some different methods of creating different textures is examined.

    private function createScene():void {       // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);           // Create a new sphere object using a bitmap material representing the earth       var earthMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(earthBitmap));       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       group.addChild(sphere);       // Create a new cube object using a tiled bitmap material       var tiledAway3DMaterial:TransformBitmapMaterial = new TransformBitmapMaterial(Cast.bitmap(away3DBitmap), {repeat:true, scaleX:.5, scaleY:.5});       cube = new Cube({material:tiledAway3DMaterial, width:75, height:75, depth:75});       cube.z = -ORBITAL_RADIUS;       group.addChild(cube);         // Create a cylinder mapping the earth data again       cylinder = new Cylinder({material:earthMaterial, radius:25, height:100, segmentsW:16});       cylinder.x = -ORBITAL_RADIUS;       group.addChild(cylinder);         // Create a torus object and use a checkered bitmap material       var checkerBitmapMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(checkerBitmap));       torus = new Torus({material:checkerBitmapMaterial, radius:40, tube:10, segmentsT:8, segmentsR:16});       torus.z = ORBITAL_RADIUS;       group.addChild(torus);         // Create a new cube object using a smoothed, precise bitmap material       var away3DMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(away3DBitmap), {smooth:true, precision:2});       centerCube = new Cube({material:away3DMaterial, width:75, height:75, depth:75});       group.addChild(centerCube);     }

As you can see, the creation of the 3D objects is very simple: just create a new primitive, pass it some initialisation arguments and create a new material for it. Of course, compared the previous example, the materials are now texture mapped. Two different materials are used in this example: the BitmapMaterial for simple texture mapping and TransformBitmapMaterial which gives us more options.

Starting with the Sphere, we use a simple BitmapMaterial and pass it bitmap data. The class Cast in Away3D provides useful transformation routines - in this case to return the BitmapData of the Bitmap object.

      var earthMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(earthBitmap));

The Sphere is then created using this material. All uv mapping is handled by the Sphere itself to render the image over the sphere triangles.

The next object is a Cube. Here I want to illustrate how to tile images over an object. Tiling allows us to repeat an image over a surface, for example imagine a scene with a grassy plain with the same grass image repeated a number of times. This allows us to keep the image size reasonable and have a texture mapped object that isn’t too pixelated.

      var tiledAway3DMaterial:TransformBitmapMaterial = new TransformBitmapMaterial(Cast.bitmap(away3DBitmap), {repeat:true, scaleX:.5, scaleY:.5});

To tile bitmap we need to use the TransformBitmapMaterial class. With this we can manipulate the image data, for example, here we scale it to half its size and indicate that it should be repeated. Scaling here implies that the maximum u and v coordinates of the texture become 0.5 rather than 1 hence we should see a two-by-two image drawn on each face of the cube. If the repeat:true initialisation parameter is skipped then the image is drawn only once… try recompiling without this to see the result.

The Cylinder and Torus objects use simple BitmapMaterials again just to show how the bitmaps are mapped to these primitives.

Finally the cube in the center shows an example of a texture map using a smoothed image and uses more precise perspective calculations when rendered. This link for texture mapping shows what happens for affine texture mapping: Away3D (as for Papervision3D) tries to optimise the calculation for rendering textures and this can result in a distorted image. You can see this in the above example for the tiled cube where the black lines appear to be wavy. The distortion can, however, be corrected but of course this produces a more cpu-intensive calculation.

      var away3DMaterial:BitmapMaterial = new BitmapMaterial(Cast.bitmap(away3DBitmap), {smooth:true, precision:2});

For the central cube we set smooth:true to have a less pixelated texture and we set precision:2 to indicate that the texture should be correctly rendered to within 2 pixels. A value of 0 turns off the precision calculation.

As an alternative to using more precise rendering of a texture we can also increase the number of triangles used so that the distortion becomes less evident. You can check out this example for Papervision3D that illustrates the result. Of course, increasing the number of triangles increases the render time but within limits it can produce a visually acceptable rendering quicker than a single precise texture mapping.

So that’s it for texture mapping at its simplest! As you can see there’s nothing too complicated in adding bitmaps to 3D objects but bear in mind that it is obviously more cpu-intensive that simple coloured objects. As with the previous articles I’d recommend having a look at the Away3D source - you’ll see that there are a lot of different types of materials in the away3D.materials package including the BitmapFileMaterial which loads an image from a URL, rather than embedding it in the movie. Anyway, hope this has been of use. As always, questions and comments are welcome!

Next article:

Saturday, November 15th, 2008

First steps in Away3D : Part 2 - Animation

Following from my previous post, I’d like to make the scene a bit more interesting by adding some animation. As you’ll see, not many modifications to the code are necessary. As with Part 1, I’m basing this example on a previous Papervision3D example that you can find in First steps in Papervision3D : Part 3.

Previous articles summary :

Using the same objects as before (a Sphere and LineSegments displaying the x, y and z axes), my objective is to rotate all of them about the origin and individually rotate the sphere. Rotation in Away3D is very easy to achieve. These objects inherit from a base class called Object3D as do all 3D object displayed in a Scene in Away3D. This class provides a number of useful functions to rotate, translate and scale an object. The simplest way to rotate an object is to use the pitch, yaw and roll functions which rotate an object about its local x, y and z axes respectively.

So, lets dive right in and take a look at the code. As I mentioned before, this is based on the previous example and very little modifications have been made. As before I’m using eclipse with the Flex Builder 3 plugin to compile the examples: take a look at my previous post if you’re new to eclipse and want to see how to set up the projects. Otherwise, create a new ActionScript class, call it Example002 and cut and paste the following code.

package {   import away3d.cameras.Camera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.base.Vertex;   import away3d.core.math.Number3D;   import away3d.materials.WireColorMaterial;   import away3d.materials.WireframeMaterial;   import away3d.primitives.LineSegment;   import away3d.primitives.Sphere;     import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;     [SWF(backgroundColor="#000000")]     public class Example002 extends Sprite {     private var scene:Scene3D;     private var camera:Camera3D;     private var view:View3D;       private var group:ObjectContainer3D;     private var sphere:Sphere;         public function Example002() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:15, focus:30, x:100, y:300, z:-200});       camera.lookAt(new Number3D(0, 0, 0));             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;       addChild(view);     }     private function createScene():void {       // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);             // Create a new sphere object using a wirecolor material       var sphereMaterial:WireColorMaterial = new WireColorMaterial(0x5500FF, {wirecolor:0xFF9900});       sphere = new Sphere({material:sphereMaterial, radius:50, segmentsW:10, segmentsH:10});       // Position the sphere and add it to the group       sphere.x = -100;       group.addChild(sphere);         // Create a origin vertex       var origin:Vertex = new Vertex(0, 0, 0);       // Create the red-coloured x-axis with a width of 2 and add it to the group       var xAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0xFF0000, {width:2})});       xAxis.start = origin;       xAxis.end = new Vertex(100, 0, 0);       group.addChild(xAxis);           // Create the green-coloured y-axis with a width of 2 and add it to the group       var yAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0x00FF00, {width:2})});       yAxis.start = origin;       yAxis.end = new Vertex(0, 100, 0);       group.addChild(yAxis);           // Create the blue-coloured z-axis with a width of 2 and add it to the group       var zAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0x0000FF, {width:2})});       zAxis.start = origin;       zAxis.end = new Vertex(0, 0, 100);       group.addChild(zAxis);       }         private function loop(event:Event):void {             // rotate the group of objects       group.yaw(5);           // rotate the sphere       sphere.yaw(-10);           // Render the 3D scene       view.render();     }     private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   } }

This should produce a scene that is rotated about the y-axis with a sphere that rotates in the opposite direction, also about its y-axis. Click on the image below to see the final result.

As always, lets take a look at the code in more detail. Since there are so many similarities with the previous example I won’t go into detail for everything, just what is new.

Starting with the constructor, you’ll notice that I’ve added a resize listener that calls the method onResize.

    private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   } }

This is used to ensure that if a user resizes the browser or flash player that the View is automatically resized to take up the full stage area. Its not really much of a 3D element of the scene but its important not to forget it!

The rest of the code takes the same form as before: initialise the 3D elements, create the scene and re-render the scene at every new frame. As you’ll see in init3D I’ve modified the camera slightly.

      // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:15, focus:30, x:100, y:300, z:-200});       camera.lookAt(new Number3D(0, 0, 0));

The camera is now in a different position, but more importantly I’ve added a call to camera.lookAt. Previously the camera was looking directly along the z-axis: now I’ve told it to look at the origin. You can similarly call the functions tilt and pan which (as it says in the code comments for the Camera3D class) is like someone nodding and shaking their head.

Moving on to the scene creation in createScene, I’ve introduced a new element used to group the objects: an ObjectContainer3D.

      // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);

This object is not itself visible on the scene but allows us to add children to it and translate, rotate and scale these children all together. So, rather than adding each individual object to the scene, they are now added to the group as is, for example, the sphere.

      // Create a new sphere object using a wirecolor material       var sphereMaterial:WireColorMaterial = new WireColorMaterial(0x5500FF, {wirecolor:0xFF9900});       sphere = new Sphere({material:sphereMaterial, radius:50, segmentsW:10, segmentsH:10});       // Position the sphere and add it to the group       sphere.x = -100;       group.addChild(sphere);  

The lines are similarly added to the group. So that’s the only difference to the scene creation itself (other than the sphere now being a different colour). Now we come to adding the animation.

To add animation we simply need to modify object parameters at each frame and then re-render the scene. As I mentioned before I simple rotate the ObjectContainer3D and the Sphere itself and this is done in the loop function that is called at the start of every new frame.

    private function loop(event:Event):void {             // rotate the group of objects       group.yaw(5);           // rotate the sphere       sphere.yaw(-10);           // Render the 3D scene       view.render();     }

As you can see there’s really not much to it: simple rotate the group about its y axis, and the same for the sphere itself. The call the view.render then updates what we see on the screen.

And that’s it! To get a feel for the animation, try changing the yaw method to roll or pitch, or try adding some translation. I’d recommend having a look at the Away3D code to see what else is available - you’ll find that there are a lot of possibilities! Anyway, I hope this has been a useful step in making more interesting 3D scenes in Away3D!

Next article:

Wednesday, November 12th, 2008

First steps in Away3D : Part 1 - Getting started

In the same spirit as the First steps in Papervision3D series of articles, I’m documenting my own progress in learning about this 3D engine for Flash. I hope as well that this provides a useful tutorial for others getting started with Away3D. I guess what I’m trying to say is that this is by no means a definitive guide to Away3D! This is my first day as well so if you find mistakes or have suggestions then please let me know!

Before starting I’d like to post a couple of links to a series of articles that I found very interesting and useful for beginners learning the basics of 3D programming. The first one one, Flash 3D Basics, provides a very good overview of the important elements for any 3D development. The second one is aimed directly at Away3D, but is relevant also to Papervision3D, and discusses the two main classes for any Away3D flash movie: The View and The Scene. You’ll see both of these being used below. Anyway, I’d really recommend taking a look at them.

For this post I’m really starting at the very beginning. My aim is simply to draw 3D shapes on the screen, exactly as I did for First steps in Papervision3D : Part 1. For this I’m assuming that you are using Flex Builder 3, or the Flex Builder 3 plugin for eclipse, as I am. I’m also assuming that you have Away3D built and ready to link to. My previous article on downloading and installing Away3D in eclipse might help if this isn’t the case.

For those of you who are new to using Flex Builder 3, we need to first of all create a new project and then set it’s build path to that of the Away3D source.

In eclipse (or Flex Builder 3), from the File menu, select New and then ActionScript project. You’ll see a New ActionScript Project window appear.

Type in a project name (for example I named it Tartiflop) and click on Finish. We then need to configure the project to use either the Away3D.swf library that I showed how to create before, or we link directly to the Away3D source. I prefer the second method simply because it gives me a more convenient access to the library source by navigating within eclipse. To do this, select the new project and right-click. Select Properties from the drop-down menu. Select the ActionScript Build Path on the left.

Select the Library path tab and click on Add Project….

Select the Away3D Flex Library project and click on OK. You’ll see that the library has now been added into the list of build path libraries.

We’re now ready to start creating our first Away3D flash movie! You’ll notice that the new project wizard of eclipse has automatically created an ActionScript class called Tartiflop.as. We don’t need this so you can delete it. For this post the class is called Example001.as so you’ll need to create a new ActionScript. Right-click on src in the Tartiflop project in the Flex Navigator tree. Select New and then ActionScript Class. Name the new class Example001 and click Finish.

The following code draws a sphere and three lines showing the x, y and z axes. Cut and paste the code below (we’ll go into the details of how it works later), save and it should hopefully compile without any errors!

package {   import away3d.cameras.Camera3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.base.Vertex;   import away3d.materials.WireColorMaterial;   import away3d.materials.WireframeMaterial;   import away3d.primitives.LineSegment;   import away3d.primitives.Sphere;     import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;     [SWF(backgroundColor="#000000")]     public class Example001 extends Sprite {     private var scene:Scene3D;     private var camera:Camera3D;     private var view:View3D;         public function Example001() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;             // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise Event loop       this.addEventListener(Event.ENTER_FRAME, loop);        }     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:20, focus:30, x:-100, y:-100, z:-500});             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;       addChild(view);     }     private function createScene():void {       // First object : a sphere             // Create a new material for the sphere : simple white wireframe       var sphereMaterial:WireColorMaterial = new WireColorMaterial(0x000000, {wirecolor:0xFFFFFF});       // Create a new sphere object using wireframe material, radius 50 with       // 10 horizontal and vertical segments       var sphere:Sphere = new Sphere({material:sphereMaterial, radius:50, segmentsW:10, segmentsH:10});       // Position the sphere (default = [0, 0, 0])       sphere.x = -100;       scene.addChild(sphere);         // Second object : x-, y- and z-axis         // Create a origin vertex       var origin:Vertex = new Vertex(0, 0, 0);       // Create the red-coloured x-axis with a width of 2       var xAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0xFF0000, {width:2})});       xAxis.start = origin;       xAxis.end = new Vertex(100, 0, 0);       scene.addChild(xAxis);           // Create the green-coloured y-axis with a width of 2       var yAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0x00FF00, {width:2})});       yAxis.start = origin;       yAxis.end = new Vertex(0, 100, 0);       scene.addChild(yAxis);           // Create the blue-coloured z-axis with a width of 2       var zAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0x0000FF, {width:2})});       zAxis.start = origin;       zAxis.end = new Vertex(0, 0, 100);       scene.addChild(zAxis);       }         private function loop(event:Event):void {       // Render the 3D scene       view.render();     }   } }

To see the final result, right-click again on the Tartiflop project and select Run As and Flex Application. What you should see is the following: click on the image below to see the real flash movie (its not much more interesting than the image though!).

Let’s go into more detail with the code. I’m taking a look at this with the point of view of someone who has been using Papervision3D and its interesting to look at the similarities and differences between the two libraries. If you compare the code to the equivalent in Papervision3D, you can see that the codes resemble a lot.

The constructor of Example001 is identical to that of the equivalent example in Paperivision3D: the stage parameters are set so that the scene is scaled correctly, the 3D elements are initialised, the scene is created and then we add a frame-enter event listener.

Example001 inherits from the Sprite class as is indeed possible with Papervision3D. Papervision3D however provides a BasicView class where the scene, camera, view and renderer are all encapsulated (see part 2 of the Papervision3D series for example). Also the stage scaling is automatically encapsulated in the BasicView class.

    public function Example001() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;             // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise Event loop       this.addEventListener(Event.ENTER_FRAME, loop);        }

However, as we can see, its not very complicated to initialise all the 3D elements individually - as we do in the init3D() function.

    private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:20, focus:30, x:-100, y:-100, z:-500});             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;       addChild(view);     }

We first of all create a Scene3D object which is used later to contain all of our rendered 3D elements. Secondly we create a Camera3D which sets up all our viewing parameters and finally we create a View3D object which provides us with our window through which we observe the scene.

Interesting to note is the way we can create Away3D objects. Unlike Papervsion3D which has well defined constructors taking specific arguments, Away3D allows us to pass an array of initialisation parameters. One good point is that this makes the code more concise, however, personally, I feel that it is less intuitive: with Papervision3D I felt I could determine relatively quickly what was needed to construct an object but here you need to search more within a class to obtain useful information. Having said that I have to admit that I haven’t yet looked at the Away3D documentation! I’m someone who is too excited to get his hands dirty before reading the manual!

With respect to the camera, unlike Papervision3D it is not possible to modify the field of view (or fov) directly and instead we need to manipulate the zoom and focus of the camera (see my post on the relationship between all three and also this really good article explaining the Away3D camera). Having an OpenGL background I find the field of view more intuitive however this is just a personal opinion.

After the 3D base elements have been created we can move on to creating the scene. This, as is fairly obvious, is done in the createScene() function.

    private function createScene():void {       // First object : a sphere             // Create a new material for the sphere : simple white wireframe       var sphereMaterial:WireColorMaterial = new WireColorMaterial(0x000000, {wirecolor:0xFFFFFF});       // Create a new sphere object using wireframe material, radius 50 with       // 10 horizontal and vertical segments       var sphere:Sphere = new Sphere({material:sphereMaterial, radius:50, segmentsW:10, segmentsH:10});       // Position the sphere (default = [0, 0, 0])       sphere.x = -100;       scene.addChild(sphere);         // Second object : x-, y- and z-axis         // Create a origin vertex       var origin:Vertex = new Vertex(0, 0, 0);       // Create the red-coloured x-axis with a width of 2       var xAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0xFF0000, {width:2})});       xAxis.start = origin;       xAxis.end = new Vertex(100, 0, 0);       scene.addChild(xAxis);           // Create the green-coloured y-axis with a width of 2       var yAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0x00FF00, {width:2})});       yAxis.start = origin;       yAxis.end = new Vertex(0, 100, 0);       scene.addChild(yAxis);           // Create the blue-coloured z-axis with a width of 2       var zAxis:LineSegment = new LineSegment({material:new WireframeMaterial(0x0000FF, {width:2})});       zAxis.start = origin;       zAxis.end = new Vertex(0, 0, 100);       scene.addChild(zAxis);       }

The scene is very simple: no lighting and just two stationary types of objects. As with Papervision3D, most objects provide only the vertices of a given shape: the rendering (what we see on the screen) is done through the material.

In this example we use two types of materials for two different types of shapes. This is different to Papervision3D where any material seems to be usable with any object. Here, the two shapes belong to two different families: an AbstractPrimitive and an AbstractWirePrimitive. Each one uses a material also belonging to a different family: an ITriangleMaterial and an ISegmentMaterial respectively. If we give the wrong type of material to a particular primitive then it is not rendered.

The two shapes used are a Sphere (an AbstractPrimitive) and a LineSegment (an AbstractWirePrimitive). We therefore give them two different types of materials. In this example I’ve used a WireColorMaterial and a WireframeMaterial respectively. The WireColorMaterial is created with a face color (black in this case) and a wire color is passed in the list of initialisation arguments (being white). The wireframe material is simply white but I’ve passed a width of 2 in the initialisation parameters.

The sphere is created first, taking a list of initialisation parameters including the sphere material. It should be noted that these parameters can be set afterwards, not necessarily in the constructor. I then create 3 lines for each axis, each one with a different color wireframe material. The lines are then given two vertices to define their start and end positions. Each 3D object is added to the scene so that they are rendered.

As a first impression of Away3D, I actually find that the construction of these primitive objects is more concise than for Papervision3D. Looking through the source directory of Away3D I also get the impression that there is more choice of primitives compared to Papervision3D.

Finally for this example, a small function is used to render the scene. We added in the constructor a frame-enter event listener called loop.

    private function loop(event:Event):void {       // Render the 3D scene       view.render();     }

Very simply, we call the function render of view to redraw the scene. This is another difference to Papervision3D: Papervision3D has a specific Renderer class used to draw the scene.

And that’s it! A very simple example of rendering a 3D scene in Away3D. Compared to Papervision3D, for something this simple, there really isn’t a huge difference. Overall I feel that Away3D is more concise but maybe loses degree of simplicity using the parameters array rather than having explicit constructor arguments… but that’s just a personal point of view though.

One surprise is the difference in file sizes between Papervision3D and Away3D. In Away3D the flash animation come to 136KB whereas in Papervision3D its at only 82KB. Okay, so neither are huge and maybe Away3D is a bigger library… but I’m interested to see how this compares for more complex examples.

Anyway, that’s it for this example. For me too its a first step - I just wanted to see what the Away3D library was like and how easy it was to convert from a Papervision3D source to an Away3D one. I hope to continue along the same theme to produce more complex examples over the coming weeks. As always, comments and suggestions as are always welcome so please don’t hesitate!

Next article: