Erain 3D
--> FDT Pure coding comfort


Author: makc
Date: August 22th 2008
version: 3.0

Euler angles

As you should already know from Tutorial 4, Sandy offers several ways to rotate objects, most notably rotateX, rotateY, and rotateZ properties that, as names imply, rotate objects around said axes. These properties were once functions, changed to properties to simplify rotation of objects by tweening them. As a side effect, many people (including myself, in the past) were thinking their values somehow represent specific orientation of the object, which is NOT the case. In fact, their values are a matter of chance, and generally cannot be used to restore current object orientation from scratch.

Three values that DO represent specific orientation of the object, are known as Euler angles. They are angles of object rotation around same three reference axes, again, but with precondition of fixed application order this time. The order matters; remember Rubik's cube, if you have any doubts :-)

In this tutorial, we will extend Cone primitive to implement tweenable eulerX, eulerY, and eulerZ properties, and make small demo illustrating the difference with standard Sandy properties.

Extending the Cone

I assume you do know how to extend classes in AS3, but in case you aren't sure, here is minimum of code required to extend Cone primitive:

package {
	import sandy.primitive.Cone
 
	public class EulerCone extends Cone {
 
		/**
		* Constructor simply calls Cone constructor.
		*/
		public function EulerCone (p_sName:String=null, p_nRadius:Number=100, p_nHeight:Number=100, p_nSegmentsW:Number=8, p_nSegmentsH:Number=6) {
			super (p_sName, p_nRadius, p_nHeight, p_nSegmentsW, p_nSegmentsH);
		}
	}
}

Next thing to do is to declare some private variables to hold Euler angles:

private var _eulerX:Number = 0;
private var _eulerY:Number = 0;
private var _eulerZ:Number = 0;

For each of these variables, we need to define a property, so that changing their values would actually have some effect on the cone:

public function get eulerX ():Number { return _eulerX; }
public function set eulerX (value:Number):void {
	_eulerX = value; _setRotation ();
}

Finally, we need to implement private _setRotation() method that will rotate our cone according to specified values. As I said earlier, it would be enough to set rotationX, rotationY and rotationZ in some specific order here; however, each of these properties internally calculates and applies its own rotation matrix. So we will take an opportunity to save some CPU cycles and learn something new about Sandy here - namely, eulerRotation() method of Matrix4 class. This method calculates composite rotation matrix out of three angles (assuming specific application order) and, together with little-known matrix property that our EulerCone class inherits from ATransformable, it can work miracles:

private function _setRotation ():void {
	// reuse last transformation matrix instance
	var m:Matrix4 = matrix;
	// store translation, because eulerRotation () call will destroy it
	var _x:Number = x;
	var _y:Number = y;
	var _z:Number = z;
	// set rotation
	m.eulerRotation (_eulerX, _eulerY, _eulerZ); initFrame (); matrix = m;
	// restore translation
	x = _x;
	y = _y;
	z = _z;
}

Testing the result

Now, it's time to test our new EulerCone properties against standard Cone stuff. For this purpose, we will use code derived from our simplest tutorial and Flash CS3 Slider component:

import fl.events.SliderEvent 
 
import sandy.core.Scene3D;
import sandy.core.scenegraph.*;
import sandy.primitive.*;
import sandy.materials.*;
 
var scene:Scene3D = new Scene3D ("rotation", this, new Camera3D (600, 300), new Group("root"));
scene.camera.z = -3000; scene.camera.fov = 6;
 
var texture:Appearance = new Appearance (new BitmapMaterial (new Texture (0, 0)));
 
var cone1:Cone = new Cone ("cone1", 40);
cone1.appearance = texture; cone1.x = -150; scene.root.addChild (cone1);
var cone2:EulerCone = new EulerCone ("cone2", 40);
cone2.appearance = texture; cone2.x = +150; scene.root.addChild (cone2);
 
function onSliderChange (e:SliderEvent):void {
	// although these rotations depend on order, we can still use a single listener
	// since user can only drag one slider - so only one setter will actually work!
	cone1.rotateX = sliderX.value;	
	cone1.rotateY = sliderY.value;	
	cone1.rotateZ = sliderZ.value;	
 
	// euler angles do not depend on order you choose
	cone2.eulerX = sliderX.value;
	cone2.eulerY = sliderY.value;
	cone2.eulerZ = sliderZ.value;
 
	scene.render();
}
 
sliderX.addEventListener (SliderEvent.CHANGE, onSliderChange);
sliderY.addEventListener (SliderEvent.CHANGE, onSliderChange);
sliderZ.addEventListener (SliderEvent.CHANGE, onSliderChange);
 
onSliderChange (null);

The result and source files for this tutorial can be found below:

Grab the sources

Gimbal lock

Note that different sets of Euler angles can actually describe same object orientation. For example, drag Y slider in the applet above to 90 degrees, then drag X and Z sliders - you will see that Z slider now compensates rotation caused by X slider.

As a consequence, functions that compute Euler angles for specific object orientation (such as getEulerAngles() method of Matrix4 class), have certain freedom in picking values they return. If you use these functions, make sure your code can handle possible discontinuities in values of Euler angles.