Skip to main content

Welcome to a Grasp lesson

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

Lesson 31

Diffuse Reflection and Lambertian Materials

Recommended resources

Hello and welcome back to your C++ ray tracing journey!

In our last lesson, you did some excellent foundational work by designing the abstract material interface. This crucial step separates an object's geometry from its surface properties, creating a flexible system that mirrors how production renderers like Arnold and Karma operate. You now have the architectural "socket" for materials; today, we'll plug in our first one.

This lesson's goal is to explain how diffuse surfaces scatter light uniformly and implement a Lambertian material. We'll explore the physics behind matte surfaces (like paper, plaster, or chalk) and translate that into a concrete C++ class that fulfills the material contract you just created.


1. The Physics of "Matte" Surfaces

From your experience in VFX, you know that a "diffuse" or "Lambertian" material represents a perfectly matte, non-reflective surface. But what does that mean physically?

On a microscopic level, these surfaces are very rough. When a light ray hits them, it doesn't bounce off cleanly in one direction. Instead, it scatters in one of many random directions.

Lambertian Reflectance Model Illustration
An illustration of diffuse reflection. An incoming light ray hits a rough surface and scatters equally in all directions.

To see this in action and connect it to the underlying math, let's watch a short, insightful video.

Diffuse reflection in 5 minutes

This video from the "Graphics in 5 Minutes" series explains the physical basis for diffuse reflection and introduces Lambert's Law, which mathematically describes this behavior.

Watch the video from the beginning up to 02:19. Pay close attention to two key ideas: the microscopic roughness causing scattering, and the formula for Lambert's Law.

As the video explained, the apparent brightness of a diffuse surface follows Lambert's Cosine Law. It states that the intensity of reflected light is proportional to the cosine of the angle () between the surface normal vector () and the light vector (). In vector math, this is conveniently calculated with the dot product: .

In traditional real-time graphics (like an older game engine), you would calculate this dot product for each light source to determine the pixel's color. In a path tracer, we take a different, more elegant approach: we simulate the scattering.



2. Simulating Uniform Scattering

Instead of explicitly calculating a dot product with a light source, a path tracer simulates the physical reality: it sends the ray bouncing off in a new, random direction. By averaging the results of many such random paths for each pixel, the final image statistically converges to the correct brightness as described by Lambert's Law.

Our task, then, is to pick a random direction for the scattered ray. But which direction? The key is that for an ideal diffuse surface, the light scatters with equal probability in all directions within the hemisphere oriented around the surface normal.

A simple and effective way to generate such a random direction is:

  1. Generate a random vector inside a unit sphere (a sphere with a radius of 1).
  2. Add this vector to the surface normal at the hit point.
  3. The resulting vector is our new, random scatter direction.

Let's read about this technique and see the code to implement it.

Ray Tracing in One Weekend - Diffuse Materials

The book 'Ray Tracing in One Weekend' provides the exact strategy we need. We'll focus on the part that explains how to generate the random scattered ray.

Find Chapter 7: Diffuse Materials. Read the paragraph that starts 'Pick a random point s from the unit radius sphere that is tangent to the hitpoint...'. Then, study the C++ code block for picking a random point in a unit sphere using a rejection method. You will need to implement a similar helper function in your project.

Based on the reading, you'll need a helper function to generate a random vector. The book uses a few variations, but the one we'll settle on for our Lambertian material generates a random point on the surface of a unit sphere. Let's call it random_unit_vector(). You can add this to your vec3.h or a utility header.

// A helper function to generate a random vector on the unit sphere's surface.
// This gives us a true Lambertian distribution.
vec3 random_unit_vector() {
    while (true) {
        auto p = vec3::random(-1, 1); // Assumes you have a function to get a random vec3 in a cube
        if (p.length_squared() >= 1) continue;
        return unit_vector(p);
    }
}

Note: This implementation picks a random point inside a unit sphere and then normalizes it to get a point on the surface. You'll need a way to generate random vectors and numbers, which you likely already have from the antialiasing and depth-of-field lessons.

The direction for our new scattered ray will then be:

This new direction vector originates from the hit point (hit_record.p) and points in a random direction within the correct hemisphere.



3. Implementing the lambertian Class

Now we can finally create our first concrete material. You'll create a new header file, lambertian.h (or add to material.h), and implement the class.

It must:

  1. Inherit from the abstract material class.
  2. Store its intrinsic color, which in PBR terms is called the albedo.
  3. Implement the scatter pure virtual function.

This blog post, which follows the same book, provides a clean look at the final lambertian class implementation.

Ray Tracing in One Weekend: Part Two - Light Scatter

This resource provides a clear C++ implementation for the lambertian class, showing exactly how to override the scatter method.

Scroll to the subsection 'Light Scatter' and examine the C++ code for the lambertian class. This is the code you will be implementing.

Here is the implementation of the lambertian class you should add to your project.

// In a new file lambertian.h or at the end of material.h

#include "material.h"
#include "hittable.h" // For hit_record

class lambertian : public material {
  public:
    lambertian(const color& a) : albedo(a) {}

    bool scatter(
        const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
    ) const override {
        auto scatter_direction = rec.normal + random_unit_vector();

        // Catch degenerate scatter direction
        if (scatter_direction.near_zero()) {
            scatter_direction = rec.normal;
        }

        scattered = ray(rec.p, scatter_direction);
        attenuation = albedo;
        return true;
    }

  private:
    color albedo;
};

Let's break down the scatter method:

  • auto scatter_direction = rec.normal + random_unit_vector();: This is the core logic we just discussed for finding a random bounce direction.
  • if (scatter_direction.near_zero()): This is a crucial edge case. If the random vector we pick is exactly opposite the normal, their sum could be a zero vector. If that happens, we just use the normal as the scatter direction to avoid issues.
  • scattered = ray(rec.p, scatter_direction);: We create the new scattered ray. It originates from the hit point (rec.p) and travels in our new random direction. This becomes an output of the function.
  • attenuation = albedo;: The attenuation (how much the light is reduced) is simply the material's own color. For a red ball, the albedo is red, so it reflects red light and absorbs others. This is the other output of the function.
  • return true;: We return true because this ideal diffuse material always scatters light; it never perfectly absorbs it.
Test your understanding!

In your main() function, you'll soon create a sphere like this:
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100, material_ground));

What does the color(0.8, 0.8, 0.0) represent in this context?

  1. The color of light emitted by the sphere.
  2. The percentage of red, green, and blue light that the surface reflects.
  3. The direction the light scatters.
Show answer
  1. The percentage of red, green, and blue light that the surface reflects.

This is the albedo. The color(0.8, 0.8, 0.0) means this material reflects 80% of incoming red light, 80% of incoming green light, and 0% of incoming blue light, resulting in a yellowish appearance. The attenuation output parameter of the scatter function is what communicates this property back to the renderer.

Finally, to use your new material, you'll need to update your main scene setup to create shared pointers to lambertian materials and pass them to your sphere constructors.



Conclusion

Fantastic work! You have now moved from an abstract concept of materials to a concrete, physically-based implementation. You've built the logic for the most fundamental material type in computer graphics, one that serves as the base for countless real-world surfaces.

Key Takeaways:

  • Diffuse surfaces are microscopically rough, causing them to scatter incoming light uniformly across their hemisphere.
  • Lambert's Cosine Law mathematically describes this behavior, stating that reflected intensity is proportional to the dot product of the normal and light vectors.
  • Path tracers simulate this law by scattering rays in random directions. A good method is to add a random unit vector to the surface normal.
  • The lambertian material class implements the scatter method to generate this new random ray and sets the ray's color attenuation to the material's albedo.

Next Steps

Right now, your ray_color function probably just returns a color after the first hit. It doesn't yet know what to do with the scattered ray your new material produces. In our next lesson, we will fix that by implementing recursive ray tracing. We will modify ray_color to call itself with the new scattered ray, allowing us to simulate light bouncing multiple times through the scene and finally see our diffuse materials in action. This is the magic that unlocks global illumination effects like color bleeding.