3D Vectors & Rotations
CS 482 Lecture,
Dr. Lawlor
JavaScript 3D vectors
There are several annoying things about
doing 3D vector work in JavaScript:
- No operator overloading, so you need "a.addSelf(b)" instead of
"a+=b". This makes expressions like "P+=dt*V+0.5*dt*dt*A;"
quite painful.
- No function overloading, so "dot(a,b)" or "normalize(c)"
contaminate the global namespace, and hence only work for
vec3. For vec2 or vec4, it's a method call like "a.dot(b)"
or "c.normalize()".
- Low performance, even for a CPU.
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:
- The difference between a 3D vector, like "vec3"; and a 1D
scalar, like "float". I'll try to consistently write
vectors in uppercase, like "vec3 A", and scalars in lowercase,
like "float a". You can't add vectors and scalars, so you do
need to keep track of the difference.
- Vectors as positions/locations/coordinates ("absolute", with
w==1.0), shifts/offsets/directions ("relative", with w==0.0),
and unit-length directions ("normalized", a special case of
relative vectors).
- Know how to calculate a vector's length, via the Euclidean
distance: len=sqrt(x*x+y*y+z*z);
- Know that "to normalize" means to make a vector have unit
length (for example, by dividing it by its length).
- Homogenous vector interpretation: (x,y,z,1) is an absolute
location; (x,y,z,0) is a relative direction.
- "It's just a bag of floats" vector interpretation: the color
(r,g,b,a) can and is treated like as a vector, especially on the
graphics hardware.
- Dot product: compute the "similarity" of two vectors
- A dot B = A.x*B.x + A.y*B.y + A.z*B.z
- For unit vectors, A dot B is the cosine of the angle between
A and B.
- You can figure this out if you write down the dot product
of two unit 2D vectors: A=(1,0) lying along the x axis,
B=(cos theta, sin theta) in polar coordinates. Then A
dot B = cos theta.
- For unit vectors, A dot B is:
- 1 if A and B lie in the same direction (angle=0)
- 0 if A and B are orthogonal (angle=90)
- -1 if A and B lie in the opposite directions (angle=180)
- Always the "signed length" of the projection of A onto B
(or vice versa).
- For any vector, A dot A is the square of A's length.
For a unit vector, A dot A is 1.
- In general, A dot B = length(A) * length(B) * cos(angle
between A and B)
- Dot product is commutative: A dot B = B dot A
- Dot product distributes like multiplication: A dot (B + C) =
A dot B + A dot C
- Dot product is friendly with scalars: c*(A dot B) = (c*A)
dot B = A dot (c*B)
- "A dot B" is the math notation. In code you usually
write "float f = dot(A,B);" or "var f=A.dot(B);".
- Dot product is very handy in computer graphics for computing
diffuse (Lambertian) lighting: for surface normal N and light
direction L, "float light = dot(N,L);".
- Coordinate frames
- A set of 3 orthogonal and normalized (orthonormal) vectors
(call them S, U, and V) is actually a rotation matrix, and it
has some magical properties.
- The magic is if I take any point X=s*S+u*U+v*V, then
- X dot S = (s*S+u*U+v*V) dot S
- X dot S = s * (S dot S) + u * (U dot S) + v * (V dot S)
- X dot S = s*1 + u * 0 + v * 0 (because S
is unit, and S, U, and V are orthogonal)
- or X dot S = s
- That is, the 's' coordinate of the (S,U,V) coordinate system
is just X dot S!
- To get into the coordinate system: dot the incoming vector
with each axis.
- s=X dot S; u=X dot U; v=X dot V;
- To get out of the coordinate system: scale each axis by
the coordinates.
- To put the coordinate origin at W, just subtract W from X
before dotting (or, subtract W dot S after dotting)
- Cross product: compute a new vector orthogonal to the two
source vectors
- A cross B = (A.y*B.z-A.z*B.y, A.z*B.x-A.x*B.z,
A.x*B.y-A.y*B.x)
- On your right hand, if your index finger is A, and your
middle finger is B, your thumb points along "A cross B".
- "A cross B" is the math notation. In code you usually
write "vec3 C = cross(A,B);" or "var C=A.cross(B);".
- The vector A cross B is orthogonal to both A and B.
(dot(A,C) = dot(B,C) = 0)
- length(cross(A,B)) == length(A)*length(B)*sin(angle between
A and B)
- Cross product can be used to make any two non-orthogonal
vectors A and B into an orthogonal frame (X,Y,Z). The
idiomatic use is:
- X = A (pick a vector to be one axis)
- Z = A cross B (new 'Z' axis is orthogonal to A/B
plane)
- Y = Z cross X
- Note that the cross product is anti-commutative: A cross B =
- B cross A
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).
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.