Three Dimensional Vectors
3D graphics requires points in three dimensional space expressed as x
y
z
There are a number of different possible orientations in a Cartesian coordinate system, however converting between different systems is reasonably straight forward.
A simple Vector3
class is all that is required to get started. Example code will be shown in typescript.
export class Vector3 {
// Initialise values to 0 if not supplied
constructor(public x: number = 0, public y: number = 0, public z: number = 0) {}
// Provide a simple to array method to export vector fields
toArray() {
return [this.x, this.y, this.z];
}
// Provide a simple from array method to import vector fields
fromArray(array: number[]) {
if (array.length !== 3) {
throw new Error('x, y, z values required');
}
this.x = array[0];
this.y = array[1];
this.z = array[2];
return this;
}
// extend the class here with functions below
}
You would create and use an instance of this class as follows:
const vector1 = new Vector3();
const vector2 = new Vector3(1, 2, 3);
console.log(vector1.toArray());
// [0, 0, 0]
console.log(vector2.toArray());
// [1, 2, 3]
In order to allow multiple operations chained together it's advantageous to always
return this;
at the end of any methods that effect the current vector instance (any methods that would otherwise return void). This technique allows for method chaining as you will see in the examples I have.
By itself this class is simply a container for the labeled data x
y
z
. It would be very useful to extend this class so that different Vector3
instances could interact with simple methods such as these:
//... after constructor
add(vector: Vector3) {
this.x += vector.x;
this.y += vector.y;
this.z += vector.z;
return this;
}
sub(vector: Vector3) {
this.x -= vector.x;
this.y -= vector.y;
this.z -= vector.z;
return this;
}
// ... before end curly brace
Multiplication can also be done with vectors, for 3D graphics it is usually enough to provide a scalar value with which to apply to all the axis:
multiplyScalar(value: number) {
this.x *= value;
this.y *= value;
this.z *= value;
return this;
}
The magnitude of a vector is just the same as the total length of the vector and can be calculated as the square root of all the individual vector elements squared:
magnitude(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
}
A normalised vector (normal) or otherwise knows as a unit vector is a vector that has a magnitude of 1
. It can be very useful to take a vector and normalise it, so that you can then apply the multiplyScalar
method to it, thereby extending it to a specific length. For example applying multiplyScalar(5)
to a unit vector will ensure that the vector is now 5 units long:
normalize() {
const currentMagnitude = this.magnitude();
this.x /= currentMagnitude;
this.y /= currentMagnitude;
this.z /= currentMagnitude;
return this;
}
Calculating a dot product for the vector yeilds a single number, and is useful in a number of other vector calculation. It can be caluclated as follows:
dot(vector: Vector3): number {
return this.x * vector.x + this.y * vector.y + this.z * vector.z;
}
The angle between two vectors can also be calculated and uses some of the previous methods we've added to get the result:
angleBetween(vector: Vector3): number {
// Note: this will return the angle in radians
return Math.acos(this.dot(vector) / (this.magnitude() * vector.magnitude()));
}
Here is an example of using the angleBetween
method:
const vector1 = new Vector3(3, 1, 0);
const vector2 = new Vector3(4, 0, 0);
console.log(vector1.angleBetween(vector2));
// 0.3217505543966423
// Convert to degrees by multiplying by 180 and dividing by PI
// 0.3217505543966423 * 180 / Math.PI;
// 18.434948822922017
A clone method is also useful in case you do not wish to change the value of a vector when performing calculations with it:
clone(): Vector3 {
return new Vector3(this.x, this.y, this.z);
}
As an example of this:
const vector1 = new Vector3(1, 2, 3);
const vector2 = new Vector3(4, 5, 5);
// The add method would normally change the vector it's called on
// vector1.add(vector2);
// will alter the values of vector1 to be [5, 7, 9]
// To create a new vector based on vector1, leaving vector1
// unmodified in the process call the clone method
const vector3 = vector1.clone().add(vector2);
console.log(vector1);
// Vector3 { x: 1, y: 2, z: 3 }
console.log(vector2);
// Vector3 { x: 4, y: 5, z: 5 }
console.log(vector3);
// Vector3 { x: 5, y: 7, z: 8 }
The cross product of a vector will modify a vector to be perpendicular to the plane of the original vector and the vector passed in. This is very useful for calculating face normals for groups of vectors or points:
cross(vector: Vector3) {
const x = this.y * vector.z - this.z * vector.y;
const y = this.x * vector.z - this.z * vector.x;
const z = this.x * vector.y - this.y * vector.x;
// The Y value needs to be inverted but I don't
// like the value -0 so we will check for that
// posibility with the turnery operator
this.x = x;
this.y = y === 0 ? 0 : -y;
this.z = z;
return this;
}
Let's look at an example of getting the plane normal from three points that we might use to create a polygon face:
// This is an array of points creating a
// simple triangle along the X-Y plane
const polyFace = [
new Vector3(1, 1, 0), // point 0
new Vector3(5, 1, 0), // point 1
new Vector3(1, 3, 0), // point 2
];
// The vector from point 0 to point 1 - Vector3 { x: 4, y: 0, z: 0 }
const vector1 = polyFace[1].clone().sub(polyFace[0]);
// The vector from point 0 to point 2 - Vector3 { x: 0, y: 2, z: 0 }
const vector2 = polyFace[2].clone().sub(polyFace[0]);
// Use the cross product to calculate the perpendicular vector
// Then normalise
const planeNormal = vector1.clone().cross(vector2).normalize();
console.log(planeNormal);
// Vector3 { x: 0, y: 0, z: 1 }
// The resulting vector points straight up on the Z axis
That is the basic toolkit for working with 3D vectors. Further improvments could be made to this class by adding in a fromArray()
method. I'm sure that other methods will also to mind when you start working with the vector class.