Vectors and the vec3 Class
Introduction: Why Vectors Matter in Ray Tracing
In the previous lesson, you successfully generated your first image: a solid red square. To create that image, you used three separate integer variables for each pixel's color components: ired for red, igreen for green, and iblue for blue. This approach worked perfectly for a simple solid color, but let's think about what happens as our ray tracer becomes more sophisticated.
Imagine you're calculating the color of a pixel based on where a ray hits a sphere. You need to track the ray's starting position (three coordinates: x, y, z), the ray's direction (another three coordinates), the point where it hits the sphere (three more coordinates), and the surface normal at that point (yet another three coordinates). If you tried to manage all of these with separate variables, you'd quickly end up with code that looks like this:
This becomes unmanageable very quickly. You'd need to pass nine separate parameters to functions, perform operations on three variables at a time, and keep track of which x goes with which y and z. The code would be verbose, error-prone, and difficult to read.
This is where vectors come in. A vector is a mathematical object that naturally groups related values together. Instead of managing three separate variables, you work with a single entity that represents a point in 3D space, a direction, or even a color. In this lesson, you'll learn about the vec3 class, which will become one of your most important tools throughout this course.
The vec3 class provides a clean, organized way to work with three-component values. It handles all the common operations you need: adding vectors together, scaling them, measuring angles between them, and more. By the end of this lesson, you'll understand how vectors work, why they're essential for ray tracing, and how to use them in your code. You'll also discover something elegant: the same vec3 class that represents 3D positions and directions can also represent RGB colors, unifying your code in a beautiful way.
Understanding Vectors: More Than Just Three Numbers
Before we dive into code, let's build a solid understanding of what vectors actually are and why they're so useful in 3D graphics. A vector is fundamentally a mathematical object that has both magnitude (size) and direction. Think of it as an arrow in space: the arrow points in a specific direction, and it has a specific length.
In 3D graphics, we typically work with vectors in three-dimensional space, which means each vector has three components. We usually call these components x, y, and z. For example, a vector might be written as (3, 4, 5), where 3 is the x-component, 4 is the y-component, and 5 is the z-component. These three numbers completely describe the vector.
Vectors can represent different things depending on the context. One common use is to represent a position or point in 3D space. If you think of a 3D coordinate system with an origin at (0, 0, 0), then the vector (3, 4, 5) represents a point that's 3 units along the x-axis, 4 units along the y-axis, and 5 units along the z-axis from the origin.
Vectors can also represent directions. Imagine you're standing at the origin and you want to describe which way to walk. The vector (1, 0, 0) means "walk in the positive x direction." The vector (0, 1, 0) means "walk in the positive y direction." The vector (1, 1, 0) means "walk diagonally, equally in the x and y directions." The magnitude of the vector tells you how far to walk, and the direction tells you which way.
Here's a real-world analogy that might help. Imagine you're giving someone directions to walk from one location to another. You might say, "Walk 3 blocks east, 4 blocks north, and go up 5 floors." That instruction is essentially a vector: (3, 4, 5) where east is x, north is y, and up is z. The vector captures both the direction (northeast and upward) and the distance (the total length of the path).
In physics, vectors represent quantities like velocity and force. If a ball is moving with velocity (2, 3, 1), that means it's moving 2 units per second in the x direction, 3 units per second in the y direction, and 1 unit per second in the z direction. The vector completely describes the ball's motion.
You might wonder how vectors differ from simple arrays. After all, we could store three numbers in an array: double position[3] = {3, 4, 5};. The key difference is that vectors come with meaningful operations. When you add two vectors, you're performing a geometric operation that has real meaning: you're combining two displacements or directions. When you multiply a vector by a number (called a scalar), you're scaling its magnitude. These operations are built into the vector abstraction, making your code more expressive and less error-prone.
In ray tracing specifically, vectors are everywhere. The camera's position is a vector. The direction a ray travels is a vector. The point where a ray hits an object is a vector. The surface normal (the direction perpendicular to the surface) is a vector. Even colors, as you'll soon see, can be treated as vectors. Understanding vectors is absolutely fundamental to understanding ray tracing.
The vec3 Class: Structure and Basic Operations
Now let's look at how we implement vectors in C++ with the vec3 class. This class will encapsulate all the functionality we need for working with three-component vectors. Let's start by examining the basic structure of the class:
At the heart of the vec3 class is a simple array: double e[3]. This array stores the three components of our vector. We use double (double-precision floating-point numbers) rather than int because vectors in 3D graphics often need to represent fractional values. A direction vector might be (0.707, 0.707, 0), which represents a 45-degree angle. A position might be (1.5, 2.3, 4.7). Using double gives us the precision we need for accurate calculations.
The class provides two constructors. The first, vec3(), is a default constructor that initializes all three components to zero. The syntax e{0,0,0} is called a member initializer list, and it's an efficient way to initialize the array. The second constructor, vec3(double e0, double e1, double e2), lets you create a vector with specific values. For example, vec3(3, 4, 5) creates a vector with components 3, 4, and 5.
The accessor methods x(), y(), and z() provide a convenient way to retrieve individual components. Instead of writing v.e[0], you can write v.x(), which is more readable and makes the intent clearer. The const keyword after these methods indicates they don't modify the vector, which is important for the compiler to enforce correctness.
The class also provides array-style access through the operator[] overload. This lets you write v[0] to get the first component, v[1] for the second, and v[2] for the third. We provide two versions: one that returns a const reference (for reading) and one that returns a non-const reference (for writing). This flexibility is useful when you need to access components by index in a loop.
Now let's look at vector addition, one of the most fundamental operations. When you add two vectors, you add their corresponding components. If you have vectors u = (1, 2, 3) and v = (4, 5, 6), then u + v = (1+4, 2+5, 3+6) = (5, 7, 9). Here's how we implement this:
This is a free function (not a member of the class) that takes two vec3 objects by const reference and returns a new vec3 containing the sum. The inline keyword suggests to the compiler that it should try to insert the function's code directly at the call site rather than making a function call, which can improve performance for small, frequently-called functions.
Geometrically, vector addition represents combining two displacements. If you walk 3 blocks east and 4 blocks north (vector u), then walk 2 more blocks east and 1 block north (vector v), you've walked a total of 5 blocks east and 5 blocks north (vector u + v). In ray tracing, you might add a ray's origin position to its direction scaled by some distance to find where the ray is after traveling that distance.
Scalar multiplication is another essential operation. When you multiply a vector by a scalar (a single number), you scale the vector's magnitude without changing its direction. If you have vector v = (1, 2, 3) and you multiply it by 2, you get 2v = (2, 4, 6). The vector points in the same direction but is twice as long. Here's the implementation:
We provide two versions of this operator: one for scalar * vector and one for vector * scalar. This way, you can write either 2 * v or v * 2, and both will work. The second version simply calls the first, ensuring consistent behavior.
Let's see these operations in action with a concrete example:
In the first line, we create vector a with components (1, 2, 3). In the second line, we create vector b with components (4, 5, 6). When we add them, we get (5, 7, 9). When we scale a by 2, we get (2, 4, 6). In the last line, we combine operations: we scale b by 3 to get (12, 15, 18), then add a to get (13, 17, 21).
These basic operations might seem simple, but they're the building blocks for everything we'll do in ray tracing. Every time we calculate where a ray goes, how light bounces off a surface, or what color a pixel should be, we'll be using vector addition and scalar multiplication.
The Dot Product: Measuring Vector Alignment
Beyond addition and scaling, there's another vector operation that's absolutely crucial for ray tracing: the dot product. The dot product takes two vectors and produces a single number (a scalar) that tells you something important about the relationship between those vectors.
The mathematical formula for the dot product is straightforward. If you have vectors u = (u₀, u₁, u₂) and v = (v₀, v₁, v₂), their dot product is u · v = u₀v₀ + u₁v₁ + u₂v₂. You multiply corresponding components and add up the results. Here's the implementation:
This function takes two vectors by const reference and returns a double. It's a simple calculation, but what does it mean geometrically? The dot product measures how much two vectors point in the same direction. Let's explore this with some examples.
Consider two vectors that point in exactly the same direction: u = (1, 0, 0) and v = (2, 0, 0). Both point along the positive x-axis; v is just twice as long. Their dot product is 1*2 + 0*0 + 0*0 = 2. The result is positive, which tells us the vectors point in generally the same direction.
Now consider two vectors that are perpendicular: u = (1, 0, 0) and v = (0, 1, 0). One points along the x-axis, the other along the y-axis. Their dot product is 1*0 + 0*1 + 0*0 = 0. When the dot product is zero, the vectors are perpendicular (orthogonal). This is an incredibly useful property.
Finally, consider two vectors that point in opposite directions: u = (1, 0, 0) and v = (-1, 0, 0). Their dot product is 1*(-1) + 0*0 + 0*0 = -1. A negative dot product tells us the vectors point in generally opposite directions.
More formally, the dot product is related to the angle between vectors by this formula: u · v = |u| |v| cos(θ), where |u| and |v| are the lengths (magnitudes) of the vectors and θ is the angle between them. When the vectors point in the same direction, θ = 0 and cos(0) = 1, giving a maximum positive dot product. When they're perpendicular, θ = 90° and cos(90°) = 0, giving a dot product of zero. When they point in opposite directions, θ = 180° and cos(180°) = -1, giving a negative dot product.

