Using Sandy 3D Flash Library
Part 6, Faces and Skins
Finally, you may sigh.
I guess you have already grown tired of the one color skins we have used so far.
So if you want to dress up your 3D objects a little better, that's what this part of the
tutorial is all about.
Download the .fla files, AS code and assets for this part.
Faces
Skins are applied to the faces of an object, so let's delve a bit around faces, before applying the variety of skins used in a Sandy world.
A 3D object is defined by its faces, and the edges of a face is drawn between vertices or points in space. When we create an object, all these vertices, edges and faces are defined, and when the object is rendered on screen, all visible edges and faces are drawn.
When we create a primitive, such as a Box, all points are calculated from the height, width and length values. The ordering of the points is important of course, so that the engine knows what corner points define each face.
You may recall the signature of the Box constructor:
public function Box ( rad : Number,h : Number, lg : Number, mode:String, quality:Number )
The first three arguments are the dimensions of the Box.
The mode is either "quad" which means the faces are divided into
quadratic slices, or "tri" that they are divided into triangular slices.
The
quality parameter is the number of slices in any direction, that a face is
divided into.
These values are more important for 3D objects with curved faces, and not so much for the rectilinear Box. An example with a sphere was given in the Part 2, Primitives section of this tutorial.
There are two more things to consider, when we skin an object. The first is that every face has two sides, the front side and the back side. The front side is by default on the outside of an object. When we apply a skin to a face both sides get the same skin by default. We can change this by setting another skin to the back side.
The second is what is called back culling. This means that the hidden side of a face is not drawn, thereby saving a lot of unnecessary drawing operations. What is hidden from your eyes ( the camera ), should not be drawn. Sometimes you may want to see the back side, however. For example, you might have a single plane, with pictures on each side. In that case you can disable the back culling.
As another example, let's think of a cube, and let's say you have some nice pictures on your cube, that you want to show off for your visitor. Normally your guest is on the outside of the cube, and can see the pictures. But if you let your guest into the interior, the cube would be empty and sterile. You could switch off the back culling, but then everything slows down, due to the drawing of both sides. In this case it would be better to switch sides, so that the outside of the cube faces are treated as the back side. With the back culling still in effect, the inside of the cube is drawn in all its color, and the outside is the dull one. Nifty? Oh yes!
Skins
Let's start with a simple Plane3D, as the one we used to build the coordinate planes earlier, and let's add a couple of controls to rotate it , so we have a look at both sides.
The controls
We add two slides on the Stage, called xSlide and ySlide to rotate the Plane3D around the x and y axis respectively. We also set up a button, called cullingButton to change the state of back culling. Here is our usual the start() method.
function start (Void):Void{ var w:World3D = World3D.getInstance (); w.setRootGroup( createScene() ); var mc:MovieClip = _root.createEmptyMovieClip('screen',1); var screen:ClipScreen = new ClipScreen(mc,200,200); var cam:Camera3D = new Camera3D(500,screen); cam.setPosition(0,0,-350); w.addCamera (cam); setUpControls() w.render(); }
The setUpControls() method sets up the controller event handlers
function setUpControls(){ var xCh:Object = new Object(); xCh.onChange = function(evt:Object){ x = evt.value; rotation.rot( x, y, 0); } xSlider.addListener(xCh); var yCh:Object = new Object(); yCh.onChange = function(evt:Object){ y = evt.value; rotation.rot( x, y, 0); } ySlider.addListener(yCh); cullingButton.onRelease = switchBackCulling; } function switchBackCulling(){ backCulling = plane.enableBackFaceCulling = !backCulling; cullingButton.setLabel ( backCulling ? "Off" : "On" ); }
The default skin
The usual createScene function returns a Group, which is set as the worlds root Group in the start() function.
function createScene(Void):Group{ var g:Group = new Group(); plane = new Plane3D(100,100,3,'tri'); //plane = new Plane3D(100,100,5,'quad'); // Transforms var tg:TransformGroup = new TransformGroup(); rotation = new Transform3D(); tg.setTransform(rotation); tg.addChild(plane); g.addChild(tg); return g; }
As we haven't explicitly set any skin, the default SimpleLineSkin is applied to the Plane3D
In the left example the quality is 5 and the mode is "quad", in the right one, the quality is 3 and the mode is "tri".
The back face culling button, when released, calls an event handler to switch back face culling on or off.
When back face culling is on ( the default ) the back side of the face is not drawn, when we switch it off, the back side is drawn, as expected.
Textured skins
The next animation is a bit more interesting. Here I created a TextureSkin for the plane.
I also changed the mode to "tri", which is recommended for bitmap
textured skins.
var skin:TextureSkin = new TextureSkin( BitmapData.loadBitmap( "monalisa" )); plane = new Plane3D(100,100,5,'tri'); plane.setSkin( skin );
The "monalisa" argument is the identifier for an image in the Flash movie's library, an embedded image. The embedded image is a 200 x 256 pixels JPEG, and is automagically shrunk to fit the face. Contrary to the documentation the back side skin is not set. Turning back culling off, shows that the back side skin still is the default SimpleLineSkin. The reason for this is that the default skin is set for both sides, when the Object3D is created.
Looking through the back side, we note that the background shines through. With the back culling on, the face simply disappears, when we look at the back side. With the back culling off we see the default SimpleLineSkin which transparent. A bit surprisingly the opaque front side skin doesn't stop the transparency.
Setting a back skin
We can use the same or another bitmap for the front and back sides of a
Face.
Let's use separate images for the two.
Using the same skin for both sides, we just have to add
plane.setBackSkin( skin );
Otherwise, we create another skin and set it as the back skin.
var skin:TextureSkin = new TextureSkin(BitmapData.loadBitmap("petit")); var bskin:TextureSkin = new TextureSkin(BitmapData.loadBitmap("monalisa")); plane = new Plane3D(100,100,5,'tri'); plane.setSkin( skin ); plane.setBackSkin( bskin );
So what happened here? The front face image is rendered as expected. As before it scales to fit the plane. For the back face image, it's quite another story. I seems just to pick up and repeat a small area of the image.
The problem here is that the front skin seems to decide how the image is divided and applied to the triangular faces. There is a mapping between 3D points on the faces and 2D points in the image ( aka UV coordinates ). The same mapping is used for the back faces, but the bitmap used for the back faces is much bigger, so only small parts of that image is projected to the back faces.
To remedy this, we have to scale the back image to the same size as the front image, like this.
Now that the images are equal in size, both sides of the surface maps nicely to the images.
Skinning a cube
We have earlier seen the cube carrying a MixedSkin with lines and colors. Now we'll investigate how textures perform on the Box primitive.
As stated before, when an Objet3D instance is created, all faces are given the default skin, a SimpleLineSkin, both as front and back skin. If that weren't the case, the object wouldn't be visible at all.
Let's create a cube with textured faces, using the smaller "monalisafit" bitmap. The createScen function creates the World3D root group, as before.
function createScene(Void):Group{ var g:Group = new Group(); var skin:TextureSkin = new TextureSkin(BitmapData.loadBitmap("monalisafit")); var box:Box = new Box(80,80,80,'tri'); // For BoxSansSkin comment out the setSkin() call! box.setSkin( skin ); // Transforms var tg:TransformGroup = new TransformGroup(); rotation = new Transform3D(); tg.setTransform(rotation); tg.addChild(box); g.addChild(tg); return g; }
To the right is the same cube with the default skin. It has two triangular faces on each surface, making for at total of twelve faces. This is the default quality.
Working with faces
You don't have to give all faces the same skin. You can set different skins to each face of an object, if you like. In that case you have to get references to the skins, which you do that by calling the getFaces() on the object. It returns an Array of references to all faces. So what happens if we set a separate skin for one face of the cube?
Let's first check how many faces we have, by tracing the length of the array.
var faces = box.getFaces(); trace( faces.length );
As expected the result is 12, two for each of the six sides of the cube. Now let's set a separate skin for the first face.
faces[0].setSkin("skin2"); // This is the "petit" skin, used earlier.
The left animations shows that a single triangular face now carries a
separate skin.
If we do the same for the next face, we get the result to the right
faces[0].setSkin("skin2"); // This is the "petit" skin, used earlier. faces[1].setSkin("skin2");
We could use this method to set different bitmaps to all or just some of the different faces. To see anything on the inside of the cube, we'll have to set the back faces as well.
var faces = box.getFaces(); faces[0].setSkin(skin); faces[0].setBackSkin(skin); faces[1].setSkin(skin); faces[1].setBackSkin(skin); faces[2].setSkin(skin); faces[2].setBackSkin(skin); faces[3].setSkin(skin); faces[3].setBackSkin(skin); faces[4].setSkin(skin2);faces[4].setBackSkin(skin2); faces[5].setSkin(skin2);faces[5].setBackSkin(skin2); faces[8].setSkin(skin); faces[8].setBackSkin(skin); faces[9].setSkin(skin); faces[9].setBackSkin(skin);
The faces, for which we didn't explicitly set a skin, will retain the default skin. When we turn back face culling off, we can see the back sides inside the open box. It looks even better in the second example above, where the mode is set to "quad". This mode is normally not recommended for textures, as the distortion of the texture, as we move the object around, is less than perfect. For rectilinear bodies like the Box, it may be acceptable, if we are not too picky ;). Here the number of faces are only half of that for the 'tri' mode, so I just commented out every second of the setSkin/setBackSkin calls.
Using external images
For the textures we have seen so far, the images were embedded in the Flash movie. This may be fine for small images, that we never change. Big images adds to the size of the SWF and consequently to the download time. Using external images, we could show our world in a simple form, and paste the textures , once they are loaded. Another advantage of course, is that we could change the textures without recompiling the SWF.
Instead of using the BitmapData.loadBitmap() to load the embedded image, we use a MovieClipLoader to load an image from a directory. When loading external images, we also have to convert them to BitmapData objects.
In the createScene function we have.
box = new Box(80,80,80,'tri'); // Create a MovieClip to hold the externally loaded image // Make it fully transparent ( or move it out of the stage ) var holder:MovieClip = createEmptyMovieClip( "holder", getNextHighestDepth() ); holder._alpha = 0; // Create a MovieClipLoader and listen to it var mcLoader:MovieClipLoader = new MovieClipLoader(); mcLoader.addListener( this ); // Delegate the event handling to get the correct scope ( this ) onLoadInit = Delegate.create( this, paste ); mcLoader.loadClip( "petit110110.png", holder );
We must have a temporary MovieClip to hold the loaded image. We don't want it to be visible on the stage, so we make it fully transparent. Then we create a MovieClipLoader and listen for its events. When the image is fully loaded, the loader fires an onLoadInit event.
Delegating the event handling.
Here it becomes just a little bit tricky. We can't make a direct reference
to the event handler, because in that function the box would be
unknown. The reason is that inside the event handler we are in the scope of the
loader, not of the createScene function.
Event handlers in AS2 and earlier works in the scope of the object firing
the event ( the target in Flash parlance ). The solution is delegation, and the mx.utils.Delegate class.
This anomaly is changed in AS3, rendering the Delegate class obsolete.
So we delegate the onLoadInit handling using Delegate.create( this, paste ), where 'this' is the current scope and 'paste' is the handler. Piuhh!!
// Loader handler function paste( mc:MovieClip ) { var bitmap:BitmapData = new BitmapData( mc._width, mc._height,true,0x00FFFFFF); bitmap.draw( mc ); box.setSkin( new TextureSkin( bitmap )); }
When the image is fully loaded, we create a BitmapData with the same size as the clip, draw the holder clip into the bitmap. Finally we make the bitmap the new TextureSkin of the box.
Using external images this way, we could easily change the used images by setting values in the Flash object embedding tag, or by reading image names from a text file or even from a data base.
Cross domain loading
The MovieClipLoader can load images from any web server using a URL. If the SWF is loaded form your web server and the images from another, it is called cross loading, and requires some special arrangement. The Flash client lives in a secured environment, the "sand box", where it's allowed to play. Access to resources from remote servers is limited. This is the same types of restrictions under which a Java Applet lives.
It will seem that a Flash movie ( a SWF ) loaded form one domain, can load assets from another server, but not manipulate them. So you could readily present an image from the other domain in a MovieClip, but not use the BitmapData.draw() method, to paste it on an Object3D. Adobe has some solutions here.
As long as the images come from the same web server ( or domain ) as the SWF itself, there are no problems.
recommended before we go on to skins in motion