-->

Back again after a break longer than predicted! In this tutorial I’ll show how we can add more stunning visual effects to objects by applying normal maps (to give the impression that an object is composed of many more triangles from the rendered shading) and environment maps (for objects to appear shiny and reflecting the surrounding environment). An equivalent tutorial for Papervision3D can be found at my post on texture mapping with lighting, bump mapping and environment mapping.

Previous articles summary :

Normal maps - or Dot3 bump maps - can be seen as something of an evolution from standard bump maps. Bump maps, as used in Papervision3D, contain a single value describing a displacement perpendicular to a surface. Normal maps, however, are more detailed because they contain three axis elements, effectively describing the normal to a specific point on a surface, replacing the triangle normal entirely. Whereas a bump map would be rendered the same for any point of view, a normal map is rendered differently depending on the viewer’s position: hence giving a much better impression of realism.

Normal maps are mapped in much the same way as a texture map, using the same uv values as for a texture. The bitmap data used in a normal map is split into the three components red, green and blue, each one giving a representation of the normal vector components in x, y and z respectively. So, normal values between -1 and 1 are discretised to an integer value between 0 and 255.

Normal maps greatly increase the performance of rendering a 3D object since the normals are given directly rather than requiring a calculation from triangles. So, a complex model requiring thousands of triangles can be reduced to, say, a few hundred (a low-poly model) with a normal map being derived from the original model. You can check out wikipedia for more information on normal mapping. For an example of a low-poly model using a normal map generated from a more complex one, check out this demo of normal mapping at Away3D.

Environment mapping, or reflection mapping, is a technique used to map a surrounding environment onto an object giving the impression that the object has a mirrored surface. The technique is much more efficient than ray tracing and, even though it is not exact, still produces realistic effects. Again, wikipedia contains some useful information on environment mapping.

In this tutorial I’m going to show normal mapping on a cube and a sphere and environment mapping on a sphere. Following from the last post on Lighting and Shading, we’re going to be using another couple of CompositeMaterials: namely the Dot3BitmapMaterial and the EnviroBitmapMaterial.

It should be noted that while normal mapping can produce a more realistic effect than bump mapping, it can be more complex to implement. Since a normal map replaces the normal to a surface we cannot use the same map on an object that has many faces (unless the normal map has been created specifically for example as shown in the Away3D demo above). This is a problem for example for Primitives. A Cube for example has six faces with six discrete principal normals - if we use the same normal map for each surface then each face will have the same normal data and hence be rendered identically, rather than each face being independent. Currently we therefore have to provide six independent normal maps.

My method for this example however is to create a cube using six individual planes using the same normal map and rotating and translating them: the normal map data is then rendered correctly for each face.

I also had a problem retrieving normal maps from the web for use in this tutorial. To help me I created a small utility that takes bump (displacement) map data and converts this into normal map data for either a plane or a sphere. You can try this out yourselves by checking out my article on displacement map to normal map conversion. With this utility you are able to specify the direction of the displacement (for a plane) or the direction against which the angle phi is calculated for a sphere.

So, lets get on with the code! Below you’ll see the next example in this series, producing three rotating texture-mapped objects each one rendered with either an environment map or normal map.

