Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 1

Object-Oriented Geometric Primitives

Recommended resources

Hello! Welcome to the first lesson in your journey to building a C++ ray tracer.

This initial module, "C++ Foundations for Ray Tracing," is designed to equip you with the essential C++ skills needed for this project. Given your background in computer science, you're likely familiar with object-oriented concepts, so we'll focus on the specific C++ syntax and best practices that form the bedrock of a ray tracer.

Today's lesson addresses the first learning outcome: implementing classes with constructors, destructors, and member functions for geometric primitives. We'll start by defining what a C++ class is and then immediately apply that knowledge by creating vec3, a fundamental class for handling vectors, points, and colors. This hands-on approach will be our pattern throughout the course.

1. The Anatomy of a C++ Class

In C++, a class is a blueprint for creating objects. It bundles data (member variables) and the functions that operate on that data (member functions). For our ray tracer, we'll use classes to represent everything from geometric concepts like vectors and rays to physical objects like spheres and materials.

Two of the most important special member functions are the constructor and the destructor.

  • A constructor is responsible for initializing an object when it's created. It ensures the object starts in a valid and predictable state.
  • A destructor is responsible for cleaning up an object's resources just before it is destroyed (e.g., when it goes out of scope).

To get a precise understanding of these concepts, let's start with a foundational reading.

Understanding C++ Constructors, Destructors and Some ...

This article provides a clear, in-depth explanation of what constructors and destructors are and how they function in an object's lifecycle.

Please read sections 1 ('Constructors') and 2 ('Destructors'). Focus on the distinction between object creation (memory allocation) and initialization, and the role of the destructor in resource cleanup.


2. Implementing a Geometric Primitive: The vec3 Class

Now, let's put theory into practice. The most fundamental building block in any 3D graphics application is a 3-dimensional vector. We will use a single class, which we'll call vec3, to represent several related concepts:

  • 3D Vectors: For directions (like a ray's direction) and normals.
  • 3D Points: For locations in space (like a ray's origin or an intersection point).
  • RGB Colors: To store color data, as colors are often represented by three components (Red, Green, Blue).

Your strong linear algebra background will make the mathematical operations here feel very natural. Our goal is to create a class that is not only functional but also intuitive to use, closely mirroring mathematical vector notation.

The book Ray Tracing in One Weekend provides an excellent, straightforward implementation of a vec3 class. We will use it as our guide.

The vec3 Class

This section from Peter Shirley's Ray Tracing in One Weekend introduces a complete and practical vec3 class, which we will use as the foundation for our own.

Read the section titled 'The vec3 Class'. Pay close attention to the class definition, its member variables, constructors, member functions, and the overloaded operators. We will break this down piece by piece.

Dissecting the vec3 Class

Let's analyze the key C++ features used in the vec3 implementation from the reading.

Constructors and Initializer Lists

You saw two constructors in the vec3 class:

vec3() : e{0,0,0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}

Notice the syntax : e{...}. This is a member initializer list. It's the preferred way to initialize member variables in C++.

  • Initialization vs. Assignment: An initializer list initializes members directly. Assigning values inside the constructor body ({...}) happens after the members have already been default-initialized, which is less efficient and can be problematic for certain types (like const members).
  • Best Practice: Always use member initializer lists to initialize your class members.

For a deeper dive into why initializer lists are essential, especially for const members, let's turn back to our C++ concepts resource.

Understanding C++ Constructors, Destructors and Some ...

This section explains the 'why' and 'how' of member initializer lists, a crucial concept for writing correct and efficient constructors.

Please read Section 4, 'Constructor Initializer Lists'. Focus on why assignment fails for const members and how initializer lists solve this by initializing members before the constructor body executes.

Member Functions and const-Correctness

The vec3 class has several member functions, like x(), y(), z(), and length(). Notice the const keyword at the end of their declarations:

double x() const { return e[0]; }
double length() const { return std::sqrt(length_squared()); }

This const signifies that the member function is a "read-only" operation. It promises not to modify the state of the object it is called on. This practice is known as const-correctness.

Why is this important?

  1. Compiler Guarantees: The compiler will produce an error if you accidentally try to modify a member variable inside a const member function.
  2. API Clarity: It clearly communicates to anyone using your class that a function is safe to call without causing side effects.
  3. Flexibility: const member functions can be called on both const and non-const objects, whereas non-const member functions can only be called on non-const objects.

To solidify this concept, please read the following section.

Understanding C++ Constructors, Destructors and Some ...

This final reading explains the this pointer and the critical role of const in member function declarations.

Read Section 5, 'The this Pointer and Const Member Functions'. The key takeaway is understanding how adding const changes the type of the hidden this pointer, allowing the function to be called on const objects.

Operator Overloading

One of the powerful features of C++ demonstrated in the vec3 class is operator overloading. It allows you to define the behavior of standard operators (like +, -, *, []) for your custom types.

For example:

  • vec3& operator+=(const vec3& v) lets you add one vector to another with the intuitive += syntax.
  • double operator[](int i) const allows you to access vector components using array-like syntax (e.g., my_vec[0]).

This makes the code that uses your class cleaner, more readable, and closer to the mathematical domain it represents. Since you have a strong math background, you'll appreciate how this makes vector arithmetic in C++ look almost identical to how you'd write it on paper.

Test your understanding!

Based on the provided vec3.h code, consider the following main function:

#include "vec3.h"
#include <iostream>

int main() {
    const vec3 v1(1.0, 2.0, 3.0);
    vec3 v2(4.0, 5.0, 6.0);

    // Which of the following lines would cause a compilation error?
    // A) std::cout << v1.x() << std::endl;
    // B) v2[1] = 0.0;
    // C) v1[0] = 5.0;
    // D) v2 += v1;
}

Which line will not compile, and why?

Show answer

Line C) v1[0] = 5.0; will cause a compilation error.

Reason: The object v1 is declared as const, meaning its state cannot be modified. The vec3 class provides two versions of the operator[]:

  1. double operator[](int i) const: This const version is called for const objects. It returns a double by value, so you can read from it but not write to it.
  2. double& operator[](int i): This non-const version is called for non-const objects. It returns a double& (a reference), which allows you to modify the element.

When v1[0] is used on the left side of an assignment, the compiler tries to find a version of operator[] that allows modification. It finds the const version (because v1 is const), but that version doesn't permit assignment, leading to an error. This is a direct application of const-correctness.


3. Key Takeaways

This lesson provided a practical introduction to one of the most fundamental concepts in C++: classes.

  • Classes are blueprints that bundle data (members) and functions (methods).
  • Constructors initialize objects, ensuring they start in a valid state. Using member initializer lists is the correct and most efficient way to do this.
  • Destructors clean up an object's resources before it is deallocated.
  • const member functions are a promise not to modify an object's state, enabling const-correctness and making your code safer and more robust.
  • Operator overloading helps create intuitive, readable APIs for your classes that mimic familiar syntax.

You now have a solid vec3 class that will be the workhorse of our ray tracer, and you understand the C++ principles that make it work.


Next Steps

In the next lesson, we will tackle the second learning outcome: using std::vector to manage dynamic collections of objects. This will be our first step towards creating a "scene" that can hold multiple objects for our rays to interact with.