-->

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

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:

  • onMouseDown : called when the mouse button is pressed over the object
  • onMouseUp : called when the mouse button is released over the object
  • onMouseMove : called when the mouse moves over the object
  • onMouseOver : called when the mouse enters the object
  • onMouseOut : called when the mouse leaves the object

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:

-->
Saturday, September 6th, 2008

First steps in Papervision3D : Part 8 - Movie materials

Previous articles summary :

This article continues investigating the wide range of materials available in Papervision3D and probably represents the last one of this theme. Whereas the previous examples tried to improve the realism of a 3D scene, this article takes a look at the more dynamic materials available with Papervision3D.

First, apologies for the length of this entry : actually, the more you look at what is available in Papervision3D the more you realise how much it offers! The aim here is to look at MovieMaterials. These offer the ability of having interactive and dynamic surfaces on 3D objects either with Flash movies or Flash video streams. Summarizing this to a few lines probably wouldn’t do it justice so I’ve tried to illustrate here some of the more exciting features offered… and even in doing so am probably still missing a lot!

Anyway, this article introduces two new kinds of materials: MovieMaterial and VideoStreamMaterial (which inherits from the former).

The MovieMaterial allows us to create a material using a pre-existing Flash movie (embedded in a Papervision3D movie) or simply from any MovieClip / Sprite inheriting class instance. Papervision3D provides mapping functions that allow us to interact with these Flash movies with mouse clicks and movements even in a 3D environment.

The VideoStreamMaterial, as its name implies, allows us to stream flash video streams (flv files) onto a 3D object.

The example shown in this article includes these three possibilities including: a flash video stream from a given URL, an embedded standard (non-3D) Flash movie and an example showing a Papervision3D scene being animated as a material in another Papervision3D scene… did I mention before that this article might be quite long?!

So, what we essentially have here are three Actionscript classes: the main 3D scene, a non-3D Sprite-inheriting class and another, secondary Papervision3D scene. I’m only going to discuss the first one here but I’ll include the source for the others at the end.

The main source code is shown below. The example shows the three movie materials projected onto a specific face of three projectors (Cube instances), all rotating about the y-axis. The projectors can be double-clicked to enlarge them, stop them from rotating and provide a more simple means of interacting with them. Double-clicking again puts them back with the others. The whole scene can be rotated by clicking on the background and moving the mouse. The code, as warned, is a little longer than hoped for, but we’ll look at each part in more details afterwards and really there’s nothing very complicated there. I’m using the Tweener library again to provide smoother visual effects (see Part 3 - animation - for more details).

