-->

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:

Away3D can be installed either from the latest releases on the Away3D downloads page or from the SVN repository located on the googlecode site.

I’ve chosen the second method so that I can easily and regularly update the source to be up to date with the latest fixes and enhancements. I’m using the same technique that I showed downloading and compiling Papervision3D using SVN in eclipse. Since I use eclipse as my development environment (with the Flex Builder 3 plugin), I like to keep all my sources together in the same environment and the SVN plugin for eclipse works very well.

In eclipse select Import… from the File menu. You’ll see the following window appear.

Under the SVN item, select Projects from SVN and click Next. You’ll then be requested to enter details for the SVN repository.

For the URL enter http://away3d.googlecode.com/svn. Leave the user details empty - we’ll use anonymous access to obtain the source. Click Next to continue. Eclipse will examine the SVN repository and show the repository structure.

We’ll download everything from the trunk (which includes the source, docs and examples), so select trunk and click on Finish. You’ll then be asked how you want to check out the source.

We want to create a Flex Library project so select Check out as a project configured using the New Project Wizard.

Under Flex Builder, choose Flex Library Project and click Next.

Enter Away3D as the Project name (or any other name you’d like…) then select Next to configure the source directory.

Click next to src under Classes to include in the library. The Main source folder should show src, and the Output folder should show bin. Sometimes selecting the source folder here doesn’t always work and we’ll have to explicitly give the source folder again after the project has been created, as shown below.

You should now see in the Flex Navigator in eclipse a new project called Away3D with the latest revision number next to it. If you have the error nothing was specified to be included in the library shown in the Problems view, then you need to re-specify the source directory as I mentioned above. Simply right-click on the Away3D project and select Properties.

Under Flex Library Build Path, once again click next to src in the Classes to include in library box. After clicking OK you should see that eclipse is compiling the sources.

The final result is the Away3D.swc Flex library, located in the bin directory, that can be used with other Away3D projects that you create afterwards. Similarly, with eclipse, you can compile Away3D projects by linking directly to this project. Personally I prefer the second method simply because it makes developing and debugging easier as you can directly look at the source for a particular Away3D class.

Hope this is of some use. I’ll be taking a look, as I did with Papervision3D, at producing a few simple examples just to get a feel of the library… more soon I hope!

-->
Saturday, September 6th, 2008

First steps in Papervision3D : Part 8 - Movie materials

Previous articles summary :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    override protected function onRenderTick(event:Event=null):void {       // rotate the object group: angle kept between 0 and 360 degrees       objectGroup.rotationY += 1;       if (objectGroup.rotationY > 360) {         objectGroup.rotationY -= 360;       }             // if an object is active (double clicked) rotate it in the opposite direction       // to the group so that it is stationary       if (currentActiveObject != null) {         currentActiveObject.rotationY -=1;         if (currentActiveObject.rotationY < 0) {           currentActiveObject.rotationY += 360;         }       }             // If the mouse button has been clicked then update the camera position            if (doRotation && canRotate) {         // convert the change in mouse position into a change in camera angle         var dPitch:Number = (mouseY - lastMouseY) / 2;         var dYaw:Number = (mouseX - lastMouseX) / 2;                 // update the camera angles         cameraPitch -= dPitch;         cameraYaw -= dYaw;         // limit the pitch of the camera         if (cameraPitch <= 0) {           cameraPitch = 0.1;         } else if (cameraPitch >= 180) {           cameraPitch = 179.9;         }               // reset the last mouse position         lastMouseX = mouseX;         lastMouseY = mouseY;                 // reposition the camera         camera.orbit(cameraPitch, cameraYaw);       }             // call the renderer       super.onRenderTick(event);     }

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

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

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

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

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

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

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

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

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

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

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

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

Here’s drawTool.as…

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

… and finally Example006b.as :

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

Next article:

Sunday, August 3rd, 2008

First steps in Papervision3D : Part 5 - scene interaction

Previous articles summary :

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

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

To illustrate the interactions with the scene objects, I’m adding a third new element to code : tweens. Without having to calculate and specify the change in positions (for example) of objects within the code, a tween does all the hard work for us. A tween works on a particular property of an object and at each frame of an animation modifies this property in a particular manner. I’m not going to go into much detail here as its not the purpose of this post but you’ll find a lot of information on the web - here’s wikipedia’s definition for what its worth. The tweens I use here come from the opensource library Tweener which provides tweens for Actionscript2 and Actionscript3 as well as JavaScript and HaXE. To install it in your Eclipse/Flex Builder 3 environment follow the same guidelines that I gave with installing Papervision3D but rather than importing the source using SVN, simply download the zip file of the source from the Tweener homepage and extract it into the src folder of a new Tweener project. When its compiled link your own Actionscript project to the Tweener project.

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

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

  public class Example005 extends BasicView {
 
    private static const ORBITAL_RADIUS:Number = 200;
   
    private var sphere1:Sphere;
    private var sphere2:Sphere;
    private var sphere3:Sphere;
    private var sphere4:Sphere;
    private var sphereGroup:DisplayObject3D;
    private var light:PointLight3D;
   
    private var doRotation:Boolean = false;
    private var lastMouseX:int;
    private var lastMouseY:int;
    private var cameraPitch:Number = 60;
    private var cameraYaw:Number = -60;
   
    public function Example005() {
      // specify that we want the scene to be interactive
      super(0, 0, true, true);
     
      // set up the stage
      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;

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

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

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

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

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

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

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

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

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

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

      // If the mouse button has been clicked then update the camera position     
      if (doRotation) {
       
        // convert the change in mouse position into a change in camera angle
        var dPitch:Number = (mouseY - lastMouseY) / 2;
        var dYaw:Number = (mouseX - lastMouseX) / 2;
       
        // update the camera angles
        cameraPitch -= dPitch;
        cameraYaw -= dYaw;
        // limit the pitch of the camera
        if (cameraPitch <= 0) {
          cameraPitch = 0.1;
        } else if (cameraPitch >= 180) {
          cameraPitch = 179.9;
        }
     
        // reset the last mouse position
        lastMouseX = mouseX;
        lastMouseY = mouseY;
       
        // reposition the camera
        camera.orbit(cameraPitch, cameraYaw);
      }
     
      // call the renderer
      super.onRenderTick(event);
    }

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

    // called when mouse up on stage
    private function onMouseUp(event:MouseEvent):void {
      doRotation = false;
    }
   
    // called when mouse down on a sphere
    private function onMouseDownOnSphere(event:InteractiveScene3DEvent):void {
      var object:DisplayObject3D = event.displayObject3D;
      Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });
    }
   
    // called when a tween created in onMouseDownOnSphere has terminated
    private function goBack(object:DisplayObject3D):void {
      Tweener.addTween(object, {y:0, time:2, transition:"easeOutBounce"});
    }
  }
}

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


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

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

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

