Using Sandy 3D Flash Library
Part 4, Transformations
We have seen in Part 2, Primitives of this
series, that we could rotate a 3D object, to get a better sense of
perspective. In Part 3, The Camera we had a static scene and moved the camera
around to look at our world from different angles and positions.
We would make more interesting worlds, if we could animate the geometric
objects; make them move around and
dance in the world.
Sandy features the TransformGroup and Transform3D classes to accomplish this. We can apply rotations and translations to any group of objects. The transformations may also be automatically and smoothly animated from a start value to an end value using interpolators. We'll start by making ourselves acquainted with simple rotations and translations. You can download the Actionscript code for this part of the tutorial here and as usual the Actionscript is included in frame one of the fla file.
Rotating a cube
As you may recall from "Part 2, Primitives" in this series, we created a cube using the Box primitive, and to get the 3D impression, we rotated it slightly from its original position.
function createScene( bg:Group ):Void { var cube:Object3D = new Box( 50, 50, 50, 'quad' ); var skin:Skin = new MixedSkin( 0x00FF00, 80, 0, 100, 1 ); cube.setSkin( skin ); var tg:TransformGroup = new TransformGroup(); var rotation:Transform3D = new Transform3D(); rotation.rot(20,30,0); tg.setTransform( rotation ); tg.addChild( cube ); bg.addChild( tg ) }
We created a TransformGroup to which we added both a Transform3D and the geometric object, the Box. Then we added the transform group to the world's rootGroup bg. The Transform3D, the rotation affects all children in the transform group. In this case we only have one, the Box
To test this transform a little further I have built on the BoxTest.as and added some a control panel. To be able to control the Transform3D, I have a global reference 'rotation' to the Transform3D. I also added the coordinate system from "Part 3, The Camera", for reference, this time just showing the axes.
On the Flash stage I have added three dynamic text fields named xStep, yStep and zStep, and a button named rotateButton.
They are used to change the rotation of the cube.
Let's start with the function, that initiates the world.
function init( Void ):Void { screen = new ClipScreen( this.createEmptyMovieClip('screen', 1), 200, 200 ); var cam:Camera3D = new Camera3D( 800, screen ); cam.setPosition(0,0,-500); world.addCamera( cam ); var bg:Group = new Group(); world.setRootGroup( bg ); createScene( bg ); // Create the global coordinate system for reference, only axes createCoordinateSystem( bg, false, 1 ); // Listen to the rotation button rotateButton.onRelease = rotate; // Finally we start rendering the world world.render(); } function rotate(){ rotation.rot( Number(xStep.text),Number(yStep.text),Number(zStep.text) ); }
After creating the world, adding the camera and creating the scene, I have added the coordinate system axes. Then I listen to the "release" event from the rotateButton. The event triggers the rotate() function that rotates our transform group.
In the createScene() function, we have already set the rotation to [20,30,0], a rotation around the x axes by 20 and around the y axis by 30 degrees. To beautify the cube, I have set another skin on the cube and enabled the use of light, like this:
var skin:Skin = new MixedSkin( 0xF28F35, 100, 0, 0, 0 ); // Enable the default light source on the skin skin.setLightingEnable( true ) // Apply the skin to the cube. cube.setSkin( skin );
You can try it out here.
A conclusion from this experiment is that a Transform3D can be changed after adding it to a parent group, in this case the rootGroup. Another is that whenever you call the rot() function, you set the absolute rotation of the group, from its default position, and not from its present position. When you set [0, 0, 0] and click the "Rotate" button, you get the unaffected cube.
Transformations are exclusive
A Transform3D can perform lots of different transformations, such as rot(φx, φy, φz), rotX(φ), rotY(φ), rotZ(φ) and translate(x, y, z). When you call a transformation function in a Transform3D, earlier transforms on the same transform object are cancelled. That's to say transforms called on the same Transform3D object are mutually exclusive.
This is demonstrated in the following applet, where the functions rotX(), rotY() and rotZ() are called with values given by three sliders. The values of the sliders go from 0 to 360 degrees. I used the same the same Transform3D object as in the last example.
Here is the code for the eventhandlers for the sliders
function setUpControls(){ var xCh:Object = new Object(); xCh.onChange = function(evt:Object){ xRot.text = evt.value; rotation.rotX(evt.value); } xSlider.addListener(xCh); var yCh:Object = new Object(); yCh.onChange = function(evt:Object){ yRot.text = evt.value; rotation.rotY(evt.value); } ySlider.addListener(yCh); var zCh:Object = new Object(); zCh.onChange = function(evt:Object){ zRot.text = evt.value; rotation.rotZ(evt.value); } zSlider.addListener(zCh); }
Try it out!
The conclusion is, that if you want to add transformations you need to use transformation functions that affect more variables at the same time or you must combine transformation groups. More on that in a little while.
Translating the Cube
For translations in three dimensional space, we use, guess what?
Exactly! A Transform3D, but this time we call the translate function on the
Transform3D object. The control button is called translateButton and the
event handler translate()
These are the new constructs in the init() function and the event handler // Listen to the rotation button translateButton.onRelease = translate; // Finally we start rendering the world world.render(); } function translate(){ translation.translate(Number(xStep.text),Number(yStep.text),Number(zStep.text)); }
And here are the new transformations in the createScene() function
// First we create a TransformationGroup to facilitate the translation var tTrans:TransformGroup = new TransformGroup(); // We create Transform3D to carry out the transformation in 3 dimensions translation = new Transform3D; // We have to tell the Transformation what to do. tTrans.setTransform( translation ); // Add the cube to the translation tTrans.addChild( cube ); // And finally the transformgroup is added as a child to the rootGroup bg.addChild( tTrans );
Combined transformations
It is possible to combine transformations by adding one transformation as a child of another. To understand this, you may think of your hand holding a string attached to some object, like a small stone at its lower end, ( I don't want your arm to hurt here ;). While you move your hand horizontally, the object rotates around the string. We have a combination of transforms, a rotation and a translation.
The order in which the transforms are applied, is essential. If we first translate, then rotate around an axis, the center of the object will move in a circle around the axis. If, on the other hand, the rotation is applied first, the rotated object will move in a straight line along the chosen axis, like with the string and the stone.
Here we want to show, that what we translate is in fact a 3D object, so we want to rotate it slightly first and then apply a translation along one or more axes.
// First we create the TransformationGroups var tRot:TransformGroup = new TransformGroup(); var tTrans:TransformGroup = new TransformGroup(); // We create Transform3D to carry out the transformation in 3 dimensions rotation = new Transform3D(); translation = new Transform3D; // We want to rotate our cube around the global axes rotation.rot(20,30,0); // The rotation is set in the transform group which will hold our cube tRot.setTransform( rotation ); tTrans.setTransform( translation ); // Our cube is added to the rotation group tRot.addChild( cube ); // The rotation is added to the translation tTrans.addChild(tRot); // And finally the transformgroup is added as a child to the rootGroup bg.addChild( tTrans ); }
I didn't add any amount of translation to the tTrans group here, as this is done by the event handler for the translateButton.
How do we know which of the transforms is applied first? Well, that's simple
enough. The transform group, that holds the geometric object, in this case the
rotation group will be applied first.
When you make the cube a child of the tRot group, the cube is rotated, and when
you make that rotated group a child of the tTrans group, the whole rotation
group is translated. Let's have a look!
Simple automatic transformations
Sometimes you really want to create interactive transformations, like the ones we have seen here, maybe using the keyboard or just moving your mouse over some object on the stage. For some purposes you want to automatically move things around, to make your world come to life. Sandy offers both simple and more elaborate methods to animate your world.
For simple animated motions, you can use some functions of the Transform3D objects, but instead of controlling them with user controls, you may use timer events or some other events, to programmatically animate your objects.
If you ever used the Flash event onEnterFrame to call a function once every frame, you will recognize the behavior of this event. If your frame rate is set at 12 frames per second ( fps ), the onEnterFrame your event handler is called every 1/12:th of a second.
In the same way the World3D onRenderEvent in Sandy is fired ( Broadcasted ) every time the world is rendered, once for each Sandy "frame".
Let's create an automatic rotation of a group around any of the global axes! We need to listen for the onRenderEvent and set up an event handler function, that updates the rotation angles and rotates our old cube. To make the movements a little more interesting, I'll use a combined transformation and give the cube an initial translation by [25, 25, 0] ;)
The control panel this time has three buttons to select one of the global axes as rotation axis, a start and a stop button for the rotation to take place.
To keep track of the rotation around each axis I'll use the global variables xRot, yRot and zRot and to hold the angle steps for each I'll use xDel, yDel and zDel.
Here are the event handlers for the control panel and for the onRenderEvent.
// Event handlers function createHandlers(){ // On the render event call rotate() world.addEventListener (World3D.onRenderEVENT, this, rotate); // Start and stop the rotation startButton.onRelease = function(){ running = true; } stopButton.onRelease = function(){ running = false; } // Select rotation axis xRotButton.onRelease = function(){ xDel = angleStep; yDel = zDel = 0; }; yRotButton.onRelease = function(){ yDel = angleStep; xDel = zDel = 0; }; zRotButton.onRelease = function(){ zDel = angleStep; xDel = yDel = 0; }; }
For each frame, or onRenderEvent, the rotate() function is called to increase one of the rotation angles by angleStep.
The start and stop buttons just sets the value of a boolean variable to true or false. This is used by the rotation function to decide on whether to rotate or not.
If we look at the onRelease handler for the xRotButton, we see that it sets
the xDel to angleStep, and sets yDel = zDel = 0, making the x axis the
active axis of rotation.
The event handlers for yRotButton and zRotButton sets the rotation step for
the other axes.
Finally, here is the rotate() function.
function rotate(){ if( running ){ xRot += xDel; yRot += yDel; zRot += zDel; rotation.rot( xRot, yRot, zRot ); } }
If we are running, it calculates the new values for rotation around the three axes, and calls the rot() function on the Transform3D object called rotation. It will increase the angle for the active axis by angleStep and keep the rotation values for the other two axes.
Here we go ;)
Make it dance, and answer the questions!
Is there one point on the cube , that never moves from its original position?
Does it rotate the way you expect?
On that last question, I suppose your answer is no, not always. The reason is that the order of rotations around the axes is important. This is what the documentation says should happen, when we use the rot() function:
public function rot(px:Number, py:Number, pz:Number):Void Realize the euler rotation matrix based on the three angles in degrees. Parameters px Number Angle of rotation in degrees around the X axis py Number Angle of rotation in degrees around the Y axis pz Number Angle of rotation in degrees around the Z axis
This doesn't say much about the problem, but it mentions the Euler matrix, and information can be found in many places on the big Net. If you are a hard core mathematician, you may want to read "Euler angles", with discussion and matrix calculations. If not, take a quick look at "Euler Angles are Evil" - you don't have to delve in the math here, just read the comments.
The conclusion is, that to see clean rotations around one axis at a time, you need to use three TransformGroup's and three Transform3D's, one transform per axis. I'll leave that for you to tinker with.
Take sip here!
Next we'll look at the more flexible ways to do animations, using interpolators.