-->

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:

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

Previous articles summary :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next article:

-->
Saturday, November 15th, 2008

First steps in Away3D : Part 2 - Animation

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

Previous articles summary :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next article:

Wednesday, November 12th, 2008

First steps in Away3D : Part 1 - Getting started

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next article:

-->
Monday, September 29th, 2008

First steps in Papervision3D : Part 9 - Importing and working with 3D objects

Previous articles summary :

Back again for another article in the First Steps series… its been a while since the last one mainly because I’ve moved from Blogger to Wordpress and that’s been quite a hassle. But, hopefully, now that its up and running it won’t be so long before the next one… fingers crossed!

In this article (which again is probably going to be quite a long one) I’m looking at how to import 3D objects that have been created using specialised applications such as 3DS Max, Maya, Blender and Google Sketchup to name a few.

To improve the exchange of digital assets between these different applications, various file formats exists that allow 3D objects from one application to be used in another. One particular format that is readable by Papervision3D, is the Collaborative Design Activity format or COLLADA which saves the information of a 3D scene as XML.

The Collada format specifications are maintained by the Khronos Group and embedded within the file are details on geometry, shaders and effects, physics, animation and kinematics. The specifications are supported by a number of companies including 3DS Max, Maya and Blender. The files themselves have the .dae extension, standing for Digital Asset Exchange.

Papervision3D contains a extensive package to parse and import Collada files yet has a simple interface making it easy to use complex 3D models created in specialised applications. Worth noting are a couple of sites that contain Collada files created by different communities that can be downloaded and used fairly freely. These are

As I’ve mentioned from the beginning of this series, these posts represent also what I’ve learned over the last few weeks and are by no means expert tutorials. I’ve therefore made use of a number of sites in understanding how to import objects into Papervision3D and I’m sure you’ll find useful information on them as well.

Since there are a number of points to look at I decided it was best to split this post into two parts. In the first part of this post I’ll illustrate a selection of different ways of importing objects into Papervision3D based on the above examples and also from exploring the source code of Paperivison3D itself. In the second part I’m going to look at how to add interactivity and shading with Collada objects. I’ll also present some problems that exist with shading in the current version of Papervision3D - well, it is beta after all!

Lets have a look at the source for the first example. Here, my objective is simply to import 3D objects into a scene using a number of different sources. These include

The last one is making use of a special class within Papervision3D dedicated to file with the .kmz file extension (Google Earth). These files are actually .zip files that contain texture maps and Collada data.

Papervision3D also has (as far as I can tell) two main ways of importing Collada data: one is using the Collada class, the other is using the DAE class, both of which are in the org.papervision3d.objects.parsers package. Again, as far as I can tell, the DAE class appears to be more mature making extensive use of the org.ascollada package and I’ve had more success over the last few days using this rather than the Collada class.

So here’s the source code, importing four objects using different sources and different importing methods.

