Hello! Let's dive into the next major step in building our ray tracer.
In the last module, we successfully built and tested a fully positionable camera. We can now frame our scene from any angle and with any field of view. However, our world itself is still very simple and rigid. Our main rendering loop likely contains logic that is hardcoded to test for intersections with spheres. If we wanted to add planes, triangles, or cubes, we would have to add more and more specific if-else blocks, which is messy and unscalable.
This lesson tackles that problem head-on. We are going to design an abstract 'hittable' interface using C++'s object-oriented features. This powerful technique will allow us to create a generic "contract" that any object, regardless of its shape, can agree to. By doing this, we can ask any object in our scene, "Did this ray hit you?" in a uniform way, making our renderer dramatically more flexible and capable of handling complex scenes.
This is a fundamental design pattern in graphics programming and is conceptually similar to how large-scale systems in DCC tools like Houdini manage collections of diverse geometric objects.
1. The Problem: A Non-Scalable World
Imagine our current ray_color function. It takes a ray and figures out what color it should be. Inside, it likely loops through a list of sphere objects and calls a hit function on each one.
What happens when we want to add a triangle? We'd have to add a separate list of triangles and another loop. And for a plane? Another list, another loop. The ray_color function becomes a bottleneck of specialized code.
The goal is to have a single, simple loop:
// Desired logic
for (const auto& object : world_objects) {
if (object.hit(my_ray)) {
// We found a hit!
}
}
For this to work, we need to be able to store different types of objects (spheres, triangles, etc.) in a single list (world_objects) and call the same hit method on all of them. This is where polymorphism comes in.

2. The C++ Solution: Abstract Classes and Pure Virtual Functions
To achieve this generic behavior in C++, we use a feature called pure virtual functions. A class containing at least one pure virtual function is called an abstract class.
An abstract class acts as a template or a "contract." It cannot be instantiated on its own. Instead, other classes (like sphere or triangle) must inherit from it and provide concrete implementations for its pure virtual functions.
Your computer science background has likely introduced you to the concept of interfaces or abstract base classes. In C++, this is how we implement that pattern.
To get a clear and practical refresher on how this works in C++, let's watch a video from The Cherno.
Interfaces in C++ (Pure Virtual Functions)
This video, 'Interfaces in C++ (Pure Virtual Functions)', explains the concept and syntax for creating abstract classes, which are the foundation for our generic hittable object.
Please watch from the beginning until 04:21. Focus on these key points: What a pure virtual function is and the = 0; syntax. Why making a function pure virtual prevents you from creating an instance of the base class. The Printable example, which shows how an abstract class can be used to guarantee that different types of objects all share a common function.
The key takeaway is that by defining a function like virtual void do_something() = 0;, we force any derived class to implement do_something() if it wants to be a concrete, usable object. This is exactly the "contract" we need for our geometric primitives.
3. Designing the hittable Interface
Now, let's apply this concept to our ray tracer. We will create an abstract class called hittable. This will be the "contract" that all intersectable objects in our scene must adhere to. The contract is simple: they must provide a hit function.
The blog post "Ray Tracing in One Weekend: Part Two" offers a very clean and direct implementation of this idea.
Let's look at the code for our hittable interface. This resource provides a focused look at the abstract class definition and the hit_record struct that goes with it.
Please read the section titled 'Multiple Spheres with the Hittable Class' and carefully examine the code block for hittable.h. This is the file you will be creating.
Let's break down the core of this new header file, hittable.h:
#ifndef HITTABLE_H
#define HITTABLE_H
#include "ray.h"
// We will expand this struct in the next lesson
struct hit_record {
point3 p;
vec3 normal;
double t;
};
class hittable {
public:
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
#endif
The most important line is the declaration of the hit method:virtual bool hit(...) const = 0;
virtual: This keyword tells C++ to use dynamic dispatch. When we callhiton a pointer to ahittableobject, the compiler will ensure the correct version ofhit(e.g., thesphere's version or thetriangle's version) is called at runtime.bool hit(...): The function returnstrueif the ray hits the object within the valid range[t_min, t_max], andfalseotherwise.hit_record& rec: This is an "out parameter." If a hit occurs, the function will populate this struct with information about the intersection (the pointp, the normalnormal, etc.). We will focus on this struct in detail in the next lesson.const: This promises that thehitmethod will not change the state of the object it's called on. A sphere's radius and center don't change just because a ray intersects it.= 0: This makes the function pure virtual, which in turn makes thehittableclass abstract.
4. The Object-Oriented Style in Context
This object-oriented design is a classic and powerful way to structure a renderer. It prioritizes flexibility and extensibility. For a high-level view that reinforces this design choice, let's watch a quick segment from a talk by C++ expert Matt Godbolt on path tracing.
CppCon 2019: Matt Godbolt “Path Tracing Three Ways: A Study of C++ Style”
This clip from a CppCon talk provides excellent context for why we're choosing this object-oriented approach. It directly discusses using virtual methods for scene intersections.
Watch from 11:11 to 13:03. Notice how he introduces the idea of a virtual method to handle dispatch for different object types in the scene. This is a professional validation of the exact architecture we are now building.
As he describes, the core idea is to have an abstraction for scene geometry that can be queried for intersections, which is precisely what our hittable interface accomplishes.
Test your understanding!
After creating your hittable.h file with the code above, try to add the following line to your main() function:
hittable my_object;
What error message does your compiler give you? Why is this error expected and, in fact, desirable for our design?
Show answer
The compiler will issue an error similar to "cannot declare variable 'my_object' to be of abstract type 'hittable'" because hittable has a pure virtual function (hit).
This is desirable because the hittable class itself doesn't know how to be hit by a ray. It's just a contract. It makes no sense to have a generic, shapeless "hittable" object. We only want concrete objects like spheres and triangles that have implemented the hit function, thereby fulfilling the contract.
Conclusion
In this lesson, we've laid a critical architectural foundation for our ray tracer. By abstracting the concept of an intersectable object, we've paved the way for a much more complex and interesting world.
Key Takeaways:
- Hardcoding intersection logic for each object type is not scalable.
- An abstract class with a pure virtual function creates a "contract" that derived classes must implement.
- We designed a
hittableinterface with a pure virtualhitmethod:virtual bool hit(...) const = 0;. - This allows us to treat all objects in our scene uniformly, asking each one, "Did this ray hit you?" without needing to know its specific type.
Next Steps
The hittable interface is defined, but it's not useful without two other components: concrete classes that implement it, and a way to store the detailed results of an intersection.
In the next lesson, we will focus on the hit_record struct. We will detail all the information it needs to hold—the intersection point, the t value, the surface normal, and more. Afterward, we will update our sphere class to officially become a hittable object by implementing the hit method.