Skip to main content

Welcome to a Grasp lesson

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

Lesson 21

Creating the Hit Record

Recommended resources

Hello! Welcome back to our ray tracing journey.

In our last lesson, we established a powerful architectural pattern: the hittable abstract class. This "contract" ensures that any object in our scene, regardless of its shape, can be asked the same simple question: "Did this ray hit you?"

However, a simple true or false answer isn't enough. If a ray does hit something, we need to know where it hit, the orientation of the surface at that point, and more. The hit method's signature included a hit_record& rec parameter for this exact purpose.

This lesson is dedicated to fleshing out that hit_record struct. We will transform it from a simple placeholder into a comprehensive data packet that bundles all the essential information about a ray-object intersection. This struct is the critical link between the geometry-finding part of our renderer and the shading part that comes later. Understanding it is key to simulating how light interacts with surfaces, a concept central to the production renderers like Arnold and Karma you've worked with.

1. What Information Does an Intersection Carry?

When a ray intersects an object, what are the fundamental pieces of information we need to capture? At a minimum, we need to know the location of the hit and the orientation of the surface at that point.

The YouTuber Sebastian Lague provides a great high-level overview of this concept in his ray tracing series. He refers to this data packet as "hit info".

Coding Adventure: Ray Tracing

Let's watch a short segment from 'Coding Adventure: Ray Tracing' where the creator introduces his 'hit info' structure. This will give us a clear, visual starting point.

Watch from 05:43 to 06:19. Pay attention to the four pieces of information he identifies as essential for his initial 'hit info' structure: whether a hit occurred, the distance, the hit position, and the surface normal.

His list—hit/miss status, distance, position, and normal—forms the core of our hit_record. The hit method's bool return value handles the hit/miss status, so our struct will focus on the other three.

Let's look at the C++ implementation from the Ray Tracing in One Weekend guide. This is the initial version of the hit_record we sketched out in the last lesson, now with its purpose made clear.

Ray Tracing in One Weekend Series

The 'Ray Tracing in One Weekend' series introduces the hittable abstract class and the initial hit_record struct together. Let's examine this structure.

Please read the text under the heading 'An Abstraction for Hittable Objects' and focus on the hit_record class definition within the hittable.h code block. This defines the essential members.

Based on this, our hittable.h file should contain this initial hit_record definition:

#ifndef HITTABLE_H
#define HITTABLE_H

#include "ray.h"

class hit_record {
  public:
    point3 p;      // The world-space coordinate of the intersection point.
    vec3 normal;   // The surface normal vector at the intersection point.
    double t;      // The parameter 't' along the ray where the intersection occurred.
};

// ... our hittable abstract class from the previous lesson follows ...

Let's break down these members:

  • point3 p: The actual 3D point in space where the ray pierced the object's surface.
  • double t: The value such that ray.at(t) equals p. This scalar is crucial for determining which intersection is the closest to the camera when a ray hits multiple objects. We always want the one with the smallest positive t.
  • vec3 normal: The vector perpendicular to the surface at point p. This is fundamental for all shading calculations—it tells us how the surface is oriented towards light sources and the camera.
Ray-Plane Intersection Derivation and Visualization
This diagram for a ray-plane intersection visually demonstrates the data we're capturing. The ray equation solves for `t`, and plugging that back in gives us the intersection point `P`. The plane's normal `N` is the final piece of core data for our `hit_record`.

2. A Subtle Problem: Inside or Outside?

Our current hit_record is a great start, but it has a limitation. Imagine a ray hitting a glass sphere. If the ray originates outside the sphere, the normal should point outwards. But if the ray originates inside the sphere and is on its way out, we might need the normal to point inwards for refraction calculations.

Your experience in VFX and DCCs means you're likely familiar with this concept, often represented as "two-sided" materials or the need to "flip normals." We need a robust way to handle this in our code.

The solution is to always have our geometry calculation produce an "outward" pointing normal, and then, based on the ray's direction, decide which side of the surface was hit. We can then store a consistently oriented normal (e.g., one that always points against the incoming ray) in our hit_record.

This requires a small but significant enhancement to our hit_record struct.

Ray Tracing in One Weekend Series

Let's see how 'Ray Tracing in One Weekend' solves this problem. It introduces a boolean flag and a helper method to manage normal directionality.

Read the section 'Front Faces Versus Back Faces'. Pay close attention to the updated hit_record class and the implementation of the set_face_normal() function. This is the code you should add to your project.

After incorporating this change, our hit_record in hittable.h will look like this:

class hit_record {
  public:
    point3 p;
    vec3 normal;
    double t;
    bool front_face; // Flag to indicate if the ray hit the front or back of the surface.

