Not all vector operations are component-wise; some operations require more math. In this section, you are going to learn how to implement common vector operations that are not component-based. These operations are as follows:
- How to find the length of a vector
- What a normal vector is
- How to normalize a vector
- How to find the angle between two vectors
- How to project vectors and what rejection is
- How to reflect vectors
- What the cross product is and how to implement it
Let's take a look at each one in more detail.
Vector length
Vectors represent a direction and a magnitude; the magnitude of a vector is its length. The formula for finding the length of a vector comes from trigonometry. In the following figure, a two-dimensional vector is broken down into parallel and perpendicular components. Notice how this forms a right triangle, with the vector being the hypotenuse:
Figure 2.5: A vector broken down into parallel and perpendicular components
The length of the hypotenuse of a right triangle can be found with the Pythagorean theorem, A2 + B2 = C2. This function extends to three dimensions by simply adding a Z component—X2 + Y2 + Z2 = length2.
You may have noticed a pattern here; the squared length of a vector equals the sum of its components. This could be expressed as a dot product—Length2(A) = dot(A, A):
Important note:
Finding the length of a vector involves a square root operation, which should be avoided when possible. When checking the length of a vector, the check can be done in squared space to avoid the square root. For example, if you wanted to check if the length of vector A is less than 5, that could be expressed as (dot(A, A) < 5 * 5).
- To implement the square length function, sum the result of squaring each component of the vector. Implement the
lenSq
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:float lenSq(const vec3& v) {
return v.x * v.x + v.y * v.y + v.z * v.z;
}
- To implement the length function, take the square root of the result of the square length function. Take care not to call
sqrtf
with 0
. Implement the lenSq
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:float len(const vec3 &v) {
float lenSq = v.x * v.x + v.y * v.y + v.z * v.z;
if (lenSq < VEC3_EPSILON) {
return 0.0f;
}
return sqrtf(lenSq);
}
Important note:
You can find the distance between two vectors by taking the length of the difference between them. For example, float distance = len(vec1 - vec2).
Normalizing vectors
A vector with a length of 1 is called a normal vector (or unit vector). Generally, unit vectors are used to represent a direction without a magnitude. The dot product of two unit vectors will always fall in the -1 to 1 range.
Aside from the 0 vector, any vector can be normalized by scaling the vector by the inverse of its length:
- Implement the
normalize
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:void normalize(vec3 &v) {
float lenSq = v.x * v.x + v.y * v.y + v.z * v.z;
if (lenSq < VEC3_EPSILON) { return; }
float invLen = 1.0f / sqrtf(lenSq);
v.x *= invLen;
v.y *= invLen;
v.z *= invLen;
}
- Implement the
normalized
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:vec3 normalized(const vec3 &v) {
float lenSq = v.x * v.x + v.y * v.y + v.z * v.z;
if (lenSq < VEC3_EPSILON) { return v; }
float invLen = 1.0f / sqrtf(lenSq);
return vec3(
v.x * invLen,
v.y * invLen,
v.z * invLen
);
}
The normalize
function takes a reference to a vector and normalizes it in place. The normalized
function, on the other hand, takes a constant reference and does not modify the input vector. Instead, it returns a new vector.
The angle between vectors
If two vectors are of unit length, the angle between them is the cosine of their dot product:
If the two vectors are not normalized, the dot product needs to be divided by the product of the length of both vectors:
To find the actual angle, not just the cosine of it, we need to take the inverse of the cosine on both sides, which is the arccosine function:
Implement the angle
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:
float angle(const vec3 &l, const vec3 &r) {
float sqMagL = l.x * l.x + l.y * l.y + l.z * l.z;
float sqMagR = r.x * r.x + r.y * r.y + r.z * r.z;
if (sqMagL<VEC3_EPSILON || sqMagR<VEC3_EPSILON) {
return 0.0f;
}
float dot = l.x * r.x + l.y * r.y + l.z * r.z;
float len = sqrtf(sqMagL) * sqrtf(sqMagR);
return acosf(dot / len);
}
Important note:
The acosf
function returns angles in radians. To convert radians to degrees, multiply by 57.2958f
. To convert degrees to radians, multiply by 0.0174533f
.
Vector projection and rejection
Projecting vector A onto vector B yields a new vector that has the length of A in the direction of B. A good way to visualize vector projection is to imagine that vector A is casting a shadow onto vector B, as shown:
Figure 2.6: Vector A casting a shadow onto vector B
To calculate the projection of A onto B (projB A), vector A must be broken down into parallel and perpendicular components with respect to vector B. The parallel component is the length of A in the direction of B—this is the projection. The perpendicular component is the parallel component subtracted from A—this is the rejection:
Figure 2.7: Vector projection and rejection showing parallel and perpendicular vectors
If the vector that is being projected onto (in this example, vector B) is a normal vector, then finding the length of A in the direction of B is a simple dot product between A and B. However, if neither input vector is normalized, the dot product needs to be divided by the length of vector B (the vector being projected onto).
Now that the parallel component of A with respect to B is known, vector B can be scaled by this component. Again, if B wasn't of unit length, the result will need to be divided by the length of vector B.
Rejection is the opposite of projection. To find the rejection of A onto B, subtract the projection of A onto B from vector A:
- Implement the
project
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:vec3 project(const vec3 &a, const vec3 &b) {
float magBSq = len(b);
if (magBSq < VEC3_EPSILON) {
return vec3();
}
float scale = dot(a, b) / magBSq;
return b * scale;
}
- Implement the
reject
function in vec3.cpp
. Don't forget to declare this function in vec3.h
:vec3 reject(const vec3 &a, const vec3 &b) {
vec3 projection = project(a, b);
return a - projection;
}
Vector projection and rejection are generally used for gameplay programming. It is important that they are implemented in a robust vector library.
Vector reflection
Vector reflection can mean one of two things: a mirror-like reflection or a bounce-like reflection. The following figure shows the different types of reflections:
Figure 2.8: A comparison of the mirror and bounce reflections
The bounce reflection is more useful and intuitive than the mirror reflection. To make a bounce projection work, project vector A onto vector B. This will yield a vector that points in the opposite direction to the reflection. Negate this projection and subtract it twice from vector A. The following figure demonstrates this:
Figure 2.9: Visualizing a bounce reflection
Implement the reflect
function in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:
vec3 reflect(const vec3 &a, const vec3 &b) {
float magBSq = len(b);
if (magBSq < VEC3_EPSILON) {
return vec3();
}
float scale = dot(a, b) / magBSq;
vec3 proj2 = b * (scale * 2);
return a - proj2;
}
Vector reflection is useful for physics and AI. We won't need to use reflection for animation, but it's good to have the function implemented in case it is needed.
Cross product
When given two input vectors, the cross product returns a third vector that is perpendicular to both input vectors. The length of the cross product equals the area of the parallelogram formed by the two vectors.
The following figure demonstrates what the cross product looks like visually. The input vectors don't have to be 90 degrees apart, but it's easier to visualize them this way:
Figure 2.10: Visualizing the cross product
Finding the cross product involves some matrix math, which will be covered in more depth in the next chapter. For now, you need to create a 3x3 matrix, with the top row being the result vector. The second and third rows should be filled in with the input vectors. The value of each component of the result vector is the minor of that element in the matrix.
What exactly is the minor of an element in a 3x3 matrix? It's the determinant of a smaller, 2x2 sub-matrix. Assuming you want to find the value of the first component, ignore the first row and column, which yields a smaller 2x2 sub-matrix. The following figure shows the smaller sub-matrix for each component:
Figure 2.11: The submatrix for each component
To find the determinant of a 2x2 matrix, you need to cross multiply. Multiply the top-left and bottom-right elements, then subtract the product of the top-right and bottom-left elements. The following figure shows this for each element of the resulting vector:
Figure 2.12: The determinant of each component in the result vector
Implement the cross
product in vec3.cpp
. Don't forget to add the function declaration to vec3.h
:
vec3 cross(const vec3 &l, const vec3 &r) {
return vec3(
l.y * r.z - l.z * r.y,
l.z * r.x - l.x * r.z,
l.x * r.y - l.y * r.x
);
}
The dot product has a relationship to the cosine of the angle between two vectors and the cross product has a relationship to the sine of the angle between the two vectors. The length of the cross product between the two vectors is the product of both vectors, lengths, scaled by the sine of the angle between them:
In the next section, you will learn how to interpolate between vectors using three different techniques.