Math Operations on Points and Vectors

Now that we have explained the concept of (cartesian) coordinate system (and how points' and vectors' coordinates relate to coordinate systems), we can look at some of the most common operations which can be performed on points and vectors. This should cover the most common functions you will find in any 3D application and renderer.

Vector Class in C++

First lets define what our C++ Vector class will look like:

1
2
3
4
5
6
7
8
9
template<typename T> class Vec3 { public: Vec3() : x(T(0)), y(T(0)), z(T(0)) {} Vec3(T xx) : x(xx), y(xx), z(xx) {} Vec3(T xx, T yy, T zz) : x(xx), y(yy), z(zz) {} T x, y, z; };

Vector Length

As we mentioned in the previous paragraph, a vector can be seen as an arrow starting from one point and finishing to another. The vector itself indicates not only the direction of point B from A but also can be used to find out the distance that separates point B from point A. This is given by the length of a vector which can easily be computed with the following formula:

$$\scriptsize ||V|| = \sqrt{V.x * V.x + V.y * V.y + V.z * V.z}$$

In mathematics, the double bar (||V||) notation indicates the lentgh of a vector. The vector's length is sometimes also called norm or magnitude (figure 1).

1
T magnitude() const { return sqrt(x * x + y * y + z * z); }

Note that the axes of the three-dimensional cartesian coordinate systems are unit vectors.

Normalizing a Vector

A normalized vector (we will use normalize with a z here which is the standard in the industry), is a vector whose length is 1 (vector B in figure 1). Such a vector is also called a unit vector (it is a vector which has unit length). Normalizing a vector is very simple. We first compute the length of the vector and divide each one of the vectors coordinates with this length. The mathematical notation is:

$$\scriptsize \hat{V} = {V \over { || V || }}$$

Figure 1: the magnitude or length of vector A and B is denoted by the double bar notation. A normalized vector is a vector whose length is 1 (in this example vector B).

Note that the C++ implementation can be optimised. First we only normalize the vector if its length is greater than 0 (as dividing by 0 is forbidden). We then compute a temporary variable which is the invert of the vector length, and multiply each coordinate of the vector with this value rather than dividing them with the vector's length. As you may know, multiplications in a program are less costly than divisions. This optimisation can be important, as normalizing of vector is an extremely common operations in a renderer which can be applied to thousands, hundreds of thousands, millions of vectors (when not more). At this level, any possible optimisation will have an impact on the final render time. Note though that some compilers will manage that for you under the hood. But you can always make that optimisation explicit in your code.

1
void normalize() { T mag = magnitude(); if (mag) *this *= 1 / mag; }

In mathematics, you will also find the term norm to define a function that assigns a length or size (or distance) to a vector. The function we have just described is called the Euclidean norm.

Dot Product

Figure 2: the dot product of two vectors can be seen as the projection of A over B. if the two vectors A and B have unit length then the result of the dot product the is cosine of the angle subtended by the two vectors (θ).

The dot product or scalar product requires two vectors A and B and can be seen as the projection of one vector onto the other. The result of the dot product is a real number (a float or double in programming). A dot product between two vectors is denoted with the dot sign: \(\scriptsize A \cdot B\) (but can also be sometimes written as \(\scriptsize <a, b>\)). The dot product consists of multiplying each element of the A vector with its counterpart from vector B and taking the sum of each product. In the case of 3D vectors (length of the vector is three, they have three coefficients or elements which are x, y and z), it consists of the following operation:

$$\scriptsize A \cdot B = A.x * B.x + A.y * B.y + A.z * B.z$$

Note that this is quite similar to the way we compute the length (distance this time) of a vector. If we take the square root (\\scriptsize (\sqrt{A \cdot B}\)) of the dot product between two vectors which are equal (A=B), then we obtain the length of the vector. We can write:

$$\scriptsize ||V||^2=V \cdot V$$

It can be used to simplify the implementation the C++ code sometimes:

1
2
3
4
5
6
7
8
9
T dot(const Vec3<T> &v) const { return x * v.x + y * v.y + z * v.z; } void normalize() { T len2 = *this.dot(*this); if (len2) { T invLength = T(1) / sqrt(len2); *this *= invLength; } }

The dot product between two vectors is an extremely important and common operation in any 3D application because the result of this operation relates to the cosine of the angle between the two vectors. Figure 2 illustrates the geometric interpretation of the dot product. In this example vector A is projected in the direction of vector B. 

  • if B is a unit vector then the product \(\scriptsize A \cdot B\) gives \(\scriptsize ||A||cos(\theta)\), the magnitude of the projection of A in the direction of B, with a minus sign if the direction is opposite. This is called the scalar projection of A onto B.

  • when neither A nor B is a unit vector, we can write that \(\scriptsize A \cdot { B \over ||B|| } \) since B as a unit vector is \(\scriptsize B \over ||B||\).

  • when the two vectors are normalised then taking the arc corsine of the dot product gives you the angle \(\scriptsize \theta\) between the two vectors: \(\scriptsize \theta = acos({{A \cdot B} \over {||A||\:||B||}})\) or \(\scriptsize \theta=acos(\hat A \cdot \hat B)\).

The dot product is a very important operation in 3D. It can be used for many things. As a test of orthogonality. When two vectors are perpendicular to each other (A.B), the result of the dot product between these two vectors is 0. When the two vectors are pointing in opposite directions (A.C), the dot product returns -1. When they are pointing in the exact same direction (A.D), it returns 1. It is also used intensively to find out the angle between two vectors or compute the angle between a vector and the axis of a coordinate system (which is useful when the coordinates of a vector are converted to spherical coordinates. This explained in the chapter on trigonometric functions).

Cross Product

The cross product is also an operation on two vectors, but to the difference of the dot product which returns a number, the cross product returns a vector. The particularity of this operation is that the vector resulting from the cross product is perpendicular to the other two (this is shown in figure 3). The cross product operation is written using the following syntax:

$$\scriptsize C = A \times B$$

Figure 3: the cross product of two vectors A and B gives a vector C perpendicular to the plane defined by A and B. When A and B are orhotogonal to each other (and have unit length), A, B, C form a Cartesian coordinate system.

To compute the cross product we will need to implement the following formula:

$$\scriptsize \begin{array}{l} C_X = A_Y * B_Z - A_Z * B_Y \\ C_Y = A_Z * B_X - A_X * B_Z \\ C_Z = A_X * B_Y - A_Y * B_X \\ \end{array}$$

The result of the cross product is another vector which is orthogonal to the other two. A cross product between two vectors is denoted with the cross sign: \(\scriptsize A \times B\). The two vectors A and B define a plane and the resulting vector C is perpendicular to that plane. Vectors A and B don't have to be perpendicular to each other but when they are the resulting A B and C vectors form a cartesian coordinate system (assuming the vectors have unit length). This is particularly useful to create coordinate systems which we will explain in the chapter Creating a Local Coordinate System.

1
2
3
4
5
6
7
Vec3 cross(const Vec3<T> &v) const { return Vec3( y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x); }

If you need a mnemonic way of remembering this formula we like to the technique that consists of asking ourselves the question "why z?", y and z being the coordinates of vector A and B which are used to compute the x coordinate of the resulting vector C. More seriously, logic can easily be used to reconstruct this formula. Since you know that the result of the cross product is a vector perpendicular to the other two, you know that if A and B are the x- and y-axis of a cartesian coordinate system, the cross product of A and B should give you the z-axis that is (0,0,1). The only way you can get this result is if Cz = 1 which is only true when Cz = A.z * B.y - A.y * B.x. From there you can deduce the other coordinates which are used to compute Cx and Cy. Finally the easiest method might simple be to write the cross production operation in the following form:

$$\scriptsize \begin{pmatrix}a_x \\ a_y \\ a_z\end{pmatrix} \times \begin{pmatrix}b_x \\ b_y \\ b_z\end{pmatrix} = \begin{pmatrix}a_yb_z - a_zb_y \\ a_zb_x - a_xb_z \\  a_xb_y - a_yb_x\end{pmatrix}$$