Why is this so important for ray tracing? Consider lighting calculations. When light hits a surface, the brightness depends on the angle between the light direction and the surface normal (the direction perpendicular to the surface). If the light hits straight on (light direction and normal are aligned), the surface is brightest. If the light hits at a glancing angle (light direction and normal are nearly perpendicular), the surface is dimmer. The dot product between the light direction and the surface normal gives us exactly this information.
Here's a concrete example of using the dot product:
In this example, the surface normal points straight up (0, 1, 0) and the light is coming from directly above, so its direction vector points straight down (0, -1, 0). The dot product is -1, which tells us they're pointing in opposite directions. In lighting calculations, we'd typically take the absolute value or clamp negative values to zero, but the dot product gives us the raw information we need.
The dot product also appears in many other ray tracing calculations. When determining if a ray hits a sphere, you use the dot product. When calculating reflections, you use the dot product. When checking if a point is in front of or behind a plane, you use the dot product. It's one of the most frequently used operations in the entire ray tracer.
The Cross Product: Finding Perpendicular Vectors
Another essential vector operation in 3D graphics is the cross product. While the dot product measures how much two vectors point in the same direction, the cross product produces a new vector that is perpendicular (at a right angle) to both input vectors. This is especially useful in ray tracing for calculating surface normals, which are needed for lighting and reflection calculations.
Here is an example, where the blue vector is the result of the cross product between A (red) and B (green)

Mathematically, if you have vectors u = (u₀, u₁, u₂) and v = (v₀, v₁, v₂), their cross product is:
Here's how we implement the cross product in code:
The resulting vector is perpendicular to both u and v, and its length is proportional to the area of the parallelogram formed by the two vectors. In ray tracing, the cross product is often used to compute the normal vector to a surface, which is critical for determining how light interacts with that surface.
For example, if you have two vectors lying on a surface, their cross product gives you the direction that is perpendicular to that surface—exactly what you need for lighting calculations.
Normalizing Vectors: The unit_vector Function
In many ray tracing calculations, you need a vector that points in a certain direction but has a length (magnitude) of exactly 1. Such a vector is called a unit vector. Normalizing a vector means scaling it so that its length is 1, while keeping its direction the same.
The length (or magnitude) of a vector v = (x, y, z) is calculated using the Pythagorean theorem:
To normalize a vector, you divide each component by its length:
Here's how we implement this in code:
The length function computes the magnitude of the vector, and unit_vector returns a new vector pointing in the same direction but with a length of 1. Unit vectors are crucial in ray tracing for representing directions, such as the direction a ray travels or the direction of a surface normal. Using unit vectors ensures that calculations like lighting and reflection behave correctly and consistently.
Dual Purpose: Vectors for Both Geometry and Color
Now we come to one of the most elegant aspects of the vec3 class: it can represent both geometric quantities (positions, directions) and colors (RGB values). This might seem surprising at first, but it makes perfect sense when you think about it mathematically.
Remember from the previous lesson that colors are represented as RGB triplets: three numbers for red, green, and blue. A vector is also three numbers: x, y, and z. Mathematically, they're identical structures. The operations we perform on them are also similar. When you add two colors, you add their components. When you scale a color (to make it brighter or dimmer), you multiply each component by a scalar. These are exactly the same operations we perform on geometric vectors.
To make this dual purpose explicit and to make our code more readable, we use type aliases. At the end of the vec3 header file, you'll see these lines:
The using keyword creates an alias: point3 is just another name for vec3, and color is also just another name for vec3. They're all the same type under the hood, but using different names in different contexts makes the code's intent much clearer.
When you write point3 camera_position(0, 0, 0);, it's immediately obvious that you're talking about a position in 3D space. When you write color pixel_color(1.0, 0.0, 0.0);, it's clear you're talking about a red color. Both are vec3 objects, but the names document what they represent.
As we progress through the course, you'll see how powerful this unified approach is. When we calculate lighting, we'll multiply colors by scalars to adjust brightness. When we blend colors, we'll add color vectors together. When we calculate how much light reflects off a surface, we'll use the same vector operations we use for geometric calculations. The vec3 class, with its color and point3 aliases, provides a consistent, elegant foundation for all of these operations.
Summary: Your Vector Toolkit for Ray Tracing
In this lesson, you learned that vectors are mathematical objects with both magnitude and direction, represented by three components: x, y, and z. The vec3 class in C++ provides a clean way to work with these three-component values, supporting essential operations like addition, scalar multiplication, the dot product (measuring alignment), and the cross product (finding perpendicular vectors). You also saw how to normalize vectors to unit length, which is important for consistent calculations.
Importantly, the same vec3 class can represent both geometric quantities (like positions and directions) and colors (RGB values), thanks to type aliases (point3 and color). This unifies your code and makes it more readable.
You don’t need to memorize every detail now—what matters is understanding how vectors work and why they’re essential for ray tracing. In the next practice exercises, you’ll use the vec3 class hands-on to solidify these concepts. Let's continue building your ray tracer!
