Hello! Welcome back to your ray tracing journey.
In our last few lessons, we meticulously assembled the components of a fully-featured camera. We've established its position with lookfrom, its target with lookat, its orientation with vup, and finally, its "lens" with an adjustable vertical field of view (vfov). Now, it's time to see our creation in action.
This lesson is the capstone for our work on the camera. We will focus on rendering a simple scene from multiple viewpoints to verify that all our camera controls work together as intended. By changing the camera's parameters and generating new images, we'll confirm that we have complete artistic control over how our scene is framed. This is a crucial and satisfying step, directly mirroring the process of setting up camera shots in 3D applications like Houdini.
1. Setting the Stage: A Test Scene
To test a camera, we first need something to look at. A good test scene should be simple enough to render quickly but have enough distinct elements to make changes in perspective obvious. We'll use a scene inspired by Ray Tracing in One Weekend, which includes a few spheres and a large "ground" sphere.
You can create a scene like this in your main() function. Here's an example setup:
#include "camera.h"
#include "hittable_list.h"
#include "sphere.h"
int main() {
// World
hittable_list world;
world.add(make_shared<sphere>(point3(0.0, -100.5, -1.0), 100.0)); // Ground
world.add(make_shared<sphere>(point3(0.0, 0.0, -1.0), 0.5)); // Center sphere
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5)); // Left sphere
world.add(make_shared<sphere>(point3(1.0, 0.0, -1.0), 0.5)); // Right sphere
// Camera
camera cam;
// ... We will set camera properties here ...
// Render
cam.render(world);
}
(Note: You will need to add materials to these spheres as you did in previous lessons for them to show up with distinct colors. The code above focuses on just the geometry.)
2. Composing the Shots: Defining and Testing Viewpoints
Now for the fun part. We will define a series of "shots" by configuring our camera's parameters in different ways. For each shot, you will set the properties on your cam object, render the world, and save the image.
The text Ray Tracing in One Weekend provides excellent examples for this process.
The chapter 'Positioning and Orienting the Camera' in Ray Tracing in One Weekend provides the exact code and expected output for testing different camera settings.
Please review the section titled 'Positioning and Orienting the Camera'. Focus on the code blocks under 'We'll change back to the prior scene, and use the new viewpoint:' and 'And we can change field of view:'. These will be our guide for the tests.
Shot 1: The Distant, Wide-Angle View
Let's start with a viewpoint that is off-axis and looks at our scene from a distance. This will test the lookfrom, lookat, and vup vectors working together.
Set the following properties on your camera object in main.cc:
cam.aspect_ratio = 16.0 / 9.0;cam.image_width = 400;cam.vfov = 90;cam.lookfrom = point3(-2, 2, 1);cam.lookat = point3(0, 0, -1);cam.vup = vec3(0, 1, 0);
Now, compile and run your ray tracer. The resulting image should match the one titled "A distant view" in the resource you just read. It shows the spheres from an elevated position to the side.
Shot 2: The Telephoto "Zoom"
Next, let's test the vfov control we implemented in the last lesson. A smaller field of view should "zoom in" on the scene.
Using the exact same settings as Shot 1, make only one change:
cam.vfov = 20;
Render the scene again. You should see a result that matches the image titled "Zooming in" from the book. The composition is the same, but the spheres now fill more of the frame, confirming that your FOV adjustment is working correctly.
Test your understanding!
Imagine your camera is set up with lookfrom = point3(0, 1, 5) and lookat = point3(0, 1, 0). The vup vector is the standard vec3(0, 1, 0). What happens when you try to compute the camera's basis vectors, specifically w and u? What kind of problem will arise?
Show answer
The view direction w = unit_vector(lookfrom - lookat) will be (0, 0, 1). The problem arises when you calculate u = unit_vector(cross(vup, w)). The cross product of (0, 1, 0) and (0, 0, 1) is (1, 0, 0), which is fine.
However, consider the case where lookfrom = (0, 5, 0) and lookat = (0, 0, 0). Here, the view direction w becomes (0, 1, 0). When you try to compute u by taking the cross product of vup (0, 1, 0) and w (0, 1, 0), you get a zero vector, because the two vectors are parallel. This makes it impossible to form a valid orthonormal basis. This is why vup cannot be parallel to the view direction.
3. Verification through Visualization
The ultimate test is visual. By generating images from these different camera configurations, you can immediately see if your implementation is correct. If the rendered image matches the expected output, your camera code is solid. If not, it provides a clear starting point for debugging. For example:
- Upside-down image? Check the calculation of your
vvector or thepixel_delta_vdirection. - Stretched or squashed image? Verify your aspect ratio calculations for
viewport_width. - Wrong viewpoint? Double-check the math for your
u, v, wbasis vectors and thepixel00_loccalculation.
This iterative process of coding a feature and then immediately testing it visually is central to graphics development.
Below is an example of a simple scene rendered from a particular viewpoint. Your goal is to be able to produce such an image and then move the camera to frame it differently.
4. Connection to Interactive and Production Cameras
While we are manually setting camera parameters and running our renderer, the concepts are universal. In your VFX work, you don't recompile code to move a camera; you use an interactive viewport and parameter panels. The underlying mechanism, however, is the same.
When you drag a camera in a DCC tool like Houdini, the software is updating the lookfrom, lookat, and vup parameters behind the scenes. When you change the focal length or FOV, it's updating the vfov parameter. Every time the scene needs to be redrawn, the renderer (like Karma or Arnold) uses these updated parameters to generate primary rays—just as we are.
To see this in action, watch this video demonstrating an interactive camera in a real-time ray tracer.
Adding an Interactive 3D Camera System // Ray Tracing series
The video 'Adding an Interactive 3D Camera System' by The Cherno demonstrates how our camera parameters can be updated every frame from user input (mouse and keyboard) to create a fluid, interactive experience.
Watch the final demonstration from 25:50 to 26:54. Notice how moving the mouse and using the keys allows orbiting and flying around the sphere. This is a real-time application of the very same principles we've just implemented for our offline renderer.
The key difference is the update loop. Our "loop" is you editing main.cc and rerunning the program. An interactive application's loop runs many times per second, checking for input and recalculating the camera's state before rendering each new frame.
Conclusion
Congratulations! You have successfully implemented and verified a complete, positionable camera. You now have the power to frame any scene, from any angle, with any field of view. This is a fundamental skill in computer graphics and a massive step towards creating compelling imagery.
Key Takeaways:
- A camera's final view is determined by the combination of its position (
lookfrom), target (lookat), orientation (vup), and field of view (vfov). - Verifying camera controls is best done by rendering a consistent test scene from multiple, deliberate viewpoints and comparing the output.
- The camera model we've built is conceptually identical to those used in production DCC applications and real-time engines; the parameters map directly.
Next Steps
With a robust camera system in place, our next limitation is the scene itself. A world made of only spheres is a great start, but we need a way to handle many objects of different shapes (planes, triangles, meshes, etc.) in a unified way.
In the next lesson, we will start Module 5 by designing an abstract hittable interface. This powerful object-oriented approach will allow us to create a list of generic "hittable objects," making our ray tracer far more scalable and capable of rendering truly complex worlds.