    // A helper method to set the normal vector and the front_face flag.
    void set_face_normal(const ray& r, const vec3& outward_normal) {
        // Sets the hit record normal vector.
        // NOTE: the parameter `outward_normal` is assumed to have unit length.

        front_face = dot(r.direction(), outward_normal) < 0;
        normal = front_face ? outward_normal : -outward_normal;
    }
};

The logic in set_face_normal is elegant and efficient:

  1. It calculates the dot product of the ray's direction and the geometry's outward-facing normal.
  2. If the dot product is negative, the vectors are pointing in generally opposite directions, meaning the ray hit the object from the outside (a "front face" hit).
  3. If the dot product is positive, the vectors point in similar directions, meaning the ray originated inside the object.
  4. It then sets the normal member to always point against the incident ray, which simplifies later shading calculations.

3. Looking Ahead: What is the Surface Made Of?

We now have a robust way to describe the geometry of an intersection. But for shading, we need one more piece of information: what is the surface made of? Is it diffuse like matte paint, reflective like metal, or transparent like glass?

In a real-time game engine or a production renderer, this information is stored in a material. The hit_record is the perfect place to store a reference to the material of the object that was hit.

Let's add this final piece to our hit_record. This is a forward-looking step that prepares our data structure for the material and shading systems we will build in Module 7.

Ray Tracing in One Weekend Series

The 'Ray Tracing in One Weekend' guide completes the hit_record by adding a pointer to a material. This connects our geometry intersection system to the future shading system.

Read the short section 'A Data Structure to Describe Ray-Object Intersections'. Note the use of shared_ptr and the class material; forward declaration.

Here is our final, complete hit_record for this module:

#ifndef HITTABLE_H
#define HITTABLE_H

#include "ray.h"
#include <memory> // Required for std::shared_ptr

// Forward declaration to resolve circular include dependencies.
// The hittable objects need to know about materials, and materials will need to know about hittables.
class material;

class hit_record {
  public:
    point3 p;
    vec3 normal;
    std::shared_ptr<material> mat_ptr; // A pointer to the material of the hit object.
    double t;
    bool front_face;

    void set_face_normal(const ray& r, const vec3& outward_normal) {
        front_face = dot(r.direction(), outward_normal) < 0;
        normal = front_face ? outward_normal : -outward_normal;
    }
};

// ... the hittable abstract class ...

Notice two C++ details here:

  1. class material;: This is a forward declaration. It tells the compiler "a class named material exists," without needing the full definition. This is necessary because hittable.h refers to material, and soon material.h will refer to hit_record.
  2. std::shared_ptr<material>: We use a smart pointer to manage the material's memory. Multiple objects might share the same material, and std::shared_ptr handles this ownership scenario automatically. This reinforces the memory management concepts from our first module.
Test your understanding!

Imagine you are writing the hit function for a plane class. The plane is mathematically defined by a point on the plane, P_0, and its normal vector, N.

Your geometric calculation successfully finds that a ray intersects the plane at parameter t_hit, resulting in an intersection point p_hit.

Which members of the hit_record can you populate immediately with this information? What crucial piece of information are you missing that you would need from the plane object itself to fully populate the record?

Show answer

You can immediately populate:

  • rec.t = t_hit;
  • rec.p = p_hit;

You can also call the set_face_normal helper method:

  • rec.set_face_normal(ray, N); This will correctly set both rec.normal and rec.front_face.

The piece of information you are still missing is the material of the plane. The geometric calculation alone doesn't know if the plane is supposed to be a mirror, a wooden floor, or something else. You would need to access the plane object's own material member (e.g., this->material) and assign it to the record:

  • rec.mat_ptr = this->material;

Conclusion

In this lesson, we have meticulously designed the hit_record, a data structure that is central to our ray tracer's architecture. It serves as a comprehensive report detailing everything we need to know about a point of intersection.

Key Takeaways:

  • A hit_record bundles all information about a ray-object intersection.
  • The core members are the intersection point (p), the surface normal (normal), and the ray parameter (t).
  • A robust hit_record also handles surface directionality (front_face and a helper method) to distinguish between hitting an object from the outside versus the inside.
  • Including a pointer to the object's material makes the hit_record a complete data packet, connecting the geometry stage to the shading stage.

With this structure in place, our hittable::hit method now has a standardized way to report its findings.


Next Steps

We now have an abstract hittable interface and a hit_record struct to store results. But how do we manage a scene containing many hittable objects? In the next lesson, we will create a hittable_list class, a container that can hold any object that implements our hittable contract, allowing us to build and render our first multi-object scene.