Lab 4: Vectors and Matrices#
Computer graphics relies heavily on mathematics of vectors and matrices. In this lab we will be revising the important concepts needed for computer graphics and using a library to perform calculations.
In this lab we will not be drawing any graphical objects, but we will be writing JavaScript code to perform calculations. So the first thing we are going to do is set up a simple HTML page and write a JavaScript function to print console output to the page.
Task
Create a folder called 04 Vectors and Matrices inside which create a file called index.html and enter the following into it.
<!doctype html>
<html lang="en">
<head>
<title>Lab 4 - Vectors and Matrices</title>
</head>
<body>
<div id="console-output"
style="font-family:monospace; white-space: pre; padding:10px;">
</div>
<script src="maths.js"></script>
<script src="vectors_and_matrices.js"></script>
</body>
</html>
Create another file called vectors_and_matrices.js and enter the following into it.
function setupConsoleOutput(elementId) {
const output = document.getElementById(elementId);
function write(args) {
const line = document.createElement("div");
line.textContent = [...args].join(" ");
output.appendChild(line);
}
console.log = (...args) => write(args);
}
setupConsoleOutput("console-output");
console.log('Lab 4 - Vectors and Matrices\n----------------------------');
Here we have the function setupConsoleOutput() in the vectors_and_matrices.js file which means that any call to console.log() will output to the HTML page.
Lab 4 - Vectors and Matrices
----------------------------
Vectors#
A vector in is an object with magnitude (length) and direction. A vector is denoted by a lower case letter in boldface, e.g., \(\vec{a}\) (or underlined when writing by hand), and represented mathematically by a tuple which is an ordered set of numbers. In geometry, each number in the vector represents the length along the co-ordinate axes. For example, consider the 3-element vector
Here \(\vec{a}\) has 3 elements so is a vector in 3D space where \(a_x\), \(a_y\) and \(a_z\) are the lengths of the vector in the \(x\), \(y\), and \(z\) directions.
Fig. 24 A 3D vector.#
Note
The reason the diagram above has the \(y\)-axis pointing upwards and the \(z\)-axis pointing along the horizontal is because this is the way OpenGL represents 3D space (see 5. Transformations for more details). The configuration of the axes does not matter for the calculations we will be performing in this lab, but I wanted to be consistent.
Since we will be using vectors (and matrices) a lot over the rest of the labs we will create a vector class to define vectors and perform operations on them.
Task
Create file called maths.js and enter the following class definition.
// 3-element vector class
class Vec3 {
constructor(x = 0, y = 0, z = 0) {
this.x = x;
this.y = y;
this.z = z;
}
// Print vector
print() {
return `[ ${this.x.toFixed(4)}, ${this.y.toFixed(4)}, ${this.z.toFixed(4)} ]`;
}
}
Here we have declared a class called Vec3 inside which we have defined the constructor function and a function to print the vector. Now let’s create the following vector objects in JavaScript and print them.
Task
Add the following code to the vectors_and_matrices.js file.
// Define vector objects
console.log('\nVectors\n-------');
const a = new Vec3(3, 0, 4)
const b = new Vec3(1, 2, 3);
console.log("a = ", a.print());
console.log("b = ", b.print());
Here we have created two vector objects a and b that contain the elements of \(\vec{a}\) and \(\vec{b}\) and printed these to our webpage which should now look like
Vectors
-------
a = [ 3.0000, 0.0000, 4.0000 ]
b = [ 1.0000, 2.0000, 3.0000 ]
Arithmetic operations on vectors#
Like numbers, we can define the arithmetic operations of addition, subtraction for vectors as well as multiplication and division by a scalar.
Vector addition and subtraction#
The addition and subtraction of two vectors \(\vec{a} = (a_x, a_y, a_z)\) and \(\vec{b} = (b_x, b_y, b_z)\) is defined by
For example, given the vectors \(\vec{a} = (3,0,4)\) and \(\vec{b} = (1, 2, 3)\)
What is happening in a geometrical sense when we add and subtract vectors? Take a look at Fig. 25, here the vector \(\vec{b}\) has been added to the vector \(\vec{a}\) where the tail of \(\vec{b}\) is placed at the head of \(\vec{a}\). The resulting vector \(\vec{a} + \vec{b}\) points from the tail of \(\vec{a}\) to the head of \(\vec{b}\).
Fig. 25 Vector addition.#
The subtraction of the vector \(\vec{b}\) does similar, but since \(\vec{a} - \vec{b} = \vec{a} + (-1)\vec{b}\) then the direction of \(\vec{b}\) is reversed so \(\vec{a} - \vec{b}\) is the same as placing the tail of \(-\vec{b}\) at the head of \(\vec{a}\).
Fig. 26 Vector subtraction.#
To calculate the addition and subtraction of vectors we are going to write functions to do this.
Task
Add the following functions to your Vec3 class.
// Arithmetic operations
add(v) {
return new Vec3(this.x + v.x, this.y + v.y, this.z + v.z);
}
subtract(v) {
return new Vec3(this.x - v.x, this.y - v.y, this.z - v.z);
}
Here we have defined two similar functions add() and subtract() that add and subtract two vectors (not surprisingly). Both functions return the this keyword so when we call these functions on a vector object it will change the values in the vector.
Task
Add the following to the Vectors_and_matrices.js file.
// Arithmetic operations on vectors
console.log('\nArithmetic operations on vectors\n--------------------------------');
console.log("a + b =", a.add(b).print());
console.log("a - b =", a.subtract(b).print());
Refresh your web page, and you should see the following has been added.
Arithmetic operations on vectors
--------------------------------
a + b = [ 4.0000, 2.0000, 7.0000 ]
a - b = [ 2.0000, -2.0000, 1.0000 ]
Multiplication by a scalar#
Multiplication of a vector \(\vec{a} = (a_x, a_y, a_z)\) by a scalar (a number) \(k\) are defined by
For example, multiplying the vector \(\vec{a} = (3, 0, 4)\) by the scalar 2 gives
If we wanted to divide by a scale \(k\) then we simply multiply by \(\dfrac{1}{k}\). For example, dividing the vector \(\vec{b} = (1, 2, 3)\) by 3 gives
Multiplying a vector by a positive scalar has the effect of scaling the length of the vector. Multiplying by a negative scalar reverses the direction of the vector.
Task
Add the following function definition to the vector class.
scale(s) {
return new Vec3(this.x * s, this.y * s, this.z * s);
}
Now add the following to the vectors_and_matrices.js file.
console.log("2a =", a.scale(2).print());
console.log("b/3 =", b.scale(1/3).print());
Refresh your web page, and you should see the following has been added.
2a = [ 6.0000, 0.0000, 8.0000 ]
b/3 = [ 0.3333, 0.6667, 1.0000 ]
Vector magnitude#
The length or magnitude of a vector \(\vec{a} = (a_x, a_y, a_z)\) is denoted by \(\|\vec{a}\|\) is the length from the tail of the vector to the head.
Fig. 27 Vector magnitude (length).#
The magnitude is calculated using an extension of Pythagoras’ theorem, for example for 3D vectors the magnitude is
For example, if \(\vec{a} = (3, 0, 4)\) and \(\vec{b} = (1, 2, 3)\) then their magnitudes are
Task
Add the following function definition to the vector class.
// Length and normalization
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
Now add enter the following code to the vectors_and_matrices.js file.
// Vector magnitude and normalization
console.log("\nVector magnitude and normalization\n----------------------------------");
console.log("length(a) =",a.length());
console.log("length(b) =",b.length());
Refresh your web page, and you should see the following has been added.
Vector magnitude and normalization
----------------------------------
length(a) = 5
length(b) = 3.7416573867739413
Unit vectors#
A unit vector is a vector that has a length of 1. We can find a unit vector that points in the same direction as a non-zero vector \(\vec{a}\), which is denoted by \(\hat{a}\) (pronounced a-hat), by dividing by its magnitude, i.e.,
This process is called normalising a vector. For example, to determine a unit vector pointing in the same direction as the vector \(\vec{a} = (3, 0, 4)\), we normalize it by dividing by its magnitude which is 5.
Checking that \(\hat{a}\) has a magnitude of 1
Normalising a vector is an operation that is used a lot in graphics programming, so it would be useful to have a function that does this.
Task
Add the following function definition to the vector class.
normalize() {
const len = this.length();
if (len === 0) return new Vec3(0, 0, 0);
const inv = 1 / len;
return new Vec3(this.x * inv, this.y * inv, this.z * inv);
}
Now add enter the following code to the vectors_and_matrices.js file.
const aHat = a.normalize();
const bHat = b.normalize();
console.log("aHat =", aHat.print());
console.log("bHat =", bHat.print());
console.log("length(aHat) =", aHat.length());
console.log("length(bHat) =", bHat.length());
Refresh your web page, and you should see the following has been added.
aHat = [ 0.6000, 0.0000, 0.8000 ]
bHat = [ 0.2673, 0.5345, 0.8018 ]
length(aHat) = 1
length(bHat) = 1
Both aHat and bHat have magnitudes of 1 which shows they are both unit vectors.
The dot product#
The dot product between two vectors \(\vec{a} = (a_x, a_y, a_z)\) and \(\vec{b} = (b_x, b_y, b_z)\) is denoted by \(\vec{a} \cdot \vec{b}\) and returns a scalar. The dot product is calculated using
The dot product is related to the angle \(\theta\) between the two vectors (Fig. 28) by
Fig. 28 The angle \(\theta\) between the vectors \(\vec{a}\) and \(\vec{b}\).#
A useful result for computer graphics is that if \(\theta=90^\circ\) then \(\cos(\theta) = 0\) and equation (5) becomes
In order words, if the dot product of two vectors is zero then the two vectors are perpendicular. For example, given the vectors \(\vec{a} = (3, 0, 4)\) and \(\vec{b} = (1, 2, 3)\) the dot product between these are
Task
Add the following function definition to the vector class.
// Dot and cross products
console.log("\nDot and cross products\n----------------------");
console.log("a . b =", a.dot(b));
Now add enter the following code to the vectors_and_matrices.js file.
// Dot and cross products
console.log("\nDot and cross products\n----------------------");
const aDotB = a.clone().dot(b);
console.log("a . b =", aDotB);
Refresh your web page, and you should see the following has been added.
Dot and cross products
----------------------
a . b = 15
The cross product#
The cross product between two 3-element vectors \(\vec{a} = (a_x, a_y, a_z)\) and \(\vec{b} = (b_x, b_y, b_z)\) is denoted by \(\vec{a} \times \vec{b}\) and returns a vector. The cross product is calculated using
The cross product between two vectors produces another vector that is perpendicular to both of the vectors (Fig. 29). This is another incredibly useful result as it allows us to calculate a normal vector to a polygon which are used in calculating how light is reflected off surfaces (see Lab 8: Lighting).
Fig. 29 The cross product between two vectors gives a vector that is perpendicular to both vectors.#
For example, given the vectors \(\vec{a} = (3,0,4)\) and \(\vec{b} = (1, 2, 3)\) the cross product \(\vec{a} \times \vec{b}\) is
We can show that \(\vec{a} \times \vec{b}\) is perpendicular to both \(\vec{a}\) and \(\vec{b}\) using the dot product
Task
Add the following function definition to the vector class.
cross(v) {
return new Vec3(
this.y * v.z - this.z * v.y,
this.z * v.x - this.x * v.z,
this.x * v.y - this.y * v.x
)
}
Now add enter the following code to the vectors_and_matrices.js file.
const aCrossB = a.cross(b);
console.log("a x b =", aCrossB.print());
console.log("a . (a x b) =", a.dot(aCrossB));
console.log("b . (a x b) =", b.dot(aCrossB));
Refresh your web page, and you should see the following has been added.
a x b = [ -8.0000, -5.0000, 6.0000 ]
a . (a x b) = 0
b . (a x b) = 0
Here we have also shown that the cross product of a and b is perpendicular to both vectors.
Matrices#
Another type of mathematic object that is fundamental to computer graphics is the matrix. A matrix is a rectangular array of numbers.
It is common to use uppercase characters for the name of a matrix and lowercase characters for the individual elements. The elements of a matrix are referenced by an index which is a pair of numbers, the first of which is the horizontal row number and the second is the vertical column number so \(a_{ij}\) is the element in row \(i\) and column \(j\) of the matrix \(A\).
We refer to the size of a matrix by the number of rows by the number of columns. Here the matrix \(A\) has \(m\) rows and \(n\) columns, so we call this matrix a \(m \times n\) matrix. Computer graphics mostly works with \(4 \times 4\) matrices (see Lab 5: Transformations for why this is) so we will create a matrix class to define \(4 \times 4\) matrices and perform operations on them.
Task
Add the following class declaration to the maths.js file.
// 4x4 Matrix class
class Mat4 {
constructor() {
this.m = new Float32Array(16);
}
// Print matrix
print() {
let string = "";
for (let i = 0; i < 4; i++) {
const row = [
this.m[i * 4 + 0].toFixed(4),
this.m[i * 4 + 1].toFixed(4),
this.m[i * 4 + 2].toFixed(4),
this.m[i * 4 + 3].toFixed(4),
];
string += " [ " + row.join(" ") + " ]\n";
}
return string;
}
// Set
set(...values) {
if (values.length !== 16) {
throw new Error("Mat4.set() requires 16 values");
}
for (let i = 0; i < 16; i++) {
this.m[i] = values[i];
}
return this;
}
}
Now add enter the following code to the vectors_and_matrices.js file.
// Matrices
console.log("\nMatrices\n--------");
const A = new Mat4().set(
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16
);
console.log("A =\n", A.print());
Here we have declared a class called Mat4 inside which we have defined the constructor() function that defines a \(4\times 4\) matrix of zeros, a print() function, a set() function that sets the elements of a matrix to values from a 16-element array and a clone() function. We have then created a matrix object and set the values equal to
And printed the matrix. Refresh your web page, and you should see the following has been added.
Matrices
--------
A =
[ 1.0000 2.0000 3.0000 4.0000 ]
[ 5.0000 6.0000 7.0000 8.0000 ]
[ 9.0000 10.0000 11.0000 12.0000 ]
[ 13.0000 14.0000 15.0000 16.0000 ]
Matrix transpose#
The transpose of a matrix \(A\) is denoted by \(A^\mathsf{T}\) and is defined
i.e., the rows and columns of \(A\) are swapped so row \(i\) of \(A\) is column \(i\) of \(A^\mathsf{T}\). For example, the matrix \(A\) we defined above
then \(A^\mathsf{T}\) is
Task
Add the following function definition to the matrix class.
// Arithmetic operations
transpose() {
let m = this.m;
return new Mat4().set(
m[0], m[4], m[8], m[12],
m[1], m[5], m[6], m[13],
m[2], m[6], m[10], m[14],
m[3], m[7], m[11], m[15]
);
}
Now add enter the following code to the vectors_and_matrices.js file.
console.log("\nA^T =\n", A.transpose().print());
Refresh your web page, and you should see the following has been added.
A^T =
[ 1.0000 5.0000 9.0000 13.0000 ]
[ 2.0000 6.0000 10.0000 14.0000 ]
[ 3.0000 7.0000 11.0000 15.0000 ]
[ 4.0000 8.0000 12.0000 16.0000 ]
Matrix multiplication#
Scalar multiplication of a matrix by a scalar is the same for matrices as it is for vectors. However, the multiplication of two matrices \(A\) and \(B\) is defined in a very specific way. If \(A\) and \(B\) are two matrices then the element in row \(i\) and column \(j\) of the matrix \(AB\) is calculated using
Where \(\vec{a}_i\) is the vector formed from row \(i\) of \(A\) and \(\vec{b}_j\) is the vector formed from column \(j\) of \(B\). In computer graphics we mainly work with \(4 \times 4\) matrices, so consider the following matrix multiplication
For the element in row 2 and column 3, \([AB]_{23}\), we have the dot product between row 2 of the left-hand matrix and column 3 of the right-hand matrix
so
Doing similar for the other elements gives
Task
Add the following function definition to the matrix class.
multiply(mat) {
const c = new Float32Array(16);
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
for (let i = 0; i < 4; i++) {
c[col * 4 + row] += this.m[i * 4 + row] * mat.m[col * 4 + i];
}
}
}
return new Mat4().set(...c);
}
Now add enter the following code to the vectors_and_matrices.js file.
const B = new Mat4().set(
17, 18, 19, 20,
21, 22, 23, 24,
25, 26, 27, 28,
29, 30, 31, 32
);
console.log("\nB =\n", B.print());
console.log("\nAB =\n", A.multiply(B).print());
Refresh your web page, and you should see the following has been added.
B =
[ 17.0000 18.0000 19.0000 20.0000 ]
[ 21.0000 22.0000 23.0000 24.0000 ]
[ 25.0000 26.0000 27.0000 28.0000 ]
[ 29.0000 30.0000 31.0000 32.0000 ]
AB =
[ 538.0000 612.0000 686.0000 760.0000 ]
[ 650.0000 740.0000 830.0000 920.0000 ]
[ 762.0000 868.0000 974.0000 1080.0000 ]
[ 874.0000 996.0000 1118.0000 1240.0000 ]
What… wait… hang on a minute, this matrix isn’t the same as the one from equation (8). Our .multiply() function hasn’t given us the result shown above. The reason for this is something called column-major order.
Column-major order#
Linear memory is a contiguous block of addresses that can be sequentially accessed. So a 1D array is stored in adjacent memory locations. Since matrices are 2D we have a choice whether to store the elements in the rows or columns in adjacent locations. These are known as column-major order and row-major order. Consider the \(4 \times 4\) matrix
Using column-major order this will be stored in the memory as
i.e., we move down and across the matrix. Alternatively, using row-major order the matrix will be stored as
i.e., we move across and down the matrix. WebGL uses column-major order because it is based upon OpenGL which was written for early GPUs that treated vertex data as column vectors. So a matrix containing vertices is stored column-by-column which means, when working with WebGL, we need to switch the rows and columns around when multiplying matrices. This is why our .multiply() function produced the wrong result.
To output the matrix multiplication \(AB\) as we would expect it to appear, we can swap A and B.
Task
Edit the last line you entered so the A and B are swapped.
console.log("\nAB =\n", B.multiply(A).print());
Refresh your browser and you should now see that we have the matrix seen in equation matrix-multiplication-example.
AB =
[ 250.0000 260.0000 270.0000 280.0000 ]
[ 618.0000 644.0000 670.0000 696.0000 ]
[ 986.0000 1028.0000 1070.0000 1112.0000 ]
[ 1354.0000 1412.0000 1470.0000 1528.0000 ]
Note
Microsoft’s graphics library directX and Unreal Engine uses row-major order whilst WebGL, OpenGL, Vulkan (successor to OpenGL), Metal (Apple’s graphics library) and Unity all use column-major order. This means when porting code between the graphics libraries developers have to change all of their matrix calculations.
Exercises#
Three points have the co-ordinates \(A = (5, 1, 3)\), \(B = (10, 7, 4)\) and \(C = (0, 5, -3)\). Use pen and paper to calculate the following:
(a) The vector \(\vec{p}\) that points from \(A\) to \(B\);
(b) The vector \(\vec{q}\) that points from \(B\) to \(C\);
(c) The vector \(\vec{r}\) that points from \(C\) to \(A\);
(d) The length of the vector \(\vec{p}\);
(e) A unit vector that points in the direction of the vector \(\vec{q}\);
(f) The dot product \(\vec{p} \cdot \vec{q}\);
(g) The cross product \(\vec{q} \times \vec{r}\).Repeat exercise 1 using your functions from the maths.js file.
The three matrices \(A\), \(B\) and \(C\) are defined by
Use pen and paper to calculate the following:
(a) \(AB\);
(b) \(ABC\);
(c) \(B^\mathsf{T}A^\mathsf{T}\).
A transformation can be applied to a vector by matrix multiplication. If \(T\) is a transformation matrix and \(\vec{v}\) is a vector then the transformed vector is \(T \vec{v}\). Given the following transformation matrices and vector
use pen and paper to calculate the following transformations:
(a) \(S \, \vec{v}\);
(b) \(T \, \vec{v}\);
(c) \(T\,S\,\vec{v}\).
For each one, describe what effect the transformation has on \(\vec{v}\).
Solutions
(a) \(\vec{p} = (5, 6, 1)\)
(b) \(\vec{q} = (-10, -2, -7)\)
(c) \(\vec{r} = (5, -4, 6)\)
(d) \(\| \vec{p} \| = 7.8740\)
(e) \(\hat{q} = (-0.8085, -0.1617, -0.5659)\)
(f) \(\vec{p} \cdot \vec{q} = -69\)
(g) \(\vec{q} \times \vec{r} = (-40, 25, 50)\)
// Exercise 2
console.log("\nExercise 2\n----------");
const A1 = new Float32Array([5, 1, 3]);
const B1 = new Float32Array([10, 7, 4]);
const C1 = new Float32Array([0, 5, -3]);
p = subtractVectors(B1, A1);
printVector(p, "(a) p");
q = subtractVectors(C1, B1);
printVector(q, "(b) q");
r = subtractVectors(A1, C1);
printVector(r, "(c) r");
console.log("(d) length(p) = " + length(p));
printVector(normalize(q), "(e) qHat")
console.log("(f) p . q = " + dot(p, q));
printVector(cross(q, r), "(g) q x r")
Exercise 2
----------
(a) p = [ 5.0000, 6.0000, 1.0000 ]
(b) q = [ -10.0000, -2.0000, -7.0000 ]
(c) r = [ 5.0000, -4.0000, 6.0000 ]
(d) length(p) = 7.874007874011811
(e) qHat = [ -0.8085, -0.1617, -0.5659 ]
(f) p . q = -69
(g) q x r = [ -40.0000, 25.0000, 50.0000 ]
(a) \(AB = \begin{pmatrix} 21 & 1 \\ -35 & -1 \end{pmatrix}\)
(b) \(ABC = \begin{pmatrix} 60 & 38 \\ -102 & -66 \end{pmatrix}\)
(c) \(B^\textsf{T} A^\textsf{T} = \begin{pmatrix} 21 & -35 \\ 1 & -1 \end{pmatrix}\)(a) \(S \vec{v} = \begin{pmatrix} 10 \\ 16 \\ 20 \\ 1 \end{pmatrix}\)
The first three elements of \(\vec{v}\) have been scaled up by a factor of 2, i.e., \(\begin{pmatrix} 2 \times 10 \\ 2 \times 16 \\ 2 \times 20 \\ 1 \end{pmatrix}\).
(b) \(T \vec{v} = \begin{pmatrix} 8 \\ 10 \\ 9 \\ 1 \end{pmatrix}\)
The first three elements of \(\vec{v}\) have been increased by 3, 2 and \(-\)1 respectively, i.e., \(\begin{pmatrix} 5 + 3 \\ 8 + 2 \\ 10 - 1 \\ 1 \end{pmatrix}\).
(c) \(T \, S \, \vec{v} = \begin{pmatrix} 13 \\ 18 \\ 19 \\ 1 \end{pmatrix}\) The first three elements of \(\vec{v}\) have been scaled up by a factor or 2 and then increased by 3, 2 and \(-\)1 respectively, i.e., \(\begin{pmatrix} 2 \times 5 + 3 \\ 2 \times 8 + 2 \\ 2 \times 10 - 1 \\ 1\end{pmatrix}\).