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 :
- First steps in Away3D : Part 1 – Getting started
Creation of a new Away3D project within eclipse or Flex Builder 3 and a simple example of a 3D scene illustrating some basic Away3D classes. - First steps in Away3D : Part 2 – Animation
Adding some basic animation to the scene by modifying 3D object parameters when entering a frame. - First steps in Away3D : Part 3 – Texture mapping
Create materials using bitmap data with the aim of having more detailed or realistic objects. - First steps in Away3D : Part 4 – Scene interaction
Listen to mouse events to interact with the scene and with individual rendered objects. - First steps in Away3D : Part 5 – Lighting and shading
A light source is added to the scene and the materials are changed to illustrate how Away3D renders objects with different shading characteristics.
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:
- First steps in Away3D : Part 7 – Movie and video materialsAdd depth to website by displaying interactive movies or videos as materials on 3D surfaces.




[...] link to look at are the series of tutorials from tartiflop. They have some interesting ideas and code to go with [...]
so if i want to use a bump map to do the bumping (sounded weird :D) how it would be like?, or how to increase the deeping of the dot3 in the mesh, via code, cause the code only let u go to 20, and ask for some html in the bin.