package {   import away3d.cameras.HoverCamera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.render.Renderer;   import away3d.core.utils.Cast;   import away3d.lights.DirectionalLight3D;   import away3d.materials.Dot3BitmapMaterial;   import away3d.materials.EnviroBitmapMaterial;   import away3d.primitives.Plane;   import away3d.primitives.Sphere;     import flash.display.Bitmap;   import flash.display.BitmapData;   import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;     [SWF(backgroundColor="#000000")]     public class Example006 extends Sprite {     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/normalMap.png")] private var NormalImage:Class;     private var normalBitmap:Bitmap = new NormalImage();     [Embed(source="/../assets/asteroidNormal.png")] private var SphereNormalImage:Class;     private var sphereNormalBitmap:Bitmap = new SphereNormalImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();     [Embed(source="/../assets/envMap.png")] private var EnvironmentImage:Class;     private var envBitmap:Bitmap = new EnvironmentImage();     private static const ORBITAL_RADIUS:Number = 150;     private static const CAMERA_ORBIT:Number = 600;     private var scene:Scene3D;     private var camera:HoverCamera3D;     private var view:View3D;       private var planeGroup:ObjectContainer3D;     private var normalSphere:Sphere;     private var envSphere:Sphere;     private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var lastPanAngle:Number = 60;     private var lastTiltAngle:Number = -60;         public function Example006() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Initialise Away3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     /**     * Initialise all 3D components.     */     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new HoverCamera3D({zoom:25, focus:30, distance:600});       camera.targetpanangle = camera.panangle = -10;       camera.targettiltangle = camera.tiltangle = 20;       camera.yfactor = 1;             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the view to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             // ensure that the z-order is calculated correctly       view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }     /**     * Create the objects and lighting of the scene     */     private function createScene():void {       // Create 3 different materials: two normal mapped ones (planar and spherical) and an       // environment mapped one.       var normalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(Cast.bitmap(away3DBitmap), Cast.bitmap(normalBitmap), {smooth:true, precision:5});       var envMapMaterial:EnviroBitmapMaterial = new EnviroBitmapMaterial(Cast.bitmap(checkerBitmap), Cast.bitmap(envBitmap), {smooth:true, precision:5});       var sphereNormalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(new BitmapData(1, 1, false, 0x666666), Cast.bitmap(sphereNormalBitmap), {smooth:true, precision:5});       // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 10000;       light.z = 50000;       light.y = 50000;       scene.addChild(light);       // Create six with the same (normal mapped) material and position them them to create a cube       var topPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       topPlane.y = 50;       var leftPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       leftPlane.rotationZ = 90;       leftPlane.x = -50;       var frontPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       frontPlane.rotationX = 90;       frontPlane.z = -50;       var bottomPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       bottomPlane.rotationX = 180;       bottomPlane.y = -50;       var rightPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       rightPlane.rotationZ = -90;       rightPlane.x = 50;       var backPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       backPlane.rotationX = -90;       backPlane.z = 50;             // Create an object container to group the sides of the cube       planeGroup = new ObjectContainer3D();       scene.addChild(planeGroup);       planeGroup.addChild(topPlane);       planeGroup.addChild(leftPlane);       planeGroup.addChild(frontPlane);       planeGroup.addChild(bottomPlane);       planeGroup.addChild(rightPlane);       planeGroup.addChild(backPlane);       planeGroup.x = -100;       planeGroup.z = 100;       // Create a sphere with normal-mapped material       normalSphere = new Sphere({material:sphereNormalMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       normalSphere.x = 100;       normalSphere.z = 100;       scene.addChild(normalSphere);       // Create a sphere with environment-mapped material       envSphere = new Sphere({material:envMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       envSphere.z = -90;       scene.addChild(envSphere);     }         /**     * Frame-enter event handler     */     private function loop(event:Event):void {             // rotate the objects       planeGroup.rotationY += 2;       normalSphere.rotationY += 2;       envSphere.rotationY += 2;       // update camera position       updateCamera();       camera.hover();       // Render the 3D scene       view.render();     }     /**     * Update the camera position from mouse movements     */     private function updateCamera():void {       if (doRotation) {         camera.targetpanangle = 0.5 * (stage.mouseX - lastMouseX) + lastPanAngle;         camera.targettiltangle = 0.5 * (stage.mouseY - lastMouseY) + lastTiltAngle;       }     }         /**     * Mouse down listener for camera rotation     */     private function onMouseDown(event:MouseEvent):void {       lastPanAngle = camera.targetpanangle;       lastTiltAngle = camera.targettiltangle;       lastMouseX = stage.mouseX;       lastMouseY = stage.mouseY;       doRotation = true;       stage.addEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }         /**     * Mouse up listener for camera rotation     */     private function onMouseUp(event:MouseEvent):void {       doRotation = false;       stage.removeEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }     /**     * Mouse stage leave listener for camera rotation     */     private function onStageMouseLeave(event:Event):void {       doRotation = false;       stage.removeEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }         /**     * Resize the scene when the stage resizes     */      private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   }     }

If you want to use the same images as in this example then away3D.png and checker.jpg you’ll find in the previous post on texture mapping in Away3D. The new images are shown below.

The first two images (the normal maps) were created using the utility described above. The first one from a bump map I used for a Papervision3D tutorial on bump mapping, the second one comes from a bump map I found on the internet at gamedev.net. The environment map comes from and old OpenGL example I discovered on my hard disk… if you search for “opengl sphere map” in Google you’ll find plenty of instances!

Once its all compiled you should see three rotating objects with either normal and environment mapped data. Click on the image below to see the Flash movie. You can click anywhere and move the mouse to rotate the scene.

So, lets go into more detail on each part of the code. There’s nothing too complicated here but a few changes from the last time…

The first change comes from the initialisation of the 3D basics of the scene, specifically with the camera.

    private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new HoverCamera3D({zoom:25, focus:30, distance:600});       camera.targetpanangle = camera.panangle = -10;       camera.targettiltangle = camera.tiltangle = 20;       camera.yfactor = 1;             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera});       // center the view to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             // ensure that the z-order is calculated correctly       view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }

I’ve now chosen to use a HoverCamera3D. This simplifies the movement and positioning of the camera. You’ll remember that previously I used the basic Camera3D class and had to calculate specific values of x, y and z as well as redirect the camera towards the origin. Much of this is included in the HoverCamera3D class. By specifying targettiltangle and targetpanangle the camera will calculate a trajectory necessary to move the camera to a new position and keep it focused on a particular target. To maintain the orbital radius of the camera we set the yfactor to 1 otherwise a more elliptical orbit is calculated.

You’ll see towards the end of the code how we perform an interaction with the camera. If we look at the loop function, called on every frame-enter event we can see how the camera position is updated.

    private function loop(event:Event):void {             // rotate the objects       planeGroup.rotationY += 2;       normalSphere.rotationY += 2;       envSphere.rotationY += 2;       // update camera position       updateCamera();       camera.hover();       // Render the 3D scene       view.render();     }

The object rotation is self evident so we’ll ignore that. The updateCamera function recalculates the camera position from mouse position. More importantly the camera.hover() call makes the camera move gradually towards the target tilt and pan angles giving a smoother animation of the camera than in previous examples.

For completeness, the updateCamera function and the mouseDown event listener are shown below.

    private function updateCamera():void {       if (doRotation) {         camera.targetpanangle = 0.5 * (stage.mouseX - lastMouseX) + lastPanAngle;         camera.targettiltangle = 0.5 * (stage.mouseY - lastMouseY) + lastTiltAngle;       }     }         private function onMouseDown(event:MouseEvent):void {       lastPanAngle = camera.targetpanangle;       lastTiltAngle = camera.targettiltangle;       lastMouseX = stage.mouseX;       lastMouseY = stage.mouseY;       doRotation = true;       stage.addEventListener(Event.MOUSE_LEAVE, onStageMouseLeave);     }

As you can see, the target angles are updated in the updateCamera function, taking into account the current mouse position. The onMouseDown function performs the necessary initialisation of pan and tilt angles. It also adds an event listener to detect when the mouse leaves the stage: this is useful because if the mouse button is released outside the stage, the onMouseUp listener is not called and we can obtain a state in which the scene is continuously rotated even if the mouse button is up.

So, on to more interesting elements of the code: normal mapped and environment mapped materials!

All the interesting stuff happens in createScene where the materials and objects are created.

    private function createScene():void {       // Create 3 different materials: two normal mapped ones (planar and spherical) and an       // environment mapped one.       var normalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(Cast.bitmap(away3DBitmap), Cast.bitmap(normalBitmap), {smooth:true, precision:5});       var envMapMaterial:EnviroBitmapMaterial = new EnviroBitmapMaterial(Cast.bitmap(checkerBitmap), Cast.bitmap(envBitmap), {smooth:true, precision:5});       var sphereNormalMapMaterial:Dot3BitmapMaterial = new Dot3BitmapMaterial(new BitmapData(1, 1, false, 0x666666), Cast.bitmap(sphereNormalBitmap), {smooth:true, precision:5});       // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 10000;       light.z = 50000;       light.y = 50000;       scene.addChild(light);       // Create six with the same (normal mapped) material and position them them to create a cube       var topPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       topPlane.y = 50;       var leftPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       leftPlane.rotationZ = 90;       leftPlane.x = -50;       var frontPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       frontPlane.rotationX = 90;       frontPlane.z = -50;       var bottomPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       bottomPlane.rotationX = 180;       bottomPlane.y = -50;       var rightPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       rightPlane.rotationZ = -90;       rightPlane.x = 50;       var backPlane:Plane = new Plane({material:normalMapMaterial, width:100, height:100, segmentsW:2, segmentsH:2, ownCanvas:true});       backPlane.rotationX = -90;       backPlane.z = 50;             // Create an object container to group the sides of the cube       planeGroup = new ObjectContainer3D();       scene.addChild(planeGroup);       planeGroup.addChild(topPlane);       planeGroup.addChild(leftPlane);       planeGroup.addChild(frontPlane);       planeGroup.addChild(bottomPlane);       planeGroup.addChild(rightPlane);       planeGroup.addChild(backPlane);       planeGroup.x = -100;       planeGroup.z = 100;       // Create a sphere with normal-mapped material       normalSphere = new Sphere({material:sphereNormalMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       normalSphere.x = 100;       normalSphere.z = 100;       scene.addChild(normalSphere);       // Create a sphere with environment-mapped material       envSphere = new Sphere({material:envMapMaterial, radius:65, segmentsW:10, segmentsH:10, ownCanvas:true});       envSphere.z = -90;       scene.addChild(envSphere);     }

To start off with the three different materials are created. Firstly a Dot3BitmapMaterial taking the Away3D texture and a random normal map. I’ve chosen to smooth the material and have increased the precision of rendering to 5 pixels. The second material uses the EnviroBitmapMaterial using the checkered texture and the OpenGl sphere-map for the environment data, again smoothed as before. Incidentally, the shininess of the material can be modified by specifying the reflectiveness parameter in the initialisation parameters, taking a value between 0 and 1 (for most reflective). The final Dot3BitmapMaterial material, rather than using a bitmap file, creates a plain grey bitmap. The normal map comes from the asteroid spherical normal map.

We add a DirectionalLight3D to the scene to provide uniform lighting, independent of light distance. It should be noted that the Dot3BitmapMaterial will not currently work with a PointLight3D. Also, the EnviroBitmapMaterial is independent of the source: the environment map provides the equivalent shading.

For the cube we create six Planes, each using the same material. These are then rotated and translated to form a cube. They are finally added to a ObjectContainer3D so that we can rotate all of them together in the loop function. Note as well that, as before, each object needs to have its own canvas which is specified within the initialisation parameters.

The two spheres are then created: one using an environment map, the other with normal mapped data. These are then positioned and added to the scene, again with independent canvases.

And that’s all there is to it! As you’ll see, the normal map produces fantastic results and really adds a feeling of depth to the object surface. The environment map really gives the impression that the object is very shiny. These effects are very simple to produce in Away3D but be aware too that added realism comes at a cost - the frame rate is bound to be slower than without these effects - so be warned!

Anyway, I hope this has been a useful addition to basic lighting and shading. As always, let me know if you have any comments or questions!

Next article:

Having obtained an interactive, texture mapped scene I’d now like to a bit more realism into it by adding a light source and shading the objects. The price of course for having a more realistic scene is at the cost of reduced performance as you’ll find many more calculations are necessary to render the scene. You can find the equivalent article for Papervision3D in my post on First steps in Papervision3D : Part 4 - lighting and shading, however I find that there is quite a difference for Away3D, at least compared to the other tutorials in this series.

Previous articles summary :

Lighting in Away3D can be split into three basic components:

  • the light source(s) defining the properties of the light(s)
  • shaders providing algorithms that determine how a light source interacts with a material
  • a material type combining different shaders along with base colours or bitmaps

The first difference I notice compared to Papervision3D is that we can specify more than one light source. This of course increases the cpu load but in certain cases may produce a more realistic rendering of a scene.

There are three main types of light sources:

  • AmbientLight3D which has no position and provides a uniform ambient light to all objects
  • DirectionalLight3D which provides light from a specified position but with a strength that does not diminish with distance
  • PointLight3D providing light from a specified position that weakens in intensity with distance

In terms of shaders we have the following posibilities:

  • AmbientShader which provides ambient shading to a triangle face
  • DiffusePhongShader which, depending on the angle between the light source and a triangle face, produces a diffuse shading pattern
  • SpecularPhongShader which produces specular shading on triangles to imitate a surface that is reflecting light, depending on the observer’s position and the light position
  • EnviroShader allowing for environment mapping, producing a reflection from a surrounding environment on an object, independent of a light source. Requires bitmap data for the environment
  • DiffuseDot3Shader to increase scene reality by adding more small scale structure through the use of normal mapping to an object. Requires bitmap data for the normal map

The above shaders, with the exception of the EnviroShader assume that the light source is a DirectionalLight3D.

We then have a choice of CompositeMaterials that uses a combination of different shaders and base materials as shown in the following list:

  • PhongBitmapMaterial used to create ambient, diffuse and specular lighting on a texture-mapped material
    Base material : BitmapMaterial
    Shaders : AmbientShader, DiffusePhongShader and SpecularPhongShader
  • PhongMovieMaterial used to create ambient, diffuse and specular lighting on a material based on another Flash movie
    Base material : MovieMaterial
    Shaders : AmbientShader, DiffusePhongShader and SpecularPhongShader
  • PhongColorMaterial used to create ambient, diffuse and specular lighting on a simple coloured material
    Base material : ColorMaterial
    Shaders : AmbientShader, DiffusePhongShader and SpecularPhongShader
  • EnviroBitmapMaterial used to create environmental lighting on a texture-mapped material
    Base material : BitmapMaterial
    Shaders : EnviroShader
  • Dot3BitmapMaterial used to create ambient and normal-mapped lighting on a texture-mapped material
    Base material : BitmapMaterial
    Shaders : AmbientShader and DiffuseDot3Shader,
  • Dot3MovieMaterial used to create ambient and normal-mapped lighting on a material based on another Flash movie
    Base material : MovieMaterial
    Shaders : AmbientShader and DiffuseDot3Shader

The above materials all produce smoothly shaded objects which naturally has a price in terms of performance. If we want to have objects that are more simply shaded then we can use a CenterLightingMaterial. This type of material produces the equivalent of flat-shaded materials in Papervision3D. We have in this category two choices:

  • ShadingColorMaterial for simple flat shading of a ColorMaterial
  • WhiteShadingBitmapMaterial for simple flat shading of a BitmapMaterial, but assuming that the light source is always white (independent of colour given to light source).

In the example code for this article I’m going to look at two of these materials: the PhongBitmapMaterial and the WhiteShadingBitmapMaterial. These two materials are very simple to use and provide a lot of control over the lighting effects on the material. One advantage here, personally speaking, over Papervision3D is the ability to mix ambient, diffuse and specular lighting all together which is necessary to produce real Phong reflection. Papervision3D provides materials for ambient plus diffuse and ambient plus specular but not all three together. This obviously is quicker to render but is less realistic when drawing shiny materials for example. Mixing all three is still possible but requires more effort as I showed in my post on phong reflection with Papervision3D.

For the light source I’m simply going to use a DirectionalLight3D which, as I mentioned before, correctly renders these materials. Another aspect that I like with Away3D is that the light source itself can be given a number of characteristics. Specifically we can give it a colour and define relative strengths of ambient, diffuse and specular shading, none of which are possible with Papervision3D. Being able to specify a light colour allows us to create new effects with materials of different colours. Specifying the ambient level directly with the light source, for example, also eliminates the need to specify the ambient light with every material created, as is the case with Papervision3D.

Away3D also allows us to create more than one light source and have a material rendered appropriately, which is not possible in Papervision3D. This, again, is obviously a drain on computational resources, but, in certain cases, this can open up new possibilities for rendering a more realistic scene.

I guess what I like most is that, conceptually, Away3D allows us to disassociate the light source from the material meaning that we can create a specific material without thinking about the environment and lighting in which it will be placed.

Anyway, enough of the theory and personal opinions, lets move onto the code. I’m using exactly the same scene as for the previous example only this time adding a DirectionalLight3D and replacing the BitmapMaterial with PhongBitmapMaterial and WhiteShadingBitmapMaterial.

package {   import away3d.cameras.Camera3D;   import away3d.containers.ObjectContainer3D;   import away3d.containers.Scene3D;   import away3d.containers.View3D;   import away3d.core.base.Object3D;   import away3d.core.math.Number3D;   import away3d.core.render.Renderer;   import away3d.core.utils.Cast;   import away3d.events.MouseEvent3D;   import away3d.lights.DirectionalLight3D;   import away3d.materials.PhongBitmapMaterial;   import away3d.materials.WhiteShadingBitmapMaterial;   import away3d.primitives.Cube;   import away3d.primitives.Cylinder;   import away3d.primitives.Sphere;   import away3d.primitives.Torus;     import caurina.transitions.Tweener;     import flash.display.Bitmap;   import flash.display.Sprite;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;     [SWF(backgroundColor="#000000")]     public class Example005 extends Sprite {     [Embed(source="/../assets/earth.jpg")] private var EarthImage:Class;     private var earthBitmap:Bitmap = new EarthImage();     [Embed(source="/../assets/away3D.png")] private var Away3DImage:Class;     private var away3DBitmap:Bitmap = new Away3DImage();     [Embed(source="/../assets/checker.jpg")] private var CheckerImage:Class;     private var checkerBitmap:Bitmap = new CheckerImage();     private static const ORBITAL_RADIUS:Number = 150;     private static const CAMERA_ORBIT:Number = 600;     private var scene:Scene3D;     private var camera:Camera3D;     private var view:View3D;       private var group:ObjectContainer3D;     private var sphere:Sphere;     private var cube:Cube;     private var centerCube:Cube;     private var cylinder:Cylinder;     private var torus:Torus;       private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var cameraPitch:Number = 60;     private var cameraYaw:Number = -60;         public function Example005() {             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Add resize event listener       stage.addEventListener(Event.RESIZE, onResize);             // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();             // Initialise frame-enter loop       this.addEventListener(Event.ENTER_FRAME, loop);     }     private function init3D():void {       // Create a new scene where all the 3D object will be rendered       scene = new Scene3D();             // Create a new camera, passing some initialisation parameters       camera = new Camera3D({zoom:25, focus:30});       setCameraPosition();             // Create a new view that encapsulates the scene and the camera       view = new View3D({scene:scene, camera:camera, renderer:Renderer.CORRECT_Z_ORDER});       // center the viewport to the middle of the stage       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;             //view.renderer = Renderer.CORRECT_Z_ORDER;             addChild(view);     }     private function createScene():void {       // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 100;       light.z = 500;       light.y = 500;       scene.addChild(light);       // Create an object container to group the objects on the scene       group = new ObjectContainer3D();       scene.addChild(group);           // Create a new sphere object using a very shiny phong-shaded bitmap material representing the earth       var earthMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(earthBitmap));       earthMaterial.shininess = 100;       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       sphere.ownCanvas = true;       group.addChild(sphere);       // Create a new cube object using a tiled, phong-shaded bitmap material       var tiledAway3DMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(away3DBitmap), {repeat:true, scaleX:.5, scaleY:.5});       cube = new Cube({material:tiledAway3DMaterial, width:75, height:75, depth:75});       cube.z = -ORBITAL_RADIUS;       cube.ownCanvas = true;       group.addChild(cube);         // Create a cylinder mapping the earth data again       cylinder = new Cylinder({material:earthMaterial, radius:25, height:100, segmentsW:16});       cylinder.x = -ORBITAL_RADIUS;       cylinder.ownCanvas = true;       group.addChild(cylinder);         // Create a torus object and use a checkered, flat-shaded (from white light) bitmap material       var checkerBitmapMaterial:WhiteShadingBitmapMaterial = new WhiteShadingBitmapMaterial(Cast.bitmap(checkerBitmap));       torus = new Torus({material:checkerBitmapMaterial, radius:40, tube:10, segmentsT:8, segmentsR:16});       torus.z = ORBITAL_RADIUS;       torus.ownCanvas = true;       group.addChild(torus);         // Create a new cube object using a smoothed, precise, phong-shaded, mat (not shiny) bitmap material       var away3DMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(away3DBitmap), {smooth:true, precision:2});       away3DMaterial.shininess = 0;       centerCube = new Cube({material:away3DMaterial, width:75, height:75, depth:75});       centerCube.ownCanvas = true;       group.addChild(centerCube);       // add mouse listeners to all the 3D objects       sphere.addOnMouseDown(onMouseDownOnObject);       cube.addOnMouseDown(onMouseDownOnObject);       cylinder.addOnMouseDown(onMouseDownOnObject);       torus.addOnMouseDown(onMouseDownOnObject);       centerCube.addOnMouseDown(onMouseDownOnObject);     }         private function loop(event:Event):void {             // rotate the group of objects       group.yaw(2);           // rotate the objects       sphere.yaw(-4);       cube.yaw(-4);       cylinder.yaw(-4);       torus.yaw(-4);       // update the camera position       updateCamera();       // Render the 3D scene       view.render();     }     // updates the camera position     private function updateCamera():void {             // If the mouse button has been clicked then update the camera position            if (doRotation) {                 // convert the change in mouse position into a change in camera angle         var dPitch:Number = (mouseY - lastMouseY) / 2;         var dYaw:Number = (mouseX - lastMouseX) / 2;                 // update the camera angles         cameraPitch -= dPitch;         cameraYaw -= dYaw;         // limit the pitch of the camera         if (cameraPitch <= 0) {           cameraPitch = 0.1;         } else if (cameraPitch >= 180) {           cameraPitch = 179.9;         }               // reset the last mouse position         lastMouseX = mouseX;         lastMouseY = mouseY;                 // reposition the camera         setCameraPosition();       }           }     // sets the camera position given pitch and yaw angles     private function setCameraPosition():void {       camera.y = CAMERA_ORBIT * Math.cos(cameraPitch * Math.PI / 180);       camera.x = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.cos(cameraYaw * Math.PI / 180);       camera.z = CAMERA_ORBIT * Math.sin(cameraPitch * Math.PI / 180) * Math.sin(cameraYaw * Math.PI / 180);             // keep the camera looking at the origin       camera.lookAt(new Number3D(0, 0, 0));     }     // called when mouse down on stage     private function onMouseDown(event:MouseEvent):void {       doRotation = true;       lastMouseX = event.stageX;       lastMouseY = event.stageY;     }     // called when mouse up on stage     private function onMouseUp(event:MouseEvent):void {       doRotation = false;     }       // called when mouse down on a 3D object     private function onMouseDownOnObject(event:MouseEvent3D):void {       var object:Object3D = event.object;       Tweener.addTween(object, {y:200, time:1, transition:"easeOutSine", onComplete:function():void {goBack(object);} });     }         // called when a tween created in onMouseDownOnObject has terminated     private function goBack(object:Object3D):void {       Tweener.addTween(object, {y:0, time:2, transition:"easeOutBounce"});     }         // called when the window is resized     private function onResize(event:Event):void {       view.x = stage.stageWidth / 2;       view.y = stage.stageHeight / 2;     }   }     }

When compiled you should get the same result as below (click on the image). As before, the scene is interactive so you can click on the background and move the mouse to move the camera and click on an object to make it bounce.

Since the code is so similar to the previous example I’m going to concentrate solely on the function createScene - this is the only function that has been modified.

In this example we use a simple white light source and see four different types of materials rendered: one that is very shiny, another that has a mat texture, one that takes default lighting parameters (being quite shiny) and another that is flat shaded.

Starting with the light source, we use a DirectionLight3D for which we specify its position and lighting properties.

      // create a new directional white light source with specific ambient, diffuse and specular parameters       var light:DirectionalLight3D = new DirectionalLight3D({color:0xFFFFFF, ambient:0.25, diffuse:0.75, specular:0.9});       light.x = 100;       light.z = 500;       light.y = 500;       scene.addChild(light);

As you can see the constructor takes an array of initialisation parameters in which we set the colour to white and give it ambient, diffuse and specular factors (being between 0 and 1).

The first object that we create is the rotating globe. Here I wanted the object to appear to be very shiny so modify its shininess.

      // Create a new sphere object using a very shiny phong-shaded bitmap material representing the earth       var earthMaterial:PhongBitmapMaterial = new PhongBitmapMaterial(Cast.bitmap(earthBitmap));       earthMaterial.shininess = 100;       sphere = new Sphere({material:earthMaterial, radius:50, segmentsW:10, segmentsH:10});       sphere.x = ORBITAL_RADIUS;       sphere.ownCanvas = true;       group.addChild(sphere);

First of all, the PhongBitmapMaterial takes bitmap data for the texture mapping, as is the case for an ordinary, unshaded BitmapMaterial. Next we modify the shininess property (by default this is set at 20). This property, on the rendered scene, in effect modifies the extent of the specular lighting. The bright white dot that we see on the globe is reduced by increasing this property as if the light source is reflected off a more concentrated region of the material.

An important point to note is the line

      sphere.ownCanvas = true;

Without this line the scene is not correctly rendered as objects remain partially visible when they pass behind others.

Moving on to the tiled material object, I wanted to show that the initialisation parameters that we send to a material are still taken into account for the base material. The PhongBitmapMaterial is composed of a TransformBitmapMaterial (as well as the shaders) as we discussed in the previous example. The initialisation parameters {repeat:true, scaleX:.5, scaleY:.5} are passed directly to this element when it is constructed and therefore the texture map is still tiled. This object uses the default shading parameters of the PhongBitmapMaterial and is therefore appears less shiny than the globe.

The cylinder uses the same material as the globe just to illustrate that we can use the same material more than once.

Next, for the torus, we implement flat shading by using a WhiteShadingBitmapMaterial. As its name indicates this assumes a white light source. You can test this by experimenting with the light source colour. You’ll notice that all the other objects correctly reflect the change in light colour but this object remains white and red. This material is obviously less complex in terms of necessary calculations and therefore provides better rendering performance. However, we see that smoothly shaded materials, as with the other objects, allow us to reduce the number of rendered triangles to produce more rounded objects.

Finally, the cube in the center is rendered with shininess set to zero. Only diffuse and ambient lighting remain in this case. This is useful for rendering mat materials but unfortunately has no performance benefits. We can see however that there is a difference in the visible result for the two cubes.

So thats it for this tutorial! I hope this has given a useful overview of lighting and shading in Away3D. Of course I’ve not gone into detail for all the different types of lights and shaders - you’ll discover a lot by experimenting with this example, for example by changing the materials, the type of lighting, its properties and its position. I’ve purposely ignored the Dot3BitmapMaterial and EnviroBitmapMaterial for this post because I felt that it was important to look at simple lighting and shading first - they should appear in the next article. But for now, at least, I hope this provides a useful starting point - comments and suggestions, as always, are very welcome!

Next article:

-->
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!

-->
Tuesday, August 26th, 2008

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

Previous articles summary :

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

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

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

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

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

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

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

package {     import flash.display.Bitmap;   import flash.display.StageAlign;   import flash.display.StageScaleMode;   import flash.events.Event;   import flash.events.MouseEvent;   import flash.text.TextField;   import flash.text.TextFieldAutoSize;   import flash.text.TextFormat;     import org.papervision3d.events.InteractiveScene3DEvent;   import org.papervision3d.lights.PointLight3D;   import org.papervision3d.materials.BitmapMaterial;   import org.papervision3d.materials.shaders.CellShader;   import org.papervision3d.materials.shaders.EnvMapShader;   import org.papervision3d.materials.shaders.FlatShader;   import org.papervision3d.materials.shaders.GouraudShader;   import org.papervision3d.materials.shaders.PhongShader;   import org.papervision3d.materials.shaders.ShadedMaterial;   import org.papervision3d.materials.shaders.Shader;   import org.papervision3d.objects.DisplayObject3D;   import org.papervision3d.objects.primitives.Sphere;   import org.papervision3d.view.BasicView;   public class Example007 extends BasicView {       [Embed(source="/../assets/pv3d.png")] private var Pv3dBitmapImage:Class;     [Embed(source="/../assets/randomBump.png")] private var BumpImage:Class;     [Embed(source="/../assets/mountains.png")] private var EnvImage:Class;     private var pv3dBitmap:Bitmap = new Pv3dBitmapImage();     private var bumpMap:Bitmap = new BumpImage();     private var envMap:Bitmap = new EnvImage();     private var bitmapMaterial:BitmapMaterial;         private var sphere:Sphere;     private var light:PointLight3D;         private var doRotation:Boolean = false;     private var lastMouseX:int;     private var lastMouseY:int;     private var cameraPitch:Number = 60;     private var cameraYaw:Number = -60;         private var shaders:Array = ["flat", "cell", "gouraud", "phong", "phongBump", "env", "envBump"];     private var shaderIndex:int = 0;         private var shaderText:TextField;     private var textFormat:TextFormat;     public function Example007() {       super(0, 0, true, true);             // set up the stage       stage.align = StageAlign.TOP_LEFT;       stage.scaleMode = StageScaleMode.NO_SCALE;       // Initialise Papervision3D       init3D();             // Create the 3D objects       createScene();       // Listen to mouse up and down events on the stage       stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);       stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);       stage.addChild(shaderText);       // Start rendering the scene       startRendering();     }         private function init3D():void {       // position the camera       camera.z = -500;       camera.orbit(60, -60);     }     private function createScene():void {       // create text and format to display current shader type       textFormat = new TextFormat();       textFormat.size = 20;       textFormat.font = "Arial";             shaderText = new TextField();       shaderText.x = 50;       shaderText.y = 50;       shaderText.textColor = 0xFFFFFF;       shaderText.text = "flat";       shaderText.setTextFormat(textFormat);       shaderText.autoSize = TextFieldAutoSize.LEFT;       // Specify a point light source and its location       light = new PointLight3D(true);       light.x = 500;       light.y = 500;       light.z = -200;       // create bitmap material with smoothing       bitmapMaterial = new BitmapMaterial(pv3dBitmap.bitmapData, false);       bitmapMaterial.smooth = true;         // create sphere       sphere = new Sphere(getShadedBitmapMaterial(bitmapMaterial, "flat"), 150, 20, 20);       // Add a listener to the spheres to listen to InteractiveScene3DEvent events       sphere.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onMouseDownOnObject);       // Add the light and sphere to the scene       scene.addChild(sphere);       scene.addChild(light);     }         private function getShadedBitmapMaterial(bitmapMaterial:BitmapMaterial, shaderType:String):ShadedMaterial {       var shader:Shader;             if (shaderType == "flat") {         // create new flat shader         shader = new FlatShader(light, 0xFFFFFF, 0x333333);       } else if (shaderType == "cell") {         // create new cell shader with 5 colour levels         shader = new CellShader(light, 0xFFFFFF, 0x333333, 5);       } else if (shaderType == "gouraud") {         // create new gouraud shader         shader = new GouraudShader(light, 0xFFFFFF, 0x333333);       } else if (shaderType == "phong") {         // create new phong shader         shader = new PhongShader(light, 0xFFFFFF, 0x333333, 50);       } else if (shaderType == "phongBump") {         // create new phong shader with bump map         shader = new PhongShader(light, 0xFFFFFF, 0x333333, 50, bumpMap.bitmapData);       } else if (shaderType == "env") {         // create new environment map shader         shader = new EnvMapShader(light, envMap.bitmapData, envMap.bitmapData, 0x333333);       } else if (shaderType == "envBump") {         // create new environment map shader with bump map         shader = new EnvMapShader(light, envMap.bitmapData, envMap.bitmapData, 0x333333, bumpMap.bitmapData);       }       // create new shaded material by combining the bitmap material with shader       var shadedMaterial:ShadedMaterial = new ShadedMaterial(bitmapMaterial, shader);       shadedMaterial.interactive = true;             return shadedMaterial;     }         override protected function onRenderTick(event:Event=null):void {       // rotate the sphere       sphere.yaw(-1);             // If the mouse button has been clicked then update the camera position            if (doRotation) {                 // convert the change in mouse position into a change in camera angle         var dPitch:Number = (mouseY - lastMouseY) / 2;         var dYaw:Number = (mouseX - lastMouseX) / 2;                 // update the camera angles         cameraPitch -= dPitch;         cameraYaw -= dYaw;         // limit the pitch of the camera         if (cameraPitch <= 0) {           cameraPitch = 0.1;         } else if (cameraPitch >= 180) {           cameraPitch = 179.9;         }               // reset the last mouse position         lastMouseX = mouseX;         lastMouseY = mouseY;                 // reposition the camera         camera.orbit(cameraPitch, cameraYaw);       }             // call the renderer       super.onRenderTick(event);     }     // called when mouse down on stage     private function onMouseDown(event:MouseEvent):void {       doRotation = true;       lastMouseX = event.stageX;       lastMouseY = event.stageY;     }     // called when mouse up on stage     private function onMouseUp(event:MouseEvent):void {       doRotation = false;     }         // called when mouse down on a sphere     private function onMouseDownOnObject(event:InteractiveScene3DEvent):void {       var object:DisplayObject3D = event.displayObject3D;             // calculate index of next shader       shaderIndex++;       if (shaderIndex == shaders.length) {         shaderIndex = 0;       }             // dynamically modify the material of the object and update text       object.material = getShadedBitmapMaterial(bitmapMaterial, shaders[shaderIndex]);       shaderText.text = shaders[shaderIndex];       shaderText.setTextFormat(textFormat);     }   } }

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

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

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

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

Texture map image:

Bump map:

Environment map:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next article:

Saturday, August 2nd, 2008

First steps in Papervision3D : Part 4 - lighting and shading

Previous articles summary :

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

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

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

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

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

The code to illustrate these shading models is as follows :

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

  public class Example004 extends BasicView {
 
    private static const ORBITAL_RADIUS:Number = 200;
   
    private var sphere1:Sphere;
    private var sphere2:Sphere;
    private var sphere3:Sphere;
    private var sphere4:Sphere;
    private var sphereGroup:DisplayObject3D;
   
    public function Example004() {
      super(0, 0, true, false);
     
      // set up the stage
      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;

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

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

    private function createScene():void {

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

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

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

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

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

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

      // Add the light and spheres to the scene
      scene.addChild(sphereGroup);
      scene.addChild(light);
    }
   
    override protected function onRenderTick(event:Event=null):void {

      // rotate the spheres
      sphere1.yaw(-8);
      sphere2.yaw(-8);
      sphere3.yaw(-8);
      sphere4.yaw(-8);
     
      // rotate the group of spheres
      sphereGroup.yaw(3);
     
      // call the renderer
      super.onRenderTick(event);
    }

  }
}

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Next article: