Simulating Rotation with Euler Angles, a Quaternion, or a
Rotation Matrix
CS 482
Lecture, Dr. Lawlor
To move the camera, objects in the world, or physical effects, we
need a reliable way to represent arbitrary rotations in 3D.
Euler Angles
Euler Angles
are by far the simplest scheme--you store three rotation angles, and
rotate first about X, then about Y, and finally about Z. The
only problem is that the Z has changed by the time you get to it, so
it's difficult to control the later rotations properly, and there
are some orientations where the new Z is equivalent to the old X or
Y, so you've lost a degree of control ("gimble lock"). By
default, THREE.js supports a THREE.Euler
object in the "rotation" field of each Object3D or Mesh. So
"sim.spheres[i].rotation.set(xrot,0,0);" sets the rotation angle
around the X axis, measured in radians.
You can try this out with the "WASD" (X and Y tilt) and "QZ" (screen
rotation) keys here:
Keyboard
Euler Rotation Demo
Note that initially, the X and Y rotations tip the head correctly,
in orthogonal directions. Now use Q to rotate the head
onscreen by 90 degrees. X and Y now tip the head in the same
direction.
Quaternions
Quaternions
are a cool mathematical construct that lets you represent an
arbitrary rotation as a 4D vector. The 3D XYZ parts of the
vector give the rotation axis for the direction. Typically
quaternions are stored normalized, so the sum of the squares of the
components is 1. This makes the magnitude of the 3D part the
sine of half the rotation angle; the cosine of half the rotation
angle is stored in the final W component of the vector.
It's a strange definition, but the neat part is that 'multiplying'
quaternions (using a cross product looking formula) gives you the
composition of the underlying rotations. Because rotations
don't commute (this is the problem with Euler angles), quaternion
multiplication is non-commutative: you get a different position from
multiplying the quaternions in the opposite order, because you get a
different position from applying the rotations in the opposite
order.
In THREE.js, you can access the object's THREE.Quaternion
orientation using "obj.quaternion". You can now rotate the
object by building a quaternion to represent the new rotation, for
example via "newRot.setFromAxisAngle(myVec3,myAngleRadians)".
To apply the rotation in object space, you'd multiply on the right:
obj.quaternion.multiply(newRot);
You can also apply a rotation in world space by multiplying on the
left, with:
obj.quaternion.multiplyQuaternions(newRot,obj.quaternion);
It's good practice to obj.quaternion.normalize() to prevent the
object from changing size due to roundoff in the quaternion
manipulation.
Keyboard
Quaternion Rotation Demo
It's not immediately obvious there's a difference from Euler mode,
but now the rotations stick to the coordinate frame of the world--X
and Y rotations always tip in orthogonal directions, regardless of
the object orientation.
Rotation Matrix / Orthonormal Coordinate Frame
I personally find quaternions fun but confusing, and I usually find
a need for object-local 3D vectors pointing in all directions: for
example, Z is used for run, X is used for strafe, Y is used for
jump. So I usually just keep my own object-local X, Y, and Z
vectors for each object, and keep them (1) normalized to unit
length, and (2) orthogonal to each other, or "orthonormal".
You can force your X, Y, and Z to be orthonormal using cross
products. If you didn't start out orthogonal, you might need
to choose the order of orthogonalization carefully to preserve the
axes you want.
// Orthonormalize
s.X.crossVectors(s.Y,s.Z).normalize();
s.Y.crossVectors(s.Z,s.X).normalize();
s.Z.crossVectors(s.X,s.Y).normalize();
Any set of three orthonormal vectors actually forms the 3x3 core of
a rotation matrix. To get from object-Local coordinates
L into world-Global coordinates G:
[ G.x ] [ X.x Y.x Z.x ] [ L.x ]
[ G.y ] = [ X.y Y.y Z.y ] [ L.y ]
[ G.z ] [ X.z Y.z Z.z ] [ L.z ]
This is equivalent to the vector equation G = L.x*X + L.y*Y + L.z*Z.
To get back into local coordinates, we just transpose the matrix
(transposing a rotation matrix gives you the inverse matrix):
[ L.x ] [ X.x X.y X.z ] [ G.x ]
[ L.y ] = [ Y.x Y.y Y.z ] [ G.y ]
[ L.z ] [ Z.x Z.y Z.z ] [ G.z ]
This is equivalent to L=vec3(dot(X,G),dot(Y,G),dot(Z,G)).
To adjust a rotation matrix, I often just push the coordinate system
around. For example, to push the camera Z direction in the +X
direction due to mouse movement, I'd use something like:
Z' = Z + 0.001*mouse_dx*X;
The scalar in front of X is approximately an angle in radians (for
small angles). Of course, after pushing the coordinate axes
around, you need to re-orthonormalize by taking cross products.
In THREE.Matrix4,
there's a handy method makeBasis that takes your X, Y, and Z axes as
vec3's. You can call this on any object's obj.matrix member,
although you need to set the magic booleans
obj.matrixAutoUpdate=false to avoid trashing the matrix from the
quaternion.
Here's a demo of building an object coordinate system and uploading
it as a matrix:
Keyboard
Rotation Matrix Demo
Comparison
Euler angles are trivial to define and apply, but don't compose or
generalize well. For CAD, where objects are often positioned
via Euler angles, unless aligned with the coordinate system it's
often difficult to determine the angles required to put an object in
the correct orientation.
Quaternions are a compact, mathematically robust way to represent
rotations. In animation, you can interpolate between two
quaternion rotations using simple vector interpolation, and
normalizing the quaternion at each step, to get a smooth and
plausible rotation animation--Euler angles tend to lurch around the
poles if you try this, and matrices need to be re-orthonormalized
and can make strange axis-flipping choices.
Matrices are larger than quaternions, with an inconvenient 9
entries, and you must manually orthonormalize them or else they can
get skewed (due to roundoff, or manual manipulation). The big
advantage is it's easy to build the code, and you can draw 3D
vectors along the coordinate edges if you get confused. Older
graphics interfaces like fixed-function OpenGL expect you to
represent orientation using matrices, although you could push
quaternions directly to the vertex shader in modern programmable
shaders.