Ray.Tracing.in.One.Weekend.v4.0.0-alpha.1
Ray.Tracing.in.One.Weekend.v4.0.0-alpha.1
Contents
1 Overview
2 Output an Image
2.1 The PPM Image Format
2.2 Creating an Image File
2.3 Adding a Progress Indicator
5 Adding a Sphere
5.1 Ray-Sphere Intersection
5.2 Creating Our First Raytraced Image
8 Antialiasing
8.1 Some Random Number Utilities
8.2 Generating Pixels with Multiple Samples
9 Diffuse Materials
9.1 A Simple Diffuse Material
9.2 Limiting the Number of Child Rays
9.3 Fixing Shadow Acne
9.4 True Lambertian Reflection
9.5 Using Gamma Correction for Accurate Color Intensity
10 Metal
10.1 An Abstract Class for Materials
10.2 A Data Structure to Describe Ray-Object Intersections
10.3 Modeling Light Scatter and Reflectance
10.4 Mirrored Light Reflection
10.5 A Scene with Metal Spheres
10.6 Fuzzy Reflection
11 Dielectrics
11.1 Refraction
11.2 Snell's Law
11.3 Total Internal Reflection
11.4 Schlick Approximation
11.5 Modeling a Hollow Glass Sphere
12 Positionable Camera
12.1 Camera Viewing Geometry
12.2 Positioning and Orienting the Camera
13 Defocus Blur
13.1 A Thin Lens Approximation
13.2 Generating Sample Rays
14 Where Next?
14.1 A Final Render
14.2 Next Steps
15 Acknowledgments
1. Overview
I’ve taught many graphics classes over the years. Often I do them in ray tracing, because you are forced to write all the
code, but you can still get cool images with no API. I decided to adapt my course notes into a how-to, to get you to a
cool program as quickly as possible. It will not be a full-featured ray tracer, but it does have the indirect lighting which
has made ray tracing a staple in movies. Follow these steps, and the architecture of the ray tracer you produce will be
good for extending to a more extensive ray tracer if you get excited and want to pursue that.
When somebody says “ray tracing” it could mean many things. What I am going to describe is technically a path tracer,
and a fairly general one. While the code will be pretty simple (let the computer do the work!) I think you’ll be very happy
with the images you can make.
I’ll take you through writing a ray tracer in the order I do it, along with some debugging tips. By the end, you will have a
ray tracer that produces some great images. You should be able to do this in a weekend. If you take longer, don’t worry
about it. I use C++ as the driving language, but you don’t need to. However, I suggest you do, because it’s fast,
portable, and most production movie and video game renderers are written in C++. Note that I avoid most “modern
features” of C++, but inheritance and operator overloading are too useful for ray tracers to pass on.
I do not provide the code online, but the code is real and I show all of it except for a few straightforward
operators in the vec3 class. I am a big believer in typing in code to learn it, but when code is available I
use it, so I only practice what I preach when the code is not available. So don’t ask!
I have left that last part in because it is funny what a 180 I have done. Several readers ended up with subtle errors that
were helped when we compared code. So please do type in the code, but you can find the finished source for each
book in the RayTracing project on GitHub.
A note on the implementing code for these books — our philosophy for the included code prioritizes the following goals:
We use C++, but as simple as possible. Our programming style is very C-like, but we take advantage of modern
features where it makes the code easier to use or understand.
Our coding style continues the style established from the original books as much as possible, for continuity.
Line length is kept to 96 characters per line, to keep lines consistent between the codebase and code listings in
the books.
The code thus provides a baseline implementation, with tons of improvements left for the reader to enjoy. There are
endless ways one can optimize and modernize the code; we prioritize the simple solution.
We assume a little bit of familiarity with vectors (like dot product and vector addition). If you don’t know that, do a little
review. If you need that review, or to learn it for the first time, check out the online Graphics Codex by Morgan McGuire,
Fundamentals of Computer Graphics by Steve Marschner and Peter Shirley, or Fundamentals of Interactive Computer
Graphics by J.D. Foley and Andy Van Dam.
Peter maintains a site related to this book series at https://in1weekend.blogspot.com/, which includes further reading
and links to resources.
If you want to communicate with us, feel free to send us an email at:
Finally, if you run into problems with your implementation, have general questions, or would like to share your own ideas
or work, see the GitHub Discussions forum on the GitHub project.
Thanks to everyone who lent a hand on this project. You can find them in the acknowledgments section at the end of
this book.
2. Output an Image
#include <iostream>
int main() {
// Image
// Render
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
}
4. By convention, each of the red/green/blue components are represented internally by real-valued variables that
range from 0.0 to 1.0. These must be scaled to integer values between 0 and 255 before we print them out.
5. Red goes from fully off (black) to fully on (bright red) from left to right, and green goes from fully off at the top to
black at the bottom. Adding red and green light together make yellow so we should expect the bottom right
corner to be yellow.
(This example assumes that you are building with CMake, using the same approach as the CMakeLists.txt file in the
included source. Use whatever build environment (and language) you're comfortable with.)
This is how things would look on Windows with CMake. On Mac or Linux, it might look like this:
Opening the output file (in ToyViewer on my Mac, but try it in your favorite image viewer and Google “ppm viewer” if your
viewer doesn’t support it) shows this result:
Image 1: First PPM image
Hooray! This is the graphics “hello world”. If your image doesn’t look like that, open the output file in a text editor and
see what it looks like. It should start something like this:
P3
256 256
255
0 0 0
1 0 0
2 0 0
3 0 0
4 0 0
5 0 0
6 0 0
7 0 0
8 0 0
9 0 0
10 0 0
11 0 0
12 0 0
...
If your PPM file doesn't look like this, then double-check your formatting code. If it does look like this but fails to render,
then you may have line-ending differences or something similar that is confusing your image viewer. To help debug this,
you can find a file test.ppm in the images directory of the Github project. This should help to ensure that your viewer can
handle the PPM format and to use as a comparison against your generated PPM file.
Some readers have reported problems viewing their generated files on Windows. In this case, the problem is often that
the PPM is written out as UTF-16, often from PowerShell. If you run into this problem, see Discussion 1114 for help with
this issue.
If everything displays correctly, then you're pretty much done with system and IDE issues — everything in the remainder
of this series uses this same simple mechanism for generated rendered images.
If you want to produce other image formats, I am a fan of stb_image.h, a header-only image library available on GitHub
at https://github.com/nothings/stb.
Our program outputs the image to the standard output stream (std::cout), so leave that alone and instead write to the
logging output stream (std::clog):
std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
Now when running, you'll see a running count of the number of scanlines remaining. Hopefully this runs so fast that you
don't even see it! Don't worry — you'll have lots of time in the future to watch a slowly updating progress line as we
expand our ray tracer.
We define the vec3 class in the top half of a new vec3.h header file, and define a set of useful vector utility functions in
the bottom half:
#ifndef VEC3_H
#define VEC3_H
#include <cmath>
#include <iostream>
using std::sqrt;
class vec3 {
public:
double e[3];
vec3() : e{0,0,0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}
vec3& operator*=(double t) {
e[0] *= t;
e[1] *= t;
e[2] *= t;
return *this;
}
vec3& operator/=(double t) {
return *this *= 1/t;
}
// point3 is just an alias for vec3, but useful for geometric clarity in the code.
using point3 = vec3;
#endif
We use double here, but some ray tracers use float. double has greater precision and range, but is twice the size
compared to float. This increase in size may be important if you're programming in limited memory conditions (such as
hardware shaders). Either one is fine — follow your own tastes.
#ifndef COLOR_H
#define COLOR_H
#include "vec3.h"
#include <iostream>
#endif
#include "color.h"
#include "vec3.h"
#include <iostream>
int main() {
// Image
// Render
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
We can represent the idea of a ray as a class, and represent the function P(t) as a function that we'll call ray::at(t):
#ifndef RAY_H
#define RAY_H
#include "vec3.h"
class ray {
public:
ray() {}
private:
point3 orig;
vec3 dir;
};
#endif
When first developing a ray tracer, I always do a simple camera for getting the code up and running.
I’ve often gotten into trouble using square images for debugging because I transpose x and y too often, so we’ll use a
non-square image. A square image has a 1∶1 aspect ratio, because its width is the same as its height. Since we want a
non-square image, we'll choose 16∶9 because it's so common. A 16∶9 aspect ratio means that the ratio of image width to
image height is 16∶9. Put another way, given an image with a 16∶9 aspect ratio,
width/height = 16/9 = 1.7778
For a practical example, an image 800 pixels wide by 400 pixels high has a 2∶1 aspect ratio.
The image's aspect ratio can be determined from the ratio of its height to its width. However, since we have a given
aspect ratio in mind, it's easier to set the image's width and the aspect ratio, and then using this to calculate for its
height. This way, we can scale up or down the image by changing the image width, and it won't throw off our desired
aspect ratio. We do have to make sure that when we solve for the image height the resulting height is at least 1.
In addition to setting up the pixel dimensions for the rendered image, we also need to set up a virtual viewport through
which to pass our scene rays. The viewport is a virtual rectangle in the 3D world that contains the grid of image pixel
locations. If pixels are spaced the same distance horizontally as they are vertically, the viewport that bounds them will
have the same aspect ratio as the rendered image. The distance between two adjacent pixels is called the pixel
spacing, and square pixels is the standard.
To start things off, we'll choose an arbitrary viewport height of 2.0, and scale the viewport width to give us the desired
aspect ratio. Here's a snippet of what this code will look like:
// Viewport widths less than one are ok since they are real valued.
auto viewport_height = 2.0;
auto viewport_width = viewport_height * (static_cast<double>(image_width)/image_height);
If you're wondering why we don't just use aspect_ratio when computing viewport_width, it's because the value set to
aspect_ratio is the ideal ratio, it may not be the actual ratio between image_width and image_height. If image_height was
allowed to be real valued—rather than just an integer—then it would fine to use aspect_ratio. But the actual ratio
between image_width and image_height can vary based on two parts of the code. First, integer_height is rounded down
to the nearest integer, which can increase the ratio. Second, we don't allow integer_height to be less than one, which
can also change the actual aspect ratio.
Note that aspect_ratio is an ideal ratio, which we approximate as best as possible with the integer-based ratio of image
width over image height. In order for our viewport proportions to exactly match our image proportions, we use the
calculated image aspect ratio to determine our final viewport width.
Next we will define the camera center: a point in 3D space from which all scene rays will originate (this is also
commonly referred to as the eye point). The vector from the camera center to the viewport center will be orthogonal to
the viewport. We'll initially set the distance between the viewport and the camera center point to be one unit. This
distance is often referred to as the focal length.
For simplicity we'll start with the camera center at (0, 0, 0) . We'll also have the y-axis go up, the x-axis to the right, and
the negative z-axis pointing in the viewing direction. (This is commonly referred to as right-handed coordinates.)
Figure 3: Camera geometry
Now the inevitable tricky part. While our 3D space has the conventions above, this conflicts with our image coordinates,
where we want to have the zeroth pixel in the top-left and work our way down to the last pixel at the bottom right. This
means that our image coordinate Y-axis is inverted: Y increases going down the image.
As we scan our image, we will start at the upper left pixel (pixel 0, 0 ), scan left-to-right across each row, and then scan
row-by-row, top-to-bottom. To help navigate the pixel grid, we'll use a vector from the left edge to the right edge (Vu ),
and a vector from the upper edge to the lower edge (Vv ).
Our pixel grid will be inset from the viewport edges by half the pixel-to-pixel distance. This way, our viewport area is
evenly divided into width × height identical regions. Here's what our viewport and pixel grid look like:
Figure 4: Viewport and pixel grid
In this figure, we have the viewport, the pixel grid for a 7×5 resolution image, the viewport upper left corner Q , the pixel
P0,0 location, the viewport vector V u (viewport_u), the viewport vector V v (viewport_v), and the pixel delta vectors
Δu and Δv.
Drawing from all of this, here's the code that implements the camera. We'll stub in a function ray_color(const ray& r)
that returns the color for a given scene ray — which we'll set to always return black for now.
#include "color.h"
#include "ray.h"
#include "vec3.h"
#include <iostream>
int main() {
// Image
// Camera
// Calculate the vectors across the horizontal and down the vertical viewport edges.
auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
auto pixel_delta_u = viewport_u / image_width;
auto pixel_delta_v = viewport_v / image_height;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
Notice that in the code above, I didn't make ray_direction a unit vector, because I think not doing that makes for simpler
and slightly faster code.
Now we'll fill in the ray_color(ray) function to implement a simple gradient. This function will linearly blend white and
blue depending on the height of the y coordinate after scaling the ray direction to unit length (so −1.0 < y < 1.0 ).
Because we're looking at the y height after normalizing the vector, you'll notice a horizontal gradient to the color in
addition to the vertical gradient.
I'll use a standard graphics trick to linearly scale 0.0 ≤ a ≤ 1.0. When a = 1.0, I want blue. When a = 0.0, I want
white. In between, I want a blend. This forms a “linear blend”, or “linear interpolation”. This is commonly referred to as a
lerp between two values. A lerp is always of the form
#include "color.h"
#include "ray.h"
#include "vec3.h"
#include <iostream>
...
5. Adding a Sphere
Let’s add a single object to our ray tracer. People often use spheres in ray tracers because calculating whether a ray
hits a sphere is relatively simple.
2 2 2 2
x + y + z = r
You can also think of this as saying that if a given point (x, y, z) is on the sphere, then x2 + y 2 + z 2 = r2 . If a given
point (x, y, z) is inside the sphere, then x2 + y 2 + z 2 < r
2
, and if a given point (x, y, z) is outside the sphere, then
x + y + z > r .
2 2 2 2
If we want to allow the sphere center to be at an arbitrary point (C x , C y , C z ) , then the equation becomes a lot less
nice:
2 2 2 2
(x − C x ) + (y − C y ) + (z − C z ) = r
In graphics, you almost always want your formulas to be in terms of vectors so that all the x /y/z stuff can be simply
represented using a vec3 class. You might note that the vector from center C = (Cx , Cy , Cz ) to point P = (x, y, z) is
(P − C). If we use the definition of the dot product:
2 2 2
(P − C) ⋅ (P − C) = (x − C x ) + (y − C y ) + (z − C z )
Then we can rewrite the equation of the sphere in vector form as:
2
(P − C) ⋅ (P − C) = r
We can read this as “any point P that satisfies this equation is on the sphere”. We want to know if our ray
P(t) = A + tb ever hits the sphere anywhere. If it does hit the sphere, there is some t for which P(t) satisfies the
2
(P(t) − C) ⋅ (P(t) − C) = r
2
((A + tb) − C) ⋅ ((A + tb) − C) = r
We have three vectors on the left dotted by three vectors on the right. If we solved for the full dot product we would get
nine vectors. You can definitely go through and write everything out, but we don't need to work that hard. If you
remember, we want to solve for t , so we'll separate the terms based on whether there is a t or not:
2
(tb + (A − C)) ⋅ (tb + (A − C)) = r
And now we follow the rules of vector algebra to distribute the dot product:
2 2
t b ⋅ b + 2tb ⋅ (A − C) + (A − C) ⋅ (A − C) = r
Move the square of the radius over to the left hand side:
2 2
t b ⋅ b + 2tb ⋅ (A − C) + (A − C) ⋅ (A − C) − r = 0
It's hard to make out what exactly this equation is, but the vectors and r in that equation are all constant and known.
Furthermore, the only vectors that we have are reduced to scalars by dot product. The only unknown is t , and we have
a t2 , which means that this equation is quadratic. You can solve for a quadratic equation by using the quadratic formula:
−− −−−−−
2
−b ± √b − 4ac
2a
a = b ⋅ b
b = 2b ⋅ (A − C)
2
c = (A − C) ⋅ (A − C) − r
Using all of the above you can solve for t , but there is a square root part that can be either positive (meaning two real
solutions), negative (meaning no real solutions), or zero (meaning one real solution). In graphics, the algebra almost
always relates very directly to the geometry. What we have is:
Figure 5: Ray-sphere intersection results
Now this lacks all sorts of things — like shading, reflection rays, and more than one object — but we are closer to
halfway done than we are to our start! One thing to be aware of is that we are testing to see if a ray intersects with the
sphere by solving the quadratic equation and seeing if a solution exists, but solutions with negative values of t work just
fine. If you change your sphere center to z = +1 you will get exactly the same picture because this solution doesn't
distinguish between objects in front of the camera and objects behind the camera. This is not a feature! We’ll fix those
issues next.
We have a key design decision to make for normal vectors in our code: whether normal vectors will have an arbitrary
length, or will be normalized to unit length.
It is tempting to skip the expensive square root operation involved in normalizing the vector, in case it's not needed. In
practice, however, there are three important observations. First, if a unit-length normal vector is ever required, then you
might as well do it up front once, instead of over and over again “just in case” for every location where unit-length is
required. Second, we do require unit-length normal vectors in several places. Third, if you require normal vectors to be
unit length, then you can often efficiently generate that vector with an understanding of the specific geometry class, in its
constructor, or in the hit() function. For example, sphere normals can be made unit length simply by dividing by the
sphere radius, avoiding the square root entirely.
Given all of this, we will adopt the policy that all normal vectors will be of unit length.
For a sphere, the outward normal is in the direction of the hit point minus the center:
Figure 6: Sphere surface-normal geometry
On the earth, this means that the vector from the earth’s center to you points straight up. Let’s throw that into the code
now, and shade it. We don’t have any lights or anything yet, so let’s just visualize the normals with a color map. A
common trick used for visualizing normals (because it’s easy and somewhat intuitive to assume n is a unit length vector
— so each component is between −1 and 1) is to map each component to the interval from 0 to 1, and then map
(x, y, z) to (red, green, blue) . For the normal, we need the hit point, not just whether we hit or not (which is all we're
calculating at the moment). We only have one sphere in the scene, and it's directly in front of the camera, so we won't
worry about negative values of t yet. We'll just assume the closest hit point (smallest t ) is the one that we want. These
changes in the code let us compute and visualize n:
if (discriminant < 0) {
return -1.0;
} else {
return (-b - sqrt(discriminant) ) / (2.0*a);
}
}
if (discriminant < 0) {
return -1.0;
} else {
return (-b - sqrt(discriminant) ) / (2.0*a);
}
}
First, recall that a vector dotted with itself is equal to the squared length of that vector.
Second, notice how the equation for b has a factor of two in it. Consider what happens to the quadratic equation if
b = 2h:
−− −−−−−
2
−b ± √b − 4ac
2a
−−−−−−−−−
2
−2h ± √(2h) − 4ac
=
2a
−− −−−−
2
−2h ± 2√h − ac
=
2a
−− −−−−
2
−h ± √h − ac
=
a
Using these observations, we can now simplify the sphere-intersection code to this:
if (discriminant < 0) {
return -1.0;
} else {
return (-half_b - sqrt(discriminant) ) / a;
}
}
This hittable abstract class will have a hit function that takes in a ray. Most ray tracers have found it convenient to add
a valid interval for hits tmin to tmax , so the hit only “counts” if tmin < t < tmax . For the initial rays this is positive t , but
as we will see, it can simplify our code to have an interval tmin to tmax . One design question is whether to do things like
compute the normal if we hit something. We might end up hitting something closer as we do our search, and we will only
need the normal of the closest thing. I will go with the simple solution and compute a bundle of stuff I will store in some
structure. Here’s the abstract class:
#ifndef HITTABLE_H
#define HITTABLE_H
#include "ray.h"
class hit_record {
public:
point3 p;
vec3 normal;
double t;
};
class hittable {
public:
virtual ~hittable() = default;
virtual bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const = 0;
};
#endif
#ifndef SPHERE_H
#define SPHERE_H
#include "hittable.h"
#include "vec3.h"
bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override {
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius*radius;
rec.t = root;
rec.p = r.at(rec.t);
rec.normal = (rec.p - center) / radius;
return true;
}
private:
point3 center;
double radius;
};
#endif
We need to choose one of these possibilities because we will eventually want to determine which side of the surface
that the ray is coming from. This is important for objects that are rendered differently on each side, like the text on a two-
sided sheet of paper, or for objects that have an inside and an outside, like glass balls.
If we decide to have the normals always point out, then we will need to determine which side the ray is on when we
color it. We can figure this out by comparing the ray with the normal. If the ray and the normal face in the same
direction, the ray is inside the object, if the ray and the normal face in the opposite direction, then the ray is outside the
object. This can be determined by taking the dot product of the two vectors, where if their dot is positive, the ray is inside
the sphere.
If we decide to have the normals always point against the ray, we won't be able to use the dot product to determine
which side of the surface the ray is on. Instead, we would need to store that information:
bool front_face;
if (dot(ray_direction, outward_normal) > 0.0) {
// ray is inside the sphere
normal = -outward_normal;
front_face = false;
} else {
// ray is outside the sphere
normal = outward_normal;
front_face = true;
}
We can set things up so that normals always point “outward” from the surface, or always point against the incident ray.
This decision is determined by whether you want to determine the side of the surface at the time of geometry
intersection or at the time of coloring. In this book we have more material types than we have geometry types, so we'll
go for less work and put the determination at geometry time. This is simply a matter of preference, and you'll see both
implementations in the literature.
We add the front_face bool to the hit_record class. We'll also add a function to solve this calculation for us:
set_face_normal(). For convenience we will assume that the vector passed to the new set_face_normal() function is of
unit length. We could always normalize the parameter explicitly, but it's more efficient if the geometry code does this, as
it's usually easier when you know more about the specific geometry.
class hit_record {
public:
point3 p;
vec3 normal;
double t;
bool front_face;
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
return true;
}
...
};
#include "hittable.h"
#include <memory>
#include <vector>
using std::shared_ptr;
using std::make_shared;
hittable_list() {}
hittable_list(shared_ptr<hittable> object) { add(object); }
bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override {
hit_record temp_rec;
bool hit_anything = false;
auto closest_so_far = ray_tmax;
return hit_anything;
}
};
#endif
shared_ptr<type> is a pointer to some allocated type, with reference-counting semantics. Every time you assign its value
to another shared pointer (usually with a simple assignment), the reference count is incremented. As shared pointers go
out of scope (like at the end of a block or function), the reference count is decremented. Once the count goes to zero,
the object is safely deleted.
Typically, a shared pointer is first initialized with a newly-allocated object, something like this:
make_shared<thing>(thing_constructor_params ...) allocates a new instance of type thing, using the constructor
parameters. It returns a shared_ptr<thing>.
Since the type can be automatically deduced by the return type of make_shared<type>(...), the above lines can be more
simply expressed using C++'s auto type specifier:
The second C++ feature you may be unfamiliar with is std::vector. This is a generic array-like collection of an arbitrary
type. Above, we use a collection of pointers to hittable. std::vector automatically grows as more values are added:
objects.push_back(object) adds a value to the end of the std::vector member variable objects.
Finally, the using statements in listing 21 tell the compiler that we'll be getting shared_ptr and make_shared from the std
library, so we don't need to prefix these with std:: every time we reference them.
#ifndef RTWEEKEND_H
#define RTWEEKEND_H
#include <cmath>
#include <limits>
#include <memory>
// Usings
using std::shared_ptr;
using std::make_shared;
using std::sqrt;
// Constants
// Utility Functions
// Common Headers
#include "ray.h"
#include "vec3.h"
#endif
#include "rtweekend.h"
#include "color.h"
#include "hittable.h"
#include "hittable_list.h"
#include "sphere.h"
#include <iostream>
int main() {
// Image
// World
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
// Camera
// Calculate the vectors across the horizontal and down the vertical viewport edges.
auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
auto pixel_delta_u = viewport_u / image_width;
auto pixel_delta_v = viewport_v / image_height;
// Render
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
This yields a picture that is really just a visualization of where the spheres are located along with their surface normal.
This is often a great way to view any flaws or specific characteristics of a geometric model.
Image 5: Resulting render of normals-colored sphere with ground
#ifndef INTERVAL_H
#define INTERVAL_H
class interval {
public:
double min, max;
#endif
// Common Headers
#include "interval.h"
#include "ray.h"
#include "vec3.h"
class hittable {
public:
...
virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0;
};
return hit_anything;
}
...
};
...
color ray_color(const ray& r, const hittable& world) {
hit_record rec;
if (world.hit(r, interval(0, infinity), rec)) {
return 0.5 * (rec.normal + color(1,1,1));
}
In this refactoring, we'll collect the ray_color() function, along with the image, camera, and render sections of our main
program. The new camera class will contain two public methods initialize() and render(), plus two private helper
methods get_ray() and ray_color().
Ultimately, the camera will follow the simplest usage pattern that we could think of: it will be default constructed no
arguments, then the owning code will modify the camera's public variables through simple assignment, and finally
everything is initialized by a call to the initialize() function. This pattern is chosen instead of the owner calling a
constructor with a ton of parameters or by defining and calling a bunch of setter methods. Instead, the owning code only
needs to set what it explicitly cares about. Finally, we could either have the owning code call initialize(), or just have
the camera call this function automatically at the start of render(). We'll use the second approach.
After main creates a camera and sets default values, it will call the render() method. The render() method will prepare
the camera for rendering and then execute the render loop.
#ifndef CAMERA_H
#define CAMERA_H
#include "rtweekend.h"
#include "color.h"
#include "hittable.h"
class camera {
public:
/* Public Camera Parameters Here */
private:
/* Private Camera Variables Here */
void initialize() {
...
}
#endif
class camera {
...
private:
...
#endif
...
#include "rtweekend.h"
#include "color.h"
#include "hittable.h"
#include <iostream>
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
private:
int image_height; // Rendered image height
point3 center; // Camera center
point3 pixel00_loc; // Location of pixel 0, 0
vec3 pixel_delta_u; // Offset to pixel to the right
vec3 pixel_delta_v; // Offset to pixel below
void initialize() {
image_height = static_cast<int>(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
// Calculate the vectors across the horizontal and down the vertical viewport edges.
auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
#endif
#include "rtweekend.h"
#include "camera.h"
#include "hittable_list.h"
#include "sphere.h"
int main() {
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
camera cam;
cam.render(world);
}
Listing 35: [main.cc] The new main, using the new camera
Running this newly refactored program should give us the same rendered image as before.
8. Antialiasing
If you zoom into the rendered images so far, you might notice the harsh “stair step” nature of edges in our rendered
images. This stair-stepping is commonly referred to as “aliasing”, or “jaggies”. When a real camera takes a picture, there
are usually no jaggies along edges, because the edge pixels are a blend of some foreground and some background.
Consider that unlike our rendered images, a true image of the world is continuous. Put another way, the world (and any
true image of it) has effectively infinite resolution. We can get the same effect by averaging a bunch of samples for each
pixel.
With a single ray through the center of each pixel, we are performing what is commonly called point sampling. The
problem with point sampling can be illustrated by rendering a small checkerboard far away. If this checkerboard consists
of an 8×8 grid of black and white tiles, but only four rays hit it, then all four rays might intersect only white tiles, or only
black, or some odd combination. In the real world, when we perceive a checkerboard far away with our eyes, we
perceive it as a gray color, instead of sharp points of black and white. That's because our eyes are naturally doing what
we want our ray tracer to do: integrate the (continuous function of) light falling on a particular (discrete) region of our
rendered image.
Clearly we don't gain anything by just resampling the same ray through the pixel center multiple times — we'd just get
the same result each time. Instead, we want to sample the light falling around the pixel, and then integrate those
samples to approximate the true continuous result. So, how do we integrate the light falling around the pixel?
We'll adopt the simplest model: sampling the square region centered at the pixel that extends halfway to each of the
four neighboring pixels. This is not the optimal approach, but it is the most straight-forward. (See A Pixel is Not a Little
Square for a deeper dive into this topic.)
Figure 8: Pixel samples
A simple approach to this is to use the rand() function that can be found in <cstdlib>, which returns a random integer in
the range 0 and RAND_MAX. Hence we can get a real random number as desired with the following code snippet, added to
rtweekend.h:
#include <cmath>
#include <cstdlib>
#include <limits>
#include <memory>
...
// Utility Functions
C++ did not traditionally have a standard random number generator, but newer versions of C++ have addressed this
issue with the <random> header (if imperfectly according to some experts). If you want to use this, you can obtain a
random number with the conditions we need as follows:
#include <random>
First we'll update the write_color() function to account for the number of samples we use: we need to find the average
across all of the samples that we take. To do this, we'll add the full color from each iteration, and then finish with a single
division (by the number of samples) at the end, before writing out the color. To ensure that the color components of the
final result remain within the proper [0, 1] bounds, we'll add and use a small helper function: interval::clamp(x).
class interval {
public:
...
And here's the updated write_color() function that takes the sum total of all light for the pixel and the number of
samples involved:
Now let's update the camera class to define and use a new camera::get_ray(i,j) function, which will generate different
samples for each pixel. This function will use a new helper function pixel_sample_square() that generates a random
sample point within the unit square centered at the origin. We then transform the random sample from this ideal square
back to the particular pixel we're currently sampling.
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
...
};
#endif
(In addition to the new pixel_sample_square() function above, you'll also find the function pixel_sample_disk() in the
Github source code. This is included in case you'd like to experiment with non-square pixels, but we won't be using it in
this book. pixel_sample_disk() depends on the function random_in_unit_disk() which is defined later on.)
int main() {
...
camera cam;
cam.render(world);
}
9. Diffuse Materials
Now that we have objects and multiple rays per pixel, we can make some realistic looking materials. We’ll start with
diffuse materials (also called matte). One question is whether we mix and match geometry and materials (so that we
can assign a material to multiple spheres, or vice versa) or if geometry and materials are tightly bound (which could be
useful for procedural objects where the geometry and material are linked). We’ll go with separate — which is usual in
most renderers — but do be aware that there are alternative approaches.
They might also be absorbed rather than reflected. The darker the surface, the more likely the ray is absorbed (that’s
why it's dark!). Really any algorithm that randomizes direction will produce surfaces that look matte. Let's start with the
most intuitive: a surface that randomly bounces a ray equally in all directions. For this material, a ray that hits the
surface has an equal probability of bouncing in any direction away from the surface.
Figure 10: Equal reflection above the horizon
This very intuitive material is the simplest kind of diffuse and — indeed — many of the first raytracing papers used this
diffuse method (before adopting a more accurate method that we'll be implementing a little bit later). We don't currently
have a way to randomly reflect a ray, so we'll need to add a few functions to our vector utility header. The first thing we
need is the ability to generate arbitrary random vectors:
class vec3 {
public:
...
Then we need to figure out how to manipulate a random vector so that we only get results that are on the surface of a
hemisphere. There are analytical methods of doing this, but they are actually surprisingly complicated to understand,
and quite a bit complicated to implement. Instead, we'll use what is typically the easiest algorithm: A rejection method. A
rejection method works by repeatedly generating random samples until we produce a sample that meets the desired
criteria. In other words, keep rejecting samples until you find a good one.
There are many equally valid ways of generating a random vector on a hemisphere using the rejection method, but for
our purposes we will go with the simplest, which is:
Figure 11: Two vectors were rejected before finding a good one
...
Figure 12: The accepted random vector is normalized to produce a unit vector
...
We can take the dot product of the surface normal and our random vector to determine if it's in the correct hemisphere.
If the dot product is positive, then the vector is in the correct hemisphere. If the dot product is negative, then we need to
invert the vector.
...
If a ray bounces off of a material and keeps 100% of its color, then we say that the material is white. If a ray bounces off
of a material and keeps 0% of its color, then we say that the material is black. As a first demonstration of our new diffuse
material we'll set the ray_color function to return 50% of the color from a bounce. We should expect to get a nice gray
color.
class camera {
...
private:
...
color ray_color(const ray& r, const hittable& world) const {
hit_record rec;
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
int main() {
...
camera cam;
cam.render(world);
}
class camera {
...
private:
...
color ray_color(const ray& r, int depth, const hittable& world) const {
hit_record rec;
We can create this distribution by adding a random unit vector to the normal vector. At the point of intersection on a
surface there is the hit point, p, and there is the normal of the surface, n. At the point of intersection, this surface has
exactly two sides, so there can only be two unique unit spheres tangent to any intersection point (one unique sphere for
each side of the surface). These two unit spheres will be displaced from the surface by the length of their radius, which
is exactly one for a unit sphere.
One sphere will be displaced in the direction of the surface's normal (n) and one sphere will be displaced in the
opposite direction (−n). This leaves us with two spheres of unit size that will only be just touching the surface at the
intersection point. From this, one of the spheres will have its center at (P + n) and the other sphere will have its center
at (P − n). The sphere with a center at (P − n) is considered inside the surface, whereas the sphere with center
(P + n) is considered outside the surface.
We want to select the tangent unit sphere that is on the same side of the surface as the ray origin. Pick a random point
S on this unit radius sphere and send a ray from the hit point P to the random point S (this is the vector (S − P)):
Figure 14: Randomly generating a vector according to Lambertian distribution
class camera {
...
color ray_color(const ray& r, int depth, const hittable& world) const {
hit_record rec;
It's hard to tell the difference between these two diffuse methods, given that our scene of two spheres is so simple, but
you should be able to notice two important visual differences:
Both of these changes are due to the less uniform scattering of the light rays—more rays are scattering toward the
normal. This means that for diffuse objects, they will appear darker because less light bounces toward the camera. For
the shadows, more light bounces straight-up, so the area underneath the sphere is darker.
Not a lot of common, everyday objects are perfectly diffuse, so our visual intuition of how these objects behave under
light can be poorly formed. As scenes become more complicated over the course of the book, you are encouraged to
switch between the different diffuse renderers presented here. Most scenes of interest will contain a large amount of
diffuse materials. You can gain valuable insight by understanding the effect of different diffuse methods on the lighting of
a scene.
class camera {
...
color ray_color(const ray& r, int depth, const hittable& world) const {
hit_record rec;
If you look closely, or if you use a color picker, you should notice that the 50% reflectance render (the one in the middle)
is far too dark to be half-way between white and black (middle-gray). Indeed, the 70% reflector is closer to middle-gray.
The reason for this is that almost all computer programs assume that an image is “gamma corrected” before being
written into an image file. This means that the 0 to 1 values have some transform applied before being stored as a byte.
Images with data that are written without being transformed are said to be in linear space, whereas images that are
transformed are said to be in gamma space. It is likely that the image viewer you are using is expecting an image in
gamma space, but we are giving it an image in linear space. This is the reason why our image appears inaccurately
dark.
There are many good reasons for why images should be stored in gamma space, but for our purposes we just need to
be aware of it. We are going to transform our data into gamma space so that our image viewer can more accurately
display our image. As a simple approximation, we can use “gamma 2” as our transform, which is the power that you use
when going from gamma space to linear space. We need to go from linear space to gamma space, which means taking
the inverse of “gamma 2", which means an exponent of 1/gamma, which is just the square-root.
10. Metal
#ifndef MATERIAL_H
#define MATERIAL_H
#include "rtweekend.h"
class hit_record;
class material {
public:
virtual ~material() = default;
#endif
#include "rtweekend.h"
class material;
class hit_record {
public:
point3 p;
vec3 normal;
shared_ptr<material> mat;
double t;
bool front_face;
hit_record is just a way to stuff a bunch of arguments into a class so we can send them as a group. When a ray hits a
surface (a particular sphere for example), the material pointer in the hit_record will be set to point at the material pointer
the sphere was given when it was set up in main() when we start. When the ray_color() routine gets the hit_record it
can call member functions of the material pointer to find out what ray, if any, is scattered.
To achieve this, hit_record needs to be told the material that is assigned to the sphere.
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat = mat;
return true;
}
private:
point3 center;
double radius;
shared_ptr<material> mat;
};
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
auto scatter_direction = rec.normal + random_unit_vector();
scattered = ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
private:
color albedo;
};
Note the third option that we could scatter with some fixed probability p and have attenuation be albedo/p . Your
choice.
If you read the code above carefully, you'll notice a small chance of mischief. If the random unit vector we generate is
exactly opposite the normal vector, the two will sum to zero, which will result in a zero scatter direction vector. This leads
to bad scenarios later on (infinities and NaNs), so we need to intercept the condition before we pass it on.
In service of this, we'll create a new vector method — vec3::near_zero() — that returns true if the vector is very close to
zero in all dimensions.
class vec3 {
...
...
};
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
auto scatter_direction = rec.normal + random_unit_vector();
private:
color albedo;
};
The reflected ray direction in red is just v + 2b . In our design, n is a unit vector, but v may not be. The length of b
...
...
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return true;
}
private:
color albedo;
};
...
#include "rtweekend.h"
#include "color.h"
#include "hittable.h"
#include "material.h"
...
class camera {
...
private:
...
color ray_color(const ray& r, int depth, const hittable& world) const {
hit_record rec;
#include "rtweekend.h"
#include "camera.h"
#include "color.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
int main() {
hittable_list world;
camera cam;
cam.render(world);
}
The bigger the sphere, the fuzzier the reflections will be. This suggests adding a fuzziness parameter that is just the
radius of the sphere (so zero is no perturbation). The catch is that for big spheres or grazing rays, we may scatter below
the surface. We can just have the surface absorb those.
class metal : public material {
public:
metal(const color& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected + fuzz*random_unit_vector());
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
private:
color albedo;
double fuzz;
};
We can try that out by adding fuzziness 0.3 and 1.0 to the metals:
int main() {
...
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
...
}
11. Dielectrics
Clear materials such as water, glass, and diamond are dielectrics. When a light ray hits them, it splits into a reflected ray
and a refracted (transmitted) ray. We’ll handle that by randomly choosing between reflection and refraction, only
generating one scattered ray per interaction.
11.1. Refraction
The hardest part to debug is the refracted ray. I usually first just have all the light refract if there is a refraction ray at all.
For this project, I tried to put two glass balls in our scene, and I got this (I have not told you how to do this right or wrong
yet, but soon!):
Image 15: Glass first
Is that right? Glass balls look odd in real life. But no, it isn’t right. The world should be flipped upside down and no weird
black stuff. I just printed out the ray straight through the middle of the image and it was clearly wrong. That often does
the job.
Where θ and θ′ are the angles from the normal, and η and η ′ (pronounced “eta” and “eta prime”) are the refractive
indices (typically air = 1.0, glass = 1.3–1.7, diamond = 2.4). The geometry is:
In order to determine the direction of the refracted ray, we have to solve for sin θ′ :
′
η
sin θ = ⋅ sin θ
′
η
On the refracted side of the surface there is a refracted ray R ′ and a normal n′ , and there exists an angle, θ′ , between
them. We can split R ′ into the parts of the ray that are perpendicular to n′ and parallel to n′ :
′ ′ ′
R = R ⊥
+ R ∥
′
η
R ⊥ = (R + cos θn)
′
η
−−−−−−−−
′ ′ 2
R ∥
= −√1 − |R ⊥
| n
You can go ahead and prove this for yourself if you want, but we will treat it as fact and move on. The rest of the book
will not require you to understand the proof.
We know the value of every term on the right-hand side except for cos θ . It is well known that the dot product of two
vectors can be explained in terms of the cosine of the angle between them:
a ⋅ b = |a||b| cos θ
a ⋅ b = cos θ
′
η
R ⊥ = (R + (−R ⋅ n)n)
′
η
...
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
private:
double ir; // Index of Refraction
};
Now we'll update the scene to change the left and center spheres to glass:
If the ray is inside glass and outside is air (η = 1.5 and η ′ = 1.0 ):
1.5
′
sin θ = ⋅ sin θ
1.0
1.5
⋅ sin θ > 1.0
1.0
the equality between the two sides of the equation is broken, and a solution cannot exist. If a solution does not exist, the
glass cannot refract, and therefore must reflect the ray:
Here all the light is reflected, and because in practice that is usually inside solid objects, it is called “total internal
reflection”. This is why sometimes the water-air boundary acts as a perfect mirror when you are submerged.
and
cos θ = R ⋅ n
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
And the dielectric material that always refracts (when possible) is:
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
if (cannot_refract)
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, refraction_ratio);
private:
double ir; // Index of Refraction
};
Attenuation is always 1 — the glass surface absorbs nothing. If we try that out with these parameters:
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
private:
double ir; // Index of Refraction
This gives:
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
private:
...
void initialize() {
image_height = static_cast<int>(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
// Calculate the vectors across the horizontal and down the vertical viewport edges.
auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
...
};
We'll test out these changes with a simple scene of two touching spheres, using a 90° field of view.
int main() {
hittable_list world;
auto R = cos(pi/4);
camera cam;
cam.vfov = 90;
cam.render(world);
}
We also need a way to specify the roll, or sideways tilt, of the camera: the rotation around the lookat-lookfrom axis.
Another way to think about it is that even if you keep lookfrom and lookat constant, you can still rotate your head around
your nose. What we need is a way to specify an “up” vector for the camera.
We can specify any up vector we want, as long as it's not parallel to the view direction. Project this up vector onto the
plane orthogonal to the view direction to get a camera-relative up vector. I use the common convention of naming this
the “view up” (vup) vector. After a few cross products and vector normalizations, we now have a complete orthonormal
basis (u, v, w) to describe our camera’s orientation. u will be the unit vector pointing to camera right, v is the unit
vector pointing to camera up, w is the unit vector pointing opposite the view direction (since we use right-hand
coordinates), and the camera center is at the origin.
Figure 20: Camera view up direction
Like before, when our fixed camera faced −Z , our arbitrary view camera faces −w . Keep in mind that we can — but
we don’t have to — use world up (0, 1, 0) to specify vup. This is convenient and will naturally keep your camera
horizontally level until you decide to experiment with crazy camera angles.
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
...
private:
int image_height; // Rendered image height
point3 center; // Camera center
point3 pixel00_loc; // Location of pixel 0, 0
vec3 pixel_delta_u; // Offset to pixel to the right
vec3 pixel_delta_v; // Offset to pixel below
vec3 u, v, w; // Camera frame basis vectors
void initialize() {
image_height = static_cast<int>(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
center = lookfrom;
// Calculate the u,v,w unit basis vectors for the camera coordinate frame.
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge
vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
...
private:
};
int main() {
hittable_list world;
camera cam;
cam.vfov = 90;
cam.lookfrom = point3(-2,2,1);
cam.lookat = point3(0,0,-1);
cam.vup = vec3(0,1,0);
cam.render(world);
}
to get:
cam.vfov = 20;
The reason we have defocus blur in real cameras is because they need a big hole (rather than just a pinhole) through
which to gather light. A large hole would defocus everything, but if we stick a lens in front of the film/sensor, there will be
a certain distance at which everything is in focus. Objects placed at that distance will appear in focus and will linearly
appear blurrier the further they are from that distance. You can think of a lens this way: all light rays coming from a
specific point at the focus distance — and that hit the lens — will be bent back to a single point on the image sensor.
We call the distance between the camera center and the plane where everything is in perfect focus the focus distance.
Be aware that the focus distance is not usually the same as the focal length — the focal length is the distance between
the camera center and the image plane. For our model, however, these two will have the same value, as we will put our
pixel grid right on the focus plane, which is focus distance away from the camera center.
In a physical camera, the focus distance is controlled by the distance between the lens and the film/sensor. That is why
you see the lens move relative to the camera when you change what is in focus (that may happen in your phone
camera too, but the sensor moves). The “aperture” is a hole to control how big the lens is effectively. For a real camera,
if you need more light you make the aperture bigger, and will get more blur for objects away from the focus distance. For
our virtual camera, we can have a perfect sensor and never need more light, so we only use an aperture when we want
defocus blur.
We don’t need to simulate any of the inside of the camera — for the purposes of rendering an image outside the
camera, that would be unnecessary complexity. Instead, I usually start rays from an infinitely thin circular “lens”, and
send them toward the pixel of interest on the focus plane (focal_length away from the lens), where everything on that
plane in the 3D world is in perfect focus.
In practice, we accomplish this by placing the viewport in this plane. Putting everything together:
So, how large should the defocus disk be? Since the size of this disk controls how much defocus blur we get, that
should be a parameter of the camera class. We could just take the radius of the disk as a camera parameter, but the
blur would vary depending on the projection distance. A slightly easier parameter is to specify the angle of the cone with
apex at viewport center and base (defocus disk) at the camera center. This should give you more consistent results as
you vary the focus distance for a given shot.
Since we'll be choosing random points from the defocus disk, we'll need a function to do that: random_in_unit_disk().
This function works using the same kind of method we use in random_in_unit_sphere(), just for two dimensions.
Now let's update the camera to originate rays from the defocus disk:
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
...
private:
int image_height; // Rendered image height
point3 center; // Camera center
point3 pixel00_loc; // Location of pixel 0, 0
vec3 pixel_delta_u; // Offset to pixel to the right
vec3 pixel_delta_v; // Offset to pixel below
vec3 u, v, w; // Camera frame basis vectors
vec3 defocus_disk_u; // Defocus disk horizontal radius
vec3 defocus_disk_v; // Defocus disk vertical radius
void initialize() {
image_height = static_cast<int>(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
center = lookfrom;
// Calculate the u,v,w unit basis vectors for the camera coordinate frame.
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge
vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge
// Calculate the horizontal and vertical delta vectors to the next pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
...
point3 defocus_disk_sample() const {
// Returns a random point in the camera defocus disk.
auto p = random_in_unit_disk();
return center + (p[0] * defocus_disk_u) + (p[1] * defocus_disk_v);
}
int main() {
...
camera cam;
cam.vfov = 20;
cam.lookfrom = point3(-2,2,1);
cam.lookat = point3(0,0,-1);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 10.0;
cam.focus_dist = 3.4;
cam.render(world);
}
We get:
camera cam;
cam.vfov = 20;
cam.lookfrom = point3(13,2,3);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0.6;
cam.focus_dist = 10.0;
cam.render(world);
}
(Note that the code above differs slightly from the project sample code: the samples_per_pixel is set to 500 above for a
high-quality image that will take quite a while to render. The sample code uses a value of 10 in the interest of
reasonable run times while developing and validating.)
This gives:
An interesting thing you might note is the glass balls don’t really have shadows which makes them look like they are
floating. This is not a bug — you don’t see glass balls much in real life, where they also look a bit strange, and indeed
seem to float on cloudy days. A point on the big sphere under a glass ball still has lots of light hitting it because the sky
is re-ordered rather than blocked.
1. Lights — You can do this explicitly, by sending shadow rays to lights, or it can be done implicitly by making some
objects emit light, biasing scattered rays toward them, and then downweighting those rays to cancel out the bias.
Both work. I am in the minority in favoring the latter approach.
2. Triangles — Most cool models are in triangle form. The model I/O is the worst and almost everybody tries to get
somebody else’s code to do this.
3. Surface Textures — This lets you paste images on like wall paper. Pretty easy and a good thing to do.
4. Solid textures — Ken Perlin has his code online. Andrew Kensler has some very cool info at his blog.
5. Volumes and Media — Cool stuff and will challenge your software architecture. I favor making volumes have the
hittable interface and probabilistically have intersections based on density. Your rendering code doesn’t even
have to know it has volumes with that method.
6. Parallelism — Run N copies of your code on N cores with different random seeds. Average the N runs. This
averaging can also be done hierarchically where N /2 pairs can be averaged to get N /4 images, and pairs of
those can be averaged. That method of parallelism should extend well into the thousands of cores with very little
coding.
15. Acknowledgments
Original Manuscript Help
Special Thanks
These books are entirely written in Morgan McGuire's fantastic and free Markdeep library. To see what this looks
like, view the page source from your browser.
Thanks to Helen Hu for graciously donating her https://github.com/RayTracing/ GitHub organization to this project.
16.2. Snippets
16.2.1 Markdown
<a href="https://raytracing.github.io/books/RayTracingInOneWeekend.html">
<cite>Ray Tracing in One Weekend</cite>
</a>
~\cite{Shirley2023RTW1}
@misc{Shirley2023RTW1,
title = {Ray Tracing in One Weekend},
author = {Peter Shirley, Trevor David Black, Steve Hollasch},
year = {2023},
month = {August},
note = {\small \texttt{https://raytracing.github.io/books/RayTracingInOneWeekend.html}},
url = {https://raytracing.github.io/books/RayTracingInOneWeekend.html}
}
16.2.4 BibLaTeX
\usepackage{biblatex}
~\cite{Shirley2023RTW1}
@online{Shirley2023RTW1,
title = {Ray Tracing in One Weekend},
author = {Peter Shirley, Trevor David Black, Steve Hollasch},
year = {2023},
month = {August},
url = {https://raytracing.github.io/books/RayTracingInOneWeekend.html}
}
16.2.5 IEEE
16.2.6 MLA: