3D Vectors & Rotations

CS 482 Lecture, Dr. Lawlor

JavaScript 3D vectors

There are several annoying things about doing 3D vector work in JavaScript:
One of the most frustrating things about doing high performance code in interpreted languages is the high cost of dynamic allocations with "new".  C++ makes extensive use of stack allocation, so "vec3 a=2.0;" gets inlined and transformed into registers or temporaries by the compiler.  By contrast, in interpreted languages, this is a memory allocation.  Thus the API contorts to avoid creating new objects, typically by providing interfaces that mostly mutate existing objects rather than creating new objects.  You can work around this by explicitly asking for new objects (e.g., ".clone()") when you'd prefer not to trash the old values, but this adds more syntactic overhead.

For example:
Operation
GLSL or "osl/vec4.h"
PixAnvil
Vector addition
vec3 C=A+B;
var C=A.p(B);
Incremental addition
C+=D;
C.pe(D);
Vector scaling
vec3 S=A*0.5;
var S=A.t(0.5);
Vector average
vec3 C=(A+B)*0.5;
var C=A.p(B).t(0.5);
Dot product
float x=dot(A,B);
var x=dot(A,B);
Cosine of angle between vectors
float cos_ang=dot(A,B) / (length(A)*length(B));
var cos_ang=dot(A,B) / (length(A)*length(B));
Cross product
vec3 D=cross(A,B);
var D=cross(A,B);
Make unit vector
vec3 D=normalize(P);
var D=normalize(P);

(Sometime around 2015, the THREE.Vector3 class changed the semantics of .add from "add this vector and return it" to "add to this vector", like addSelf.  This was about the time I gave up on THREE.js)

Vectors, dot, and cross products (warp-speed review)

You should know basic 2D and 3D vectors, including dot products and cross products. Vectors are basically all we ever deal with in computer graphics, so it's important to understand these pretty well. 

In particular, you should know at least:

Representing Object Rotation


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"). 

You can try this out with the "WASD" (X and Y tilt) and "QE" (Z rotation) keys here.  Euler angles are the default.
   Object Orientation Demo

Note that initially, the X and Y rotations tip the object in directions corresponding to their axes. Now use Q to rotate the object onscreen by 90 degrees.  Y rotation now tips the object in the direction that used to be X, and vice versa.

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 orientation from multiplying the quaternions in the opposite order, because you really do get a different orientation from applying the rotations in the opposite order.

In Babylon.js, "obj.rotationQuaternion" is a BABYLON.Quaternion object, and if you set it, it overrides the "obj.rotation" field.

You can apply a rotation in local coordinates by multiplying on the left, with:
    obj.rotationQuaternion.multiplyInPlace(newRot);

To apply the rotation in global coordinates, you multiply on the right, with:
    newRot.multiplyToRef(o.box.rotationQuaternion,o.box.rotationQuaternion);

It's good practice to obj.quaternion.normalize() to prevent the object from changing size due to roundoff in the quaternion manipulation.

Press 't' in the demo for Quaternions (sorry, 'q' was busy!).  See the Trace tab to see the values of the quaternions.
   Object Orientation 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
m.X=normalize(cross(m.Y,m.Z));
m.Y=normalize(cross(m.Z,m.X));
m.Z=normalize(m.Z);
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=new 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 exactly an angle in radians (for small angles).  Of course, after pushing the coordinate axes around, you need to re-orthonormalize by taking cross products.

Press 'm' in the demo for Matrix mode.  This mode also affects the rocket thrust (toggle with 'z', like Kerbal Space Program).
   Object Orientation Demo

You can convert between any of these representations.  I personally prefer the rotation matrix because I need to compute vectors anyway, and the old fixed-function hardware only accepted matrix transforms (now in the programmable era, your vertex shader can use any representation it wants).
In  \  Out
Euler
Quaternion
Matrix
Euler
-
BABYLON.Quaternion.
RotationYawPitchRoll

BABYLON.Matrix.
RotationYawPitchRoll
Quaternion
BABYLON.Quaternion.
toEulerAngles
-
BABYLON.Quaternion.
toRotationMatrix

Matrix
BABYLON.Vector3.
RotationFromAxis
BABYLON.Quaternion.
fromRotationMatrix
-


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 inconveniently non-power-of-two 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.