Home What is a vector, anyway?
Post
Cancel

What is a vector, anyway?

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 floats 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

  1. Luna, Frank (2012) Introduction to 3D Game Programming with DirectX 11. Mercury Learning and Information. p.4 

  2. Dunn, Fletcher & Parberry, Ian (2011) 3D Math Primer for Graphics and Game Development. CRC Press. p.31 

  3. Dunn, Fletcher & Parberry, Ian (2011) 3D Math Primer for Graphics and Game Development. CRC Press. p.34 

  4. Lengyel, Eric (2016) Foundations of Game Engine Development: Mathematics. Terathon Software LLC. p.1. 

  5. Van Verth, James M. & Bishop, Lars M. (2016) Essential Mathematics for Games and Interactive Applications. CRC Press. p.33 

This post is licensed under CC BY 4.0 by the author.
Recent Tags