-->

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

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

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

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

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

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

Two additional features are available:

  • Selection of the displacement direction. For example if you have a displacement on a xz surface, for the lighting to be correct you need to specify that the displacement is in the y direction.
  • Amplitude selection. You can modify the coarseness of the displacements by adjusting the amplitude factor - low is a smooth surface, high is a very rough surface.

Update !

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

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

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

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

So, big pause since my last post… unfortunately sometimes real-world work gets the better of us and consumes most of our time! Not good… so time to get back into the swing of more interesting things!

I’m going to take a little break from Papervision3D for a while and check out another library - Away3D. Its not that I’m turning my back on pv3d but since its not the only 3D motor out there for flash I thought I’d see what its like to work with… I’m simply curious!

Finding recent comparisons between different 3D motors is not so obvious. Here’s one from Mr.Doob dating from July 2007 for example but since both Away3D and Papervision3D have advanced enormously over the last 12 months its difficult to know if its still valid.

However, I did come across this blog by Vincent Helwig which is work in progress but aims to compare Alterniva3D, Papervision3D, Away3D and Sandy3D. Each one is briefly presented and simple examples for all four are given. The impression I get from this is that Sandy3D is lagging behind the others in terms of being actively developed (no easy task of course), Alternativa3D is not open source which leaves (speaking personally) Papervision3D and Away3D as the two main players.

What’s interesting to note as well is that Flash Player 10 includes some 3D capabilities (z-sorting for example is not included) which for some circumstances greatly improves performance (see this comparison between Away3D and FP10 for example). These new embedded capabilities will surely be integrated by both pv3d and Away3D over the coming months - interesting times ahead for sure!

-->
Sunday, October 5th, 2008

Nice and shiny! Phong reflection in Papervision3D

In my post First steps in Papervision3D : Part 4 - lighting and shading, I mentioned that the materials provided by Papervision3D do not allow us to include ambient, diffuse and specular lighting all together at the same time. The two principal shaded materials provided by Papervision3D are

The Phong reflection model however is a combination of all three and allows us to model surfaces that have both a rough (diffusive) characteristic and a shiny (specular) one. This is particularly good, for example, in modeling plastic-type materials.

Even though there is not a specific material for which we can specify ambient, diffuse and specular lighting all together, using the viewport layers interface of Papervision3D we can create different lighting effects on an object and combine them together on the screen.

In theory, diffusive lighting is added to a scene in a multiplicative fashion. The resulting colour seen on the screen is the product of the colour of the material and the quantity of the diffused light. For example we can have a material that is blue (0×0000FF) with a diffusive light pattern that varies from bright white (0xFFFFFF) to grey (ambient) (0×222222). The resulting colours seen on the screen therefore vary from bright blue (0×0000FF) to dark blue (0×000022).

Specular light is added to the scene in an additive way: light is reflected off an object towards an observer thereby brightening the object. Therefore even parts of a black material can be seen with specular light.

Phong reflection is the combination of these two lighting techniques: a material darkened with a diffusive and abient light and enhanced by a specular one.

The demo shown here tries to implement these characteristics. Using ViewportLayers a surface is rendered three times: once with a simple material colour, once with a diffuse light pattern (using a GouraudMaterial to create both diffusive and ambient effects) and finally with a specular one (using a modified PhongMaterial for which the ambient light is turned off, hence producing only specular light).

Each layer is added to the scene with a different BlendMode: MULTIPLY for the diffusive light pattern and ADD for the specular one. The order of each layers is important hence the coloured material is rendered first, followed by the diffusive light and finally the specular light added to the end.

You can take a look at the demo by clicking on the image below. To rotate the scene click and move the mouse. Clicking on the surface separates it into the different components for five seconds so you can see what each individual layer looks like.

Surface showing phong reflection

You can take a look at the source at http://www.tartiflop.com/pv3d/PhongReflection/srcview/.

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