When a mouse click occurs on the stage, calls are made to onMouseDown and onMouseUp. These methods modify a boolean value to indicate that the user wants to translate the camera and initialises the position of the mouse, necessary to determine the amount of movement that occurs during a frame.

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

    // called when mouse up on stage
    private function onMouseUp(event:MouseEvent):void {
      doRotation = false;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next article:

-->
Tuesday, July 29th, 2008

First steps in Papervision3D : Part 1

In this post I hope to show how, in a few simple steps, we can create a simple 3D scene in Actionscript 3 using Papervision3D. As I’ve mentioned in previous posts, I’m no expert and am merely posting on the web the experiences I’ve had over the last few weeks in starting myself. A couple of useful links providing an introduction to 3D techniques and Papervision3D features provide a good starting point.

My main objective here is to highlight the principal elements necessary to render a 3D scene using certain principal pv3d classes namely Scene3D, Viewport3D, Camera3D and BasicRenderEngine. In the next post in this series I’ll show how Papervsion3D v2.0 provides a simple wrapper class, BasicView, that encapsulates all of these making it even simpler to get started. However, for this post I wanted to show the basic ingredients involved in Papervision3D.

Before starting, I’m assuming that you’re using Flex Builder 3 (or the Eclipse plugin) and have downloaded and compiled Papervision3D v2.0 (Great White) within this environment. You can check out these previous posts if you need help :

1. Creating a new project
From the File menu in Eclipse, select New -> ActionScript Project. In the new project wizard, enter the name of the project (here I’ve called it FirstSteps) and click on Next.


We now need to specific the source path. Rather unimaginatively, I use src entered next to the Main source folder label. I also change the name of the Main application file to Example001.as. Note that this can also be changed later. The output folder I left as its default value. Next click on the Library path tab to set up the import of Papervision3D.

Initially only the Flex 3 classes are included and we need to import the Papervision3D ones. You now have two options : either link this project to the Papervision3D one (see my last post) by clicking on the Add Project… button or copy the compiled as3 library from that project into the new project and link to it by clicking on Add SWC… For this example I’m going to use the first option - I’ve found it useful over the last few weeks to be able to wander through the Papervision3D source code during the development stages. I don’t know if there’s an advantage or not with using one option rather than the other… In any case you can always change how you import Papervision3D later by modifying the project properties. So, for the time being, click on Add Project… and select the Papervision3D project that we created before.


2. Example001.as
We’re now ready to start our first example. Open the newly created Example001.as file and replace what’s been automatically generated with the following code. This example draws a wire-frame sphere and the x-, y- and z- axes. What I really want to illustrate though is how the scene is set up.

package {
 
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.events.Event;
 
  import org.papervision3d.cameras.Camera3D;
  import org.papervision3d.core.geom.Lines3D;
  import org.papervision3d.core.geom.renderables.Line3D;
  import org.papervision3d.core.geom.renderables.Vertex3D;
  import org.papervision3d.core.proto.MaterialObject3D;
  import org.papervision3d.materials.WireframeMaterial;
  import org.papervision3d.materials.special.LineMaterial;
  import org.papervision3d.objects.DisplayObject3D;
  import org.papervision3d.objects.primitives.Sphere;
  import org.papervision3d.render.BasicRenderEngine;
  import org.papervision3d.scenes.Scene3D;
  import org.papervision3d.view.Viewport3D;

  public class Example001 extends Sprite {
 
    private var scene:Scene3D;
    private var camera:Camera3D;
    private var viewport:Viewport3D;
    private var renderer:BasicRenderEngine;
 
    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 viewport
      viewport = new Viewport3D(0, 0, true, false);
      addChild(viewport);

      // Create new camera with fov of 60 degrees (= default value)
      camera = new Camera3D(60);
     
      // initialise the camera position (default = [0, 0, -1000])
      camera.x = -100;
      camera.y = -100;
      camera.z = -500;
     
      // target camera on origin
      camera.target = DisplayObject3D.ZERO;

      // Create a new scene where our 3D objects will be displayed
      scene = new Scene3D();
 
      // Create new renderer
      renderer = new BasicRenderEngine();
    }

    private function createScene():void {

      // First object : a sphere
     
      // Create a new material for the sphere : simple white wireframe
      var sphereMaterial:MaterialObject3D = new WireframeMaterial(0xFFFFFF);

      // Create a new sphere object using wireframe material, radius 50 with
      //   10 horizontal and vertical segments
      var sphere:Sphere = new Sphere(sphereMaterial, 50, 10, 10);

      // Position the sphere (default = [0, 0, 0])
      sphere.x = -100;

      // Second object : x-, y- and z-axis
     
      // Create a default line material and a Lines3D object (container for Line3D objects)
      var defaultMaterial:LineMaterial = new LineMaterial(0xFFFFFF);
      var axes:Lines3D = new Lines3D(defaultMaterial);

      // Create a different colour line material for each axis
      var xAxisMaterial:LineMaterial = new LineMaterial(0xFF0000);
      var yAxisMaterial:LineMaterial = new LineMaterial(0x00FF00);
      var zAxisMaterial:LineMaterial = new LineMaterial(0x0000FF);

      // Create a origin vertex
      var origin:Vertex3D = new Vertex3D(0, 0, 0);

      // Create a new line (length 100) for each axis using the different materials and a width of 2.
      var xAxis:Line3D = new Line3D(axes, xAxisMaterial, 2, origin, new Vertex3D(100, 0, 0));
      var yAxis:Line3D = new Line3D(axes, yAxisMaterial, 2, origin, new Vertex3D(0, 100, 0));
      var zAxis:Line3D = new Line3D(axes, zAxisMaterial, 2, origin, new Vertex3D(0, 0, 100));
     
      // Add lines to the Lines3D container
      axes.addLine(xAxis);
      axes.addLine(yAxis);
      axes.addLine(zAxis);

      // Add the sphere and the lines to the scene
      scene.addChild(sphere);
      scene.addChild(axes);
    }
   
    private function loop(event:Event):void {
      // Render the 3D scene
      renderer.renderScene(scene, camera, viewport);
    }
  

  }
}

When executed you should see a wireframe sphere to the left and the a red x-axis, green y-axis and blue z-axis (click on image to execute the Flash animation):

Now let’s go into a bit more detail into the code.

3. Initialising the stage
During the setup of the project we specified that Example001.as should be the main application file. This means that it has access to the stage and its associated variables. In the constructor of Example001.as we include the following lines of standard Actionscript code :

      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;

The first line indicates that rendered scene should be aligned to the top-left of the browser. The second indicates that the displayed scene will remain the same size when the browser window is redimensioned.

The other pure Actionscript part is to specify an event listener so that the scene can be drawn on the stage (and updated if the scene changes). This is specified using the event listener :

      this.addEventListener(Event.ENTER_FRAME, loop);

4. Initialising the 3D scene
There are three essential classes necessary to set up the scene : Scene3D, Viewport3D and Camera3D. I’d recommend playing around with the values that I’ve given in the code to see what differences they make - its always easier to learn through trial and error !

Firstly we specify the viewport which is added directly as a child of of Example001 and rendered on the stage :

      viewport = new Viewport3D(0, 0, true, false);
      addChild(viewport);

The first two parameters specify the size of the viewport (width and height respectively). By setting them both to 0 we indicate to Papervision3D that we want to use the full width of the stage. We could though for example have a viewport that is smaller than the stage. The first boolean value we pass is to resize the viewport when the stage (or browser window) is resized. The final value indicates that we are not rendering an interactive scene.

Next we set up the camera which specifies the user location relative to the scene and in which direction they are looking in. We imagine that the user is seeing the scene through a camera because variables such as zoom and focus can be specified.

      // Create new camera with fov of 60 degrees (= default value)
      camera = new Camera3D(60);
     
      // initialise the camera position (default = [0, 0, -1000])
      camera.x = -100;
      camera.y = -100;
      camera.z = -500;
     
      // target camera on origin
      camera.target = DisplayObject3D.ZERO;

The camera is created with a field of vision of 60 degrees, indicating the vertical visible angle. We then give it a position which by default is located at -1000 along the z-axis (positive values go into the screen). We then indicate that we want the camera to point towards the origin - if you leave this out you’ll notice that the camera by default looks along the z-axis.

Finally we create a Scene3D and a BasicRenderEngine.

      // Create a new scene where our 3D objects will be displayed
      scene = new Scene3D();
 
      // Create new renderer
      renderer = new BasicRenderEngine();

All 3D objects that we create with Papervision are added to the Scene3D, as we’ll see later, and the Renderer is used, as one might expect, to draw the scene.

That, at its very simplest, is how we set up Papervision3D before creating the 3D objects (actually, that’s not true : I’ll show in the next post an easier way !). As I said before, play around with the value and, more importantly, explore the Papervision3D code to see what the different parameters do.

5. Adding 3D objects to the scene
Here I’m going to add two simple objects to the scene : a sphere with a wireframe structure, and three lines to show the x-, y- and z-axes, each with a different colour.

We use a Papervision3D primitive object for the sphere - the people at Papervision3D have done all the hard work for us, all we need to do is give a few parameters for its size and amount of detail.

      // Create a new material for the sphere : simple white wireframe
      var sphereMaterial:MaterialObject3D = new WireframeMaterial(0xFFFFFF);

      // Create a new sphere object using wireframe material, radius 50 with
      //   10 horizontal and vertical segments
      var sphere:Sphere = new Sphere(sphereMaterial, 50, 10, 10);

      // Position the sphere (default = [0, 0, 0])
      sphere.x = -100;

First of all we create a new material. All objects in Papervision3D have an associated material. To name but a few examples, the material can be simply to display edges (as we use here), a coloured material for a solid colour, a shaded one that takes into account lighting or a bitmapped material to show images. If you explore in the Papervision3D source to org/papervision3d/materials you’ll find a huge variety. For our example we create a wireframe material (showing the edges of the polygons used for the sphere) having a white colour.

The sphere is then created with this material. We give it a radius of 50 and specify that it is made up of 10 horizontal and vertical segments.

After the sphere, we create the axes.

      // Create a default line material and a Lines3D object (container for Line3D objects)
      var defaultMaterial:LineMaterial = new LineMaterial(0xFFFFFF);
      var axes:Lines3D = new Lines3D(defaultMaterial);

      // Create a different colour line material for each axis
      var xAxisMaterial:LineMaterial = new LineMaterial(0xFF0000);
      var yAxisMaterial:LineMaterial = new LineMaterial(0x00FF00);
      var zAxisMaterial:LineMaterial = new LineMaterial(0x0000FF);

      // Create a origin vertex
      var origin:Vertex3D = new Vertex3D(0, 0, 0);

      // Create a new line (length 100) for each axis using the different materials and a width of 2.
      var xAxis:Line3D = new Line3D(axes, xAxisMaterial, 2, origin, new Vertex3D(100, 0, 0));
      var yAxis:Line3D = new Line3D(axes, yAxisMaterial, 2, origin, new Vertex3D(0, 100, 0));
      var zAxis:Line3D = new Line3D(axes, zAxisMaterial, 2, origin, new Vertex3D(0, 0, 100));
     
      // Add lines to the Lines3D container
      axes.addLine(xAxis);
      axes.addLine(yAxis);
      axes.addLine(zAxis);

The lines are created using the Line3D class. These lines need to be grouped together in a Lines3D object. As for the sphere, all these objects need to have an associated material : here we use the LineMaterial with different colours for each axis (red, green and blue for x, y and z respectively). Lines are defined simply by specifying two vertices. After they are created we add them to the Lines3D group.

We now have our main 3D objects. All that is left to do is add them to the scene.

      // Add the sphere and the lines to the scene
      scene.addChild(sphere);
      scene.addChild(axes);

We now have Papervision3D initialised and the 3D objects created and added to the scene. All that’s left is to render the scene.

6. Rendering the scene
As mentioned previously, we use the Actionscript event listener to call a method at every frame of the Flash movie. In this method we simply use the BasicRendererEngine that we created earlier to update the rendered scene.