O'Reilly Hacks
oreilly.comO'Reilly NetworkSafari BookshelfConferences Sign In/My Account | View Cart   
Book List Learning Lab PDFs O'Reilly Gear Newsletters Press Room Jobs  


 
Buy the book!
Flash Hacks
By Sham Bhangal
June 2004
More Info

HACK
#35
Simulate 3D
Flash doesn't support true 3D, but you can fake it by arranging 2D image slices as if they are filling a 3D space
[Discuss (0) | Link to this hack]

Although Flash doesn't support 3D assets, you can simulate 3D in various ways using 2D assets. One simple approach is to arrange slices of 2D imagery but offset them to create the illusion of 3D. That is, a stack of flat 2D slices is used to create the illusion of a 3D object with volume.

This hack is inspired by a number of sites including the 3D head at sofake (go to http://www.sofake.com, click the 10th white square at the bottom of the home page, and click the link for haircut.swf ) and Andy Foulds's "Leader of the Free World" (go to http://www.foulds2000.freeserve.co.uk/index_v2.html and click on the 8th square above the Amusements heading). However, I first saw this trick used back in Flash 4 by Rouien Zarin (http://www.zarinmedia.com) and Ant Eden (http://www.arseiam.com). Ant Eden also designed a wonderful 3D slice engine, which extends the concept by rotating and scaling the slices (click on the 3D icon at http://www.arseiam.com/index2.htm). This allows his slices to move in 3D space, rather than in just two dimensions as per this hack.

We learned earlier how to create a Flash animation by stripping a GIF, importing it into Flash, and then rebuilding it with a richer set of animation options . This hack introduces the O'Reilly mascot (a tarsier from the cover of Learning the vi Editor) to the world of 3D. Although he doesn't look very 3D in , see the critter02.fla on this book's web site to fully appreciate the effect.

Figure 1. A 3D tarsier

Cut Up the Tarsier

We create our slices using the all-purpose "you'll never think of this" solution: masking. Think about what the tarsier would look like if we sliced him into cross sections (from front to back) and put him back together. Instead of seeing his internal organs, all you see is his outer pelt. Taking that into account, the slices we need would look something like those shown in .

Figure 2. Tarsier slices

Get the picture? It's simply an expanding circular mask centered on the tarsier's snout (the closest point to us in 3D space). However, if we stack these slices on a flat screen, they don't appear to have any depth; all we see is the original tarsier. We add depth by emulating what happens when the tarsier turns his body. If the tarsier turns to his right (our left), in reality, the slices must rotate to display his left side (on our right). But if we keep the rotation small, we can approximate the 3D movement by simply sliding the slices around. The topmost slice (the one in the foreground) moves slightly to the left, and the bottommost slice (furthest away from the viewer) moves slightly to the right.

That's the whole premise of the hack, and once you know it, the rest is relatively simple.

To create the slices, we tween an expanding circle and use it to mask the original tarsier clip. The mask tween stops at frame 19 so that at frame 20 we can see our full, unmasked critter as the last slice. To see the slices, load up critter02.fla from this book's site, edit symbol critter in 3D in the Library, and scrub the playhead along the symbol's timeline, as shown in .

Figure 3. The tweened circular mask progressively reveals the tarsier

To display 20 slices of the tarsier on the Stage, attach 20 instances of the critter in 3D clip to the main timeline. Each slice's timeline is stopped at one of the 20 frames in our animation.

this.attachMovie("menu", "menu_mc", 1000);
// Place the O'Reilly bar on the Stage
menu_mc._x = 200;
menu_mc._y = 270;
// Create an initialization object for the 
// movie clips we are placing on stage.
var init:Object = new Object( );
init._x = 200;
init._y = 200;
var sliceDepth:Number = 20;
// Attach 20 critter movie clips to the Stage, each 
// stopped at a different frame to create the slices.
for (var i:Number = 1; i < sliceDepth + 1; i++) {
  var critterName:String = "critter" + (sliceDepth - i);
  var critter:MovieClip = this.attachMovie("critter", 
                          critterName, sliceDepth - i, init);
  critter.gotoAndStop(i);
  critter.offset = ((sliceDepth - i) - (sliceDepth / 2)) * 0.05;
  critter.onEnterFrame = initClip;
}

Attach the preceding code to frame 1 of the main timeline. The 20 slices (named critter20 through critter1) are placed at depths from 20 down to 1 at position (200, 200). All standard stuff. This line is the important one:

critter.gotoAndStop(i);

It stops each instance at one of the 20 frames in the masking animation, giving us 20 unique slices.

Another important line is this one:

critter.offset = ((sliceDepth - i) - (sliceDepth / 2)) * 0.05;

It adds a property named offset, whose value varies between -0.5 and 0.5, to each clip. The offset controls how much our slices move to the left and right in response to mouse movement (our pet tarsier is trained to turn toward the mouse position).

The event handlers that control the movement per slice are shown next. The initClip( ) function is used as the onEnterFrame( ) event handler for each slice; it's a simple initialization script that deletes itself after running once.

initClip = function ( ) {
  // Initialize this slice
  this.startX = this._x;
  this.startY = this._y;
  delete (this.onEnterFrame);
  this.onMouseMove = mover;
};
mover = function ( ) {
  // Move this slice according to its position in the slice stack.
  var distX = this.offset * (_xmouse - this._x) / 50;
  var distY = this.offset * (_ymouse - this._y) / 50;
  this._x = this.startX + distX;
  this._y = this.startY + distY;
  updateAfterEvent( );
};

Following initialization, the mover( ) function runs whenever the mouse moves. This onMouseMove( ) event handler is almost identical to the earlier eye movement code implemented within an onEnterFrame( ) handler . The major difference is that in this slice animation, the offset property gives a slightly different range of movement for each slice.

That's all there is to the effect. It's a surprisingly simple concept and simple code.

Why did we choose a different event than the one used in earlier eye movement animation? Flash issues onMouseMove events as frequently as it can while the mouse is moving. The onEnterFrame event runs at the frame rate. When the mouse is stationary, onEnterFrame events are still issued, but during mouse movement, onEnterFrame events occur less often than onMouseMove events. You can confirm this by running both event handlers at the same time with the onMouseMove( ) handler incrementing a counter variable each time it is called, and the onEnterFrame( ) handler decrementing the same counter. You will find that the counter climbs when the mouse is moving because, during that time, onMouseMove( ) is invoked more often than onEnterFrame( ).

So we choose an event to supply responsiveness in the user's area of focus: we concentrate more processing power on the 3D animation because we expect the user to look at it more intently. The earlier eye movement animation is too subtle to look any better if we throw more processing resources at it, so we use the less-processor-intensive onEnterFrame event instead.

You can also vary the width and height of the slices as they move. As an object rotates left or right, each slice's width should change when viewed from the front. Likewise, when an object rotates up or down, each slice's height should change. Adding the following lines (shown in bold) to the main animation script sometimes (but not always) helps the effect look more realistic:

mover = function ( ) {
  // Move this slice according to its position in the slice stack.
  var distX = this.offset * (_xmouse - this._x) / 50;
  var distY = this.offset * (_ymouse - this._y) / 50;
  this._x = this.startX + distX;
  this._y = this.startY + distY;
  this._width = 100 - Math.abs(distX)
  this._height = 100 - Math.abs(distY)
  updateAfterEvent( );
};


O'Reilly Home | Privacy Policy

© 2007 O'Reilly Media, Inc.
Website: | Customer Service: | Book issues:

All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.