Andrew Trice
Real-World Rich Internet Applications
Friday January 22, 2010
Flash, Flex & Native 3D Transformations
Back with the release of Flash Player 10 last summer (yes, it's been that long), Adobe introduced native 3D transformations on visual objects in Flash. I've heard people say this isn't "all that", and been disappointed with it's capabilities, but I wanted to share a few examples that show it really is quite powerful. Granted, it doesn't support 3D models, or meshes... However, you can really do some cool things with 2.5D (aka 3D transformations on flat objects).
Not every use case requires 3D models. In those cases, you can add interesting 3D visual capabilities to otherwise "flat" objects to make them more interactive, more engaging, or perhaps more informative. Not to mention, you can get those visualizations at a relatively low cost, performance wise. Since the transformations are handled by the Flash Player, you don't need to handle any of it in your own code (which makes it far easier and much faster).
Below, you will find a fairly simple example that demonstrates my point. It is a viewer, which allows you to view multi-story architectural floorplans in a pseudo-3D model. You can show and hide floorplans, view their overlay, click and drag to rotate it, and toggle back and forth between 2D and 3D representations.
It depends on the use case as to whether this adds to the user experience... What makes this extremely simple is that it is all based on standard Flex components. There is no use of a 3D engine in these examples. In this case, there are three floor plans within a spark Group container. Each of the floor plans is actually a FXG file that I exported from Fireworks.
<s:Group
id="rotationParent"
horizontalCenter="100" verticalCenter="50"
width="300" height="300" scaleX=".65" scaleY=".65">
<assets:floor1 x="-550" y="-200" id="floor1" />
<assets:floor2 x="-550" y="-200" id="floor2" />
<assets:floor3 x="-550" y="-200" id="floor3" />
</s:Group>
I changed the scale so that it fits on screen better, and I also changed some x and y offsets. You might be wondering why? Well, the default registration point for native Flash/Flex 3D transformations is the top left corner. In order to offset this, I adjusted the x and y positions so that the registration point is now in the exact center of the visual elements. With this change, any 3D changes (such as rotation) occur from the center point, and do not cause undesired visual distortion.
There are also a few other tricks that I used to get this to behave properly. First, whenever the stage is resized, I reset the projection so that it always uses the center of the application as the center point of the 3D transformation:
private function resetProjection() : void
{
if ( root )
root.transform.perspectiveProjection.projectionCenter = new Point( width/2, height/2 );
}
The other trick is that you have to manually reorder children on the display list, in order for them to overlap correctly. Lucky enough, Flex 4 makes it really easy to do this. All that you have to do is set the depth property of the parent object, and voila:
public function reorderChildren() : void
{
for(var i:uint = 0; i < rotationParent.numElements; i++)
{
var g : Graphic = rotationParent.getElementAt( i ) as Graphic;
if ( g )
g.depth = perspective ? -g.transform.getRelativeMatrix3D(root).position.z : i;
}
}
Another factor that makes this so simple & powerful is that transformations to an object are applied to the entire contents of that object. So, rotating a container actually rotates all of the children of that container. If you add rotation or x/y/z properties of those children, the rotation is applied to the rotated child, and everything rotates as a single unit. You will still have to manage the child indexes manually, if you get very complicated with it, but simple scenarios like the ones above and below are no big deal.
It is easier to understand if you look at the sample code above (in the first code segment). Each child has x/y/z properties assigned to them, and then the rotationContainer is rotated. All rotation changes are just made to that one container, and all of the children are rotated as a single unit.
Here's a simplified example, which demonstrates the exact same concepts.
And, here's the code that makes it all work. This is all standard Flex4 code, targeting Flash Player 10. I used the Tweener AS3 library to make transitions a bit easier to manage. As you can see, there's nothing overly complex happening in there, with a pretty good wow factor.
<?xml version="1.0" encoding="utf-8"?>
<s:Application
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/halo"
width="100%" height="100%"
resize="resetProjection()"
applicationComplete="toggle3D()">
<fx:Script>
<![CDATA[
import caurina.transitions.Tweener;
import mx.core.IVisualElement;
import mx.core.UIComponent;
private static const transition : String = "easeInOutCirc";
private static const duration : Number = .5;
private var lastX : int = 0;
[Bindable]
private var rotating : Boolean = false;
[Bindable]
private var perspective : Boolean = false;
private function resetProjection() : void
{
if ( root )
root.transform.perspectiveProjection.projectionCenter = new Point( width/2, height/2 );
}
private function toggle3D() : void
{
resetProjection();
var i : int;
var child : IVisualElement;
if ( perspective )
{
clearRotate();
for ( i = 0; i < rotationParent.numElements; i ++ )
{
child = rotationParent.getElementAt( i );
Tweener.addTween(child, {z:0, time:duration, transition:transition});
}
Tweener.addTween(rotationParent, {rotationX:0, rotationY:0, rotationZ:0, time:duration, transition:transition, onUpdate:reorderChildren});
systemManager.removeEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
perspective = false;
}
else
{
var offset : Number = 40;
for ( i = 0; i < rotationParent.numElements; i ++ )
{
child = rotationParent.getElementAt( i );
var _z:Number = (((rotationParent.numChildren/2)-i)) * offset;
Tweener.addTween(child, {z:_z, time:duration, transition:transition});
}
Tweener.addTween(rotationParent, {rotationX:-45, time:duration, transition:transition, onComplete: doRotate});
systemManager.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
perspective = true;
}
}
private function onMouseDown( event : MouseEvent ) : void
{
lastX = mouseX;
systemManager.addEventListener( MouseEvent.MOUSE_UP, onMouseUp );
systemManager.addEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
clearRotate();
}
private function onMouseUp( event : MouseEvent ) : void
{
systemManager.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp );
systemManager.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
}
private function onMouseMove( event : MouseEvent ) : void
{
rotationParent.rotationY += (mouseX - lastX)/3;
lastX = mouseX;
reorderChildren();
}
public function reorderChildren() : void
{
for(var i:uint = 0; i < rotationParent.numElements; i++)
{
var r : Rect = rotationParent.getElementAt( i ) as Rect;
if ( r )
r.depth = perspective ? -r.transform.getRelativeMatrix3D(root).position.z : i;
}
}
private function doRotate() : void
{
if ( !rotating )
{
rotating = true;
addEventListener( Event.ENTER_FRAME, onRotateEnterFrame );
}
}
private function clearRotate() : void
{
rotating = false;
removeEventListener( Event.ENTER_FRAME, onRotateEnterFrame );
}
private function onRotateEnterFrame( event : Event ) : void
{
rotationParent.rotationY ++;
reorderChildren();
}
]]>
</fx:Script>
<s:Group
id="rotationParent"
horizontalCenter="150" verticalCenter="150"
width="300" height="300">
<s:Rect width="300" height="300" x="-150" y="-150" alpha=".8" >
<s:fill><s:SolidColor color="#FF0000" /></s:fill>
</s:Rect>
<s:Rect width="300" height="300" x="-150" y="-150" alpha=".8">
<s:fill><s:SolidColor color="#00FF00" /></s:fill>
</s:Rect>
<s:Rect width="300" height="300" x="-150" y="-150" alpha=".8">
<s:fill><s:SolidColor color="#0000FF" /></s:fill>
</s:Rect>
<s:Rect width="300" height="300" x="-150" y="-150" alpha=".8">
<s:fill><s:SolidColor color="#FF00FF" /></s:fill>
</s:Rect>
<s:Rect width="300" height="300" x="-150" y="-150" alpha=".8">
<s:fill><s:SolidColor color="#FFFF00" /></s:fill>
</s:Rect>
<s:Rect width="300" height="300" x="-150" y="-150" alpha=".8">
<s:fill><s:SolidColor color="#00FFFF" /></s:fill>
</s:Rect>
</s:Group>
<s:HGroup>
<s:Button label="Toggle 3D" click="toggle3D()" />
<s:Button label="Rotate" click="doRotate()" enabled="{ !rotating }" visible="{ perspective }" />
</s:HGroup>
<s:RichText text="(Click and drag to manually rotate)" bottom="0" visible="{ perspective }" />
<s:layout>
<s:BasicLayout />
</s:layout>
</s:Application>






