Arcball with AGAL
Moussa Dembélé – www.mousman.com- mousman@hotmail.com
Arcball is a common way of rotating objects with a mouse created by Ken Shoemake.
Imagine an object embeded in an invisible sphere. By dragging it you can make it spin around its center.
In this article I will explain two implementations of the Arcball for stage3D – AGAL.
One based on quaternions, another based on axis angles.
Sorry, either Adobe flash is not installed or you do not have it enabled
Principle.
The principle is the same with the two implementations :
The first step is to calculate the projection of the mouse cursor coordinates (that are in 2D) on the sphere (in fact a half sphere).
In order to do that we calculate the distance from the sphere’s center to the mouse in x and y axis.
(The sphere’s center is defined in the 2D space so depending on the object position and perspective you will have to calculate it, and it’s radius as well.)
Then we divide these distances by the sphere’s radius. So if these values are superior to 1 it means that the cursor is outside the sphere.
In that case, in my implementations I consider the cursor to be at the border of the sphere with a z value equals to zero.
Else we can compute the z value.
Because we have divided the distances by the sphere’s radius we can consider them coordinates of a point on a sphere which have a radius equal to 1.
The equation of such a point is
x² + y² + z² = 1
so we can calculate the z value :
z = √(1 – (x² + y²))
In as3 we can do that by creating a Vector3D instance, setting its x and y values to the x, y coordinates calculated and assigning zero to it’s z value,
then by doing
vector.z = Math.sqrt(1 – vector.lengthSquared);
We now have a point coordinates on the sphere,
and we can also considere these coordinates as a unit vector pointing the mouse cursor projection on the sphere and starting form the sphere center !
On a mouseDown event we calculate and store this vector,
and on a mouseMove event we calculate a second vector.
Cool we have two vectors…..
why not a third ?
By determining a vector which is perpendicular to both of the vectors and therefore normal to the plane containing them,
we have an axis of rotation !
For that there is a mathematical operation called cross product.
In as3 we can do that with the Vector3D.crossProduct method :
var axis:Vector3D = startVector.crossProduct(endVector);
And by determining the angle between the two vectors around this axis we have the angle of rotation to apply.
That’s all we need !
The next steps will depend on the use of quaternions or the use of axis angles.
Quaternions implementation
With AGAL Matrix3D are used to represent position, rotation and scale settings of a model.
The matrix3D has a method to extract a unit quaternion representing the current rotation of the object : Matrix3D.decompose(Orientation3D.QUATERNION).
This method returns a Vector of three Vectors3D, the first one about position, the second one is our quaternion and the third one is about scale settings.
The quaternion is represented in as3 by a Vector3D where q = w + ix + jy + kz.
The product of the quaternion that defines the rotation we want to apply with the quaternion containing the current state of the object’s rotation will result in a new quaternion combining the rotations of the two quaternions.
How to do that :
with each update of the arcball (on a frame event or on a mouseMove event) we decompose the Matrix3D and store the three vector3D returned.
var vecs:Vector.<Vector3D> = _targetObjectMatrix.decompose(Orientation3D.QUATERNION);
Then we need to create a new quaternion corresponding to the rotation we want.
There is a formula which can give us this quaternion from the axis angle of rotation :
qx = ax * sin(angle/2)
qy = ay * sin(angle/2)
qz = az * sin(angle/2)
qw = cos(angle/2)
where ax, ay, az are the normalized axis, and angle the angle between the starting vector and the ending vector discussed earlier.
But… we won’t use it…
Instead we will get the cross product of the two vectors. The cross product is a vector which is perpendicular to both of the vectors and normal to the plane containing them. (our axis) .
We will set the x, y, z values of this vector to the quaternion.
In as3 the Vector3D has a method for that :
var resultVector:Vector3D = startVector.crossProduct(endVector);
var quat:Vector3D = new Vector3D(resultVector.x,resultVector.y,resultVector.z);
And qw will be the dot product of the two vectors, which is cos(angle).
quat.w = startVector.dotProduct(endVector);
By doing this the rotation angle will be doubled , so dragging from the border of the sphere to the opposite position will make the object rotate 360°.
This is more convenient and as a bonus we’re saving some calculations.
Now let’s multiply these quaternions , but be careful, the multiplication of quaternions is not commutative !
So to apply a rotation defined by a quaternion Q1 to a quaternion Q2 representing the current rotation state of an object, the formula is :
(Q1 * Q2).w = (w1w2 – x1x2 – y1y2 – z1z2)
(Q1 * Q2).x = (w1x2 + x1w2 + y1z2 – z1y2)
(Q1 * Q2).y = (w1y2 – x1z2 + y1w2 + z1x2)
(Q1 * Q2).z = (w1z2 + x1y2 – y1x2 + z1w2)
Great !
The final step is to recompose the matrix with this new quaternion.
We can do that with The Matrix3D.recompose method.
It takes two parameters , the first one is a Vector containing three Vector3D, the first one about position, the second one is our quaternion and the third is about scale setting.
We use the vector3D about position and the one about scale saved earlier and add our quaternion.
The second parameter specifies the orientation style that was used for the transformation. For us it will be Orientation3D.QUATERNION.
vecs[1] = multiplyQuats(Q1, Q2);
targetObjectMatrix.recompose(vecs,Orientation3D.QUATERNION);
And that’s it !
The rotation is done
see the complete class code
package { import flash.geom.Matrix3D; import flash.geom.Orientation3D; import flash.geom.Point; import flash.geom.Vector3D; /** * @author Dembélé Moussa - www.mousman.com */ public class QuatArcBall { private var _targetObjectMatrix:Matrix3D; private var _radius:Number; private var _center:Point; private var _mouseX:Number; private var _mouseY:Number; private var _mouseIsDown:Boolean; private var _vecs:Vector.<Vector3D>; private var _quaternion:Vector3D; private var _savePos:Point; private var _saveFromVector:Vector3D; public function QuatArcBall(targetObjectMatrix:Matrix3D, center:Point,radius:Number = 10) { _targetObjectMatrix = targetObjectMatrix; _center = center; _radius = radius; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseDown():void { _mouseIsDown = true; var x:Number = (_center.x - _mouseX ) / _radius; var y:Number = (_mouseY - _center.y) / _radius; _savePos = new Point(x, y); _saveFromVector = coordinate2DToSphere(x, y); _vecs = _targetObjectMatrix.decompose(Orientation3D.QUATERNION); _quaternion = _vecs[1]; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function update():void { if (!_mouseIsDown) return; var x:Number = (_center.x - _mouseX ) / _radius; var y:Number = (_mouseY - _center.y) / _radius; // waiting for delta to be significant var deltaX:Number = Math.abs(_savePos.x - x); var deltaY:Number = Math.abs(_savePos.y - y); if ( deltaX < 0.01 && deltaY < 0.01) return; var newVector:Vector3D = coordinate2DToSphere(x, y); drag(newVector); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function drag(newVector:Vector3D):void { var resultVector:Vector3D = _saveFromVector.crossProduct(newVector); var quat:Vector3D = new Vector3D(resultVector.x,resultVector.y,resultVector.z); quat.w = _saveFromVector.dotProduct(newVector); _vecs[1] = multiplyQuats(quat, _quaternion); _targetObjectMatrix.recompose(_vecs,Orientation3D.QUATERNION); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseUp():void { _mouseIsDown = false; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseMove(mouseX:Number, mouseY:Number):void { _mouseX = mouseX; _mouseY = mouseY; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function coordinate2DToSphere(x:Number, y:Number):Vector3D { var targetVector:Vector3D = new Vector3D(); targetVector.x = x; targetVector.y = y; targetVector.z = 0; if(targetVector.lengthSquared > 1){ targetVector.normalize(); } else{ targetVector.z = Math.sqrt(1 - targetVector.lengthSquared); } return targetVector; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function multiplyQuats(q1:Vector3D, q2:Vector3D):Vector3D { var quat:Vector3D = new Vector3D(); quat.x = q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y; quat.y = q1.w * q2.y + q1.y * q2.w + q1.z * q2.x - q1.x * q2.z; quat.z = q1.w * q2.z + q1.z * q2.w + q1.x * q2.y - q1.y * q2.x; quat.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z; return quat; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseWheel(delta:Number):void { var pos:Vector3D = _targetObjectMatrix.position; _targetObjectMatrix.appendTranslation(0, 0, delta*10); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function destroy():void { _targetObjectMatrix = null; _center = null; _vecs = null; _quaternion = null; _savePos = null; _saveFromVector = null; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function get matrix():Matrix3D { return _targetObjectMatrix; } public function set radius(value:Number):void{ _radius = value; } public function set center(value:Point):void {_center = value;} } }
Axis angle implementation
This implementation is simpler.
Remember : on the mouseDown event we have stored the starting vector,
on each update we’re calculating the end vector as discussed earlier,
and the axis vector via the cross product method.
What we need now is the angle between the starting vector and the ending vector.
The Vector3D has a method for that :
var angle:Number = Vector3D.angleBetween(startVector, endVector);
Has the result is returned in degrees we just have to convert into radians, double this angle, and that’s done.
Now we have to apply the rotation around the axis.
There is a method for that with Matrix3D :
targetObjectMatrix.appendRotation(angle, axis, _targetObjectMatrix.position);
And… done ! Cool.
see the complete class code
package { import flash.geom.Matrix3D; import flash.geom.Point; import flash.geom.Vector3D; /** * ... * @author Dembélé Moussa - www.mousman.com */ public class MArcBall { private const RAD_TO_DEG:Number = 180 / Math.PI; private var _targetObjectMatrix:Matrix3D; private var _radius:Number; private var _mouseX:Number; private var _mouseY:Number; private var _mouseIsDown:Boolean; private var _savePos:Point; private var _saveFromVector:Vector3D; private var _center:Point; public function MArcBall(targetObjectMatrix:Matrix3D, center:Point,radius:Number = 10) { _targetObjectMatrix = targetObjectMatrix; _center = center; _radius = radius; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseDown():void { _mouseIsDown = true; var x:Number = (_center.x - _mouseX ) / _radius; var y:Number = (_mouseY - _center.y) / _radius; _savePos = new Point(x, y); _saveFromVector = coordinate2DToSphere(x, y); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function update():void { if (!_mouseIsDown) return; var x:Number = (_center.x - _mouseX ) / _radius; var y:Number = (_mouseY - _center.y) / _radius; // waiting for delta to be significant var deltaX:Number = Math.abs(_savePos.x - x); var deltaY:Number = Math.abs(_savePos.y - y); if ( deltaX < 0.01 && deltaY < 0.01) return; var newVector:Vector3D = coordinate2DToSphere(x, y); drag(newVector); _saveFromVector = newVector; _savePos = new Point(x, y); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function drag(newVector:Vector3D):void { var axis:Vector3D = _saveFromVector.crossProduct(newVector); var angle:Number = Vector3D.angleBetween(_saveFromVector, newVector); angle = RAD_TO_DEG * angle *2; _targetObjectMatrix.appendRotation(angle, axis, _targetObjectMatrix.position); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseUp():void { _mouseIsDown = false; _saveFromVector = coordinate2DToSphere(_savePos.x, _savePos.y); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseMove(mouseX:Number, mouseY:Number):void { _mouseX = mouseX; _mouseY = mouseY; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function coordinate2DToSphere(x:Number, y:Number):Vector3D { var targetVector:Vector3D = new Vector3D(); targetVector.x = x; targetVector.y = y; targetVector.z = 0; if(targetVector.lengthSquared > 1){ targetVector.normalize(); } else{ targetVector.z = Math.sqrt(1 - targetVector.lengthSquared); } return targetVector; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function mouseWheel(delta:Number):void { var pos:Vector3D = _targetObjectMatrix.position; _targetObjectMatrix.appendTranslation(0, 0, delta*10); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function destroy():void { _targetObjectMatrix = null; _center = null; _savePos = null; _saveFromVector = null; } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- public function get matrix():Matrix3D {return _targetObjectMatrix;} public function set radius(value:Number):void{ _radius = value; } public function set center(value:Point):void {_center = value;} } }
Any questions, any comments,Don’t hesitate.
Moussa Dembélé
Categories: Non classé