Presenting the vector in a column vector form, shows that for the coordinate computed (for example x) we need to use the other two (for example y and z) from vector A and B.

It is very important to note that the order of the vectors involved in the cross product has an effect on the resulting vector C. If we take the previous example (taking the cross product between the x- and they y-axis of a cartesian coordinate system), note that A x B doesn't give you the same result as B x A:

AxB = (1,0,0)x(0,1,0) = (0,0,1) BxA=(0,1,0)x(1,0,0)=(0,0,-1)

Figure 4: using your left or right hand to determine the orientation of vector C (the normal for instance) when the index fingers points along A and the middle finger points along B.

We say that the cross product is anticommutative (swapping the position of any two arguments negates the result): If AxB=C then BxA=-C. Remember from the previous chapter that when two vectors are used to define the first two basis of a coordinate system, the third vector can point on either side of the plane. We also described a technique in which you use your hands to differentiate the two systems. When you compute a cross product between vectors you will always get the same unique solution. For instance if A = (1, 0, 0) and B = (0, 1, 0), C can only be (0, 0, 1). So you might ask why should I care about the handedness of my coordinate system then? Because if the result of the computation is always the same, the way you will draw the resulting vector however, depends on the handedness of your coordinate system. You can use the same mnemonic technique to find out in which direction the vector should point to depending on the convention you are using. In the case of a right-hand coordinate system, if you align the index finger along the A vector (for example the tangent at a point on the surface) and the middle finger along the B vector (the bitangent if you try to figure out the orientation of a normal), the thumb will point in the direction of the C vector (the normal). Note that if you use the same technique but with the left hand on the same vectors A and B, your the thumb will point in the opposite direction. Remember though, that this only a representation issue.

Figure 5: using you right hand, you can align your index finger along either A or B and the middle finger against the other vector (B or A) to find out if C (the normal for instance) point upwards or inwards in the right-hand coordinate system.

In mathematics, the result of a cross product is called a pseudovector. The order of the vector in the cross product operation is important when surface normals are computed from the tangent and bitangent at the point where the normal is computed. Depending on this order, the resulting normal can either be pointing towards the interior of the surface (inward-pointing normal) or away from it (outward-pointing normal). You can find more information on this topic in the chapter Creating an Orientation Matrix.

Vector/Point Addition and Subtraction

Other mathematical operations one points are usually straightforward. A multiplication of a vector by a scalar or another vector gives a point. We can add two vectors to each other, subtract them, divide them, etc. Note that some 3D APIs makes the distinction between points, normals and vectors. Technically they are subtle differences between each of them which can justify to create three separate C++ classes. For example: normals are not transformed like points and vectors (we will learn about that in this lesson), subtracting two points technically gives a vector, adding a vector to another vector or a point gives a point, etc. However, from practice, we found that writing these three C++ distinct classes to represent each type is not worth some of the complexity that comes with it. Similarly to OpenEXR which has become an industry standard, we chose to represent all types with a single templated class called Vec3. We therefore make no distinction between normal, vector and points (from a coding point of view). We will just need to manage the (rare) exceptions where variables representing different types (normal, vector, points) but declared under the generic type Vec3, should be processed differently. Here is some C++ code to represent the most common operations (see the Download section of this lesson for the full code):

1
2
3
4
5
6
7
Vec3 operator + (const Vec3<T> &v) const { return Vec3<T>(x + v.x, y + v.y, z + v.z); } Vec3 operator * (const T &val) const { return Vec3<T>(x * val, y * val, z * val); } Vec3 operator / (const T &val) const { T invVal = T(1) / val; return Vec3<T>(x * invVal, y * invVal, z * invVal); } Vec3 operator / (const Vec3<T> &v) const { return Vec3<T>(x / v.x, y / v.y, z / v.z); } Vec3 operator * (const Vec3<T> &v) const { return Vec3<T>(x * v.x, y * v.y, z * v.z); } Vec3 operator - (const Vec3<T> &v) const { return Vec3<T>(x - v.x, y - v.y, z - v.z); } Vec3 operator - () const { return Vec3<T>(-x, -y, -z); }

Chapter 3 of 13