When you have values for $x$, $y$, and $z$, you can describe a point in three dimensional space. Something like $(3, 5, 1)$ or $(0, -2, 6)$, for example. Now let’s say you’ve got two points. Subtracting one from the other tells you the difference between them in the sense that:
\[\begin{equation} \begin{aligned} 3 - 0 = 0 \\ 5 - (-2) = 7 \\ 1 - 6 = -5 \end{aligned} \end{equation}\]So that:
\[(3, 5, 1) - (0, -2, 6) = (0, 7, -5)\]More interestingly than that though, is that the difference between these points is how you might get from one point to the other. In 3D graphics specifically and linear algebra generally, this difference is known as a vector.
If you think of a vector as the difference between two points, you could say it has two properties: a direction and a length or magnitude. When considering a vector on its own, a property it lacks is position. A vector can tell you the direction and distance a point might change by, but only the point can tell you where something is in space.
Just like real numbers, vectors can be added to and subtracted from one another. Vectors can also be multiplied and divided by real numbers, which changes their magnitude. You can think of the vector as being “scaled” by the number, so these numbers are often called scalars.
Let’s see what some textbooks have to say about points and vectors.
“A vector refers to a quantity that possesses both magnitude and direction.” 1
“To mathematicians, a vector is a list of numbers. Programmers will recognize the synonymous term array. 2
“Geometrically speaking, a vector is a directed line segment that has magnitude and direction.” 3
“A scalar is a quantity such as distance, mass, or time that can be fully described using a single numerical value representing its size, or its magnitude. A vector is a quantity that carries enough information to represent a direction in space, in addition to a magnitude.” 4
“Points represent locations in space, which can be used either as measurements on the surface of an object to approximate the object’s shape (this approximation is called a model), or simply the position of a particular object. (…) Vectors, on the other hand, represent the difference or displacement between two points.” 5
Cool. Let’s write some code.
Structure
On a practical level, the difference between a vector and a point comes down to interpretation and application. In the code, I’ll use a single struct
to represent both. A Vec3
will contain three float
s and support these basic operations:
- Index access to the member variables
- Test for equality
- Addition and subtraction with another
Vec3
- Simple negation
- Multiplication and division by a scalar
I’ll also throw in some static
definitions for known vectors. A vector of length zero is creatively called the zero vector. A vector of length one is called a unit vector, and I’ll define the unit vectors for the $x$, $y$, and $z$ dimensions in advance.
Starting with the struct
declaration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct Vec3 {
static Vec3 const zero;
static Vec3 const unit_x;
static Vec3 const unit_y;
static Vec3 const unit_z;
// =============================================================================
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
[[nodiscard]] float & operator[](uint8_t i) { return ((&x)[i]); }
[[nodiscard]] float operator[](uint8_t i) const { return ((&x)[i]); }
// =============================================================================
[[nodiscard]] bool operator==(Vec3 const &other) const;
// =============================================================================
Vec3() = default;
~Vec3() = default;
Vec3(float x, float y, float z);
Vec3(Vec3 &&) = default;
Vec3(Vec3 const &) = default;
Vec3 & operator=(Vec3 &&) = default;
Vec3 & operator=(Vec3 const &) = default;
};
And defining what’s required to start writing unit tests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Vec3 const Vec3::zero { 0.0f, 0.0f, 0.0f };
Vec3 const Vec3::unit_x { 1.0f, 0.0f, 0.0f };
Vec3 const Vec3::unit_y { 0.0f, 1.0f, 0.0f };
Vec3 const Vec3::unit_z { 0.0f, 0.0f, 1.0f };
// =============================================================================
bool Vec3::operator==(const Vec3 &other) const {
// If the absolute value of the difference between this.x and other.x is
// less than the chosen float epsilon, then the x components of the two
// vectors are the same. If all three hit this criterion, the vectors are
// equal.
return (
std::abs(x - other.x) < epsilon &&
std::abs(y - other.y) < epsilon &&
std::abs(z - other.z) < epsilon
);
}
// =============================================================================
Vec3::Vec3(float x, float y, float z) :
x { x },
y { y },
z { z }
{ }
You might notice epsilon
being used for equality testing. If you’ve ever had a floating point operation go awry, you probably know that even comparing values can be tricky. Originally, I had epsilon
set to std::numeric_limits<float>::epsilon()
, which my toolchain has as just over 1.0e-7
. This proved too restrictive for even basic tests, let alone as the component values got further from zero. I opted for four places of precision in comparisons, which seems to work just fine.
1
static float constexpr epsilon = 1.0e-4;
The first unit tests establish that we can access and manipulate Vec3
member data as intended.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
TEST_CASE("Vector structure", "[vectors][vector basics]") {
Vec3 a; // Test the default constructor
// Accessing by member name
REQUIRE_THAT(a.x, WithinAbs(0.0f, epsilon));
REQUIRE_THAT(a.y, WithinAbs(0.0f, epsilon));
REQUIRE_THAT(a.z, WithinAbs(0.0f, epsilon));
// Accessing by index
REQUIRE_THAT(a[0], WithinAbs(0.0f, epsilon));
REQUIRE_THAT(a[1], WithinAbs(0.0f, epsilon));
REQUIRE_THAT(a[2], WithinAbs(0.0f, epsilon));
// Modifying by member name
a.x = 1.0f;
a.y = 2.0f;
a.z = 3.0f;
REQUIRE_THAT(a.x, WithinAbs(1.0f, epsilon));
REQUIRE_THAT(a.y, WithinAbs(2.0f, epsilon));
REQUIRE_THAT(a.z, WithinAbs(3.0f, epsilon));
// Modifying by index
a[0] = 3.0f;
a[1] = 2.0f;
a[2] = 1.0f;
REQUIRE_THAT(a[0], WithinAbs(3.0f, epsilon));
REQUIRE_THAT(a[1], WithinAbs(2.0f, epsilon));
REQUIRE_THAT(a[2], WithinAbs(1.0f, epsilon));
}
Before moving on to basic arithmetic, I’ll add a helper function leveraging Catch2’s generators so random, fresh vectors can be used each time the tests are run.
1
2
3
4
5
6
7
inline auto random_vec3(float const min = -10.0f, float const max = 10.0f) {
return btx::math::Vec3(
Catch::Generators::random(min, max).get(),
Catch::Generators::random(min, max).get(),
Catch::Generators::random(min, max).get()
);
}
Having random vectors means looping over certain tests can add some robustness and variety, so I’ll just configure those loops all at once:
1
static uint32_t constexpr TEST_REPEATS = 3u;
With all of that out of the way, we can write a test case for the equality operator:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEST_CASE("Vector equality", "[vectors][vector basics]") {
// Unit vectors are distinct
REQUIRE(Vec3::unit_x != Vec3::unit_y);
REQUIRE(Vec3::unit_x != Vec3::unit_z);
REQUIRE(Vec3::unit_y != Vec3::unit_z);
// Zero vector is itself
REQUIRE(Vec3::zero == Vec3::zero);
// And some random samples for good measure
for(uint32_t i = 0u; i < TEST_REPEATS; ++i) {
Vec3 const a = random_vec3();
REQUIRE(a == a);
}
}
Arithmetic
First, vectors need to know how to be added to and subtracted from one another.
1
2
3
4
5
6
7
[[nodiscard]] Vec3 operator+(Vec3 const &other) const;
[[nodiscard]] Vec3 operator-(Vec3 const &other) const;
[[nodiscard]] Vec3 operator-() const;
Vec3 & operator+=(Vec3 const &other);
Vec3 & operator-=(Vec3 const &other);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Vec3 Vec3::operator+(Vec3 const &other) const {
return { x + other.x, y + other.y, z + other.z };
}
Vec3 Vec3::operator-(Vec3 const &other) const {
return { x - other.x, y - other.y, z - other.z };
}
Vec3 Vec3::operator-() const {
return { -x, -y, -z };
}
Vec3 & Vec3::operator+=(Vec3 const &other) {
x += other.x;
y += other.y;
z += other.z;
return *this;
}
Vec3 & Vec3::operator-=(Vec3 const &other) {
x -= other.x;
y -= other.y;
z -= other.z;
return *this;
}
Vector addition should be commutative and associative, so the unit tests will check for that both via the arithmetic operators and the assignment operators.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
TEST_CASE("Vector addition", "[vectors][vector basics]") {
// Start with fixed values
Vec3 a(0.0f, 9.0f, -3.0f);
Vec3 b(2.0f, -1.0f, 6.0f);
Vec3 c(2.0f, 8.0f, 3.0f);
// Addition is commutative
REQUIRE(a + b == b + a);
// Addition is commutative via assignment
Vec3 d = a;
Vec3 e = b;
REQUIRE((d += b) == c);
REQUIRE((e += a) == c);
// Addition is associative
Vec3 const f = a + b + c;
REQUIRE((a + b) + c == f);
REQUIRE(a + (b + c) == f);
// Addition is associative via assignment
Vec3 g = a;
Vec3 h = b;
REQUIRE((g += b) + c == f);
REQUIRE(a + (h += c) == f);
// Now run the same tests with random values
for(uint32_t i = 0u; i < TEST_REPEATS; ++i) {
a = random_vec3();
b = random_vec3();
c = a + b;
REQUIRE(a + b == b + a);
Vec3 d = a;
Vec3 e = b;
REQUIRE((d += b) == c);
REQUIRE((e += a) == c);
Vec3 const f = a + b + c;
REQUIRE((a + b) + c == f);
REQUIRE(a + (b + c) == f);
Vec3 g = a;
Vec3 h = b;
REQUIRE((g += b) + c == f);
REQUIRE(a + (h += c) == f);
}
}
And testing the behavior of vector subtraction (and negation) rounds out these unit tests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
TEST_CASE("Vector subtraction", "[vectors][vector basics]") {
// Start with fixed values
Vec3 a(0.0f, 9.0f, -3.0f);
Vec3 b(2.0f, -1.0f, 6.0f);
Vec3 c(-2.0f, 10.0f, -9.0f);
Vec3 d = a;
REQUIRE(a - b == c); // Basic operation
REQUIRE(a + (-b) == c); // Negation operator
REQUIRE((d -= b) == c); // Assignment operator
REQUIRE(a - a == Vec3::zero); // A vector minus itself is zero
// Now run the same tests with random values
for(uint32_t i = 0u; i < TEST_REPEATS; ++i) {
a = random_vec3();
b = random_vec3();
c = a - b;
d = a;
REQUIRE(a - b == c);
REQUIRE(a + (-b) == c);
REQUIRE((d -= b) == c);
REQUIRE(a - a == Vec3::zero);
}
}
Vector Length
Given a three dimensional vector $\vec v$, the length of $\vec v$ is:
\[||\vec v|| = \sqrt((\vec v_x)^2+(\vec v_y)^2+(\vec v_z)^2)\]Kinda looks like the Pythagorean theorem, doesn’t it? You might’ve originally learned the Pythagorean theorem in two dimensions as a means by which to complete a triangle. If you like, you could think of the two given legs of the triangle as the $x$ and $y$ components of a 2D vector. The “hypotenuse” you’d be calculating via the Pythagorean theorem is then the length of this 2D vector.
You could also look at it from the perspective of vector addition. If the two legs of the triangle are themselves vectors, the sum of these vectors again produces something like a hypotenuse. Because vectors have no position, you’d have to draw them in a particular way to actually create a triangle, but the same idea holds.
In code, I’ve added two functions. The first returns the squared length of the vector. The second returns the true length, with a couple of tests to avoid the square root operation where possible.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[nodiscard]] inline float length_squared(Vec3 const &v) {
return (v.x * v.x) + (v.y * v.y) + (v.z * v.z);
}
[[nodiscard]] inline float length(Vec3 const &v) {
float length_sq = length_squared(v);
// If we're dealing with the zero vector, return zero
if(std::abs(length_sq) < epsilon) {
return 0.0f;
}
// If it's a unit vector, skip the square root calculation
if(std::abs(length_sq - 1.0f) < epsilon) {
return length_sq;
}
return std::sqrt(length_sq);
}
The test cases for these two functions are pretty straight forward.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
TEST_CASE("Vector length", "[vectors][vector basics]") {
// Test the unit and zero vectors
REQUIRE_THAT(length(Vec3::unit_x), WithinAbs(1.0f, epsilon));
REQUIRE_THAT(length(Vec3::unit_y), WithinAbs(1.0f, epsilon));
REQUIRE_THAT(length(Vec3::unit_z), WithinAbs(1.0f, epsilon));
REQUIRE_THAT(length(Vec3::zero), WithinAbs(0.0f, epsilon));
REQUIRE_THAT(length_squared(Vec3::unit_x), WithinAbs(1.0f, epsilon));
REQUIRE_THAT(length_squared(Vec3::unit_y), WithinAbs(1.0f, epsilon));
REQUIRE_THAT(length_squared(Vec3::unit_z), WithinAbs(1.0f, epsilon));
REQUIRE_THAT(length_squared(Vec3::zero), WithinAbs(0.0f, epsilon));
// Then three samples
Vec3 a(6, 10, 1);
float a_length = 11.70469991f;
float a_length_squared = 137.0f;
REQUIRE_THAT(length(a), WithinAbs(a_length, epsilon));
REQUIRE_THAT(length_squared(a), WithinAbs(a_length_squared, epsilon));
a = { 2, 9, 8 };
a_length = 12.20655562f;
a_length_squared = 149.0f;
REQUIRE_THAT(length(a), WithinAbs(a_length, epsilon));
REQUIRE_THAT(length_squared(a), WithinAbs(a_length_squared, epsilon));
a = { 10, -10, 5 };
a_length = 15.0f;
a_length_squared = 225.0f;
REQUIRE_THAT(length(a), WithinAbs(a_length, epsilon));
REQUIRE_THAT(length_squared(a), WithinAbs(a_length_squared, epsilon));
}
Vector Normalization
A vector is considered normalized when it has a length of one. This means the unit vectors are normalized by default, but anyone else may or may not be. There are some operations we’ll see later that can be made much more efficient if the vectors they’re working with are normalized first, so this is an important function to have. In order to normalize a vector, you divide each component by the vector’s length.
In the code, I do a couple of checks in case I can exit early, then multiply the components by the reciprocal of the length. The result is the same, with the off chance that some calls to this function will be quicker.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vec3 normalize(Vec3 const &v) {
Vec3 result = v;
auto const length = length_squared(v);
// If we're working with the zero vector or a vector that's already
// normalized, just return
if(length < epsilon || std::abs(length - 1.0f) < epsilon) {
return result;
}
float const length_recip = 1.0f / std::sqrt(length);
result.x *= length_recip;
result.y *= length_recip;
result.z *= length_recip;
return result;
}
Testing normalization inherently makes use of length()
from above, too.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
TEST_CASE("Vector normalization", "[vectors][vector basics]") {
// The unit vectors won't change
REQUIRE(normalize(Vec3::unit_x) == Vec3::unit_x);
REQUIRE(normalize(Vec3::unit_y) == Vec3::unit_y);
REQUIRE(normalize(Vec3::unit_z) == Vec3::unit_z);
// But these three will
Vec3 a(-4, -3, -5);
float a_length = 7.071067812f;
Vec3 b(-0.5656854249f, -0.4242640687f, -0.7071067812f);
REQUIRE_THAT(length(a), WithinAbs(a_length, epsilon));
REQUIRE(normalize(a) == b);
REQUIRE_THAT(length(normalize(a)), WithinAbs(1.0f, epsilon));
a = { -1, 1, 1 };
a_length = 1.732050808f;
b = { -0.5773502692f, 0.5773502692f, 0.5773502692f };
REQUIRE_THAT(length(a), WithinAbs(a_length, epsilon));
REQUIRE(normalize(a) == b);
REQUIRE_THAT(length(normalize(a)), WithinAbs(1.0f, epsilon));
a = { 0, -4, 5 };
a_length = 6.403124237;
b = { 0.0f, -0.6246950476f, 0.7808688094f };
REQUIRE_THAT(length(a), WithinAbs(a_length, epsilon));
REQUIRE(normalize(a) == b);
REQUIRE_THAT(length(normalize(a)), WithinAbs(1.0f, epsilon));
}
Scalars
Finally, scalar operations will round out this Vec3
. I’ll add assignment operators within the struct
:
1
2
Vec3 & operator*=(float scalar);
Vec3 & operator/=(float scalar);
And regular operators without:
1
2
3
[[nodiscard]] Vec3 operator*(float scalar, Vec3 const &v);
[[nodiscard]] Vec3 operator*(Vec3 const &v, float scalar);
[[nodiscard]] Vec3 operator/(Vec3 const &v, float scalar);
With the definitions looking like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Vec3 & Vec3::operator*=(float scalar) {
x *= scalar;
y *= scalar;
z *= scalar;
return *this;
}
Vec3 & Vec3::operator/=(float scalar) {
x /= scalar;
y /= scalar;
z /= scalar;
return *this;
}
// =============================================================================
Vec3 operator*(float scalar, Vec3 const &v) {
return { v.x * scalar, v.y * scalar, v.z * scalar };
}
Vec3 operator*(Vec3 const &v, float scalar) {
return { v.x * scalar, v.y * scalar, v.z * scalar };
}
Vec3 operator/(Vec3 const &v, float scalar) {
return { v.x / scalar, v.y / scalar, v.z / scalar };
}
Since scalars change vectors’ length, we’ll be testing with length()
as well as touching both the basic operators and assignment.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
TEST_CASE("Vector scaling", "[vectors][vector basics]") {
// Test the unit vectors
REQUIRE_THAT(length( 2.0f * Vec3::unit_x), WithinAbs(2.0f, epsilon));
REQUIRE_THAT(length(-2.0f * Vec3::unit_y), WithinAbs(2.0f, epsilon));
REQUIRE_THAT(length( 0.5f * Vec3::unit_z), WithinAbs(0.5f, epsilon));
REQUIRE_THAT(length(Vec3::unit_x * 2.0f), WithinAbs(2.0f, epsilon));
REQUIRE_THAT(length(Vec3::unit_y * -2.0f), WithinAbs(2.0f, epsilon));
REQUIRE_THAT(length(Vec3::unit_z * 0.5f), WithinAbs(0.5f, epsilon));
REQUIRE_THAT(length(Vec3::unit_x / 2.0f), WithinAbs(0.5f, epsilon));
REQUIRE_THAT(length(Vec3::unit_y / -2.0f), WithinAbs(0.5f, epsilon));
REQUIRE_THAT(length(Vec3::unit_z / 0.5f), WithinAbs(2.0f, epsilon));
// Then three samples
Vec3 a(6, 10, 1);
Vec3 b(0, -1, 4);
float a_length = 11.70469991f;
REQUIRE_THAT(length(2.0f * a), WithinAbs(2.0f * a_length, epsilon));
// Scalars via assignment operator
REQUIRE_THAT(length(a *= -2.0f), WithinAbs(2.0f * a_length, epsilon));
REQUIRE_THAT(length(a /= 2.0f), WithinAbs(a_length, epsilon));
// Scalar multiplication distributes over addition
REQUIRE(-1.5f * (a + b) == (-1.5f * a) + (-1.5f * b));
a = { 2, 9, 8 };
b = { -3, 1, 0 };
a_length = 12.20655562f;
REQUIRE_THAT(length(2.0f * a), WithinAbs(2.0f * a_length, epsilon));
// Scalars via assignment operator
REQUIRE_THAT(length(a *= -2.0f), WithinAbs(2.0f * a_length, epsilon));
REQUIRE_THAT(length(a /= 2.0f), WithinAbs(a_length, epsilon));
// Scalar multiplication distributes over addition
REQUIRE(-1.5f * (a + b) == (-1.5f * a) + (-1.5f * b));
a = { 10, -10, 5 };
b = { 2, -5, 3 };
a_length = 15.0f;
REQUIRE_THAT(length(2.0f * a), WithinAbs(2.0f * a_length, epsilon));
// Scalars via assignment operator
REQUIRE_THAT(length(a *= -2.0f), WithinAbs(2.0f * a_length, epsilon));
REQUIRE_THAT(length(a /= 2.0f), WithinAbs(a_length, epsilon));
// Scalar multiplication distributes over addition
REQUIRE(-1.5f * (a + b) == (-1.5f * a) + (-1.5f * b));
}
Conclusion
We’ve covered what a vector is and the first few things we’re going to do with them. Next time will be a couple more vector-specific operations: the dot and cross products.
Sources
Luna, Frank (2012) Introduction to 3D Game Programming with DirectX 11. Mercury Learning and Information. p.4 ↩
Dunn, Fletcher & Parberry, Ian (2011) 3D Math Primer for Graphics and Game Development. CRC Press. p.31 ↩
Dunn, Fletcher & Parberry, Ian (2011) 3D Math Primer for Graphics and Game Development. CRC Press. p.34 ↩
Lengyel, Eric (2016) Foundations of Game Engine Development: Mathematics. Terathon Software LLC. p.1. ↩
Van Verth, James M. & Bishop, Lars M. (2016) Essential Mathematics for Games and Interactive Applications. CRC Press. p.33 ↩