Hello! Welcome back.
In our last lesson, we built the conceptual foundation for depth of field, understanding how a real camera's aperture and focus distance create the beautiful, blurry backgrounds we see in photography. We discussed the "thin lens" model, where rays originate from a disk (the aperture) but converge on a focal plane to create a sharp image at a specific distance.
Today, we're moving from theory to practice. This lesson is all about implementation. Our goal is to simulate a thin lens camera by sampling rays originating from a disk representing the aperture. We'll modify our camera class in C++, add the necessary controls for aperture size and focus distance, and generate our first images with realistic depth of field. This is a major step towards creating the kind of photorealistic renders you're used to seeing from production engines like Arnold and Karma.
Let's get coding.
1. The Thin Lens Model in Code
To implement the thin lens model, our camera needs two new parameters, which directly correspond to the concepts from our last lesson:
- Focus Distance (
focus_dist): The distance from the camera's origin to the plane in the scene that is in perfect focus. - Defocus Angle (
defocus_angle): An angle that controls the size of the aperture. A larger angle will mean a larger aperture, which in turn creates more blur (a shallower depth of field). A value of zero reverts us to a perfect pinhole camera.
We will follow the excellent implementation guide from the Ray Tracing in One Weekend series. The first step is to add these parameters to our camera class.
Ray Tracing in One Weekend Series
The text 'Ray Tracing in One Weekend' by Peter Shirley provides a clear, step-by-step guide to adding depth of field. Let's start by looking at the necessary changes to our camera class definition.
Please review the code listing titled [camera.h] Camera with adjustable depth-of-field. Focus on the new public member variables defocus_angle and focus_dist, and the new private member variables defocus_disk_u and defocus_disk_v. We'll dissect what these do next.
The defocus_disk_u and defocus_disk_v vectors represent the horizontal and vertical radii of our virtual aperture disk. Their lengths are calculated based on the defocus_angle and focus_dist. As you can see in the initialize() method from the reading:
auto defocus_radius = focus_dist * std::tan(degrees_to_radians(defocus_angle / 2));
This line uses basic trigonometry to determine the radius of the aperture disk in world-space units. A larger defocus_angle or a more distant focus_dist will result in a larger defocus_radius. This radius is then used to scale the camera's basis vectors u and v to create defocus_disk_u and defocus_disk_v.
2. How to Sample the Aperture Disk
With the aperture disk defined, the core of our task is to pick a random point on it for each ray we cast. This is where the Monte Carlo aspect of our ray tracer comes into play again, just like it did for antialiasing.
A crucial detail here is how we generate these random points. We need them to be uniformly distributed across the disk's area. A common first instinct might be to pick a random radius and a random angle. However, this simple approach incorrectly bunches samples toward the center of the disk.
There are two common and simple methods to achieve a uniform distribution:
- Rejection Sampling: Generate a random point in a square that encloses the disk. If the point is outside the disk, reject it and try again. This is simple to code and statistically correct.
- Mapping with Square Root: Generate a random angle from to and a random value from to . The radius of the sample point is . This corrects the clustering issue.
The Ray Tracing in One Weekend book uses rejection sampling, which is very clear and easy to implement.
Ray Tracing in One Weekend Series
Let's study the random_in_unit_disk() function provided in the book. This is the implementation of the rejection sampling method.
Find the code listing [vec3.h] Generate random point inside unit disk and read the random_in_unit_disk() function. It's a simple loop that generates a point in a 2D square from (-1,-1) to (1,1) and returns it only if its squared length is less than 1 (i.e., it's inside the unit circle).
A Deeper Dive (Optional)
For a computer scientist, the question of why the naive approach fails and the square root trick works is interesting. It's related to the change of variables in probability density functions. The area of a ring at radius is proportional to , so to keep the density uniform, we need to distribute more points at larger radii.
The article "Unit Disk Uniform Sampling" provides an excellent mathematical breakdown of this problem.
This article explores the mathematics behind uniformly sampling a disk, explaining why naive polar sampling fails and deriving the correct solution using methods like inversion sampling.
Skim the introduction and the sections 'Intuitive Polar Sampling' and 'Uniform Polar Sampling — Inversion Method'. This will give you the formal reasoning behind the sqrt(r) trick. You don't need to follow every line of math, but focus on the concept of transforming the probability distribution.
The same article also introduces "Shirley's Concentric Mapping," a more advanced technique that reduces sampling distortion. This is relevant to production rendering, where minimizing variance in Monte Carlo integration is key to efficiency and image quality.
3. Updating the Ray Generation
Now we can put all the pieces together. We'll modify our camera::get_ray() method to generate rays according to the thin lens model.
The core idea, which you saw visualized in the last lesson, is that while the ray's origin is randomized on the aperture disk, its direction is set to pass through the correct point on the focal plane.
Let's watch a quick visual recap from Sebastian Lague's video. This animation perfectly illustrates the algorithm we are about to implement.
Watch from 34:35 to 35:34. Notice how the ray origins are offset on a disk, but they all converge at the focal plane, making the object there sharp while objects at other depths become blurry.
The code to achieve this is remarkably concise. Here is the process inside get_ray():
- Calculate the target point on the pixel grid as before, including the random jitter for antialiasing. This point lies on the focal plane.
- Get a random point in the unit disk using
random_in_unit_disk(). - Scale that unit-disk point by our
defocus_disk_uanddefocus_disk_vvectors to find the offset on the actual aperture disk. - The new ray origin is
center + offset. - The new ray direction is simply
pixel_sample - ray_origin.
Now, let's look at the complete code.
Ray Tracing in One Weekend Series
Let's return to 'Ray Tracing in One Weekend' to see the final changes in camera.h that implement this logic.
Go back to the listing [camera.h] Camera with adjustable depth-of-field. This time, focus on the initialize() and get_ray() methods. Trace the logic we just discussed: how focus_dist affects the viewport and pixel locations, and how get_ray() uses the defocus_disk_sample() helper function to set the new ray origin and direction.
4. Rendering Your First DoF Scene
With the camera class updated, you're ready to render! The final step is to set the new camera parameters in your main() function.
A good starting point for focus_dist is the distance to your subject. A simple way to do this is to set it to the length of the vector between your lookfrom and lookat points.
Ray Tracing in One Weekend Series
The book provides a sample main() function setup to demonstrate the new camera.
Review the final code listing, [main.cc] Scene camera with depth-of-field. Note how defocus_angle and focus_dist are set. Try compiling and running your code with these new settings. Experiment with different values for defocus_angle to see the effect on the blur.
You should now be able to generate images with a clear depth of field effect. Objects at focus_dist will be sharp, while others will be blurred. The amount of blur will be controlled by defocus_angle.
Test your understanding!
You render a scene with defocus_angle = 10.0 and samples_per_pixel = 100. The out-of-focus areas look very noisy, like a grainy photograph. What are two different ways you could reduce this noise, and what is the trade-off for each approach?
Show answer
- Increase
samples_per_pixel: You could increase the sample count to, say, 500 or 1000. This will average more random samples for the defocus blur, smoothing out the noise and improving quality. The trade-off is a significant increase in render time. - Decrease
defocus_angle: You could reduce the angle to5.0or2.0. This makes the aperture smaller, which reduces the amount of blur. A smaller blur effect requires fewer samples to converge, so the noise will be less apparent. The trade-off is artistic: you are sacrificing the shallow depth of field effect for a cleaner image.
Conclusion
Congratulations! You have now implemented one of the most visually important and pleasing effects in photorealistic rendering. By simulating a thin lens, you've moved beyond the limitations of a simple pinhole camera and can now control focus and depth of field just like a real photographer.
Key Takeaways:
- We added
focus_distanddefocus_angleas parameters to ourcameraclass to control depth of field. - We learned how to generate uniformly random points on a disk, using rejection sampling as a simple and effective method.
- The core of the thin lens simulation is to originate rays from random points on an aperture disk, while directing them through a consistent point on the focal plane.
- The amount of blur (and resulting noise) is directly related to the
defocus_angleand the number of samples per pixel.
Next Steps
Our camera model is now quite powerful. It supports a positionable viewpoint, variable field of view, antialiasing, and depth of field. The next logical step is to improve what our rays interact with. In the next module, we will begin our journey into Surface Materials and Light Scattering. You will learn to implement different material types, starting with diffuse (or matte) surfaces, and build a recursive ray tracing loop to simulate the way light bounces around a scene.