package {     import flash.events.Event;   import flash.events.MouseEvent;     import org.papervision3d.events.FileLoadEvent;   import org.papervision3d.lights.PointLight3D;   import org.papervision3d.materials.BitmapMaterial;   import org.papervision3d.materials.MovieMaterial;   import org.papervision3d.materials.utils.MaterialsList;   import org.papervision3d.objects.DisplayObject3D;   import org.papervision3d.objects.parsers.Collada;   import org.papervision3d.objects.parsers.DAE;   import org.papervision3d.objects.parsers.KMZ;   import org.papervision3d.view.BasicView;   public class Example009a extends BasicView {       [Embed(source="/../assets/cow.dae", mimeType="application/octet-stream")] private var CowDAE:Class;     [Embed(source="/../assets/Cow.png")] private var CowBitmapImage:Class;     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 Example009a() {       super(0, 0, true, true);             // 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 = -700;       camera.fov = 60;       camera.orbit(cameraPitch, cameraYaw);     }         private function createScene():void {       // create new Collada from URL, using original materials and scaled to 50%       var cow:Collada = new Collada("http://www.tartiflop.com/pv3d/FirstSteps/collada/cow.dae", null, 0.5);       cow.moveDown(100);       cow.moveBackward(200);       cow.yaw(90);       scene.addChild(cow);       // create a texture mapped material from embedded png       var cowMaterial:BitmapMaterial = new MovieMaterial(new CowBitmapImage(), true);             // add the texture map to a material list corresponding to the material symbols in the dae       var cowMaterials:MaterialsList = new MaterialsList();       cowMaterials.addMaterial(cowMaterial, "mat0");       // create a new Collada, specifying the materials we want to use       var cow2:Collada = new Collada(new XML(new CowDAE()), cowMaterials);       cow2.moveRight(300);       cow2.moveDown(100);       scene.addChild(cow2);           // create a new DAE that is animated and perform actions once it is loaded       var seymour:DAE = new DAE(true);       seymour.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         seymour.scale = 20;         seymour.moveForward(200);         seymour.moveDown(100);         scene.addChild(seymour);       });             // load the DAE from a specific URL       seymour.load("http://www.tartiflop.com/pv3d/FirstSteps/collada/Seymour.dae");             // create a new 3D object from a 3D google earth object file and perform actions when loaded       var kmz:KMZ = new KMZ();       kmz.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         kmz.scale = 20;         kmz.moveLeft(300);         kmz.moveDown(100);         scene.addChild(kmz);       });             // load kmz from a specific URL       kmz.load("http://www.tartiflop.com/pv3d/FirstSteps/collada/thing.kmz");           }     override protected function onRenderTick(event:Event=null):void {       // update camera position       updateCamera();           // call the renderer       super.onRenderTick(event);     }         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         camera.orbit(cameraPitch, cameraYaw);       }           }     // 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;     }       } }

This produces the following flash animation (click on the image below). Note that it can take some time for the models to be loaded into flash, but in the end you should see four models including two cows, an animated Space Boy (coming from the public directory of the Collada test model bank at Khronos) and a rather crappy thing I made in Google Sketchup! You can rotate the scene by clicking and moving the mouse at the same time.

So, lets take a closer look at the code… As with other examples I’m using the same standard BasicView derived class - the main difference from previous ones coming in the createScene function.

The first cow object is created by obtaining all the data for the Collada object from an external URL. Contained in the .dae file is information on the texture maps so we don’t need to specify anything other than the location of the Collada file itself to create a fully rendered object.

      // create new Collada from URL, using original materials and scaled to 50%       var cow:Collada = new Collada("http://www.tartiflop.com/pv3d/FirstSteps/collada/cow.dae", null, 0.5);       cow.moveDown(100);       cow.moveBackward(200);       cow.yaw(90);       scene.addChild(cow);

Here I’m using the Collada class which provides a very simple interface to import Collada data. The first argument is of course the location of the .dae file (you can just as easily use a file on the local file system). The second argument is to specify a different material for the object: in this case I’ll use the file referenced by the Collada file itself. The third parameter relates to the scaling: in this case 50%. The resulting object is then translated, rotated and added to the scene.

We can similarly import Collada data by embedding the data in the Flash animation (as we’ve seen in previous examples in this series). We have to, however, embed both the Collada data and the texture map data for it to be correctly rendered. You’ll find the embedded files at the beginning of the class definition.

    [Embed(source="/../assets/cow.dae", mimeType="application/octet-stream")] private var CowDAE:Class;     [Embed(source="/../assets/Cow.png")] private var CowBitmapImage:Class;

The .dae file format is not recognised by Flash which is why we need to explicitly give the mimeType. As you’ll see below we can use the DAE data directly as an XML document. The Collada object is then created as follows.

      // create a texture mapped material from embedded png       var cowMaterial:BitmapMaterial = new MovieMaterial(new CowBitmapImage(), true);             // add the texture map to a material list corresponding to the material symbols in the dae       var cowMaterials:MaterialsList = new MaterialsList();       cowMaterials.addMaterial(cowMaterial, "mat0");       // create a new Collada, specifying the materials we want to use       var cow2:Collada = new Collada(new XML(new CowDAE()), cowMaterials);       cow2.moveRight(300);       cow2.moveDown(100);       scene.addChild(cow2);

We specifically create a material for the 3D object: I’m using a MovieMaterial which in this case is created with a simple bitmap image. Since a Collada object can have a number of different textured materials we need to provide it with a MaterialList. The names associated with the materials have to relate to details within the Collada file… after some investigation I found that the material was referenced by the name mat0. The Collada object is then created by passing data encapsulated in an XML format and this time specifying the material list used in association with the data. As with the first object, we then translate it and add it to the scene.

The third object is an animated Collada object. This time I’m using the DAE class which has more success in importing the data. The interface remains very similar to the Collada class however we need to specifically load the data.

      // create a new DAE that is animated and perform actions once it is loaded       var seymour:DAE = new DAE(true);       seymour.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         seymour.scale = 20;         seymour.moveForward(200);         seymour.moveDown(100);         scene.addChild(seymour);       });             // load the DAE from a specific URL       seymour.load("http://www.tartiflop.com/pv3d/FirstSteps/collada/Seymour.dae");

The first argument to the DAE constructor specifies whether we want the DAE to be animated or not: in this case we do. We then add a listener which is triggered when the data is fully loaded. This allows us to add it to the scene and modify its size and position when we are sure that the data is coherent. The data is loaded by specifying a URL to the Collada file.

Finally to show how to import data from a different source, I’ve included an example that I made using Google Sketchup and exported as a Google Earth object (.kmz file extension). And yes, I know, its not very pretty…

      // create a new 3D object from a 3D google earth object file and perform actions when loaded       var kmz:KMZ = new KMZ();       kmz.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         kmz.scale = 20;         kmz.moveLeft(300);         kmz.moveDown(100);         scene.addChild(kmz);       });             // load kmz from a specific URL       kmz.load("http://www.tartiflop.com/pv3d/FirstSteps/collada/thing.kmz");

Papervision3D provides us with a class KMZ specifically to import this type of data. In fact, embedded in this file is a .dae Collada file (and the class has a reference to a DAE object as well) so the import mechanism remains the same… it is however another handy tool. As you can see we use the same technique as with the DAE import.

So, there we are: four different methods of importing Collada data. Now for the more advanced part of this post: adding shading to the objects.

In principal this should follow from the previous post on texture mapping with lighting where a ShadedMaterial is used to skin an object (as we did above with cow2 when we specified a list of materials for the DAE). However, at the time of writing this article, this is not working correctly: the main problem being black lines appearing on face edges. As you can see from the list of bugs at the home of Papervision3D, an issue has been raised explaining the problem. You can also see on the Papervision3D mailing list that this is a recurring problem (here and here for example).

This said, all is not lost! Thanks to those talented people involved in the Papervision3D project, specifically in this case to Andy Zupko, a work-around does exist! Also, I’d like to give a thanks to the Papervision3D newsgroup because the list is very active and you can find a lot of very good information on it… like this fix.

So, here’s the second example source code for this post. Here I’m showing the two different methods for rendering a shaded Collada object: the first (not fully working in the current release of Papervision3D) using a ShadedMaterial and the second using Andy’s work-around which involves blending two identical models: one with a simple shaded material, the other with a texture-mapped material.

package {     import flash.display.BlendMode;   import flash.events.Event;   import flash.events.MouseEvent;   import flash.text.TextField;   import flash.text.TextFieldAutoSize;   import flash.text.TextFormat;   import flash.utils.getTimer;     import org.papervision3d.events.FileLoadEvent;   import org.papervision3d.events.InteractiveScene3DEvent;   import org.papervision3d.lights.PointLight3D;   import org.papervision3d.materials.BitmapMaterial;   import org.papervision3d.materials.MovieMaterial;   import org.papervision3d.materials.shadematerials.GouraudMaterial;   import org.papervision3d.materials.shaders.GouraudShader;   import org.papervision3d.materials.shaders.ShadedMaterial;   import org.papervision3d.materials.utils.MaterialsList;   import org.papervision3d.objects.DisplayObject3D;   import org.papervision3d.objects.parsers.DAE;   import org.papervision3d.view.BasicView;   public class Example009b extends BasicView {       [Embed(source="/../assets/cow.dae", mimeType="application/octet-stream")] private var CowDAE:Class;     [Embed(source="/../assets/Cow.png")] private var CowBitmapImage:Class;     private var light:PointLight3D;         private var shadedMaterialCow:DAE;     private var gouraudCow:DAE;     private var texturedCow:DAE;     private var allCows:DisplayObject3D;         private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var cameraPitch:Number = 60;     private var cameraYaw:Number = -60;           private var fpsText:TextField;     private var textFormat:TextFormat;       private var frames:Number = 0;     private var lastTimeMS:Number = 0;       private var doSimple:Boolean = false;       public function Example009b() {       super(0, 0, true, true);             // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();       // create the frame rate counter label       createFPSLabel();       // 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.fov = 60;       camera.orbit(cameraPitch, cameraYaw);     }         private function createFPSLabel():void {       // create text and format to display current fps       textFormat = new TextFormat();       textFormat.size = 20;       textFormat.font = "Arial";             fpsText = new TextField();       fpsText.x = 50;       fpsText.y = 50;       fpsText.textColor = 0xFFFFFF;       fpsText.text = "";       fpsText.setTextFormat(textFormat);       fpsText.autoSize = TextFieldAutoSize.LEFT;             stage.addChild(fpsText);     }     private function createScene():void {       // Specify a point light source and its location       light = new PointLight3D(true);       light.x = 500;       light.y = 300;       light.z = -500;       scene.addChild(light);       // create a display object to group all created cows       allCows = new DisplayObject3D();       scene.addChild(allCows);       // create a cow with a shaded material       createSimpleShadedDAE();             // create a shaded cow by blending two different rendered objects       createNiceShadedDAE();     }     private function createSimpleShadedDAE():void {       // create BitmapMaterial from texture map       var cowBitmapMaterial:BitmapMaterial = new MovieMaterial(new CowBitmapImage(), true);         // create a ShadedMaterial using a Gouraud shader       var shader:GouraudShader = new GouraudShader(light, 0xFFFFFF, 0x333333);       var shadedMaterial:ShadedMaterial = new ShadedMaterial(cowBitmapMaterial, shader);       shadedMaterial.interactive = true;             // Material list linked to material symbol name in dae       var mainMaterials:MaterialsList = new MaterialsList();       mainMaterials.addMaterial(shadedMaterial, "mat0");       // create a new dae and perform actions when loaded       shadedMaterialCow = new DAE(false);       shadedMaterialCow.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         shadedMaterialCow.moveDown(100);         shadedMaterialCow.scale = 100;                 // add cow to scene when loaded         allCows.addChild(shadedMaterialCow);                 // recursively add event listeners to dae and all children         addEventListeners(shadedMaterialCow, InteractiveScene3DEvent.OBJECT_CLICK, toggleRendering);       });             // load the dae from the embedded structure and replace the materials       shadedMaterialCow.load(new XML(new CowDAE()), mainMaterials);     }         private function createNiceShadedDAE():void {       // create a simple texture mapped material for the embedded png       var cowBitmapMaterial:BitmapMaterial = new MovieMaterial(new CowBitmapImage(), true);       cowBitmapMaterial.interactive = true;             // add the material to a material list corresponding to the dae       var bitmapMaterials:MaterialsList = new MaterialsList();       bitmapMaterials.addMaterial(cowBitmapMaterial, "mat0");       // create a new dae and perform actions when loaded       texturedCow = new DAE(false);       texturedCow.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         texturedCow.moveDown(100);         texturedCow.scale = 100;         // set the dae to initially not be visible         texturedCow.visible = false;                 // add to the scene         allCows.addChild(texturedCow);                 // listen to events (applies to all children of dae as well)         addEventListeners(texturedCow, InteractiveScene3DEvent.OBJECT_CLICK, toggleRendering);               });       // load the dae from the embedded structure and replace the materials       texturedCow.load(new XML(new CowDAE()), bitmapMaterials);       // create a simple Gouraud shaded material and add to list corresponding to dae       var gouraudMaterial:GouraudMaterial = new GouraudMaterial(light, 0xFFFFFF, 0x333333);       var shadedMaterials:MaterialsList = new MaterialsList();       shadedMaterials.addMaterial(gouraudMaterial, "mat0");       // create a new dae and perform actions when loaded       gouraudCow = new DAE(false);       gouraudCow.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         gouraudCow.scale = 100;         gouraudCow.moveDown(100);         // set the dae to initially not be visible         gouraudCow.visible = false;           // add to the scene         allCows.addChild(gouraudCow);         // change the rendering so that it is blended with other rendered objects         viewport.getChildLayer(gouraudCow).blendMode = BlendMode.MULTIPLY;        });             // load the dae from the embedded structure and replace the materials       gouraudCow.load(new XML(new CowDAE()), shadedMaterials);     }     // used to ensure that all children in a dae listen to events     private function addEventListeners(displayObject:DisplayObject3D, eventType:String, listener:Function):void {       // add listener to DisplayObect       displayObject.addEventListener(eventType, listener);             // add listener to all contained childred       for each(var child:DisplayObject3D in displayObject.children) {         addEventListeners(child, eventType, listener);       }     }         // toggles between the two rendering techniques     private function toggleRendering(event:InteractiveScene3DEvent):void {       texturedCow.visible = !texturedCow.visible;       gouraudCow.visible = !gouraudCow.visible;       shadedMaterialCow.visible = !shadedMaterialCow.visible;     }     override protected function onRenderTick(event:Event=null):void {             // rotate the scene       allCows.yaw(-1);             // calculate the frame rate       calculateFrameRate();       // update the camera position       updateCamera();           // call the renderer       super.onRenderTick(event);     }         private function calculateFrameRate():void {       // calculate the time elapsed since the last calculation            var currentTimeMS:Number = getTimer();       var elapsedTimeMS:Number = currentTimeMS - lastTimeMS;       // if a second has elapsed then calculate the fps       if (elapsedTimeMS >= 1000) {         fpsText.text = frames.toString() + " fps";         fpsText.setTextFormat(textFormat);                 // reset the counter         lastTimeMS = currentTimeMS;         frames = 0;             } else {         // increment the counter         frames++;       }           }         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         camera.orbit(cameraPitch, cameraYaw);       }           }     // 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 resulting Flash animation can be seen below by clicking on the image. You can rotate the scene by clicking and moving the mouse. You can also switch between the two rendering techniques by clicking on the cow. To show that there is not a huge difference in performance between the two techniques, a frame-rate meter is shown in the top-left hand corner.

So again, lets take a closer look at the code. Starting with the ShadedMaterial version that produces the problems. This is created in the createSimpleShadedDAE function.

    private function createSimpleShadedDAE():void {       // create BitmapMaterial from texture map       var cowBitmapMaterial:BitmapMaterial = new MovieMaterial(new CowBitmapImage(), true);         // create a ShadedMaterial using a Gouraud shader       var shader:GouraudShader = new GouraudShader(light, 0xFFFFFF, 0x333333);       var shadedMaterial:ShadedMaterial = new ShadedMaterial(cowBitmapMaterial, shader);       shadedMaterial.interactive = true;             // Material list linked to material symbol name in dae       var mainMaterials:MaterialsList = new MaterialsList();       mainMaterials.addMaterial(shadedMaterial, "mat0");       // create a new dae and perform actions when loaded       shadedMaterialCow = new DAE(false);       shadedMaterialCow.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         shadedMaterialCow.moveDown(100);         shadedMaterialCow.scale = 100;                 // add cow to scene when loaded         allCows.addChild(shadedMaterialCow);                 // recursively add event listeners to dae and all children         addEventListeners(shadedMaterialCow, InteractiveScene3DEvent.OBJECT_CLICK, toggleRendering);       });             // load the dae from the embedded structure and replace the materials       shadedMaterialCow.load(new XML(new CowDAE()), mainMaterials);     }

As we’ve seen in previous posts, to create a shaded bitmap material we combine a BitmapMaterial with a Shader. Here we use the embedded bitmap data of the texture map combined with a simple Gouraud shader. These are combined in a ShadedMaterial which is then added to the material list as we did above for the first part of this post. I’m using a DAE object to load the Collada data which is then combined with material data.

What you’ll notice however is that the interactivity is added in a different manner from previous posts. For Collada objects we need to add the event listener to all of the children of the DAE as well as the DAE itself - see this post on the Papervision3D newsgroup for details. To perform this, I’ve added (as mentioned in the post) a small routine to recursively add the listener to all children.

    // used to ensure that all children in a dae listen to events     private function addEventListeners(displayObject:DisplayObject3D, eventType:String, listener:Function):void {       // add listener to DisplayObect       displayObject.addEventListener(eventType, listener);             // add listener to all contained childred       for each(var child:DisplayObject3D in displayObject.children) {         addEventListeners(child, eventType, listener);       }     }

So, as you can see from the Flash animation, this method doesn’t work - yet! So, now for the fix provided by Andy Zupko. You’ll find his original post on shadow casting very interesting. The idea is that we take advantage of the Flash architecture to blend 2D objects together. To do this we render the DAE twice: once with a texture map but no shading and another time with no texture map and simple shading. Each render produces a 2D image (what is seen on the screen). These images can be superimposed and blended so that the resulting image is a shaded texture map.

The overhead of drawing the same 3D object twice seems to be fairly small (as you can see from the fps counter in the demo). This is not really surprising since the ShadedMaterial method also does two passes: each triangle is initially rendered with a texture map and then again with a shader.

Lets look at the code in createNiceShadedDAE

    private function createNiceShadedDAE():void {       // create a simple texture mapped material for the embedded png       var cowBitmapMaterial:BitmapMaterial = new MovieMaterial(new CowBitmapImage(), true);       cowBitmapMaterial.interactive = true;             // add the material to a material list corresponding to the dae       var bitmapMaterials:MaterialsList = new MaterialsList();       bitmapMaterials.addMaterial(cowBitmapMaterial, "mat0");       // create a new dae and perform actions when loaded       texturedCow = new DAE(false);       texturedCow.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         texturedCow.moveDown(100);         texturedCow.scale = 100;         // set the dae to initially not be visible         texturedCow.visible = false;                 // add to the scene         allCows.addChild(texturedCow);                 // listen to events (applies to all children of dae as well)         addEventListeners(texturedCow, InteractiveScene3DEvent.OBJECT_CLICK, toggleRendering);               });       // load the dae from the embedded structure and replace the materials       texturedCow.load(new XML(new CowDAE()), bitmapMaterials);       // create a simple Gouraud shaded material and add to list corresponding to dae       var gouraudMaterial:GouraudMaterial = new GouraudMaterial(light, 0xFFFFFF, 0x333333);       var shadedMaterials:MaterialsList = new MaterialsList();       shadedMaterials.addMaterial(gouraudMaterial, "mat0");       // create a new dae and perform actions when loaded       gouraudCow = new DAE(false);       gouraudCow.addEventListener(FileLoadEvent.LOAD_COMPLETE, function onLoad(event:Event):void {         gouraudCow.scale = 100;         gouraudCow.moveDown(100);         // set the dae to initially not be visible         gouraudCow.visible = false;           // add to the scene         allCows.addChild(gouraudCow);         // change the rendering so that it is blended with other rendered objects         viewport.getChildLayer(gouraudCow).blendMode = BlendMode.MULTIPLY;        });             // load the dae from the embedded structure and replace the materials       gouraudCow.load(new XML(new CowDAE()), shadedMaterials);     }

As you can see two DAEs are created: one as before with the embedded texture map data and another using a Gouraud shaded material. They are both initially set with visible = false. This means that they are not visible on screen and no rendering of them occurs. The Papervision3D event listeners are added to the first one (recursively for all DAE children too).

To ensure that the two are blended we just need to add the line viewport.getChildLayer(gouraudCow).blendMode = BlendMode.MULTIPLY to the Gouraud shaded DAE. The layer contains the 2D end result (or Sprite) of the render process for the DisplayObject3D (as it is seen on the screen) so by specifying BlendMode.MULTIPLY we are directly modifying the Flash characteristics of the Sprite. As you can see from the example this works very well which little, or no, change to the fps. As a little bit of further reading, I’d recommend another post by Andy Zupko where he gives more details on what can be achieved by modifying the layer characteristics and how this can create some greate effects.

Finally just to illustrate how the two models are switched, lets have a look at the event handler

    // toggles between the two rendering techniques     private function toggleRendering(event:InteractiveScene3DEvent):void {       texturedCow.visible = !texturedCow.visible;       gouraudCow.visible = !gouraudCow.visible;       shadedMaterialCow.visible = !shadedMaterialCow.visible;     }

Very simply, we set the different DAE models to visible = true or visible = false depending on which rendering technique we want to use. This is a very efficient way of adding and removing objects from a scene.

So just before ending this post, a couple of points about this technique. Firstly, this works fine for simple Gouraud, Flat, Cell or Phong shading, however how can bump mapping be included? Bump mapping requires a ShadedMaterial (see Part 7) but as we’ve seen this type of material doesn’t fully work yet for Collada objects. Secondly, if the Collada object is animated there needs to be synchronisation between the two rendered models: each frame has to be matched identically but the animation starts as soon as the model is loaded and each model can load in a different time. How can this synchronisation be achieved?

If you have comments or suggestions on these last couple of points, or indeed on any part of this post, then please don’t hesitate to add to the discussion below - I’d be very happy to hear of solutions and experiences!

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:

Tuesday, August 26th, 2008

First steps in Papervision3D : Part 7 - Texture mapping with lighting, bump mapping and environment mapping

Previous articles summary :

This article is essentially a continuation of the previous one, building on the texture mapped materials. As I said in my last post, the aim now is to add lighting to add more realism and provide depth to the scene - much like we did in Part 4.

Previously for the shading we used specific materials to account for the different shading methods such as GouraudMaterial or PhongMaterial (as you will find in the package org.papervision3d.materials.shadematerials of the pv3d source). These however are based on coloured materials rather than textured materials.

To mix both texture maps and shading we need to create a Shader and add this with the BitmapMaterial (like we created in Part 6) together in a ShadedMaterial. Papervision3D does all the magic necessary to mix the two to create a shaded bitmap material.

As you can see if you look in the org.papervision3d.materials.shaders package in the pv3d source there are a number of different shaders available - some of which we recognise from Part 4. The list includes FlatShader, GouraudShader, PhongShader, CellShader and EnvMapShader.

The last one of the above allows us to add an environment map to the material which essentially is like adding lighting from a surrounding environment and makes the material look like it is highly reflective. Take a look at this wikipedia page on reflection mapping for more details.

Another concept that is important in producing more realistic scenes is bump mapping. This is basically an optimised way of modifying the lighting of a surface so that it appears as if the surface has bumps in it. It can make a big difference to a rendered object in terms of realism without having to specify many object vertices. Again, wikipedia has lots of information on bump mapping. With Papervision3D we can add bump maps to enhance both the Phong and environment shaders.

So, on with the code… As usual, I’m modifying the code from the previous article so there’s not a huge change. However to account for the different number of shaders, rather than show all of them at once I’m only showing one sphere at a time so that you get a better idea of what each shader does. To change the shader I’ve modified the scene interaction so that clicking on the sphere changes the material dynamically - another great feature of Papervision3D! Anyway here’s the code:

package {     import flash.display.Bitmap;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;   import flash.text.TextField;   import flash.text.TextFieldAutoSize;   import flash.text.TextFormat;     import org.papervision3d.events.InteractiveScene3DEvent;   import org.papervision3d.lights.PointLight3D;   import org.papervision3d.materials.BitmapMaterial;   import org.papervision3d.materials.shaders.CellShader;   import org.papervision3d.materials.shaders.EnvMapShader;   import org.papervision3d.materials.shaders.FlatShader;   import org.papervision3d.materials.shaders.GouraudShader;   import org.papervision3d.materials.shaders.PhongShader;   import org.papervision3d.materials.shaders.ShadedMaterial;   import org.papervision3d.materials.shaders.Shader;   import org.papervision3d.objects.DisplayObject3D;   import org.papervision3d.objects.primitives.Sphere;   import org.papervision3d.view.BasicView;   public class Example007 extends BasicView {       [Embed(source="/../assets/pv3d.png")] private var Pv3dBitmapImage:Class;     [Embed(source="/../assets/randomBump.png")] private var BumpImage:Class;     [Embed(source="/../assets/mountains.png")] private var EnvImage:Class;     private var pv3dBitmap:Bitmap = new Pv3dBitmapImage();     private var bumpMap:Bitmap = new BumpImage();     private var envMap:Bitmap = new EnvImage();     private var bitmapMaterial:BitmapMaterial;         private var sphere:Sphere;     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;         private var shaders:Array = ["flat", "cell", "gouraud", "phong", "phongBump", "env", "envBump"];     private var shaderIndex:int = 0;         private var shaderText:TextField;     private var textFormat:TextFormat;     public function Example007() {       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);       stage.addChild(shaderText);       // Start rendering the scene       startRendering();     }         private function init3D():void {       // position the camera       camera.z = -500;       camera.orbit(60, -60);     }     private function createScene():void {       // create text and format to display current shader type       textFormat = new TextFormat();       textFormat.size = 20;       textFormat.font = "Arial";             shaderText = new TextField();       shaderText.x = 50;       shaderText.y = 50;       shaderText.textColor = 0xFFFFFF;       shaderText.text = "flat";       shaderText.setTextFormat(textFormat);       shaderText.autoSize = TextFieldAutoSize.LEFT;       // Specify a point light source and its location       light = new PointLight3D(true);       light.x = 500;       light.y = 500;       light.z = -200;       // create bitmap material with smoothing       bitmapMaterial = new BitmapMaterial(pv3dBitmap.bitmapData, false);       bitmapMaterial.smooth = true;         // create sphere       sphere = new Sphere(getShadedBitmapMaterial(bitmapMaterial, "flat"), 150, 20, 20);       // Add a listener to the spheres to listen to InteractiveScene3DEvent events       sphere.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);       // Add the light and sphere to the scene       scene.addChild(sphere);       scene.addChild(light);     }         private function getShadedBitmapMaterial(bitmapMaterial:BitmapMaterial, shaderType:String):ShadedMaterial {       var shader:Shader;             if (shaderType == "flat") {         // create new flat shader         shader = new FlatShader(light, 0xFFFFFF, 0x333333);       } else if (shaderType == "cell") {         // create new cell shader with 5 colour levels         shader = new CellShader(light, 0xFFFFFF, 0x333333, 5);       } else if (shaderType == "gouraud") {         // create new gouraud shader         shader = new GouraudShader(light, 0xFFFFFF, 0x333333);       } else if (shaderType == "phong") {         // create new phong shader         shader = new PhongShader(light, 0xFFFFFF, 0x333333, 50);       } else if (shaderType == "phongBump") {         // create new phong shader with bump map         shader = new PhongShader(light, 0xFFFFFF, 0x333333, 50, bumpMap.bitmapData);       } else if (shaderType == "env") {         // create new environment map shader         shader = new EnvMapShader(light, envMap.bitmapData, envMap.bitmapData, 0x333333);       } else if (shaderType == "envBump") {         // create new environment map shader with bump map         shader = new EnvMapShader(light, envMap.bitmapData, envMap.bitmapData, 0x333333, bumpMap.bitmapData);       }       // create new shaded material by combining the bitmap material with shader       var shadedMaterial:ShadedMaterial = new ShadedMaterial(bitmapMaterial, shader);       shadedMaterial.interactive = true;             return shadedMaterial;     }         override protected function onRenderTick(event:Event=null):void {       // rotate the sphere       sphere.yaw(-1);             // 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 onMouseDownOnObject(event:InteractiveScene3DEvent):void {       var object:DisplayObject3D = event.displayObject3D;             // calculate index of next shader       shaderIndex++;       if (shaderIndex == shaders.length) {         shaderIndex = 0;       }             // dynamically modify the material of the object and update text       object.material = getShadedBitmapMaterial(bitmapMaterial, shaders[shaderIndex]);       shaderText.text = shaders[shaderIndex];       shaderText.setTextFormat(textFormat);     }   } }

Click on the image below to see this in action. Clicking on the sphere will change the material - in this case change the shader associated with the textured material - and moving the mouse while clicking anywhere in the window will rotate the camera around the sphere.

Compared to the previous posts the construction, initialisation of 3D and scene renderer remain virually the same so I won’t go into details here. You’ll see that, as with the texture map, the bump map and environment map are embedded in the flash animation and come from image files. These are then converted into Bitmap objects.

    [Embed(source="/../assets/pv3d.png")] private var Pv3dBitmapImage:Class;     [Embed(source="/../assets/randomBump.png")] private var BumpImage:Class;     [Embed(source="/../assets/mountains.png")] private var EnvImage:Class;     private var pv3dBitmap:Bitmap = new Pv3dBitmapImage();     private var bumpMap:Bitmap = new BumpImage();     private var envMap:Bitmap = new EnvImage();

For sake of completeness, the images I use are below. The pv3d symbol comes directly from the papervision3d developer site, the bump map I made myself just by adding random noise to a blank image and blurring it (thanks to Gimp) and the environment image comes from my neighbourhood Alps.

Texture map image:

Bump map:

Environment map:

Remark: Actually I had to invert the environment map image for this demo to have it appear the right way up when rendered. If anyone can tell me why I’d be happy to know…

The main modification to the code compared to the previous post occurs in the createScene method. Firstly a simple Text object is created to display the current shader type - nothing really interesting here!

      // create text and format to display current shader type       textFormat = new TextFormat();       textFormat.size = 20;       textFormat.font = "Arial";             shaderText = new TextField();       shaderText.x = 50;       shaderText.y = 50;       shaderText.textColor = 0xFFFFFF;       shaderText.text = "flat";       shaderText.setTextFormat(textFormat);       shaderText.autoSize = TextFieldAutoSize.LEFT;

We then put the light back in that we removed in the last article - each shader is associated with a single light source.

      // Specify a point light source and its location       light = new PointLight3D(true);       light.x = 500;       light.y = 500;       light.z = -200;

As with Part 6, we create a BitmapMaterial using the texture map data. As before I’m using normal rather than accurate perspective (the second parameter of the constructor set to false) to improve the performance. I am however choosing to smooth the material which produces a much nicer looking texture map. This is of course at the cost of performance…

      // create bitmap material with smoothing       bitmapMaterial = new BitmapMaterial(pv3dBitmap.bitmapData, false);       bitmapMaterial.smooth = true;

The sphere is then created using a call to getShadedBitmapMaterial to enhance the bitmap material with a shader - to start off with, just a simple flat shader - and an event listener added to allow us to change the material.

      // create sphere       sphere = new Sphere(getShadedBitmapMaterial(bitmapMaterial, "flat"), 150, 20, 20);       // Add a listener to the spheres to listen to InteractiveScene3DEvent events       sphere.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);

So how do we add a shader to a BitmapMaterial? The different types of shaders used in this demo are created in the getShadedBitmapMaterial method

    private function getShadedBitmapMaterial(bitmapMaterial:BitmapMaterial, shaderType:String):ShadedMaterial {       var shader:Shader;             if (shaderType == "flat") {         // create new flat shader         shader = new FlatShader(light, 0xFFFFFF, 0x333333);       } else if (shaderType == "cell") {         // create new cell shader with 5 colour levels         shader = new CellShader(light, 0xFFFFFF, 0x333333, 5);       } else if (shaderType == "gouraud") {         // create new gouraud shader         shader = new GouraudShader(light, 0xFFFFFF, 0x333333);       } else if (shaderType == "phong") {         // create new phong shader         shader = new PhongShader(light, 0xFFFFFF, 0x333333, 50);       } else if (shaderType == "phongBump") {         // create new phong shader with bump map         shader = new PhongShader(light, 0xFFFFFF, 0x333333, 50, bumpMap.bitmapData);       } else if (shaderType == "env") {         // create new environment map shader         shader = new EnvMapShader(light, envMap.bitmapData, envMap.bitmapData, 0x333333);       } else if (shaderType == "envBump") {         // create new environment map shader with bump map         shader = new EnvMapShader(light, envMap.bitmapData, envMap.bitmapData, 0x333333, bumpMap.bitmapData);       }       // create new shaded material by combining the bitmap material with shader       var shadedMaterial:ShadedMaterial = new ShadedMaterial(bitmapMaterial, shader);       shadedMaterial.interactive = true;             return shadedMaterial;     }

As you can see, the simple shaders (”flat”, “gouraud”, “cell” and “phong”) are created exactly the same as for the ShadeMaterials we introduced in Part 4 using essentially a light colour and an ambient colour. The new types of shaders here come from using the bump map and environment map.

The PhongShader allows us to add bump map data. This is added very simply by using the bump map image data we loaded before. As you can see with the demo this changes a lot the appearance of the sphere - we really get the impression that it has a bumpy surface.

The EnvMapShader is also very simple to use. It takes a light source as for all the other shaders then two texture maps for the environment - a front and back. Here for simplicity I just used the same image for both. It then takes an ambient colour for when it is not facing the light source. To add a bump map we simple add as well the bitmap data of the bump map image.

Finally, what we create is a ShadedMaterial: this simply takes the material texture map and the shader and combines the two.

The only other major difference from previous articles is the dynamic changing of object materials. This is performed when a mouse click event is detected on the sphere.

    // called when mouse down on a sphere     private function onMouseDownOnObject(event:InteractiveScene3DEvent):void {       var object:DisplayObject3D = event.displayObject3D;             // calculate index of next shader       shaderIndex++;       if (shaderIndex == shaders.length) {         shaderIndex = 0;       }             // dynamically modify the material of the object and update text       object.material = getShadedBitmapMaterial(bitmapMaterial, shaders[shaderIndex]);       shaderText.text = shaders[shaderIndex];       shaderText.setTextFormat(textFormat);     }

Dynamically changing the material of an object is a new feature in Papervision3D and is very simple to implement: simply change the value of the material member of the object! In the above method we just cycle through the array of shader types and update the text associated… almost too easy!

And that’s all there is too it! I’ve not really gone into the mechanics behind bump mapping and environment mapping because I’m sure you can find explanations much better that I could give elsewhere on the web. But hopefully this has given some insight into how just a few simple lines can totally change the appearance of a rendered object. Comments and suggestions, as always, are very welcome!

Next article:

Sunday, August 24th, 2008

First steps in Papervision3D : Part 6 - Texture mapping

Previous articles summary :

In this article I’d like to start taking a look at texture mapping : taking bitmap data and mapping it to a surface. In the previous animations in this series, the objects have had materials of a particular colour, enhanced by different shading models. Using bitmap data we can add much more realism to a scene.

To start with some useful links that provide a good overview of texture mapping with Papervision3D :

I’d really recommend reading the first of these links because it gives a good description of UV coordinates related with images, allowing us to map rectangular images to displayed triangles. This is also an important concept involved in tiling. Tiling allows us to have a particular image repeated over a surface.

What I’d like to do in this article is to start off very simply and show how to add a bitmap texture to cubes and spheres, with and without tiling. So, no shading for the moment - that’ll be covered in the next article - and lets just see how to cover a shape with a bitmap material.

The code below is based on code from the previous article. However, I’m removing the light source for the time being, adding cubes as well as spheres and, of course, changing the materials to use bitmap data.

package {     import caurina.transitions.Tweener;     import flash.display.Bitmap;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;     import org.papervision3d.events.InteractiveScene3DEvent;   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;   public class Example006 extends BasicView {       [Embed(source="/../assets/pv3d.png")] private var MyTextureImage:Class;     private static const ORBITAL_RADIUS:Number = 200;       private var bitmap:Bitmap = new MyTextureImage();     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 Example006() {       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(60, -60);     }     private function createScene():void {       // create interactive bitmap material       var bitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData, false);       bitmapMaterial.interactive = true;       // create an interactive tiled bitmap material (bitmap tiled as 2 x 2)       var tiledBitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData, false);       tiledBitmapMaterial.interactive = true;       tiledBitmapMaterial.tiled = true;       tiledBitmapMaterial.maxU = 2;       tiledBitmapMaterial.maxV = 2;             // create cube with simple bitmap material       cube1 = new Cube(getBitmapMaterials(bitmapMaterial), 100, 100, 100);       cube1.x = ORBITAL_RADIUS;       // create cube with tiled bitmap material       cube2 = new Cube(getBitmapMaterials(tiledBitmapMaterial), 100, 100, 100);       cube2.x = -ORBITAL_RADIUS;         // create sphere with simple bitmap material       sphere1 = new Sphere(bitmapMaterial, 50, 10, 10);       sphere1.z = ORBITAL_RADIUS;       // create sphere with tiled bitmap material       sphere2 = new Sphere(tiledBitmapMaterial, 50, 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 a listener to each of the spheres to listen to InteractiveScene3DEvent events       cube1.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);       cube2.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);       sphere1.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);       sphere2.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);       // 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);       // 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 onMouseDownOnObject(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 onMouseDownOnObject 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 objects rotating about the origin. As with Part 5, you can interact with the scene by clicking and moving the mouse (to rotate the scene) and by clicking on the objects to make them jump. What we now see though are the objects covered with a material using bitmap data.

The first thing to note with this code is the embedding of an image in the compiled Flash file :

    [Embed(source="/../assets/pv3d.png")] private var MyTextureImage:Class;

This is then converted into a Bitmap as follows :

    private var bitmap:Bitmap = new MyTextureImage();

The embed meta-tag allows assets to be embedded in to the swf file. Rather than going into the details of embedding here, you can find some useful links at assertTrue and with the Flex livedocs. You’ll note that the source is “../assets” : this is because I have my assets folder at the same level as the src folder - when the Actionscript is compiled, the root folder is the src folder. If you want to compile the above source then simply create an assets folder, put any image file in it and change the above line to point to your own image.

The constructor, intialisation of the the 3D and the event handlers are all identical to the previous article in this series so I won’t go into details here. The main changes are in the createScene method.

Rather than coat our objects with shaded materials, we are going to create BitmapMaterials. A BitmapMaterial simply takes bitmap data (which comes from our embedded image) :

      // create interactive bitmap material       var bitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData, false);       bitmapMaterial.interactive = true;

The BitmapMaterial constructor takes the bitmap data from the image we created earlier and a boolean value to indicate whether we want normal or accurate rendering to occur. Texture mapping requires a lot of CPU so Papervision3D gives the possibility to speed up the rendering at the cost of less accurate perspective. You can see an example of the distortion that occurs with this Papervision3D demo. I’ve selected false here to have quick rendering.

To create a tiled bitmap material, we create another BitmapMaterial and modify certain parameters :

      // create an interactive tiled bitmap material (bitmap tiled as 2 x 2)       var tiledBitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData, false);       tiledBitmapMaterial.interactive = true;       tiledBitmapMaterial.tiled = true;       tiledBitmapMaterial.maxU = 2;       tiledBitmapMaterial.maxV = 2;

Here we set tiled to true to allow tiling to occur. maxU and maxV are then modified to indicate how many tiles we want to cover the material. I’ve specified 2 by 2 tiling.

These materials are then simply added to the cubes and spheres exactly as we did with the shaded materials before.

      // create cube with simple bitmap material       cube1 = new Cube(getBitmapMaterials(bitmapMaterial), 100, 100, 100);       cube1.x = ORBITAL_RADIUS;       // create cube with tiled bitmap material       cube2 = new Cube(getBitmapMaterials(tiledBitmapMaterial), 100, 100, 100);       cube2.x = -ORBITAL_RADIUS;         // create sphere with simple bitmap material       sphere1 = new Sphere(bitmapMaterial, 50, 10, 10);       sphere1.z = ORBITAL_RADIUS;       // create sphere with tiled bitmap material       sphere2 = new Sphere(tiledBitmapMaterial, 50, 10, 10);       sphere2.z = -ORBITAL_RADIUS;

Actually, this is the first time that I’ve put a cube into one of these examples and I should note that rather than taking a simple material object, they take a MaterialsList that allows us to specify a different material for each face. For this demo I want all the faces to be the same so the material is added to the list using the “all” tag.

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

You can add different materials using “front”, “back”, “top”, “bottom”, “left” and “right” tags.

So, at its very simplest, that’s all there is to do to create texture mapped objects. As always I’d recommend taking a look at the Papervision3D sources as there’s a lot of stuff not covered here. For example you’ll see that there are a number of different Bitmap materials in the org.papervision3d.materials package, for example the BitmapFileMaterial that allows you to load a bitmap at run time from a URL.

In the next article I’ll introduce (or rather put back) shading on top of the texture map. To do this we need in effect a kind of composite material : one that has the texture map and another that has the shaded colour… anyway, I’ll leave that for next time! I hope at least that for the time being this gives some help in getting started!

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:

Saturday, August 2nd, 2008

First steps in Papervision3D : Part 4 - lighting and shading

Previous articles summary :

I’d like to illustrate in this post how we can add another essential ingredient to the scene : lighting. Lighting provides a much more realistic rendering of 3D objects and with Papervision3D is very simple to add. The example that I give here shows how lighting is added to a specific material. Papervision3D allows us to specify how the light interacts with the material and provides different qualities of rendering.

First its useful to have a brief summary of the commonly used terms used to describe lighting and shading in 3D graphics.

Typically objects are made up of a combination of all these lighting characteristics. Now, as I’ve said since the beginning, I’m a beginner in Papervision3D with an OpenGL background and am merely illustrating the progress I’ve personally made over the last few weeks. But, as far as I can tell, the basic shading models available combine ambient lighting with either diffuse or specular and all three is not possible. I’d be very happy if someone could correct me on this… In any case the shading available, produces some beautiful results as we can see in these examples :

You can also find here a useful article on textures, shading and materials which illustrates many techniques possible.

In this post I’d like to show simply how we can add a light source to a scene and implement the four basic types of shading on different materials. These are :

The code to illustrate these shading models is as follows :

package {
 
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.events.Event;
 
  import org.papervision3d.core.proto.MaterialObject3D;
  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 Example004 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;
   
    public function Example004() {
      super(0, 0, true, false);
     
      // set up the stage
      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;

      // Initialise Papervision3D
      init3D();
     
      // Create the 3D objects
      createScene();
     
      // Start rendering the scene
      startRendering();
    }
   
    private function init3D():void {

      // position the camera
      camera.x = -200;
      camera.y =  200;
      camera.z = -500;
    }

    private function createScene():void {

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

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

      // Create a new material (Gouraud shaded) and apply it to a sphere
      var gouraudMaterial:MaterialObject3D = new GouraudMaterial(light, 0x6654FF, 0x060433);
      sphere2 = new Sphere(gouraudMaterial, 50, 10, 10);
      sphere2.x =  ORBITAL_RADIUS;

      // Create a new material (Phong shaded) and apply it to a sphere
      var phongMaterial:MaterialObject3D = new PhongMaterial(light, 0x6654FF, 0x060433, 150);
      sphere3 = new Sphere(phongMaterial, 50, 10, 10);
      sphere3.z = -ORBITAL_RADIUS;

      // Create a new material (cell shaded) and apply it to a sphere
      var cellMaterial:MaterialObject3D = new CellMaterial(light, 0x6654FF, 0x060433, 5);
      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 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);
     
      // call the renderer
      super.onRenderTick(event);
    }

  }
}

This results in four rotating spheres, each one shaded differently and all rotating about the center of the display (click on image to launch animation).


This example uses the same code as in the previous post as a starting block. The initialisation of the 3D is identical : the viewport and the camera position are the same as before. The scene creation is however different to create the four spheres and include a light source.

Adding a light source is very simple in Papervision3D : all we need to do is create a point light source and give it a position.

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

The value of true in the constructor simply indicates to Papervision3D that we want to artificially render an object representing the light source itself. In this example its not really necessary, but its useful as a debugging tool.

To use the light source we need to create materials that provide shading. The calculation of the colours that we see on the screen are made by these materials rather than the light itself. We can assume that the light source is white light : colours are specified with the materials.

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

      // Create a new material (Gouraud shaded) and apply it to a sphere
      var gouraudMaterial:MaterialObject3D = new GouraudMaterial(light, 0x6654FF, 0x060433);
      sphere2 = new Sphere(gouraudMaterial, 50, 10, 10);
      sphere2.x =  ORBITAL_RADIUS;

      // Create a new material (Phong shaded) and apply it to a sphere
      var phongMaterial:MaterialObject3D = new PhongMaterial(light, 0x6654FF, 0x060433, 150);
      sphere3 = new Sphere(phongMaterial, 50, 10, 10);
      sphere3.z = -ORBITAL_RADIUS;

      // Create a new material (cell shaded) and apply it to a sphere
      var cellMaterial:MaterialObject3D = new CellMaterial(light, 0x6654FF, 0x060433, 5);
      sphere4 = new Sphere(cellMaterial, 50, 10, 10);
      sphere4.z =  ORBITAL_RADIUS;

This code creates the four identical spheres positioned on the x-z plane, each one with a different material (being one of the four main shaded material types mentioned above). As you can see, each material takes a light as the first parameter. The two colours are then diffuse and ambient for FlatShaded and Gouraud, specular and ambient for Phong respectively. For the Cell shaded material the two colours are the two extremes which are then split into the n different values. The Phong material takes an additional parameter specifying the level of specular reflection being between 0 and 255 - higher numbers result in more point-like reflections.

Finally I’d like to mention the use of a DisplayObjet3D to group the spheres together :

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

      // Add the light and spheres to the scene
      scene.addChild(sphereGroup);
      scene.addChild(light);

This object acts as a layer that can be manipulated as with any other 3D object. However this manipulation performs an action on all the child objects together as we’ll see when we look at the code for the animation. This object, as well as the light, are then added to the main scene to be rendered.

To animate the scene I wanted all the spheres to rotate about the origin. Rather than calculating the coordinates of each sphere (as I did in my last post), the sphereGroup object allows me to rotate all the spheres together : Papervision3D does all the hard work in calculating their coordinates.

      // rotate the spheres
      sphere1.yaw(-8);
      sphere2.yaw(-8);
      sphere3.yaw(-8);
      sphere4.yaw(-8);
     
      // rotate the group of spheres
      sphereGroup.yaw(3);

As with the previous example, each sphere is individually rotated on its axis. The rotation of all four about the origin now just takes a single line : sphereGroup.yaw(3) : much simpler to code than before !

So there you are ! This is really just a simple example of basic lighting and shading. You’ll find more complex examples on the web, for example including techniques such as bump mapping, to create more realistic scene lighting. Anyway, as always, I hope this has given a few insights into lighting with Papervision3D and please send any comments or suggestions as this article really shows only the tip of the iceberg !

Next article: