Previous articles summary :
- First steps in Papervision3D : Part 1
Creation of a new Papervision3D project within eclipse or Flex Builder 3 and a simple example of a 3D scene illustrating some basic Papervision3D classes. - First steps in Papervision3D : Part 2
Illustration of use of new Papervision3D v2.0 class BasicView that encapsulates essential elements to rapidly start developing new 3D scenes. - First steps in Papervision3D : Part 3 - animation
Adding some basic animation to the scene by modifying 3D object parameters when entering a frame. - First steps in Papervision3D : Part 4 - lighting and shading
A point light source is added to the scene. Fours materials with different characteristics are added to spheres to illustrate how Papervision3D performs shading. - First steps in Papervision3D : Part 5 - scene interaction
Listen to mouse events to interact with the scene and with individual rendered objects. - First steps in Papervision3D : Part 6 - Texture mapping
Create materials using bitmap data with the aim of having more detailed or realistic objects. - First steps in Papervision3D : Part 7 - Texture mapping with lighting, bump mapping and environment mapping
Advanced materials using specific shaders to enhance texture mapped materials with bump mapping and environment mapping. - First steps in Papervision3D : Part 8 - Movie materials
Displaying animated and interactive movies as materials on 3D surfaces.
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
- 3DVIA that has a large collection of models in a number of different formats, including Collada, supported by a community of 3D artists
- The test model bank at Khronos which has a limited number of models available to the public
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.
- Mikes blog has a number of links and source code on how to create Collada objects.
- Modern Carpentry shows how to instantiate a DAE object.
- Papervision2 has a very good tutorial on creating Collada objects that I’d really recommend… especially since the cow Collada object comes from them :)
- Emmanuel Bonnet also has a nice little demo with source for a Collada object.
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
- An embedded Collada file and bitmap
- A URL for an external Collada
- An animated Collada object from an external URL
- A Google Earth object file, exported from Google Sketchup
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!