package {     import caurina.transitions.Tweener;     import flash.display.MovieClip;   import flash.events.Event;   import flash.events.MouseEvent;   import flash.media.Video;   import flash.net.NetConnection;   import flash.net.NetStream;     import org.papervision3d.core.proto.MaterialObject3D;   import org.papervision3d.events.InteractiveScene3DEvent;   import org.papervision3d.lights.PointLight3D;   import org.papervision3d.materials.MovieMaterial;   import org.papervision3d.materials.VideoStreamMaterial;   import org.papervision3d.materials.shadematerials.FlatShadeMaterial;   import org.papervision3d.materials.utils.MaterialsList;   import org.papervision3d.objects.DisplayObject3D;   import org.papervision3d.objects.primitives.Cube;   import org.papervision3d.view.BasicView;   [SWF(backgroundColor="#222222")]   public class Example008 extends BasicView {       private static const ORBITAL_RADIUS:Number = 400;     [Embed(source="/../assets/DrawTool.swf")]     private var DrawTool:Class;     private var exampleMovie:MovieClip;     private var videoURL:String = "http://www.tartiflop.com/pv3d/FirstSteps/Radiohead_HOC.flv";     private var video:Video;     private var stream:NetStream;     private var connection:NetConnection;     private var objectGroup:DisplayObject3D;     private var light:PointLight3D;     private var currentActiveObject:DisplayObject3D = null;         private var projectors:Array = new Array();         private var doRotation:Boolean = false;     private var canRotate:Boolean = true;     private var lastMouseX:int;     private var lastMouseY:int;     private var cameraPitch:Number = 60;     private var cameraYaw:Number = -60;         public function Example008() {       super(0, 0, true, true);       // Initialise Papervision3D       init3D();       // create video stream for VideoStreamMaterial       createVideoStream();       // create the 3D Objects       createScene();       // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Start rendering the scene       startRendering();     }         private function init3D():void {       // position the camera       camera.z = -1000;       camera.fov = 60;       camera.orbit(cameraPitch, cameraYaw);     }     private function createVideoStream():void {       // Create a NetConnection. 2-way connection not necessary: connect to null       connection = new NetConnection();       connection.connect(null);       // Create a new NetStream to obtain the flv stream. Ignore client messages so use a simple Object       stream = new NetStream(connection);       stream.client = new Object();             // create a new video player       video = new Video();             // start streaming the video from the given URL and play it on the video player       stream.play(videoURL);       video.attachNetStream(stream);     }     private function createScene():void {       // Specify a point light source and its location       light = new PointLight3D();       light.x = 400;       light.y = 1000;       light.z = -400;       // Create a 3D object to group the projectors       objectGroup = new DisplayObject3D();       // Create a new video stream material with precise rendering.       var videoMaterial:VideoStreamMaterial = new VideoStreamMaterial(video, stream, true);       addProjector(videoMaterial);                 // Create a new flash movie material from an actionscript class (not transparent, animated and precise rendering)       var movieMaterial1:MovieMaterial = new MovieMaterial(new Example006b(), false, true, true);       addProjector(movieMaterial1);       // Create a new flash movie material from an embedded flash movie (not transparent, animated and precise rendering)       var movieMaterial2:MovieMaterial = new MovieMaterial(new DrawTool(), false, true, true);       addProjector(movieMaterial2);           // add the object group and light       scene.addChild(objectGroup);       scene.addChild(light);       // set up the projector positions in the scene       organiseProjectors();     }         private function addProjector(material:MovieMaterial):void {       // materials are smooth rendred, interactive and resize to the 3D object.       material.smooth = true;       material.interactive = true;       material.allowAutoResize = true;       // simple flat shaded material as default for the projector       var flatShadedMaterial:MaterialObject3D = new FlatShadeMaterial(light, 0x554D33, 0x1A120C);       flatShadedMaterial.interactive = true;             // Material list with MovieMaterial used on the front, the rest being flat shaded       var materialList:MaterialsList = new MaterialsList({"all":flatShadedMaterial, "front":material});       // create a new interactive projector       var projector:Cube = new Cube(materialList, 320, 10, 240);       projector.addEventListener(InteractiveScene3DEvent.OBJECT_DOUBLE_CLICK, onMouseDoubleClickOnObject);       projector.addEventListener(InteractiveScene3DEvent.OBJECT_OVER, onMouseOverObject);       projector.addEventListener(InteractiveScene3DEvent.OBJECT_OUT, onMouseOutObject);       // add the projector to the scene, being part of the object group       objectGroup.addChild(projector);             // store projector in an array       projectors.push(projector);     }         private function organiseProjectors():void {       // calculate angle between projectors       var theta:Number = 360 / projectors.length;             // set up each projector so that they are distributed in a circle and facing outwards       for (var i:int = 0; i < projectors.length; i++) {         var projector:Cube = projectors[i];                 // specifc angle for projector         var angle:Number = i * theta - 180;         var angleRadians:Number = angle * 2 * Math.PI / 360.;         // position of projector         var x:Number = Math.sin(angleRadians) * ORBITAL_RADIUS;         var z:Number = Math.cos(angleRadians) * ORBITAL_RADIUS;         // create tween to position, rotate and scale projector smoothly over 1 second         Tweener.addTween(projector, {x:x, y:-150, z:z, rotationY:angle, scale:0.8, time:1, transition:"linear" });       }     }         override protected function onRenderTick(event:Event=null):void {       // rotate the object group: angle kept between 0 and 360 degrees       objectGroup.rotationY += 1;       if (objectGroup.rotationY > 360) {         objectGroup.rotationY -= 360;       }             // if an object is active (double clicked) rotate it in the opposite direction       // to the group so that it is stationary       if (currentActiveObject != null) {         currentActiveObject.rotationY -=1;         if (currentActiveObject.rotationY < 0) {           currentActiveObject.rotationY += 360;         }       }             // If the mouse button has been clicked then update the camera position            if (doRotation && canRotate) {         // 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         camera.orbit(cameraPitch, cameraYaw);       }             // call the renderer       super.onRenderTick(event);     }     // 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 double clicked on a projector     private function onMouseDoubleClickOnObject(event:InteractiveScene3DEvent):void {       var object:DisplayObject3D = event.displayObject3D;             // determine if the object is to be activated (placed in center) or deactivated       if (object == currentActiveObject) {         deactivate(object);       } else {         activate(object);       }     }         // disable camera rotation when mouse is over a projector     private function onMouseOverObject(event:InteractiveScene3DEvent):void {       canRotate = false;     }         // re-enable camera rotation when mouse is out of a projector     private function onMouseOutObject(event:InteractiveScene3DEvent):void {       canRotate = true;     }         // places a projector in the center     private function activate(object:DisplayObject3D):void {       // remove projector from rotating projectors array       projectors.splice(projectors.indexOf(object), 1);             // if a projector is active already, put it back in the array of rotating projectors       if (currentActiveObject != null) {         projectors.push(currentActiveObject);       }             // create a tween to place selected projector in the center       Tweener.addTween(object, {y:100, x:0, z:0, rotationY:-objectGroup.rotationY, scale:2, time:1, transition:"linear" });       currentActiveObject = object;       // re-organise the other projectors       organiseProjectors();          }         // puts an activated projector back into the main pack of rotating projectors     private function deactivate(object:DisplayObject3D):void {       // put the projector back into the rotating projectors array       projectors.push(currentActiveObject);       currentActiveObject = null;            // re-organise all projectors       organiseProjectors();          }       } }

All of this provides the following Flash movie. As mentioned above, double-click on an object to activate it (this actually just means that the object is magnified and stops spinning - it doesn’t change any of the object characteristics). Double-click on an activated one to deactivate it (put it back with the others). Two projectors allow for user interactions at any point in time: you can draw on one and rotate the 3D scene on the other. The final projector streams House Of Cards by Radiohead (another Paperivision3D example?!). The whole scene can be rotated by clicking on the background and moving the mouse. Click on the image below to see it all in action.

So, as with the other articles in this series lets take a look at how the scene is constructed step-by-step. As usual, the code is organised in more of less the same way as previous examples. The main difference comes from creating and attaching a video stream and modifying the animation and object interaction.

Let’s start with the constructor. The only difference here is the initialisation of the video stream. If you take a look at the source code for the VideoStreamMaterial you’ll see that it takes two objects: a Video and a NetStream. These are pure Actionscript objects necessary for streaming the data and displaying it. The Flex language reference for NetConnection came in handy here to see what these objects do and how to create them. A slightly cut-down method is used here but it remains in principal the same.

    private function createVideoStream():void {       // Create a NetConnection. 2-way connection not necessary: connect to null       connection = new NetConnection();       connection.connect(null);       // Create a new NetStream to obtain the flv stream. Ignore client messages so use a simple Object       stream = new NetStream(connection);       stream.client = new Object();             // create a new video player       video = new Video();             // start streaming the video from the given URL and play it on the video player       stream.play(localVideoURL);       video.attachNetStream(stream);     }

As you can see in the example shown in the Flex livedocs, there are ways to listen to events occurring during the streaming but for this example I’ve just done a minimum to restrict the length of the code a bit.

Next we come to the scene creation. This again is based on previous examples so we have a light source, an object group to simplify the rotation of a number of objects and then the individual 3D objects, each one with a different MovieMaterial.

    private function createScene():void {       // Specify a point light source and its location       light = new PointLight3D();       light.x = 400;       light.y = 1000;       light.z = -400;       // Create a 3D object to group the projectors       objectGroup = new DisplayObject3D();       // Create a new video stream material with precise rendering.       var videoMaterial:VideoStreamMaterial = new VideoStreamMaterial(video, stream, true);       addProjector(videoMaterial);                 // Create a new flash movie material from an actionscript class (not transparent, animated and precise rendering)       var movieMaterial1:MovieMaterial = new MovieMaterial(new Example006b(), false, true, true);       addProjector(movieMaterial1);       // Create a new flash movie material from an embedded flash movie (not transparent, animated and precise rendering)       var movieMaterial2:MovieMaterial = new MovieMaterial(new DrawTool(), false, true, true);       addProjector(movieMaterial2);           // add the object group and light       scene.addChild(objectGroup);       scene.addChild(light);       // set up the projector positions in the scene       organiseProjectors();     }

As you can see the three MovieMaterials (VideoStreamMaterial inherits from this) are simple to create. Firstly the VideoStreamMaterial takes the Video and NetStream we created just before and I’ve chosen precise rendering to minimise perspective distortions. The other two MovieMaterials take in one case a Actionscript object and an embedded Flash movie in the other (see the start of the class definition to see the embedding, which is identical to how we embedded images in previous examples). The three boolean values are associated with transparent, animated and precise rendering arguments. So since the Flash movie objects are animated we need to specify true for the animated argument to ensure that the scenes are updated.

The scene is then populated with the object group (containing the 3D objects) and the light. The positioning of the 3D objects is delegated to the organiseProjectors function which we’ll come to shortly.

In this example I’m using the Cube primitive. Each one has a specific face (the “front”) showing the MovieMaterial and since each one has essentially the same characteristics I’ve factorised the code to initialise each one identically.

    private function addProjector(material:MovieMaterial):void {       // materials are smooth rendred, interactive and resize to the 3D object.       material.smooth = true;       material.interactive = true;       material.allowAutoResize = true;       // simple flat shaded material as default for the projector       var flatShadedMaterial:MaterialObject3D = new FlatShadeMaterial(light, 0x554D33, 0x1A120C);       flatShadedMaterial.interactive = true;             // Material list with MovieMaterial used on the front, the rest being flat shaded       var materialList:MaterialsList = new MaterialsList({"all":flatShadedMaterial, "front":material});       // create a new interactive projector       var projector:Cube = new Cube(materialList, 320, 10, 240);       projector.addEventListener(InteractiveScene3DEvent.OBJECT_DOUBLE_CLICK, onMouseDoubleClickOnObject);       projector.addEventListener(InteractiveScene3DEvent.OBJECT_OVER, onMouseOverObject);       projector.addEventListener(InteractiveScene3DEvent.OBJECT_OUT, onMouseOutObject);       // add the projector to the scene, being part of the object group       objectGroup.addChild(projector);             // store projector in an array       projectors.push(projector);     }

Each MovieMaterial is smoothed (to appear less pixelated), made interactive (so that the 3D object responds to mouse events) and auto-resized so that they resize automatically to the cube dimensions. The other five faces of the cube are covered in a simple flat-shaded material (as seen in Part 4) which is also interactive. The cube is then constructed with a MaterialList containing these two different materials. Event listeners are then added to the cube so that it responds to double-click events (to activate and deactivate it) and mouse over and out events which, as we’ll see later, are used to restrict the stage mouse listeners for rotating the scene (essentially they stop the scene from rotating when a user is interacting with one of the 3D objets).

The new cube is then added to the object group (so that it is rendered) and stored in an Array to allow us to access it later.

As you see in the example, the non-activated projectors are spaced evenly in a circle, facing outwards (the rotation comes simply from a rotation of the object group, handled separately). The function organiseProjectors performs the necessary calculations and animation.

    private function organiseProjectors():void {       // calculate angle between projectors       var theta:Number = 360 / projectors.length;             // set up each projector so that they are distributed in a circle and facing outwards       for (var i:int = 0; i < projectors.length; i++) {         var projector:Cube = projectors[i];                 // specifc angle for projector         var angle:Number = i * theta - 180;         var angleRadians:Number = angle * 2 * Math.PI / 360.;         // position of projector         var x:Number = Math.sin(angleRadians) * ORBITAL_RADIUS;         var z:Number = Math.cos(angleRadians) * ORBITAL_RADIUS;         // create tween to position, rotate and scale projector smoothly over 1 second         Tweener.addTween(projector, {x:x, y:-150, z:z, rotationY:angle, scale:0.8, time:1, transition:"linear" });       }     }

This function quite simply calculates the angle between each projector (Cube instance) and positions them in the x-z plane accordingly. To smoothly position each of them I’ve used a linear tween to modify the x, y, z, rotationY and scale properties of each of them, taking one second to animate. Thanks to Tweener this is very simple to perform!

Next we come to the onRenderTick function which updates the scene at every movie frame. This is essentially the same as for previous examples in the series

    override protected function onRenderTick(event:Event=null):void {       // rotate the object group: angle kept between 0 and 360 degrees       objectGroup.rotationY += 1;       if (objectGroup.rotationY > 360) {         objectGroup.rotationY -= 360;       }             // if an object is active (double clicked) rotate it in the opposite direction       // to the group so that it is stationary       if (currentActiveObject != null) {         currentActiveObject.rotationY -=1;         if (currentActiveObject.rotationY < 0) {           currentActiveObject.rotationY += 360;         }       }             // If the mouse button has been clicked then update the camera position            if (doRotation && canRotate) {         // 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         camera.orbit(cameraPitch, cameraYaw);       }             // call the renderer       super.onRenderTick(event);     }

The object group is rotated as in previous examples. This time I’m using the rotationY property rather than the function yaw to have a better control of the angle of rotation. The activated object is spun in the opposite direction at the same time so that it is effectively stationary.

The camera rotation is essentially the same as before except that we use the canRotate boolean value to restrict the rotation when the mouse is over a 3D object.

The rest of the code is essentially to handle the mouse events. The onMouseDown, onMouseUp are the same as before to initiate and stop the scene rotation. onMouseOverObject and onMouseOutObject add to this by limiting the rotation when the user is interacting with a 3D object.

To activate and deactivate a projector the double-click event on a 3D object is used.

    // called when mouse double clicked on a projector     private function onMouseDoubleClickOnObject(event:InteractiveScene3DEvent):void {       var object:DisplayObject3D = event.displayObject3D;             // determine if the object is to be activated (placed in center) or deactivated       if (object == currentActiveObject) {         deactivate(object);       } else {         activate(object);       }     }

Simply, if the object clicked is the current active object then we deactivate it. If not we activate it.

    // places a projector in the center     private function activate(object:DisplayObject3D):void {       // remove projector from rotating projectors array       projectors.splice(projectors.indexOf(object), 1);             // if a projector is active already, put it back in the array of rotating projectors       if (currentActiveObject != null) {         projectors.push(currentActiveObject);       }             // create a tween to place selected projector in the center       Tweener.addTween(object, {y:100, x:0, z:0, rotationY:-objectGroup.rotationY, scale:2, time:1, transition:"linear" });       currentActiveObject = object;       // re-organise the other projectors       organiseProjectors();          }

When activating a projector, it is removed from the Array of spinning projectors. If another projector is already activated then we put it back into this group. A simple linear tween is then used to reposition the newly activated projector in the center and to rescale it so that it is bigger than the others. We then recall organiseProjectors to reposition the remaining projectors around a circle.

Deactivating a projector simply involves putting it back into the Array and repositioning all of them around a circle.

    // puts an activated projector back into the main pack of rotating projectors     private function deactivate(object:DisplayObject3D):void {       // put the projector back into the rotating projectors array       projectors.push(currentActiveObject);       currentActiveObject = null;            // re-organise all projectors       organiseProjectors();          }

So that’s all there is to it! Really Papervision3D and Tweener do all the complicated work to display and animated the 3D scene: all that’s new here is the creation of the movie materials. Once again I hope this shows that Papervision3D is really very simple to use. Looking at the Papervision3D source code really helps a lot to understand how the materials are created and you’ll see that I haven’t covered everything but hopefully this gives a good starting point in creating your own 3D scenes with movie materials!

Just for completeness I’ve included below the source code for the animated movies used for the MovieMaterials. One is a simple 2D, standard Flash animation that reacts to mouse events. The other is based on a previous Papervision3D example shown in this series (from Part 6) but without the InteractiveScene3DEvent handlers. I found it really amazing that one 3D scene can be used as a material in another 3D scene - good work Papervision3D!

Here’s 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;     public function DrawTool() {       // create a drawing surface       graphics.beginFill(0xEEEEEE);       graphics.moveTo(0, 0);       graphics.lineTo(320, 0);       graphics.lineTo(320, 240);       graphics.lineTo(0, 240);       graphics.endFill();             // 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);           }         // start drawing circles     private function onMouseDown(event:MouseEvent):void {       isDrawing = true;       drawCircle(event.stageX, event.stageY);     }         // stop drawing circles     private function onMouseUp(event:MouseEvent):void {       isDrawing = false;     }         // draw a circle     private function onMouseMove(event:MouseEvent):void {       if (isDrawing) {         drawCircle(event.stageX, event.stageY);       }     }     // circle drawing function     private function drawCircle(x:int, y:int):void {       graphics.beginFill(Math.random() * 0xFFFFFF, 0.5);       graphics.drawCircle(x, y, 5);       graphics.endFill();     }       } }

… and finally Example006b.as :

package {     import flash.display.Bitmap;   import flash.display.Sprite;   import flash.events.Event;   import flash.events.MouseEvent;     import org.papervision3d.materials.BitmapMaterial;   import org.papervision3d.materials.utils.MaterialsList;   import org.papervision3d.objects.DisplayObject3D;   import org.papervision3d.objects.primitives.Cube;   import org.papervision3d.objects.primitives.Sphere;   import org.papervision3d.view.BasicView;   [SWF(backgroundColor="#FFFFFF")]   public class Example006b extends BasicView {       [Embed(source="/../assets/pv3d.png")] private var PV3D:Class;     private static const ORBITAL_RADIUS:Number = 100;       private var bitmap:Bitmap = new PV3D();     private var cube1:Cube;     private var cube2:Cube;     private var sphere1:Sphere;     private var sphere2:Sphere;     private var objectGroup:DisplayObject3D;         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 Example006b() {       var background:Sprite = new Sprite();       background.graphics.beginFill(0x000000);       background.graphics.moveTo(0, 0);       background.graphics.lineTo(320, 0);       background.graphics.lineTo(320, 240);       background.graphics.lineTo(0, 240);       background.graphics.endFill();       addChild(background);             super(320, 240, true, false);       // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();       // Listen to mouse up and down events on the stage       background.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       background.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);       background.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Start rendering the scene       startRendering();     }         private function init3D():void {       // position the camera       camera.z = -500;       camera.orbit(cameraPitch, cameraYaw);     }     private function createScene():void {       // create interactive bitmap material       var bitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData, false);       // create an interactive tiled bitmap material (bitmap tiled as 2 x 2)       var tiledBitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData, false);       tiledBitmapMaterial.tiled = true;       tiledBitmapMaterial.maxU = 2;       tiledBitmapMaterial.maxV = 2;             // create cube with simple bitmap material       cube1 = new Cube(getBitmapMaterials(bitmapMaterial), 50, 50, 50);       cube1.x = ORBITAL_RADIUS;       // create cube with tiled bitmap material       cube2 = new Cube(getBitmapMaterials(tiledBitmapMaterial), 50, 50, 50);       cube2.x = -ORBITAL_RADIUS;         // create sphere with simple bitmap material       sphere1 = new Sphere(bitmapMaterial, 25, 10, 10);       sphere1.z = ORBITAL_RADIUS;       // create sphere with tiled bitmap material       sphere2 = new Sphere(tiledBitmapMaterial, 25, 10, 10);       sphere2.z = -ORBITAL_RADIUS;       // Create a 3D object to group the spheres       objectGroup = new DisplayObject3D();       objectGroup.addChild(cube1);       objectGroup.addChild(cube2);       objectGroup.addChild(sphere1);       objectGroup.addChild(sphere2);       // Add the light and spheres to the scene       scene.addChild(objectGroup);     }         private function getBitmapMaterials(bitmapMaterial:BitmapMaterial):MaterialsList {       // create list of materials for all faces of the cube,       // all with the same bitmap material       var materials:MaterialsList = new MaterialsList();       materials.addMaterial(bitmapMaterial, "all");             return materials;     }         override protected function onRenderTick(event:Event=null):void {       // rotate the objects       cube1.yaw(-3);       cube2.yaw(-3);       sphere1.yaw(-3);       sphere2.yaw(-3);             // rotate the group of objects       objectGroup.yaw(1);       // call the renderer       super.onRenderTick(event);     }     // called when mouse down on stage     public function onMouseDown(event:MouseEvent):void {       doRotation = true;       lastMouseX = event.stageX;       lastMouseY = event.stageY;     }     // called when mouse up on stage     public function onMouseUp(event:MouseEvent):void {       doRotation = false;     }         // called when the mouse moves over the stage     public function onMouseMove(event:MouseEvent):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 = (event.stageY - lastMouseY) / 2;         var dYaw:Number = (event.stageX - 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 = event.stageX;         lastMouseY = event.stageY;                 // reposition the camera         camera.orbit(cameraPitch, cameraYaw);       }           }       } }

Next article:

Sunday, August 3rd, 2008

First steps in Papervision3D : Part 5 - scene interaction

Previous articles summary :

In this post, rather than adding more 3D rendering to the scene, I’d like to illustrate how Papervision3D provides us with a very simple interface so that users can interact with objects that we’ve created. As someone coming from an OpenGL background this is real magic ! Each triangle that we draw in a scene can listen to mouse events and call a specified function for example when we click on it or move the mouse over it.

The code sample that I show below is based on the previous post : four spheres with different shaded materials rotating about the origin. I’m now adding two different types of event listeners : a listener for standard Flash mouse events allowing the user can move around the scene by changing the camera position and a listener for Papervision3D InteractiveScene3DEvent events. The first of these is added to the stage, the second to individual DisplayObject3D objects.

To illustrate the interactions with the scene objects, I’m adding a third new element to code : tweens. 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. I’m not going to go into much detail here as its not the purpose of this post but you’ll find a lot of information on the web - here’s wikipedia’s definition for what its worth. The tweens I use here come from the opensource library Tweener which provides tweens for Actionscript2 and Actionscript3 as well as JavaScript and HaXE. To install it in your Eclipse/Flex Builder 3 environment follow the same guidelines that I gave with installing Papervision3D but rather than importing the source using SVN, simply download the zip file of the source from the Tweener homepage and extract it into the src folder of a new Tweener project. When its compiled link your own Actionscript project to the Tweener project.

Here’s the full code for the interactive scene and including the imports of the new Tweener library.

package {
 
  import caurina.transitions.Tweener;
 
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.events.Event;
  import flash.events.MouseEvent;
 
  import org.papervision3d.core.proto.MaterialObject3D;
  import org.papervision3d.events.InteractiveScene3DEvent;
  import org.papervision3d.lights.PointLight3D;
  import org.papervision3d.materials.shadematerials.CellMaterial;
  import org.papervision3d.materials.shadematerials.FlatShadeMaterial;
  import org.papervision3d.materials.shadematerials.GouraudMaterial;
  import org.papervision3d.materials.shadematerials.PhongMaterial;
  import org.papervision3d.objects.DisplayObject3D;
  import org.papervision3d.objects.primitives.Sphere;
  import org.papervision3d.view.BasicView;

  public class Example005 extends BasicView {
 
    private static const ORBITAL_RADIUS:Number = 200;
   
    private var sphere1:Sphere;
    private var sphere2:Sphere;
    private var sphere3:Sphere;
    private var sphere4:Sphere;
    private var sphereGroup:DisplayObject3D;
    private var light:PointLight3D;
   
    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() {
      // specify that we want the scene to be interactive
      super(0, 0, true, true);
     
      // set up the stage
      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;

      // Initialise Papervision3D
      init3D();
     
      // Create the 3D objects
      createScene();

      // Listen to mouse up and down events on the stage
      stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
      stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
     
      // Start rendering the scene
      startRendering();
    }
   
    private function init3D():void {
      // position the camera
      camera.z = -500;
      camera.orbit(cameraPitch, cameraYaw);
    }

    private function createScene():void {
      // Specify a point light source and its location
      light = new PointLight3D(true);
      light.x = 400;
      light.y = 1000;
      light.z = -400;

      // Create a new material (flat shaded) and make it interactive
      var flatShadedMaterial:MaterialObject3D = new FlatShadeMaterial(light, 0x6654FF, 0x060433);
      flatShadedMaterial.interactive = true;
      sphere1 = new Sphere(flatShadedMaterial, 50, 10, 10);
      sphere1.x = -ORBITAL_RADIUS;

      // Create a new material (flat shaded) and make it interactive
      var gouraudMaterial:MaterialObject3D = new GouraudMaterial(light, 0x6654FF, 0x060433);
      gouraudMaterial.interactive = true;
      sphere2 = new Sphere(gouraudMaterial, 50, 10, 10);
      sphere2.x =  ORBITAL_RADIUS;

      // Create a new material (flat shaded) and make it interactive
      var phongMaterial:MaterialObject3D = new PhongMaterial(light, 0x6654FF, 0x060433, 150);
      phongMaterial.interactive = true;
      sphere3 = new Sphere(phongMaterial, 50, 10, 10);
      sphere3.z = -ORBITAL_RADIUS;

      // Create a new material (flat shaded) and make it interactive
      var cellMaterial:MaterialObject3D = new CellMaterial(light, 0x6654FF, 0x060433, 5);
      cellMaterial.interactive = true;
      sphere4 = new Sphere(cellMaterial, 50, 10, 10);
      sphere4.z =  ORBITAL_RADIUS;

      // Create a 3D object to group the spheres
      sphereGroup = new DisplayObject3D();
      sphereGroup.addChild(sphere1);
      sphereGroup.addChild(sphere2);
      sphereGroup.addChild(sphere3);
      sphereGroup.addChild(sphere4);

      // Add a listener to each of the spheres to listen to InteractiveScene3DEvent events
      sphere1.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);
      sphere2.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);
      sphere3.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);
      sphere4.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);

      // Add the light and spheres to the scene
      scene.addChild(sphereGroup);
      scene.addChild(light);
    }
   
    override protected function onRenderTick(event:Event=null):void {
      // rotate the spheres
      sphere1.yaw(-8);
      sphere2.yaw(-8);
      sphere3.yaw(-8);
      sphere4.yaw(-8);
     
      // rotate the group of spheres
      sphereGroup.yaw(3);

      // 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
        camera.orbit(cameraPitch, cameraYaw);
      }
     
      // call the renderer
      super.onRenderTick(event);
    }

    // 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 sphere
    private function onMouseDownOnSphere(event:InteractiveScene3DEvent):void {
      var object:DisplayObject3D = event.displayObject3D;
      Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });
    }
   
    // called when a tween created in onMouseDownOnSphere has terminated
    private function goBack(object:DisplayObject3D):void {
      Tweener.addTween(object, {y:0, time:2, transition:"easeOutBounce"});
    }
  }
}

This produces the following Flash animation (click on image below) where we see the four spinning spheres rotating about the origin. You can rotate the scene by clicking and moving the mouse. You can interact with the spheres by clicking on them to make them jump.


Now lets look at the two different types of interaction that occur with this scene.

Stage interaction and modifying the camera position
The objective is to look at the scene from different angles by clicking the mouse button and moving the mouse. The interaction with the mouse is initialised by listening to the standard Flash event listeners. In the constructor we add the following lines :

      // 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 occurs on the stage, calls are made to onMouseDown and onMouseUp. These methods modify a boolean value to indicate that the user wants to translate the camera and initialises the position of the mouse, necessary to determine the amount of movement that occurs during a frame.

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

The movement of the camera occurs by implementing code in the onRenderTick function which is called systematically at the beginning of every frame.

      // 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
        camera.orbit(cameraPitch, cameraYaw);
      }

The amount of horizontal and vertical movement of the mouse since the last frame (or the mouse down event was captured) are converted into angular displacements. These displacements are converted into a change in camera position. Rather than specifying and x, y and z values for the camera we are able to take advantage of its orbit function. The orbit takes two angles : yaw (or angle in the x-z plane) and pitch angle from the y axis which we limit to be between 0 and 180 degrees.

The orbit occurs around any given 3D object - in this case the origin as default - and has a particular distance from the object. This distance was initialised during the init3D method when we specified camera.z = -500. All subsequent orbital positions therefore have a distance of 500 from the origin. If you look into the code of the function orbit, you’ll see that these angles are subsequently converted into positional values for the camera.

After updating the camera position, the call to super.onRenderTick performs the necessary actions to render the scene at the modified viewing position.

Object interaction and tweening
The second type of interaction that we use takes advantage of Papervision3D’s power of detecting which 3D object is beneath the mouse. We need to explicitly indicate to Papervision3D that we want to have an interactive scene when we create the viewport and for each interactive material that we create, the interactions themselves are handled by adding listeners to the 3D objects.

First of all, to specify that we want an interactive scene we modify the call to the constructor of BasicView which is done by passing the value of true to the interactive scene argument (the second boolean shown below, the first being to indicate that we want the viewport to resize to the stage).

    public function Example005() {
      // specify that we want the scene to be interactive
      super(0, 0, true, true);

Secondly, each object that we want to be interactive must have an interactive material. This is done by setting the variable interactive to true for the materials that we have created. For example, the flat shaded sphere is as such :

      // Create a new material (flat shaded) and make it interactive
      var flatShadedMaterial:MaterialObject3D = new FlatShadeMaterial(light, 0x6654FF, 0x060433);
      flatShadedMaterial.interactive = true;
      sphere1 = new Sphere(flatShadedMaterial, 50, 10, 10);

Once the 3D objects have been created we add listeners to listen to Papervision3D interactive scene events, in this case when the user clicks on a sphere.

      // Add a listener to each of the spheres to listen to InteractiveScene3DEvent events
      sphere1.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);
      sphere2.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);
      sphere3.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);
      sphere4.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnSphere);

All of these objects use the same function to handle the event.

    // called when mouse down on a sphere
    private function onMouseDownOnSphere(event:InteractiveScene3DEvent):void {
      var object:DisplayObject3D = event.displayObject3D;
      Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });
    }

The object that has been clicked on is sent as part of the InteractiveScene3DEvent data. Here we then see the implementation of Tweener.

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 onMouseDownOnSphere has terminated
    private function goBack(object:DisplayObject3D):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.

As you can see, adding user interactions to the scene is very easy. Papervision3D provides a very simple interface to modify both camera position and produce interactions with individual objects in the scene. What I’ve shown here is just is a simple introduction to object interaction : there are different types of interactions for example when an object is added to a scene, the mouse moves over the object or the object is moved. Look inside the source of InteractiveScene3DEvent to get an idea of all the possibilities. Hopefully this at least has given a taster of what can be produced. As always comments and feedback are very welcome !

Next article: