Home Labs Galleries PetitOn

Using Sandy 3.0 Flash Library

Part 4. Transformations

Introduction

It is all fine and dandy, that we can build a Sandy 3D world using primitives.
But the world, even if beautiful, would be a bit dull if couldn't animate it. That is what transformations are all about, and that's what this tutorial will cover in some detail.
We also need transforms to be able to place our objects where we want them in the world.

You can get the code for this part here.

If you have used Sandy 1.x for AS2 or followed my 1.1 tutorial, you will notice some differences, and maybe miss a thing or two. It could be a bit scary to discover, that we no longer have any PositionInterpolator or RotationInterpolator, to move smoothly form one position to another over time. As we shall see, translations and rotations are simple in Sandy 3.0, and we can use any tweening library to create those animations.

In Sandy 1.1 you had to add your 3D objects to a TransformGroup, create a Transform3D and tell it what to do, a translation or a rotation. finally you had to set the transform to the transform group. A lot of code went into the creation of a simple animation.

For example to translate a cube, we had to write something like this

var tTrans:TransformGroup = new TransformGroup(); 
translation = new Transform3D;
translation.translate( 100, 50, 0 );
tTrans.setTransform( translation );
tTrans.addChild( cube );
rootGroup.addChild( tTrans );

If you wanted to do incremental movements, you had to keep track of the position of the object yourself, because rotations and translations were absolute.

In Sandy 3.0 the Shape3D extends the Atransformable class, responsible for all kinds of transforms, so all you have to write, to move the cube to the new position is this:

cube.x = 100;
cube.y = 50;
rootGroup.addChild( cube );

The object will remember its position, so you can make incremental transforms easily:

cube.x++;
cube.z -= 2;

Of course you will want to use transform groups to accomplish more complex transformations.

The sub classes of Atransformable expose many degrees of freedom as properties, which means they are directly accessible to read and change.

Properties What it is
x, y and x The object's position in its parents coordinate system
rotateX, rotateY, rotateZ The object's rotation around its parents coordinate axes through its reference point
pan, roll, tilt The object's rotation around its own coordinate axes

The Atransformable classes also have a plethora of methods to change the position and attitude of an object in relation to its surroundings.

Best of all, and a really fine improvement, the camera is now a full member of the Sandy object tree. It can be placed anywhere, and can be animated as any other object, which opens up for really fancy flights through the world.

Lets have some code!

Basic transforms

In the following examples we sometimes use the Key class to facilitate simple user interaction over the keyboard. We will also use the CoordinateSystem class, to visualize the positions of our objects in the world. Both classes should be in the project directory, or the default package.

To discuss the basic transforms that can be done directly on 3D object, we can use a simple cube with a ColorMaterial.

The first example is about basic translation along the global axes. Lets go through this first example in some details. You will recognize most of it from the tutorial on primitives.

First we have to import classes or packages that we use

	import flash.display.Sprite; 
	import flash.events.*;
	import flash.display.Stage;
	import flash.ui.Keyboard;	
	import sandy.core.World3D;
	import sandy.core.data.Vector;
	import sandy.core.scenegraph.*;
	import sandy.materials.*;
	import sandy.materials.attributes.*;	
	import sandy.primitive.*; 

We let this class, that is the main or document class, extend Sprite, so we can let the world draw on it. We define some instance variables that we want to make accessible throughout the class.

	public class BasicTranslate extends Sprite
	{
		private var world:World3D;
		private var camera:Camera3D;
		private var box:Shape3D;

The constructor initiates the Sandy world.

	public function BasicTranslate()
	{
		world = World3D.getInstance();
		world.container = this;
		world.root = createScene();
		camera = new Camera3D( 200, 200 );
		world.camera = camera;
		camera.z = -200;
		camera.y = 40;
		camera.x = 20;
		camera.lookAt( 0, 0, 0 );
		world.root.addChild( camera );
		Key.initialize( stage );
		addEventListener( Event.ENTER_FRAME, enterFrameHandler );
	}

So what happens here?

We create the World3D object and set this main object as the container for the world to draw on. Then we add the whole scenery, created by the appropriately named createScene() method as the root group of the world.

Then we create the camera and add it to the world camera property. We back off the camera along the negative z axis to be able to see objects in the origin. We  also move the camera up ( y ) and a bit to the side ( x ), and then make it look at the origin.

It may seem a little strange that we now have to add the camera as a child of the world's root group. Doesn't it already belong to the world? Well, yes it does, but remember that we can place the camera wherever we want in the tree graph. So we must explicitly place the camera in the root group.

Note that the root group must exist before we add the camera. So this must be done after the the call to the createScene() method..

Key.initialize() initializes the key class, and tells it what object to listen to, so we can use the keyboard in an old fashioned manner ;)

Now let's have a look at the createScene method!

private function createScene():Group
{
	var root:Group = new Group();
	root.addChild( new CoordinateSystem( 
				new Vector(-100, -100, -100), 
				new Vector(200, 200, 200), 
				new Vector(0xFF0000,0x00FF00,0x0000FF)) );

	box = new Box( 'box', 40, 40, 40, PrimitiveMode.QUAD, 2);

	var materialAttr:MaterialAttributes = 
		      new MaterialAttributes( new LightAttributes( true, 0.1 ));

	var material:Material = new ColorMaterial( 0xFFCC33, 0.6, materialAttr );

	material.lightingEnable = true;
	var app:Appearance = new Appearance( material );
	box.appearance = app;

	root.addChild( box );
	return root;
}

We start here by creating the root group and adding a coordinate system for reference.
The CoordinateSystem class extends the Group class.

Then we create our fancy demo box. We will use a simple color material, so we can use the QUAD mode and a low polygon count here.

We want to use the world light to enhance the 3D experience, so we use a MaterialAttributes with a LightAttributes. The arguments to the latter means: true, we want to use full light range from black to white and the ambient light value should be 0.1.

We add this attributes to the color material with a orange color and an alpha value of 0.6. Finally we dress the box in this appearance. All this may seem a bit confusing, if you haven't worked with light and color before, but there will a dedicated tutorial on materials later in this series.

But hey! Where are the movements?
Oh, sorry - I'm talking to much again.

Translations

Here they are, interactive and nice, in the event handler for the ENTER_FRAME event.

	private function enterFrameHandler( event : Event ) : void
	{
		if (Key.isDown(Keyboard.LEFT)) {
 			box.x -= 2;
		}
		if (Key.isDown(Keyboard.RIGHT)) {
 			box.x += 2;
		}
		if (Key.isDown(Keyboard.UP)) {
 			box.z += 2;
		}
		if (Key.isDown(Keyboard.DOWN)) {
 			box.z -= 2;
		}
		if (Key.isDown(Keyboard.PAGE_UP)) {
 			box.y += 2;
		}
		if (Key.isDown(Keyboard.PAGE_DOWN)) {
			box.y -= 2;
		}
		world.render();
	}

By using the Key class, we can poll the keyboard to see if a certain key is pressed, and take action. What you see here is that the left and right arrow keys will change the x position, the up and down keys will change the z position and the PgUp and PgDown keys will change the y position of the box. The beauty of this simple example is, that it shows how simple it is to move translate things in Sandy 3.0. The box remembers where it is and you can just add or subtract from the properties x, y and z ( Reminds me of something ). Creating a linear animation is just as simple - just disregard the keys :)

Click to activate, then move it.

Rotations

There are quite a few ways of rotating bodies in Sandy, in part because all transforms from the earlier camera and those used for object transforms were merged into the Atransformable class.

The most obvious rotation scheme, is to rotate the object around an axis parallel to one of the global axes and going through the reference point of the object. These rotations are properties of the object, called rotateX, rotateY and rotateZ. Rotations inherited from the old camera, are tilt, pan and roll, which are rotations around the objects local axes. Lets make a comparison between the two.

All we have to change in the code above, is what happens when we press the keys.
Here is the new event handler, where we uncomment the version we want to use.

	private function enterFrameHandler( event : Event ) : void
	{
		if (Key.isDown(Keyboard.LEFT)) {
 			//box.rotateY -= 2;
			box.pan += 2;
		}
		if (Key.isDown(Keyboard.RIGHT)) {
 			//box.rotateY += 2;
			box.pan -= 2;
		}
		if (Key.isDown(Keyboard.UP)) {
 			//box.rotateX += 2;
			box.tilt += 2;
		}
		if (Key.isDown(Keyboard.DOWN)) {
 			//box.rotateX -= 2;
			box.tilt -= 2;
		}
		if (Key.isDown(Keyboard.PAGE_UP)) {
 			//box.rotateZ += 2;
			box.roll += 2;
		}
		if (Key.isDown(Keyboard.PAGE_DOWN)) {
			//box.rotateZ -= 2;
			box.roll -= 2;				
		}
		world.render();
	}


rotateX, rotateY, rotateZ            tilt, pan, roll

To the left the rotations are around the global axes and to the right around the local axes, that is the object's own coordinate axes. Do you see any difference, apart from the direction of rotation?

You have already guessed it. We don't see any difference as long as the objects own system is parallel to the global system. There are enough degrees of freedom in these simple rotations, that it is difficult to decide how the object rotates. We can demonstrate the difference better if we impose some restrictions.

Let's give the little yellow box an initial rotation around the global x axis, so we see the upper face of it. The object's local y axis will then point slightly towards us. Let's then allow only rotation around the y axes in both cases, in other words rotateY and pan respectively.

To the left we perform a rotateY and to the right a pan.

Here we can clearly see that rotateY rotates the box around an the global y axis, while pan means a rotation around the local y axis.

Translation along local axes

In the first example we moved our cube along the global axes, but we also have the possibility to translate along the object local axes, using the methods moveForward, moveSideways and moveUpwards. These translations will move the object to a new position in the coordinate system of its parent in the tree graph. For now this is the global coordinate system, but it might as well be a TransformGroup.

We want to show that the translations are not along the global axes, so before we begin the translations, we rotate the box around the x and y axis.

	box.rotateY = 30;
	box.rotateX = 20;

Here is the new enterFrameHandler.

	private function enterFrameHandler( event : Event ) : void
	{
		if (Key.isDown(Keyboard.LEFT)) {
			box.moveSideways (-2);
		}
		if (Key.isDown(Keyboard.RIGHT)) {
			box.moveSideways (2);
		}
		if (Key.isDown(Keyboard.UP)) {
			box.moveUpwards(2);
		}
		if (Key.isDown(Keyboard.DOWN)) {
			box.moveUpwards(-2);
		}
		if (Key.isDown(Keyboard.PAGE_UP)) {
			box.moveForward(2);
		}
		if (Key.isDown(Keyboard.PAGE_DOWN)) {
			box.moveForward(-2);
		}
		world.render();
	}

Click to gain focus and test the local axes translations.

Combining transforms

As you may have noticed, the order in which transforms are done is important. We have consider this, when we combine transformations. For example a rotateX followed by a rotateY will leave the object with another attitude, than rotating in the opposite order.

You can use this fact in clever ways, to place an object where you want it. Let's say you want our cube on the periphery of a circle with radius = 100 in the zx plane at angle of 25 degrees from the x axis. You could go the obvious way and calculate the x and z values for the position using trigonometric functions:
box.x = 100 * cos(25), box.y = 100 * sin(25).

Using your knowledge of relative transforms, you can instead let Sandy solve the problem. You just rotate the box, and then let it move sideways.

Let's test this combination!

	private function enterFrameHandler( event : Event ) : void
	{
		if (Key.isDown(Keyboard.LEFT)) {
			box.moveSideways (-2);
		}
		if (Key.isDown(Keyboard.RIGHT)) {
			box.moveSideways (2);
		}
		if (Key.isDown(Keyboard.UP)) {
			box.rotateY+=2;
		}
		if (Key.isDown(Keyboard.DOWN)) {
			box.rotateY-=2;
		}
		world.render();
	}

The left/right navigation keys will move the box sideways, which means along its local x axis, while the up/down keys will rotate it around the global y axes ( in this case it is equivalent with a pan ).

Try it out by making the transforms in different orders. Using the combination of rotateY and moveSideways, you can move around freely in the zx plane. If you allow for a roll or a rotation around any other axis, you can also rotate the plane in which you move.

Conclusions you might have drawn ;)

Basic translations and rotations are performed in relation to either the parent's coordinate system or the objects own local frame of reference.

The order in which translations are performed is important, which can lead to unexpected results, if we don't think it through. This fact can be also be used with fantasy to simplify object positioning in the world.

Next We'll look at using transform groups.

Your comment are most welcome! petit -at- petitpub.com or use my blog form.