**Ray Tracing: The Next Week**
[Peter Shirley][], [Trevor David Black][], [Steve Hollasch][]
Version 4.0.1, 2024-08-31
Copyright 2018-2024 Peter Shirley. All rights reserved.
Overview
====================================================================================================
In Ray Tracing in One Weekend, you built a simple brute force path tracer. In this installment we’ll
add textures, volumes (like fog), rectangles, instances, lights, and support for lots of objects
using a BVH. When done, you’ll have a “real” ray tracer.
A heuristic in ray tracing that many people--including me--believe, is that most optimizations
complicate the code without delivering much speedup. What I will do in this mini-book is go with the
simplest approach in each design decision I make. See [our Further Reading wiki page][wiki-further]
for additional project related resources. However, I strongly encourage you to do no
premature optimization; if it doesn’t show up high in the execution time profile, it doesn’t need
optimization until all the features are supported!
The two hardest parts of this book are the BVH and the Perlin textures. This is why the title
suggests you take a week rather than a weekend for this endeavor. But you can save those for last if
you want a weekend project. Order is not very important for the concepts presented in this book, and
without BVH and Perlin texture you will still get a Cornell Box!
See the [project README][readme] file for information about this project, the repository on GitHub,
directory structure, building & running, and how to make or reference corrections and contributions.
These books have been formatted to print well directly from your browser. We also include PDFs of
each book [with each release][releases], in the "Assets" section.
Thanks to everyone who lent a hand on this project. You can find them in the acknowledgments section
at the end of this book.
Motion Blur
====================================================================================================
When you decided to ray trace, you decided that visual quality was worth more than run-time. When
rendering fuzzy reflection and defocus blur, we used multiple samples per pixel. Once you have taken
a step down that road, the good news is that almost _all_ effects can be similarly brute-forced.
Motion blur is certainly one of those.
In a real camera, the shutter remains open for a short time interval, during which the camera and
objects in the world may move. To accurately reproduce such a camera shot, we seek an average of
what the camera senses while its shutter is open to the world.
Introduction of SpaceTime Ray Tracing
--------------------------------------
We can get a random estimate of a single (simplified) photon by sending a single ray at some random
instant in time while the shutter is open. As long as we can determine where the objects are
supposed to be at that instant, we can get an accurate measure of the light for that ray at that
same instant. This is yet another example of how random (Monte Carlo) ray tracing ends up being
quite simple. Brute force wins again!
Since the “engine” of the ray tracer can just make sure the objects are where they need to be for
each ray, the intersection guts don’t change much. To accomplish this, we need to store the exact
time for each ray:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class ray {
public:
ray() {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
ray(const point3& origin, const vec3& direction, double time)
: orig(origin), dir(direction), tm(time) {}
ray(const point3& origin, const vec3& direction)
: ray(origin, direction, 0) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
const point3& origin() const { return orig; }
const vec3& direction() const { return dir; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double time() const { return tm; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
point3 at(double t) const {
return orig + t*dir;
}
private:
point3 orig;
vec3 dir;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double tm;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [time-ray]: [ray.h] Ray with time information]
Managing Time
--------------
Before continuing, let's think about time, and how we might manage it across one or more successive
renders. There are two aspects of shutter timing to think about: the time from one shutter opening
to the next shutter opening, and how long the shutter stays open for each frame. Standard movie film
used to be shot at 24 frames per second. Modern digital movies can be 24, 30, 48, 60, 120 or however
many frames per second director wants.
Each frame can have its own shutter speed. This shutter speed need not be -- and typically isn't --
the maximum duration of the entire frame. You could have the shutter open for 1/1000th of a second
every frame, or 1/60th of a second.
If you wanted to render a sequence of images, you would need to set up the camera with the
appropriate shutter timings: frame-to-frame period, shutter/render duration, and the total number of
frames (total shot time). If the camera is moving and the world is static, you're good to go.
However, if anything in the world is moving, you would need to add a method to `hittable` so that
every object could be made aware of the current frame's time period. This method would provide a way
for all animated objects to set up their motion during that frame.
This is fairly straight-forward, and definitely a fun avenue for you to experiment with if you wish.
However, for our purposes right now, we're going to proceed with a much simpler model. We will
render only a single frame, implicitly assuming a start at time = 0 and ending at time = 1. Our
first task is to modify the camera to launch rays with random times in $[0,1]$, and our second task
will be the creation of an animated sphere class.
Updating the Camera to Simulate Motion Blur
--------------------------------------------
We need to modify the camera to generate rays at a random instant between the start time and the end
time. Should the camera keep track of the time interval, or should that be up to the user of the
camera when a ray is created? When in doubt, I like to make constructors complicated if it makes
calls simple, so I will make the camera keep track, but that’s a personal preference. Not many
changes are needed to camera because for now it is not allowed to move; it just sends out rays over
a time period.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class camera {
...
private:
...
ray get_ray(int i, int j) const {
// Construct a camera ray originating from the defocus disk and directed at a randomly
// sampled point around the pixel location i, j.
auto offset = sample_square();
auto pixel_sample = pixel00_loc
+ ((i + offset.x()) * pixel_delta_u)
+ ((j + offset.y()) * pixel_delta_v);
auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample();
auto ray_direction = pixel_sample - ray_origin;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto ray_time = random_double();
return ray(ray_origin, ray_direction, ray_time);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [time-camera]: [camera.h] Camera with time information]
Adding Moving Spheres
----------------------
Now to create a moving object. I’ll update the sphere class so that its center moves linearly from
`center1` at time=0 to `center2` at time=1. (It continues on indefinitely outside that time
interval, so it really can be sampled at any time.) We'll do this by replacing the 3D center point
with a 3D ray that describes the original position at time=0 and the displacement to the end
position at time=1.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// Stationary Sphere
sphere(const point3& static_center, double radius, shared_ptr mat)
: center(static_center, vec3(0,0,0)), radius(std::fmax(0,radius)), mat(mat) {}
// Moving Sphere
sphere(const point3& center1, const point3& center2, double radius,
shared_ptr mat)
: center(center1, center2 - center1), radius(std::fmax(0,radius)), mat(mat) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
private:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
ray center;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
double radius;
shared_ptr mat;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [moving-sphere]: [sphere.h] A moving sphere]
The updated `sphere::hit()` function is almost identical to the old `sphere::hit()` function:
we just need to now determine the current position of the animated center:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
point3 current_center = center.at(r.time());
vec3 oc = current_center - r.origin();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto a = r.direction().length_squared();
auto h = dot(r.direction(), oc);
auto c = oc.length_squared() - radius*radius;
...
rec.t = root;
rec.p = r.at(rec.t);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 outward_normal = (rec.p - current_center) / radius;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
rec.set_face_normal(r, outward_normal);
get_sphere_uv(outward_normal, rec.u, rec.v);
rec.mat = mat;
return true;
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [moving-sphere-hit]: [sphere.h] Moving sphere hit function]
Tracking the Time of Ray Intersection
--------------------------------------
Now that rays have a time property, we need to update the `material::scatter()` methods to account
for the time of intersection:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class lambertian : public material {
...
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;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
scattered = ray(rec.p, scatter_direction, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
attenuation = albedo;
return true;
}
...
};
class metal : public material {
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
vec3 reflected = reflect(r_in.direction(), rec.normal);
reflected = unit_vector(reflected) + (fuzz * random_unit_vector());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
scattered = ray(rec.p, reflected, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
...
};
class dielectric : public material {
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
scattered = ray(rec.p, direction, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
return true;
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [material-time]: [material.h] Handle ray time in the material::scatter() methods]
Putting Everything Together
----------------------------
The code below takes the example diffuse spheres from the scene at the end of the last book, and
makes them move during the image render. Each sphere moves from its center $\mathbf{C}$ at time
$t=0$ to $\mathbf{C} + (0, r/2, 0)$ at time $t=1$:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
hittable_list world;
auto ground_material = make_shared(color(0.5, 0.5, 0.5));
world.add(make_shared(point3(0,-1000,0), 1000, ground_material));
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
auto choose_mat = random_double();
point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double());
if ((center - point3(4, 0.2, 0)).length() > 0.9) {
shared_ptr sphere_material;
if (choose_mat < 0.8) {
// diffuse
auto albedo = color::random() * color::random();
sphere_material = make_shared(albedo);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto center2 = center + vec3(0, random_double(0,.5), 0);
world.add(make_shared(center, center2, 0.2, sphere_material));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
} else if (choose_mat < 0.95) {
...
}
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.image_width = 400;
cam.samples_per_pixel = 100;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
cam.max_depth = 50;
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);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-spheres-moving]: [main.cc] Last book's final scene, but with moving spheres]
Bounding Volume Hierarchies
====================================================================================================
This part is by far the most difficult and involved part of the ray tracer we are working on. I am
sticking it in this chapter so the code can run faster, and because it refactors `hittable` a
little, and when I add rectangles and boxes we won't have to go back and refactor them.
Ray-object intersection is the main time-bottleneck in a ray tracer, and the run time is linear with
the number of objects. But it’s a repeated search on the same scene, so we ought to be able to make
it a logarithmic search in the spirit of binary search. Because we are sending millions to billions
of rays into the same scene, we can sort the objects in the scene, and then each ray intersection
can be a sublinear search. The two most common methods of sorting are to 1) subdivide the space, and
2) subdivide the objects. The latter is usually much easier to code up, and just as fast to run for
most models.
The Key Idea
-------------
The key idea of creating bounding volumes for a set of primitives is to find a volume that fully
encloses (bounds) all the objects. For example, suppose you computed a sphere that bounds 10
objects. Any ray that misses the bounding sphere definitely misses all ten objects inside. If the
ray hits the bounding sphere, then it might hit one of the ten objects. So the bounding code is
always of the form:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (ray hits bounding object)
return whether ray hits bounded objects
else
return false
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Note that we will use these bounding volumes to group the objects in the scene into subgroups. We
are *not* dividing the screen or the scene space. We want any given object to be in just one
bounding volume, though bounding volumes can overlap.
Hierarchies of Bounding Volumes
--------------------------------
To make things sub-linear we need to make the bounding volumes hierarchical. For example, if we
divided a set of objects into two groups, red and blue, and used rectangular bounding volumes, we’d
have:
![Figure [bvol-hierarchy]: Bounding volume hierarchy](../images/fig-2.01-bvol-hierarchy.jpg)
Note that the blue and red bounding volumes are contained in the purple one, but they might overlap,
and they are not ordered -- they are just both inside. So the tree shown on the right has no concept
of ordering in the left and right children; they are simply inside. The code would be:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (hits purple)
hit0 = hits blue enclosed objects
hit1 = hits red enclosed objects
if (hit0 or hit1)
return true and info of closer hit
return false
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Axis-Aligned Bounding Boxes (AABBs)
------------------------------------
To get that all to work we need a way to make good divisions, rather than bad ones, and a way to
intersect a ray with a bounding volume. A ray bounding volume intersection needs to be fast, and
bounding volumes need to be pretty compact. In practice for most models, axis-aligned boxes work
better than the alternatives (such as the spherical bounds mentioned above), but this design choice
is always something to keep in mind if you encounter other types of bounding models.
From now on we will call axis-aligned bounding rectangular parallelepipeds (really, that is what
they need to be called if we're being precise) _axis-aligned bounding boxes_, or AABBs. (In the
code, you will also come across the naming abbreviation "bbox" for "bounding box".) Any method you
want to use to intersect a ray with an AABB is fine. And all we need to know is whether or not we
hit it; we don’t need hit points or normals or any of the stuff we need to display the object.
Most people use the “slab” method. This is based on the observation that an n-dimensional AABB is
just the intersection of $n$ axis-aligned intervals, often called “slabs”. Recall that an interval
is just the points within two endpoints, for example, $x$ such that $3 \leq x \leq 5$, or more
succinctly $x$ in $[3,5]$. In 2D, an AABB (a rectangle) is defined by the overlap two intervals:
![Figure [2d-aabb]: 2D axis-aligned bounding box](../images/fig-2.02-2d-aabb.jpg)
To determine if a ray hits one interval, we first need to figure out whether the ray hits the
boundaries. For example, in 1D, ray intersection with two planes will yield the ray parameters $t_0$
and $t_1$. (If the ray is parallel to the planes, its intersection with any plane will be
undefined.)
![Figure [ray-slab]: Ray-slab intersection](../images/fig-2.03-ray-slab.jpg)
How do we find the intersection between a ray and a plane? Recall that the ray is just defined by a
function that--given a parameter $t$--returns a location $\mathbf{P}(t)$:
$$ \mathbf{P}(t) = \mathbf{Q} + t \mathbf{d} $$
This equation applies to all three of the x/y/z coordinates. For example, $x(t) = Q_x + t d_x$. This
ray hits the plane $x = x_0$ at the parameter $t$ that satisfies this equation:
$$ x_0 = Q_x + t_0 d_x $$
So $t$ at the intersection is given by
$$ t_0 = \frac{x_0 - Q_x}{d_x} $$
We get the similar expression for $x_1$:
$$ t_1 = \frac{x_1 - Q_x}{d_x} $$
The key observation to turn that 1D math into a 2D or 3D hit test is this: if a ray intersects the
box bounded by all pairs of planes, then all $t$-intervals will overlap. For example, in 2D the
green and blue overlapping only happens if the ray intersects the bounded box:
![Figure [ray-slab-interval]: Ray-slab $t$-interval overlap
](../images/fig-2.04-ray-slab-interval.jpg)
In this figure, the upper ray intervals do not overlap, so we know the ray does _not_ hit the 2D box
bounded by the green and blue planes. The lower ray intervals _do_ overlap, so we know the lower ray
_does_ hit the bounded box.
Ray Intersection with an AABB
------------------------------
The following pseudocode determines whether the $t$ intervals in the slab overlap:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
interval_x ← compute_intersection_x (ray, x0, x1)
interval_y ← compute_intersection_y (ray, y0, y1)
return overlaps(interval_x, interval_y)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
That is awesomely simple, and the fact that the 3D version trivially extends the above is why people
love the slab method:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
interval_x ← compute_intersection_x (ray, x0, x1)
interval_y ← compute_intersection_y (ray, y0, y1)
interval_z ← compute_intersection_z (ray, z0, z1)
return overlaps(interval_x, interval_y, interval_z)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are some caveats that make this less pretty than it first appears. Consider again the 1D
equations for $t_0$ and $t_1$:
$$ t_0 = \frac{x_0 - Q_x}{d_x} $$
$$ t_1 = \frac{x_1 - Q_x}{d_x} $$
First, suppose the ray is traveling in the negative $\mathbf{x}$ direction. The interval $(t_{x0},
t_{x1})$ as computed above might be reversed, like $(7, 3)$ for example. Second, the denominator
$d_x$ could be zero, yielding infinite values. And if the ray origin lies on one of the slab
boundaries, we can get a `NaN`, since both the numerator and the denominator can be zero. Also, the
zero will have a ± sign when using IEEE floating point.
The good news for $d_x = 0$ is that $t_{x0}$ and $t_{x1}$ will be equal: both +∞ or -∞, if not
between $x_0$ and $x_1$. So, using min and max should get us the right answers:
$$ t_{x0} = \min(
\frac{x_0 - Q_x}{d_x},
\frac{x_1 - Q_x}{d_x})
$$
$$ t_{x1} = \max(
\frac{x_0 - Q_x}{d_x},
\frac{x_1 - Q_x}{d_x})
$$
The remaining troublesome case if we do that is if $d_x = 0$ and either $x_0 - Q_x = 0$ or
$x_1 - Q_x = 0$ so we get a `NaN`. In that case we can arbitrarily interpret that as either hit or
no hit, but we’ll revisit that later.
Now, let’s look at the pseudo-function `overlaps`. Suppose we can assume the intervals are not
reversed, and we want to return true when the intervals overlap. The boolean `overlaps()` function
computes the overlap of the $t$ intervals `t_interval1` and `t_interval2`, and uses that to
determine if that overlap is non-empty:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bool overlaps(t_interval1, t_interval2)
t_min ← max(t_interval1.min, t_interval2.min)
t_max ← min(t_interval1.max, t_interval2.max)
return t_min < t_max
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If there are any `NaN`s running around there, the compare will return false, so we need to be sure
our bounding boxes have a little padding if we care about grazing cases (and we probably _should_
because in a ray tracer all cases come up eventually).
To accomplish this, we'll first add a new `interval` function `expand`, which pads an interval by a
given amount:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class interval {
public:
...
double clamp(double x) const {
if (x < min) return min;
if (x > max) return max;
return x;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
interval expand(double delta) const {
auto padding = delta/2;
return interval(min - padding, max + padding);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
static const interval empty, universe;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [interval-expand]: [interval.h] interval::expand() method]
Now we have everything we need to implement the new AABB class.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef AABB_H
#define AABB_H
class aabb {
public:
interval x, y, z;
aabb() {} // The default AABB is empty, since intervals are empty by default.
aabb(const interval& x, const interval& y, const interval& z)
: x(x), y(y), z(z) {}
aabb(const point3& a, const point3& b) {
// Treat the two points a and b as extrema for the bounding box, so we don't require a
// particular minimum/maximum coordinate order.
x = (a[0] <= b[0]) ? interval(a[0], b[0]) : interval(b[0], a[0]);
y = (a[1] <= b[1]) ? interval(a[1], b[1]) : interval(b[1], a[1]);
z = (a[2] <= b[2]) ? interval(a[2], b[2]) : interval(b[2], a[2]);
}
const interval& axis_interval(int n) const {
if (n == 1) return y;
if (n == 2) return z;
return x;
}
bool hit(const ray& r, interval ray_t) const {
const point3& ray_orig = r.origin();
const vec3& ray_dir = r.direction();
for (int axis = 0; axis < 3; axis++) {
const interval& ax = axis_interval(axis);
const double adinv = 1.0 / ray_dir[axis];
auto t0 = (ax.min - ray_orig[axis]) * adinv;
auto t1 = (ax.max - ray_orig[axis]) * adinv;
if (t0 < t1) {
if (t0 > ray_t.min) ray_t.min = t0;
if (t1 < ray_t.max) ray_t.max = t1;
} else {
if (t1 > ray_t.min) ray_t.min = t1;
if (t0 < ray_t.max) ray_t.max = t0;
}
if (ray_t.max <= ray_t.min)
return false;
}
return true;
}
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb]: [aabb.h] Axis-aligned bounding box class]
Constructing Bounding Boxes for Hittables
------------------------------------------
We now need to add a function to compute the bounding boxes of all the hittables. Then we will make
a hierarchy of boxes over all the primitives, and the individual primitives--like spheres--will live
at the leaves.
Recall that `interval` values constructed without arguments will be empty by default. Since an
`aabb` object has an interval for each of its three dimensions, each of these will then be empty by
default, and therefore `aabb` objects will be empty by default. Thus, some objects may have empty
bounding volumes. For example, consider a `hittable_list` object with no children. Happily, the way
we've designed our interval class, the math all works out.
Finally, recall that some objects may be animated. Such objects should return their bounds over the
entire range of motion, from time=0 to time=1.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "aabb.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class material;
...
class hittable {
public:
virtual ~hittable() = default;
virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
virtual aabb bounding_box() const = 0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [hittable-bbox]: [hittable.h] Hittable class with bounding box]
For a stationary sphere, the `bounding_box` function is easy:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
// Stationary Sphere
sphere(const point3& static_center, double radius, shared_ptr mat)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
: center(static_center, vec3(0,0,0)), radius(std::fmax(0,radius)), mat(mat)
{
auto rvec = vec3(radius, radius, radius);
bbox = aabb(static_center - rvec, static_center + rvec);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
private:
ray center;
double radius;
shared_ptr mat;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [sphere-bbox]: [sphere.h] Sphere with bounding box]
For a moving sphere, we want the bounds of its entire range of motion. To do this, we can take the
box of the sphere at time=0, and the box of the sphere at time=1, and compute the box around those
two boxes.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
...
// Moving Sphere
sphere(const point3& center1, const point3& center2, double radius,
shared_ptr mat)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
: center(center1, center2 - center1), radius(std::fmax(0,radius)), mat(mat)
{
auto rvec = vec3(radius, radius, radius);
aabb box1(center.at(0) - rvec, center.at(0) + rvec);
aabb box2(center.at(1) - rvec, center.at(1) + rvec);
bbox = aabb(box1, box2);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [moving-sphere-bbox]: [sphere.h] Moving sphere with bounding box]
Now we need a new `aabb` constructor that takes two boxes as input. First, we'll add a new interval
constructor to do this:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class interval {
public:
double min, max;
interval() : min(+infinity), max(-infinity) {} // Default interval is empty
interval(double _min, double _max) : min(_min), max(_max) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
interval(const interval& a, const interval& b) {
// Create the interval tightly enclosing the two input intervals.
min = a.min <= b.min ? a.min : b.min;
max = a.max >= b.max ? a.max : b.max;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
double size() const {
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [interval-from-intervals]: [interval.h] Interval constructor from two intervals]
Now we can use this to construct an axis-aligned bounding box from two input boxes.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class aabb {
public:
...
aabb(const point3& a, const point3& b) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb(const aabb& box0, const aabb& box1) {
x = interval(box0.x, box1.x);
y = interval(box0.y, box1.y);
z = interval(box0.z, box1.z);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb-from-two-aabb]: [aabb.h] AABB constructor from two AABB inputs]
Creating Bounding Boxes of Lists of Objects
--------------------------------------------
Now we'll update the `hittable_list` object, computing the bounds of its children. We'll update the
bounding box incrementally as each new child is added.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "aabb.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
#include
class hittable_list : public hittable {
public:
std::vector> objects;
...
void add(shared_ptr object) {
objects.push_back(object);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
bbox = aabb(bbox, object->bounding_box());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
private:
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [hit-list-bbox]: [hittable_list.h] Hittable list with bounding box]
The BVH Node Class
-------------------
A BVH is also going to be a `hittable` -- just like lists of `hittable`s. It’s really a container,
but it can respond to the query “does this ray hit you?”. One design question is whether we have two
classes, one for the tree, and one for the nodes in the tree; or do we have just one class and have
the root just be a node we point to. The `hit` function is pretty straightforward: check whether the
box for the node is hit, and if so, check the children and sort out any details.
I am a fan of the one class design when feasible. Here is such a class:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef BVH_H
#define BVH_H
#include "aabb.h"
#include "hittable.h"
#include "hittable_list.h"
class bvh_node : public hittable {
public:
bvh_node(hittable_list list) : bvh_node(list.objects, 0, list.objects.size()) {
// There's a C++ subtlety here. This constructor (without span indices) creates an
// implicit copy of the hittable list, which we will modify. The lifetime of the copied
// list only extends until this constructor exits. That's OK, because we only need to
// persist the resulting bounding volume hierarchy.
}
bvh_node(std::vector>& objects, size_t start, size_t end) {
// To be implemented later.
}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
if (!bbox.hit(r, ray_t))
return false;
bool hit_left = left->hit(r, ray_t, rec);
bool hit_right = right->hit(r, interval(ray_t.min, hit_left ? rec.t : ray_t.max), rec);
return hit_left || hit_right;
}
aabb bounding_box() const override { return bbox; }
private:
shared_ptr left;
shared_ptr right;
aabb bbox;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [bvh]: [bvh.h] Bounding volume hierarchy]
Splitting BVH Volumes
----------------------
The most complicated part of any efficiency structure, including the BVH, is building it. We do this
in the constructor. A cool thing about BVHs is that as long as the list of objects in a `bvh_node`
gets divided into two sub-lists, the hit function will work. It will work best if the division is
done well, so that the two children have smaller bounding boxes than their parent’s bounding box,
but that is for speed not correctness. I’ll choose the middle ground, and at each node split the
list along one axis. I’ll go for simplicity:
1. randomly choose an axis
2. sort the primitives (`using std::sort`)
3. put half in each subtree
When the list coming in is two elements, I put one in each subtree and end the recursion. The
traversal algorithm should be smooth and not have to check for null pointers, so if I just have one
element I duplicate it in each subtree. Checking explicitly for three elements and just following
one recursion would probably help a little, but I figure the whole method will get optimized later.
The following code uses three methods--`box_x_compare`, `box_y_compare_`, and `box_z_compare`--that
we haven't yet defined.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "aabb.h"
#include "hittable.h"
#include "hittable_list.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class bvh_node : public hittable {
public:
...
bvh_node(std::vector>& objects, size_t start, size_t end) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
int axis = random_int(0,2);
auto comparator = (axis == 0) ? box_x_compare
: (axis == 1) ? box_y_compare
: box_z_compare;
size_t object_span = end - start;
if (object_span == 1) {
left = right = objects[start];
} else if (object_span == 2) {
left = objects[start];
right = objects[start+1];
} else {
std::sort(std::begin(objects) + start, std::begin(objects) + end, comparator);
auto mid = start + object_span/2;
left = make_shared(objects, start, mid);
right = make_shared(objects, mid, end);
}
bbox = aabb(left->bounding_box(), right->bounding_box());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [bvh-node]: [bvh.h] Bounding volume hierarchy node]
This uses a new function: `random_int()`:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
inline double random_double(double min, double max) {
// Returns a random real in [min,max).
return min + (max-min)*random_double();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
inline int random_int(int min, int max) {
// Returns a random integer in [min,max].
return int(random_double(min, max+1));
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [random-int]: [rtweekend.h] A function to return random integers in a range]
The check for whether there is a bounding box at all is in case you sent in something like an
infinite plane that doesn’t have a bounding box. We don’t have any of those primitives, so it
shouldn’t happen until you add such a thing.
The Box Comparison Functions
-----------------------------
Now we need to implement the box comparison functions, used by `std::sort()`. To do this, create a
generic comparator returns true if the first argument is less than the second, given an additional
axis index argument. Then define axis-specific comparison functions that use the generic comparison
function.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class bvh_node : public hittable {
...
private:
shared_ptr left;
shared_ptr right;
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static bool box_compare(
const shared_ptr a, const shared_ptr b, int axis_index
) {
auto a_axis_interval = a->bounding_box().axis_interval(axis_index);
auto b_axis_interval = b->bounding_box().axis_interval(axis_index);
return a_axis_interval.min < b_axis_interval.min;
}
static bool box_x_compare (const shared_ptr a, const shared_ptr b) {
return box_compare(a, b, 0);
}
static bool box_y_compare (const shared_ptr a, const shared_ptr b) {
return box_compare(a, b, 1);
}
static bool box_z_compare (const shared_ptr a, const shared_ptr b) {
return box_compare(a, b, 2);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [bvh-x-comp]: [bvh.h] BVH comparison function, X-axis]
At this point, we're ready to use our new BVH code. Let's use it on our random spheres scene.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "bvh.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
int main() {
...
auto material2 = make_shared(color(0.4, 0.2, 0.1));
world.add(make_shared(point3(-4, 1, 0), 1.0, material2));
auto material3 = make_shared(color(0.7, 0.6, 0.5), 0.0);
world.add(make_shared(point3(4, 1, 0), 1.0, material3));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
world = hittable_list(make_shared(world));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [random-spheres-bvh]: [main.cc] Random spheres, using BVH]
The rendered image should be identical to the non-BVH version shown in
[image 1](#image-bouncing-spheres). However, if you time the two versions, the BVH version should be
faster. I see a speedup of almost _six and a half times_ the prior version.
Another BVH Optimization
-------------------------
We can speed up the BVH optimization a bit more. Instead of choosing a random splitting axis, let's
split the longest axis of the enclosing bounding box to get the most subdivision. The change is
straight-forward, but we'll add a few things to the `aabb` class in the process.
The first task is to construct an axis-aligned bounding box of the span of objects in the BVH
constructor. Basically, we'll construct the bounding box of the `bvh_node` from this span by
initializing the bounding box to empty (we'll define `aabb::empty` shortly), and then augment it
with each bounding box in the span of objects.
Once we have the bounding box, set the splitting axis to the one with the longest side. We'll
imagine a function that does that for us: `aabb::longest_axis()`. Finally, since we're computing the
bounding box of the object span up front, we can delete the original line that computed it as the
union of the left and right sides.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class bvh_node : public hittable {
public:
...
bvh_node(std::vector>& objects, size_t start, size_t end) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// Build the bounding box of the span of source objects.
bbox = aabb::empty;
for (size_t object_index=start; object_index < end; object_index++)
bbox = aabb(bbox, objects[object_index]->bounding_box());
int axis = bbox.longest_axis();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto comparator = (axis == 0) ? box_x_compare
: (axis == 1) ? box_y_compare
: box_z_compare;
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
bbox = aabb(left->bounding_box(), right->bounding_box());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [object-span-bbox]: [bvh.h] Building the bbox for the span of BVH objects]
Now to implement the empty `aabb` code and the new `aabb::longest_axis()` function:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class aabb {
public:
...
bool hit(const ray& r, interval ray_t) const {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
int longest_axis() const {
// Returns the index of the longest axis of the bounding box.
if (x.size() > y.size())
return x.size() > z.size() ? 0 : 2;
else
return y.size() > z.size() ? 1 : 2;
}
static const aabb empty, universe;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty);
const aabb aabb::universe = aabb(interval::universe, interval::universe, interval::universe);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb-empty-and-axis]: [aabb.h] New aabb constants and longest_axis() function]
As before, you should see identical results to [image 1](#image-bouncing-spheres), but rendering a
little bit faster. On my system, this yields something like an additional 18% render speedup. Not
bad for a little extra work.
Texture Mapping
====================================================================================================
_Texture mapping_ in computer graphics is the process of applying a material effect to an object in
the scene. The "texture" part is the effect, and the "mapping" part is in the mathematical sense of
mapping one space onto another. This effect could be any material property: color, shininess, bump
geometry (called _Bump Mapping_), or even material existence (to create cut-out regions of the
surface).
The most common type of texture mapping maps an image onto the surface of an object, defining the
color at each point on the object’s surface. In practice, we implement the process in reverse: given
some point on the object, we’ll look up the color defined by the texture map.
To begin with, we'll make the texture colors procedural, and will create a texture map of constant
color. Most programs keep constant RGB colors and textures in different classes, so feel free to do
something different, but I am a big believer in this architecture because it's great being able to
make any color a texture.
In order to perform the texture lookup, we need a _texture coordinate_. This coordinate can be
defined in many ways, and we'll develop this idea as we progress. For now, we'll pass in two
dimensional texture coordinates. By convention, texture coordinates are named $u$ and $v$. For a
constant texture, every $(u,v)$ pair yields a constant color, so we can actually ignore the
coordinates completely. However, other texture types will need these coordinates, so we keep these
in the method interface.
The primary method of our texture classes is the `color value(...)` method, which returns the
texture color given the input coordinates. In addition to taking the point's texture coordinates $u$
and $v$, we also provide the position of the point in question, for reasons that will become
apparent later.
Constant Color Texture
-----------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef TEXTURE_H
#define TEXTURE_H
class texture {
public:
virtual ~texture() = default;
virtual color value(double u, double v, const point3& p) const = 0;
};
class solid_color : public texture {
public:
solid_color(const color& albedo) : albedo(albedo) {}
solid_color(double red, double green, double blue) : solid_color(color(red,green,blue)) {}
color value(double u, double v, const point3& p) const override {
return albedo;
}
private:
color albedo;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [texture]: [texture.h] A texture class]
We'll need to update the `hit_record` structure to store the $u,v$ surface coordinates of the
ray-object hit point.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class hit_record {
public:
vec3 p;
vec3 normal;
shared_ptr mat;
double t;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double u;
double v;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool front_face;
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [hit-record-uv]: [hittable.h] Adding $u,v$ coordinates to the `hit_record`]
In the future, we'll need to compute $(u,v)$ texture coordinates for a given point on each type of
`hittable`. More on that later.
Solid Textures: A Checker Texture
----------------------------------
A solid (or spatial) texture depends only on the position of each point in 3D space. You can think
of a solid texture as if it's coloring all of the points in space itself, instead of coloring a
given object in that space. For this reason, the object can move through the colors of the texture
as it changes position, though usually you would to fix the relationship between the object and the
solid texture.
To explore spatial textures, we'll implement a spatial `checker_texture` class, which implements a
three-dimensional checker pattern. Since a spatial texture function is driven by a given position in
space, the texture `value()` function ignores the `u` and `v` parameters, and uses only the `p`
parameter.
To accomplish the checkered pattern, we'll first compute the floor of each component of the input
point. We could truncate the coordinates, but that would pull values toward zero, which would give
us the same color on both sides of zero. The floor function will always shift values to the integer
value on the left (toward negative infinity). Given these three integer results ($\lfloor x \rfloor,
\lfloor y \rfloor, \lfloor z \rfloor$) we take their sum and compute the result modulo two, which
gives us either 0 or 1. Zero maps to the even color, and one to the odd color.
Finally, we add a scaling factor to the texture, to allow us to control the size of the checker
pattern in the scene.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class checker_texture : public texture {
public:
checker_texture(double scale, shared_ptr even, shared_ptr odd)
: inv_scale(1.0 / scale), even(even), odd(odd) {}
checker_texture(double scale, const color& c1, const color& c2)
: checker_texture(scale, make_shared(c1), make_shared(c2)) {}
color value(double u, double v, const point3& p) const override {
auto xInteger = int(std::floor(inv_scale * p.x()));
auto yInteger = int(std::floor(inv_scale * p.y()));
auto zInteger = int(std::floor(inv_scale * p.z()));
bool isEven = (xInteger + yInteger + zInteger) % 2 == 0;
return isEven ? even->value(u, v, p) : odd->value(u, v, p);
}
private:
double inv_scale;
shared_ptr even;
shared_ptr odd;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [checker-texture]: [texture.h] Checkered texture]
Those checker odd/even parameters can point to a constant texture or to some other procedural
texture. This is in the spirit of shader networks introduced by Pat Hanrahan back in the 1980s.
To support procedural textures, we'll extend the `lambertian` class to work with textures instead of
colors:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "texture.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
class lambertian : public material {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
lambertian(const color& albedo) : tex(make_shared(albedo)) {}
lambertian(shared_ptr tex) : tex(tex) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
attenuation = tex->value(rec.u, rec.v, rec.p);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
return true;
}
private:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
shared_ptr tex;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [lambertian-textured]: [material.h] Lambertian material with texture]
If we add this to our main scene:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "texture.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
hittable_list world;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto checker = make_shared(0.32, color(.2, .3, .1), color(.9, .9, .9));
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(checker)));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
for (int a = -11; a < 11; a++) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [checker-example]: [main.cc] Checkered texture in use]
we get:
![Image 2: Spheres on checkered ground
](../images/img-2.02-checker-ground.png class='pixel')
Rendering The Solid Checker Texture
------------------------------------
We're going to add a second scene to our program, and will add more scenes after that as we progress
through this book. To help with this, we'll set up a switch statement to select the desired scene
for a given run. It's a crude approach, but we're trying to keep things dead simple and focus on the
raytracing. You may want to use a different approach in your own raytracer, such as supporting
command-line arguments.
Here's what our main.cc looks like after refactoring for our single random spheres scene. Rename
`main()` to `bouncing_spheres()`, and add a new `main()` function to call it:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
#include "texture.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void bouncing_spheres() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
hittable_list world;
auto ground_material = make_shared(color(0.5, 0.5, 0.5));
world.add(make_shared(point3(0,-1000,0), 1000, ground_material));
...
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
int main() {
bouncing_spheres();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [main-scenes]: [main.cc] Main dispatching to selected scene]
Now add a scene with two checkered spheres, one atop the other.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
#include "texture.h"
void bouncing_spheres() {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void checkered_spheres() {
hittable_list world;
auto checker = make_shared(0.32, color(.2, .3, .1), color(.9, .9, .9));
world.add(make_shared(point3(0,-10, 0), 10, make_shared(checker)));
world.add(make_shared(point3(0, 10, 0), 10, make_shared(checker)));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
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;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (2) {
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [main-two-spheres]: [main.cc] Two checkered spheres]
We get this result:
![Image 3: Checkered spheres
](../images/img-2.03-checker-spheres.png class='pixel')
You may think the result looks a bit odd. Since `checker_texture` is a spatial texture, we're really
looking at the surface of the sphere cutting through the three-dimensional checker space. There are
many situations where this is perfect, or at least sufficient. In many other situations, we really
want to get a consistent effect on the surface of our objects. That approach is covered next.
Texture Coordinates for Spheres
--------------------------------
Constant-color textures use no coordinates. Solid (or spatial) textures use the coordinates of a
point in space. Now it's time to make use of the $u,v$ texture coordinates. These coordinates
specify the location on 2D source image (or in some 2D parameterized space). To get this, we need a
way to find the $u,v$ coordinates of any point on the surface of a 3D object. This mapping is
completely arbitrary, but generally you'd like to cover the entire surface, and be able to scale,
orient and stretch the 2D image in a way that makes some sense. We'll start with deriving a scheme
to get the $u,v$ coordinates of a sphere.
For spheres, texture coordinates are usually based on some form of longitude and latitude, _i.e._,
spherical coordinates. So we compute $(\theta,\phi)$ in spherical coordinates, where $\theta$ is the
angle up from the bottom pole (that is, up from -Y), and $\phi$ is the angle around the Y-axis (from
-X to +Z to +X to -Z back to -X).
We want to map $\theta$ and $\phi$ to texture coordinates $u$ and $v$ each in $[0,1]$, where
$(u=0,v=0)$ maps to the bottom-left corner of the texture. Thus the normalization from
$(\theta,\phi)$ to $(u,v)$ would be:
$$ u = \frac{\phi}{2\pi} $$
$$ v = \frac{\theta}{\pi} $$
To compute $\theta$ and $\phi$ for a given point on the unit sphere centered at the origin, we start
with the equations for the corresponding Cartesian coordinates:
$$ \begin{align*}
y &= -\cos(\theta) \\
x &= -\cos(\phi) \sin(\theta) \\
z &= \quad\sin(\phi) \sin(\theta)
\end{align*}
$$
We need to invert these equations to solve for $\theta$ and $\phi$. Because of the lovely ``
function `std::atan2()`, which takes any pair of numbers proportional to sine and cosine and returns
the angle, we can pass in $x$ and $z$ (the $\sin(\theta)$ cancel) to solve for $\phi$:
$$ \phi = \operatorname{atan2}(z, -x) $$
`std::atan2()` returns values in the range $-\pi$ to $\pi$, but they go from 0 to $\pi$, then flip
to $-\pi$ and proceed back to zero. While this is mathematically correct, we want $u$ to range from
$0$ to $1$, not from $0$ to $1/2$ and then from $-1/2$ to $0$. Fortunately,
$$ \operatorname{atan2}(a,b) = \operatorname{atan2}(-a,-b) + \pi, $$
and the second formulation yields values from $0$ continuously to $2\pi$. Thus, we can compute
$\phi$ as
$$ \phi = \operatorname{atan2}(-z, x) + \pi $$
The derivation for $\theta$ is more straightforward:
$$ \theta = \arccos(-y) $$
So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that takes
points on the unit sphere centered at the origin, and computes $u$ and $v$:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
...
private:
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static void get_sphere_uv(const point3& p, double& u, double& v) {
// p: a given point on the sphere of radius one, centered at the origin.
// u: returned value [0,1] of angle around the Y axis from X=-1.
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
auto theta = std::acos(-p.y());
auto phi = std::atan2(-p.z(), p.x()) + pi;
u = phi / (2*pi);
v = theta / pi;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [get-sphere-uv]: [sphere.h] get_sphere_uv function]
Update the `sphere::hit()` function to use this function to update the hit record UV coordinates.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - current_center) / radius;
rec.set_face_normal(r, outward_normal);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
get_sphere_uv(outward_normal, rec.u, rec.v);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
rec.mat = mat;
return true;
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [get-sphere-uv-call]: [sphere.h] Sphere UV coordinates from hit]
From the hitpoint $\mathbf{P}$, we compute the surface coordinates $(u,v)$. We then use these to
index into our procedural solid texture (like marble). We can also read in an image and use the 2D
$(u,v)$ texture coordinate to index into the image.
A direct way to use scaled $(u,v)$ in an image is to round the $u$ and $v$ to integers, and use that
as $(i,j)$ pixels. This is awkward, because we don’t want to have to change the code when we change
image resolution. So instead, one of the the most universal unofficial standards in graphics is to
use texture coordinates instead of image pixel coordinates. These are just some form of fractional
position in the image. For example, for pixel $(i,j)$ in an $N_x$ by $N_y$ image, the image texture
position is:
$$ u = \frac{i}{N_x-1} $$
$$ v = \frac{j}{N_y-1} $$
This is just a fractional position.
Accessing Texture Image Data
-----------------------------
Now it's time to create a texture class that holds an image. I am going to use my favorite image
utility, [stb_image][]. It reads image data into an array of 32-bit floating-point values. These are
just packed RGBs with each component in the range [0,1] (black to full white). In addition, images
are loaded in linear color space (gamma = 1) -- the color space in which we do all our computations.
To help make loading our image files even easier, we provide a helper class to manage all this:
`rtw_image`. It provides a helper function -- `pixel_data(int x, int y)` -- to get the 8-bit RGB
byte values for each pixel. The following listing assumes that you have copied the `stb_image.h`
header into a folder called `external`. Adjust according to your directory structure.
If you are writing your implementation in a language other than C or C++, you'll need to locate (or
write) an image loading library that provides similar functionality.
The `image_texture` class uses the `rtw_image` class:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "rtw_stb_image.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
class checker_texture : public texture {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class image_texture : public texture {
public:
image_texture(const char* filename) : image(filename) {}
color value(double u, double v, const point3& p) const override {
// If we have no texture data, then return solid cyan as a debugging aid.
if (image.height() <= 0) return color(0,1,1);
// Clamp input texture coordinates to [0,1] x [1,0]
u = interval(0,1).clamp(u);
v = 1.0 - interval(0,1).clamp(v); // Flip V to image coordinates
auto i = int(u * image.width());
auto j = int(v * image.height());
auto pixel = image.pixel_data(i,j);
auto color_scale = 1.0 / 255.0;
return color(color_scale*pixel[0], color_scale*pixel[1], color_scale*pixel[2]);
}
private:
rtw_image image;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [img-texture]: [texture.h] Image texture class]
Rendering The Image Texture
----------------------------
I just grabbed a random earth map from the web -- any standard projection will do for our purposes.
![Image 4: earthmap.jpg](../images/earthmap.jpg class='pixel')
Here's the code to read an image from a file and then assign it to a diffuse material:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void earth() {
auto earth_texture = make_shared("earthmap.jpg");
auto earth_surface = make_shared(earth_texture);
auto globe = make_shared(point3(0,0,0), 2, earth_surface);
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(0,0,12);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(hittable_list(globe));
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (3) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 3: earth(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [stbi-load-use]: [main.cc] Rendering with a texture map of Earth]
We start to see some of the power of all colors being textures -- we can assign any kind of texture
to the lambertian material, and lambertian doesn’t need to be aware of it.
If the photo comes back with a large cyan sphere in the middle, then `stb_image` failed to find your
Earth map photo. The program will look for the file in the same directory as the executable. Make
sure to copy the Earth into your build directory, or rewrite `earth()` to point somewhere else.
![Image 5: Earth-mapped sphere
](../images/img-2.05-earth-sphere.png class='pixel')
Perlin Noise
====================================================================================================
To get cool looking solid textures most people use some form of Perlin noise. These are named after
their inventor Ken Perlin. Perlin texture doesn’t return white noise like this:
![Image 6: White noise](../images/img-2.06-white-noise.jpg class='pixel')
Instead it returns something similar to blurred white noise:
![Image 7: White noise, blurred
](../images/img-2.07-white-noise-blurred.jpg class='pixel')
A key part of Perlin noise is that it is repeatable: it takes a 3D point as input and always returns
the same randomish number. Nearby points return similar numbers. Another important part of Perlin
noise is that it be simple and fast, so it’s usually done as a hack. I’ll build that hack up
incrementally based on Andrew Kensler’s description.
Using Blocks of Random Numbers
-------------------------------
We could just tile all of space with a 3D array of random numbers and use them in blocks. You get
something blocky where the repeating is clear:
![Image 8: Tiled random patterns
](../images/img-2.08-tile-random.jpg class='pixel')
Let’s just use some sort of hashing to scramble this, instead of tiling. This has a bit of support
code to make it all happen:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef PERLIN_H
#define PERLIN_H
class perlin {
public:
perlin() {
for (int i = 0; i < point_count; i++) {
randfloat[i] = random_double();
}
perlin_generate_perm(perm_x);
perlin_generate_perm(perm_y);
perlin_generate_perm(perm_z);
}
double noise(const point3& p) const {
auto i = int(4*p.x()) & 255;
auto j = int(4*p.y()) & 255;
auto k = int(4*p.z()) & 255;
return randfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]];
}
private:
static const int point_count = 256;
double randfloat[point_count];
int perm_x[point_count];
int perm_y[point_count];
int perm_z[point_count];
static void perlin_generate_perm(int* p) {
for (int i = 0; i < point_count; i++)
p[i] = i;
permute(p, point_count);
}
static void permute(int* p, int n) {
for (int i = n-1; i > 0; i--) {
int target = random_int(0, i);
int tmp = p[i];
p[i] = p[target];
p[target] = tmp;
}
}
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin]: [perlin.h] A Perlin texture class and functions]
Now if we create an actual texture that takes these floats between 0 and 1 and creates grey colors:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "perlin.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtw_stb_image.h"
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class noise_texture : public texture {
public:
noise_texture() {}
color value(double u, double v, const point3& p) const override {
return color(1,1,1) * noise.noise(p);
}
private:
perlin noise;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [noise-texture]: [texture.h] Noise texture]
We can use that texture on some spheres:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void perlin_spheres() {
hittable_list world;
auto pertext = make_shared();
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(pertext)));
world.add(make_shared(point3(0,2,0), 2, make_shared(pertext)));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
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;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (4) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 4: perlin_spheres(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-perlin]: [main.cc] Scene with two Perlin-textured spheres]
Add the hashing does scramble as hoped:
![Image 9: Hashed random texture
](../images/img-2.09-hash-random.png class='pixel')
Smoothing out the Result
-------------------------
To make it smooth, we can linearly interpolate:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
public:
...
double noise(const point3& p) const {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());
auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));
double c[2][2][2];
for (int di=0; di < 2; di++)
for (int dj=0; dj < 2; dj++)
for (int dk=0; dk < 2; dk++)
c[di][dj][dk] = randfloat[
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
return trilinear_interp(c, u, v, w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
private:
...
static void permute(int* p, int n) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static double trilinear_interp(double c[2][2][2], double u, double v, double w) {
auto accum = 0.0;
for (int i=0; i < 2; i++)
for (int j=0; j < 2; j++)
for (int k=0; k < 2; k++)
accum += (i*u + (1-i)*(1-u))
* (j*v + (1-j)*(1-v))
* (k*w + (1-k)*(1-w))
* c[i][j][k];
return accum;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-trilinear]: [perlin.h] Perlin with trilinear interpolation]
And we get:
![Image 10: Perlin texture with trilinear interpolation
](../images/img-2.10-perlin-trilerp.png class='pixel')
Improvement with Hermitian Smoothing
-------------------------------------
Smoothing yields an improved result, but there are obvious grid features in there. Some of it is
Mach bands, a known perceptual artifact of linear interpolation of color. A standard trick is to use
a Hermite cubic to round off the interpolation:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin (
public:
...
double noise(const point3& p) const {
auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-smoothed]: [perlin.h] Perlin with Hermitian smoothing]
This gives a smoother looking image:
![Image 11: Perlin texture, trilinearly interpolated, smoothed
](../images/img-2.11-perlin-trilerp-smooth.png class='pixel')
Tweaking The Frequency
-----------------------
It is also a bit low frequency. We can scale the input point to make it vary more quickly:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
noise_texture(double scale) : scale(scale) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(1,1,1) * noise.noise(scale * p);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double scale;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-smoothed-2]: [texture.h] Perlin smoothed, higher frequency]
We then add that scale to the `perlin_spheres()` scene description:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void perlin_spheres() {
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto pertext = make_shared(4);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(pertext)));
world.add(make_shared(point3(0, 2, 0), 2, make_shared(pertext)));
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scale-perlin]: [main.cc] Perlin-textured spheres with a scale to the noise]
This yields the following result:
![Image 12: Perlin texture, higher frequency
](../images/img-2.12-perlin-hifreq.png class='pixel')
Using Random Vectors on the Lattice Points
-------------------------------------------
This is still a bit blocky looking, probably because the min and max of the pattern always lands
exactly on the integer x/y/z. Ken Perlin’s very clever trick was to instead put random unit vectors
(instead of just floats) on the lattice points, and use a dot product to move the min and max off
the lattice. So, first we need to change the random floats to random vectors. These vectors are any
reasonable set of irregular directions, and I won't bother to make them exactly uniform:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
public:
perlin() {
for (int i = 0; i < point_count; i++) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
randvec[i] = unit_vector(vec3::random(-1,1));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
perlin_generate_perm(perm_x);
perlin_generate_perm(perm_y);
perlin_generate_perm(perm_z);
}
...
private:
static const int point_count = 256;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 randvec[point_count];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int perm_x[point_count];
int perm_y[point_count];
int perm_z[point_count];
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-randunit]: [perlin.h] Perlin with random unit translations]
The Perlin class `noise()` method is now:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
public:
...
double noise(const point3& p) const {
auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 c[2][2][2];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
for (int di=0; di < 2; di++)
for (int dj=0; dj < 2; dj++)
for (int dk=0; dk < 2; dk++)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
c[di][dj][dk] = randvec[
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return perlin_interp(c, u, v, w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-2]: [perlin.h] Perlin class with new noise() method]
And the interpolation becomes a bit more complicated:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
...
private:
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
static double trilinear_interp(double c[2][2][2], double u, double v, double w) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static double perlin_interp(const vec3 c[2][2][2], double u, double v, double w) {
auto uu = u*u*(3-2*u);
auto vv = v*v*(3-2*v);
auto ww = w*w*(3-2*w);
auto accum = 0.0;
for (int i=0; i < 2; i++)
for (int j=0; j < 2; j++)
for (int k=0; k < 2; k++) {
vec3 weight_v(u-i, v-j, w-k);
accum += (i*uu + (1-i)*(1-uu))
* (j*vv + (1-j)*(1-vv))
* (k*ww + (1-k)*(1-ww))
* dot(c[i][j][k], weight_v);
}
return accum;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-interp]: [perlin.h] Perlin interpolation function so far]
The output of the Perlin interpolation function can return negative values. These negative values
will be passed to our `linear_to_gamma()` color function, which expects only positive inputs. To
mitigate this, we'll map the $[-1,+1]$ range of values to $[0,1]$.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
noise_texture(double scale) : scale(scale) {}
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(1,1,1) * 0.5 * (1.0 + noise.noise(scale * p));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
double scale;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-smoothed-2]: [texture.h] Perlin smoothed, higher frequency]
This finally gives something more reasonable looking:
![Image 13: Perlin texture, shifted off integer values
](../images/img-2.13-perlin-shift.png class='pixel')
Introducing Turbulence
-----------------------
Very often, a composite noise that has multiple summed frequencies is used. This is usually called
turbulence, and is a sum of repeated calls to noise:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
...
public:
...
double noise(const point3& p) const {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double turb(const point3& p, int depth) const {
auto accum = 0.0;
auto temp_p = p;
auto weight = 1.0;
for (int i = 0; i < depth; i++) {
accum += weight * noise(temp_p);
weight *= 0.5;
temp_p *= 2;
}
return std::fabs(accum);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-turb]: [perlin.h] Turbulence function]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
noise_texture(double scale) : scale(scale) {}
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(1,1,1) * noise.turb(p, 7);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
double scale;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [noise-tex-2]: [texture.h] Noise texture with turbulence]
Used directly, turbulence gives a sort of camouflage netting appearance:
![Image 14: Perlin texture with turbulence
](../images/img-2.14-perlin-turb.png class='pixel')
Adjusting the Phase
--------------------
However, usually turbulence is used indirectly. For example, the “hello world” of procedural solid
textures is a simple marble-like texture. The basic idea is to make color proportional to something
like a sine function, and use turbulence to adjust the phase (so it shifts $x$ in $\sin(x)$) which
makes the stripes undulate. Commenting out straight noise and turbulence, and giving a marble-like
effect is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
noise_texture(double scale) : scale(scale) {}
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(.5, .5, .5) * (1 + std::sin(scale * p.z() + 10 * noise.turb(p, 7)));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
double scale;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [noise-tex-3]: [texture.h] Noise texture with marbled texture]
Which yields:
![Image 15: Perlin noise, marbled texture
](../images/img-2.15-perlin-marble.png class='pixel')
Quadrilaterals
====================================================================================================
We've managed to get more than half way through this three-book series using spheres as our only
geometric primitive. Time to add our second primitive: the quadrilateral.
Defining the Quadrilateral
---------------------------
Though we'll name our new primitive a `quad`, it will technically be a parallelogram (opposite sides
are parallel) instead of a general quadrilateral. For our purposes, we'll use three geometric
entities to define a quad:
1. $\mathbf{Q}$, the starting corner.
2. $\mathbf{u}$, a vector representing the first side.
$\mathbf{Q} + \mathbf{u}$ gives one of the corners adjacent to $\mathbf{Q}$.
3. $\mathbf{v}$, a vector representing the second side.
$\mathbf{Q} + \mathbf{v}$ gives the other corner adjacent to $\mathbf{Q}$.
The corner of the quad opposite $\mathbf{Q}$ is given by $\mathbf{Q} + \mathbf{u} + \mathbf{v}$.
These values are three-dimensional, even though a quad itself is a two-dimensional object. For
example, a quad with corner at the origin and extending two units in the Z direction and one unit in
the Y direction would have values $\mathbf{Q} = (0,0,0), \mathbf{u} = (0,0,2), \text{and }
\mathbf{v} = (0,1,0)$.
The following figure illustrates the quadrilateral components.
![Figure [quad-def]: Quadrilateral Components](../images/fig-2.05-quad-def.jpg)
Quads are flat, so their axis-aligned bounding box will have zero thickness in one dimension if the
quad lies in the XY, YZ, or ZX plane. This can lead to numerical problems with ray intersection, but
we can address this by padding any zero-sized dimensions of the bounding box. Padding is fine
because we aren't changing the intersection of the quad; we're only expanding its bounding box to
remove the possibility of numerical problems, and the bounds are just a rough approximation to the
actual shape anyway. To remedy this situation, we insert a small padding to ensure that newly
constructed AABBs always have a non-zero volume:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
class aabb {
public:
...
aabb(const interval& x, const interval& y, const interval& z)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
: x(x), y(y), z(z)
{
pad_to_minimums();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
aabb(const point3& a, const point3& b) {
// Treat the two points a and b as extrema for the bounding box, so we don't require a
// particular minimum/maximum coordinate order.
x = interval(std::fmin(a[0],b[0]), std::fmax(a[0],b[0]));
y = interval(std::fmin(a[1],b[1]), std::fmax(a[1],b[1]));
z = interval(std::fmin(a[2],b[2]), std::fmax(a[2],b[2]));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
pad_to_minimums();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
static const aabb empty, universe;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
private:
void pad_to_minimums() {
// Adjust the AABB so that no side is narrower than some delta, padding if necessary.
double delta = 0.0001;
if (x.size() < delta) x = x.expand(delta);
if (y.size() < delta) y = y.expand(delta);
if (z.size() < delta) z = z.expand(delta);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb]: [aabb.h] New aabb::pad_to_minimums() method]
Now we're ready for the first sketch of the new `quad` class:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef QUAD_H
#define QUAD_H
#include "hittable.h"
class quad : public hittable {
public:
quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr mat)
: Q(Q), u(u), v(v), mat(mat)
{
set_bounding_box();
}
virtual void set_bounding_box() {
// Compute the bounding box of all four vertices.
auto bbox_diagonal1 = aabb(Q, Q + u + v);
auto bbox_diagonal2 = aabb(Q + u, Q + v);
bbox = aabb(bbox_diagonal1, bbox_diagonal2);
}
aabb bounding_box() const override { return bbox; }
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
return false; // To be implemented
}
private:
point3 Q;
vec3 u, v;
shared_ptr mat;
aabb bbox;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad]: [quad.h] 2D quadrilateral (parallelogram) class]
Ray-Plane Intersection
-----------------------
As you can see in the prior listing, `quad::hit()` remains to be implemented. Just as for spheres,
we need to determine whether a given ray intersects the primitive, and if so, the various properties
of that intersection (hit point, normal, texture coordinates and so forth).
Ray-quad intersection will be determined in three steps:
1. finding the plane that contains that quad,
2. solving for the intersection of a ray and the quad-containing plane,
3. determining if the hit point lies inside the quad.
We'll first tackle the middle step, solving for general ray-plane intersection.
Spheres are generally the first ray tracing primitive taught because their implicit formula makes it
so easy to solve for ray intersection. Like spheres, planes also have an implicit formula, and we
can use their implicit formula to produce an algorithm that solves for ray-plane intersection.
Indeed, ray-plane intersection is even _easier_ to solve than ray-sphere intersection.
You may already know this implicit formula for a plane:
$$ Ax + By + Cz + D = 0 $$
where $A,B,C,D$ are just constants, and $x,y,z$ are the values of any point $(x,y,z)$ that lies on
the plane. A plane is thus the set of all points $(x,y,z)$ that satisfy the formula above. It makes
things slightly easier to use the alternate formulation:
$$ Ax + By + Cz = D $$
(We didn't flip the sign of D because it's just some constant that we'll figure out later.)
Here's an intuitive way to think of this formula: given the plane perpendicular to the normal vector
$\mathbf{n} = (A,B,C)$, and the position vector $\mathbf{v} = (x,y,z)$ (that is, the vector from the
origin to any point on the plane), then we can use the dot product to solve for $D$:
$$ \mathbf{n} \cdot \mathbf{v} = D $$
for any position on the plane. This is an equivalent formulation of the $Ax + By + Cz = D$ formula
given above, only now in terms of vectors.
Now to find the intersection with some ray $\mathbf{R}(t) = \mathbf{P} + t\mathbf{d}$. Plugging in
the ray equation, we get
$$ \mathbf{n} \cdot ( \mathbf{P} + t \mathbf{d} ) = D $$
Solving for $t$:
$$ \mathbf{n} \cdot \mathbf{P} + \mathbf{n} \cdot t \mathbf{d} = D $$
$$ \mathbf{n} \cdot \mathbf{P} + t(\mathbf{n} \cdot \mathbf{d}) = D $$
$$ t = \frac{D - \mathbf{n} \cdot \mathbf{P}}{\mathbf{n} \cdot \mathbf{d}} $$
This gives us $t$, which we can plug into the ray equation to find the point of intersection. Note
that the denominator $\mathbf{n} \cdot \mathbf{d}$ will be zero if the ray is parallel to the plane.
In this case, we can immediately record a miss between the ray and the plane. As for other
primitives, if the ray $t$ parameter is less than the minimum acceptable value, we also record a
miss.
All right, we can find the point of intersection between a ray and the plane that contains a given
quadrilateral. In fact, we can use this approach to test _any_ planar primitive, like triangles and
disks (more on that later).
Finding the Plane That Contains a Given Quadrilateral
------------------------------------------------------
We've solved step two above: solving the ray-plane intersection, assuming we have the plane
equation. To do this, we need to tackle step one above: finding the equation for the plane that
contains the quad. We have quadrilateral parameters $\mathbf{Q}$, $\mathbf{u}$, and $\mathbf{v}$,
and want the corresponding equation of the plane containing the quad defined by these three values.
Fortunately, this is very simple. Recall that in the equation $Ax + By + Cz = D$, $(A,B,C)$
represents the normal vector. To get this, we just use the cross product of the two side vectors
$\mathbf{u}$ and $\mathbf{v}$:
$$ \mathbf{n} = \operatorname{unit\_vector}(\mathbf{u} \times \mathbf{v}) $$
The plane is defined as all points $(x,y,z)$ that satisfy the equation $Ax + By + Cz = D$. Well, we
know that $\mathbf{Q}$ lies on the plane, so that's enough to solve for $D$:
$$ \begin{align*}
D &= n_x Q_x + n_y Q_y + n_z Q_z \\
&= \mathbf{n} \cdot \mathbf{Q} \\
\end{align*}
$$
Add the planar values to the `quad` class:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
public:
quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr mat)
: Q(Q), u(u), v(v), mat(mat)
{
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto n = cross(u, v);
normal = unit_vector(n);
D = dot(normal, Q);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
set_bounding_box();
}
...
private:
point3 Q;
vec3 u, v;
shared_ptr mat;
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 normal;
double D;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-plane1]: [quad.h] Caching the planar values]
We will use the two values `normal` and `D` to find the point of intersection between a given ray
and the plane containing the quadrilateral.
As an incremental step, let's implement the `hit()` method to handle the infinite plane containing
our quadrilateral.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto denom = dot(normal, r.direction());
// No hit if the ray is parallel to the plane.
if (std::fabs(denom) < 1e-8)
return false;
// Return false if the hit point parameter t is outside the ray interval.
auto t = (D - dot(normal, r.origin())) / denom;
if (!ray_t.contains(t))
return false;
auto intersection = r.at(t);
rec.t = t;
rec.p = intersection;
rec.mat = mat;
rec.set_face_normal(r, normal);
return true;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-plane2]: [quad.h] hit() method for the infinite plane]
Orienting Points on The Plane
------------------------------
At this stage, the intersection point is on the plane that contains the quadrilateral, but it could
be _anywhere_ on the plane: the ray-plane intersection point will lie inside or outside the
quadrilateral. We need to test for intersection points that lie inside the quadrilateral (hit), and
reject points that lie outside (miss). To determine where a point lies relative to the quad, and to
assign texture coordinates to the point of intersection, we need to orient the intersection point on
the plane.
To do this, we'll construct a _coordinate frame_ for the plane -- a way of orienting any point
located on the plane. We've already been using a coordinate frame for our 3D space -- this is
defined by an origin point $\mathbf{O}$ and three basis vectors $\mathbf{x}$, $\mathbf{y}$, and
$\mathbf{z}$.
Since a plane is a 2D construct, we just need a plane origin point $\mathbf{Q}$ and _two_ basis
vectors: $\mathbf{u}$ and $\mathbf{v}$. Normally, axes are perpendicular to each other. However,
this doesn't need to be the case in order to span the entire space -- you just need two axes that
are not parallel to each other.
![Figure [ray-plane]: Ray-plane intersection](../images/fig-2.06-ray-plane.jpg)
Consider figure [ray-plane] as an example. Ray $\mathbf{R}$ intersects the plane, yielding
intersection point $\mathbf{P}$ (not to be confused with the ray origin point $\mathbf{P}$ above).
Measuring against plane vectors $\mathbf{u}$ and $\mathbf{v}$, the intersection point $\mathbf{P}$
in the example above is at $\mathbf{Q} + (1)\mathbf{u} + (\frac{1}{2})\mathbf{v}$. In other words,
the $\mathbf{UV}$ (plane) coordinates of intersection point $\mathbf{P}$ are $(1,\frac{1}{2})$.
Generally, given some arbitrary point $\mathbf{P}$, we seek two scalar values $\alpha$ and $\beta$,
so that
$$ \mathbf{P} = \mathbf{Q} + \alpha \mathbf{u} + \beta \mathbf{v} $$
Pulling a rabbit out of my hat, the planar coordinates $\alpha$ and $\beta$ are given by the
following equations:
$$ \alpha = \mathbf{w} \cdot (\mathbf{p} \times \mathbf{v}) $$
$$ \beta = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{p}) $$
where
$$ \mathbf{p} = \mathbf{P} - \mathbf{Q} $$
$$ \mathbf{w} = \frac{\mathbf{n}}{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})}
= \frac{\mathbf{n}}{\mathbf{n} \cdot \mathbf{n}}$$
The vector $\mathbf{w}$ is constant for a given quadrilateral, so we'll cache that value.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
public:
quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr mat)
: Q(Q), u(u), v(v), mat(mat)
{
auto n = cross(u, v);
normal = unit_vector(n);
D = dot(normal, Q);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
w = n / dot(n,n);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
set_bounding_box();
}
...
private:
point3 Q;
vec3 u, v;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 w;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
shared_ptr mat;
aabb bbox;
vec3 normal;
double D;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-w]: [quad.h] Caching the quadrilateral's w value]
Deriving the Planar Coordinates
--------------------------------
(This section covers the derivation of the equations above. Feel free to skip to the next section if
you're not interested.)
Refer back to figure [ray-plane]. If the planar basis vectors $\mathbf{u}$ and $\mathbf{v}$ were
guaranteed to be orthogonal to each other (forming a 90° angle between them), then solving for
$\alpha$ and $\beta$ would be a simple matter of using the dot product to project $\mathbf{P}$ onto
each of the basis vectors $\mathbf{u}$ and $\mathbf{v}$. However, since we are not restricting
$\mathbf{u}$ and $\mathbf{v}$ to be orthogonal, the math's a little bit trickier.
To set things up, consider that
$$ \mathbf{P} = \mathbf{Q} + \alpha \mathbf{u} + \beta \mathbf{v}$$
$$ \mathbf{p} = \mathbf{P} - \mathbf{Q} = \alpha \mathbf{u} + \beta \mathbf{v} $$
Here, $\mathbf{P}$ is the _point_ of intersection, and $\mathbf{p}$ is the _vector_ from
$\mathbf{Q}$ to $\mathbf{P}$.
Cross the equation for $\mathbf{p}$ with $\mathbf{u}$ and $\mathbf{v}$, respectively:
$$ \begin{align*}
\mathbf{u} \times \mathbf{p} &= \mathbf{u} \times (\alpha \mathbf{u} + \beta \mathbf{v}) \\
&= \mathbf{u} \times \alpha \mathbf{u} + \mathbf{u} \times \beta \mathbf{v} \\
&= \alpha(\mathbf{u} \times \mathbf{u}) + \beta(\mathbf{u} \times \mathbf{v})
\end{align*} $$
$$ \begin{align*}
\mathbf{v} \times \mathbf{p} &= \mathbf{v} \times (\alpha \mathbf{u} + \beta \mathbf{v}) \\
&= \mathbf{v} \times \alpha \mathbf{u} + \mathbf{v} \times \beta \mathbf{v} \\
&= \alpha(\mathbf{v} \times \mathbf{u}) + \beta(\mathbf{v} \times \mathbf{v})
\end{align*} $$
Since any vector crossed with itself yields zero, these equations simplify to
$$ \mathbf{v} \times \mathbf{p} = \alpha(\mathbf{v} \times \mathbf{u}) $$
$$ \mathbf{u} \times \mathbf{p} = \beta(\mathbf{u} \times \mathbf{v}) $$
Now to solve for the coefficients $\alpha$ and $\beta$. If you're new to vector math, you might try
to divide by $\mathbf{u} \times \mathbf{v}$ and $\mathbf{v} \times \mathbf{u}$, but you can't divide
by vectors. Instead, we can take the dot product of both sides of the above equations with the plane
normal $\mathbf{n} = \mathbf{u} \times \mathbf{v}$, reducing both sides to scalars, which we _can_
divide by.
$$ \mathbf{n} \cdot (\mathbf{v} \times \mathbf{p})
= \mathbf{n} \cdot \alpha(\mathbf{v} \times \mathbf{u}) $$
$$ \mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})
= \mathbf{n} \cdot \beta(\mathbf{u} \times \mathbf{v}) $$
Now isolating the coefficients is a simple matter of division:
$$ \alpha = \frac{\mathbf{n} \cdot (\mathbf{v} \times \mathbf{p})}
{\mathbf{n} \cdot (\mathbf{v} \times \mathbf{u})} $$
$$ \beta = \frac{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})}
{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$
Reversing the cross products for both the numerator and denominator of $\alpha$ (recall that
$\mathbf{a} \times \mathbf{b} = - \mathbf{b} \times \mathbf{a}$) gives us a common denominator for
both coefficients:
$$ \alpha = \frac{\mathbf{n} \cdot (\mathbf{p} \times \mathbf{v})}
{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$
$$ \beta = \frac{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})}
{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$
Now we can perform one final simplification, computing a vector $\mathbf{w}$ that will be constant
for the plane's basis frame, for any planar point $\mathbf{P}$:
$$ \mathbf{w} = \frac{\mathbf{n}}{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})}
= \frac{\mathbf{n}}{\mathbf{n} \cdot \mathbf{n}}$$
$$ \alpha = \mathbf{w} \cdot (\mathbf{p} \times \mathbf{v}) $$
$$ \beta = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{p}) $$
Interior Testing of The Intersection Using UV Coordinates
----------------------------------------------------------
Now that we have the intersection point's planar coordinates $\alpha$ and $\beta$, we can easily use
these to determine if the intersection point is inside the quadrilateral -- that is, if the ray
actually hit the quadrilateral.
The plane is divided into coordinate regions like so:
![Figure [quad-coords]: Quadrilateral coordinates](../images/fig-2.07-quad-coords.jpg)
Thus, to see if a point with planar coordinates $(\alpha,\beta)$ lies inside the quadrilateral, it
just needs to meet the following criteria:
1. $ 0 \leq \alpha \leq 1 $
2. $ 0 \leq \beta \leq 1 $
That's the last piece needed to implement quadrilateral primitives.
In order to make such experimentation a bit easier, we'll factor out the $(\alpha,\beta)$ interior
test method from the hit method.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
auto denom = dot(normal, r.direction());
// No hit if the ray is parallel to the plane.
if (std::fabs(denom) < 1e-8)
return false;
// Return false if the hit point parameter t is outside the ray interval.
auto t = (D - dot(normal, r.origin())) / denom;
if (!ray_t.contains(t))
return false;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// Determine if the hit point lies within the planar shape using its plane coordinates.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto intersection = r.at(t);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 planar_hitpt_vector = intersection - Q;
auto alpha = dot(w, cross(planar_hitpt_vector, v));
auto beta = dot(w, cross(u, planar_hitpt_vector));
if (!is_interior(alpha, beta, rec))
return false;
// Ray hits the 2D shape; set the rest of the hit record and return true.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
rec.t = t;
rec.p = intersection;
rec.mat = mat;
rec.set_face_normal(r, normal);
return true;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
virtual bool is_interior(double a, double b, hit_record& rec) const {
interval unit_interval = interval(0, 1);
// Given the hit point in plane coordinates, return false if it is outside the
// primitive, otherwise set the hit record UV coordinates and return true.
if (!unit_interval.contains(a) || !unit_interval.contains(b))
return false;
rec.u = a;
rec.v = b;
return true;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
private:
point3 Q;
vec3 u, v;
vec3 w;
shared_ptr mat;
aabb bbox;
vec3 normal;
double D;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-final]: [quad.h] Final quad class]
And now we add a new scene to demonstrate our new `quad` primitive:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "quad.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "sphere.h"
#include "texture.h"
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void quads() {
hittable_list world;
// Materials
auto left_red = make_shared(color(1.0, 0.2, 0.2));
auto back_green = make_shared(color(0.2, 1.0, 0.2));
auto right_blue = make_shared(color(0.2, 0.2, 1.0));
auto upper_orange = make_shared(color(1.0, 0.5, 0.0));
auto lower_teal = make_shared(color(0.2, 0.8, 0.8));
// Quads
world.add(make_shared(point3(-3,-2, 5), vec3(0, 0,-4), vec3(0, 4, 0), left_red));
world.add(make_shared(point3(-2,-2, 0), vec3(4, 0, 0), vec3(0, 4, 0), back_green));
world.add(make_shared(point3( 3,-2, 1), vec3(0, 0, 4), vec3(0, 4, 0), right_blue));
world.add(make_shared(point3(-2, 3, 1), vec3(4, 0, 0), vec3(0, 0, 4), upper_orange));
world.add(make_shared(point3(-2,-3, 5), vec3(4, 0, 0), vec3(0, 0,-4), lower_teal));
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 80;
cam.lookfrom = point3(0,0,9);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (5) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 5: quads(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-scene]: [main.cc] A new scene with quads]
![Image 16: Quads](../images/img-2.16-quads.png class='pixel')
Additional 2D Primitives
-------------------------
Pause a bit here and consider that if you use the $(\alpha,\beta)$ coordinates to determine if a
point lies inside a quadrilateral (parallelogram), it's not too hard to imagine using these same 2D
coordinates to determine if the intersection point lies inside _any_ other 2D (planar) primitive!
For example, suppose we change the `is_interior()` function to return true if `sqrt(a*a + b*b) < r`.
This would then implement disk primitives of radius `r`. For triangles, try
`a > 0 && b > 0 && a + b < 1`.
We'll leave additional 2D shape possibilities as an exercise to the reader, depending on your desire
to explore. You could even create cut-out stencils based on the pixels of a texture map, or a
Mandelbrot shape! As a little Easter egg, check out the `alternate-2D-primitives` tag in the source
repository. This has solutions for triangles, ellipses and annuli (rings) in
`src/TheNextWeek/quad.h`
Lights
====================================================================================================
Lighting is a key component of raytracing. Early simple raytracers used abstract light sources, like
points in space, or directions. Modern approaches have more physically based lights, which have
position and size. To create such light sources, we need to be able to take any regular object and
turn it into something that emits light into our scene.
Emissive Materials
-------------------
First, let’s make a light emitting material. We need to add an emitted function (we could also add
it to `hit_record` instead -- that’s a matter of design taste). Like the background, it just tells
the ray what color it is and performs no reflection. It’s very simple:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class dielectric : public material {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class diffuse_light : public material {
public:
diffuse_light(shared_ptr tex) : tex(tex) {}
diffuse_light(const color& emit) : tex(make_shared(emit)) {}
color emitted(double u, double v, const point3& p) const override {
return tex->value(u, v, p);
}
private:
shared_ptr tex;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [diffuse-light]: [material.h] A diffuse light class]
So that I don’t have to make all the non-emitting materials implement `emitted()`, I have the base
class return black:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class material {
public:
virtual ~material() = default;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
virtual color emitted(double u, double v, const point3& p) const {
return color(0,0,0);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const {
return false;
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [matl-emit]: [material.h] New emitted function in class material]
Adding Background Color to the Ray Color Function
--------------------------------------------------
Next, we want a pure black background so the only light in the scene is coming from the emitters. To
do this, we’ll add a background color parameter to our `ray_color` function, and pay attention to
the new `color_from_emission` value.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
color background; // Scene background color
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
private:
...
color ray_color(const ray& r, int depth, const hittable& world) const {
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
hit_record rec;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// If the ray hits nothing, return the background color.
if (!world.hit(r, interval(0.001, infinity), rec))
return background;
ray scattered;
color attenuation;
color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p);
if (!rec.mat->scatter(r, rec, attenuation, scattered))
return color_from_emission;
color color_from_scatter = attenuation * ray_color(scattered, depth-1, world);
return color_from_emission + color_from_scatter;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [ray-color-emitted]: [camera.h] ray_color function with background and emitting materials]
`main()` is updated to set the background color for the prior scenes:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void bouncing_spheres() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 20;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void checkered_spheres() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void earth() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void perlin_spheres() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void quads() {
...
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [use-bg-color]: [main.cc] Specifying new background color]
Since we're removing the code that we used to determine the color of the sky when a ray hit it, we
need to pass in a new color value for our old scene renders. We've elected to stick with a flat
bluish-white for the whole sky. You could always pass in a boolean to switch between the previous
skybox code versus the new solid color background. We're keeping it simple here.
Turning Objects into Lights
----------------------------
If we set up a rectangle as a light:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void simple_light() {
hittable_list world;
auto pertext = make_shared(4);
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(pertext)));
world.add(make_shared(point3(0,2,0), 2, make_shared(pertext)));
auto difflight = make_shared(color(4,4,4));
world.add(make_shared(point3(3,1,-2), vec3(2,0,0), vec3(0,2,0), difflight));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.background = color(0,0,0);
cam.vfov = 20;
cam.lookfrom = point3(26,3,6);
cam.lookat = point3(0,2,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (6) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 6: simple_light(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rect-light]: [main.cc] A simple rectangle light]
We get:
![Image 17: Scene with rectangle light source
](../images/img-2.17-rect-light.png class='pixel')
Note that the light is brighter than $(1,1,1)$. This allows it to be bright enough to light things.
Fool around with making some spheres lights too.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void simple_light() {
...
auto difflight = make_shared(color(4,4,4));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
world.add(make_shared(point3(0,7,0), 2, difflight));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
world.add(make_shared(point3(3,1,-2), vec3(2,0,0), vec3(0,2,0), difflight));
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rect-light]: [main.cc] A simple rectangle light plus illuminating ball]
![Image 18: Scene with rectangle and sphere light sources
](../images/img-2.18-rect-sphere-light.png class='pixel')
Creating an Empty “Cornell Box”
--------------------------------
The “Cornell Box” was introduced in 1984 to model the interaction of light between diffuse surfaces.
Let’s make the 5 walls and the light of the box:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void cornell_box() {
hittable_list world;
auto red = make_shared(color(.65, .05, .05));
auto white = make_shared(color(.73, .73, .73));
auto green = make_shared(color(.12, .45, .15));
auto light = make_shared(color(15, 15, 15));
world.add(make_shared(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green));
world.add(make_shared(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red));
world.add(make_shared(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light));
world.add(make_shared(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(555,555,555), vec3(-555,0,0), vec3(0,0,-555), white));
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 600;
cam.samples_per_pixel = 200;
cam.max_depth = 50;
cam.background = color(0,0,0);
cam.vfov = 40;
cam.lookfrom = point3(278, 278, -800);
cam.lookat = point3(278, 278, 0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (7) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
case 6: simple_light(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 7: cornell_box(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [cornell-box-empty]: [main.cc] Cornell box scene, empty]
We get:
![Image 19: Empty Cornell box
](../images/img-2.19-cornell-empty.png class='pixel')
This image is very noisy because the light is small, so most random rays don't hit the light source.
Instances
====================================================================================================
The Cornell Box usually has two blocks in it. These are rotated relative to the walls. First, let’s
create a function that returns a box, by creating a `hittable_list` of six rectangles:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "hittable_list.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
inline shared_ptr box(const point3& a, const point3& b, shared_ptr mat)
{
// Returns the 3D box (six sides) that contains the two opposite vertices a & b.
auto sides = make_shared();
// Construct the two opposite vertices with the minimum and maximum coordinates.
auto min = point3(std::fmin(a.x(),b.x()), std::fmin(a.y(),b.y()), std::fmin(a.z(),b.z()));
auto max = point3(std::fmax(a.x(),b.x()), std::fmax(a.y(),b.y()), std::fmax(a.z(),b.z()));
auto dx = vec3(max.x() - min.x(), 0, 0);
auto dy = vec3(0, max.y() - min.y(), 0);
auto dz = vec3(0, 0, max.z() - min.z());
sides->add(make_shared(point3(min.x(), min.y(), max.z()), dx, dy, mat)); // front
sides->add(make_shared(point3(max.x(), min.y(), max.z()), -dz, dy, mat)); // right
sides->add(make_shared(point3(max.x(), min.y(), min.z()), -dx, dy, mat)); // back
sides->add(make_shared(point3(min.x(), min.y(), min.z()), dz, dy, mat)); // left
sides->add(make_shared(point3(min.x(), max.y(), max.z()), dx, -dz, mat)); // top
sides->add(make_shared(point3(min.x(), min.y(), min.z()), dx, dz, mat)); // bottom
return sides;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [box-class]: [quad.h] A box object]
Now we can add two blocks (but not rotated).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void cornell_box() {
...
world.add(make_shared(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green));
world.add(make_shared(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red));
world.add(make_shared(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light));
world.add(make_shared(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(555,555,555), vec3(-555,0,0), vec3(0,0,-555), white));
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
world.add(box(point3(130, 0, 65), point3(295, 165, 230), white));
world.add(box(point3(265, 0, 295), point3(430, 330, 460), white));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [add-boxes]: [main.cc] Adding box objects]
This gives:
![Image 20: Cornell box with two blocks
](../images/img-2.20-cornell-blocks.png class='pixel')
Now that we have boxes, we need to rotate them a bit to have them match the _real_ Cornell box. In
ray tracing, this is usually done with an _instance_. An instance is a copy of a geometric primitive
that has been placed into the scene. This instance is entirely independent of the other copies of
the primitive and can be moved or rotated. In this case, our geometric primitive is our hittable
`box` object, and we want to rotate it. This is especially easy in ray tracing because we don’t
actually need to move objects in the scene; instead we move the rays in the opposite direction. For
example, consider a _translation_ (often called a _move_). We could take the pink box at the origin
and add two to all its x components, or (as we almost always do in ray tracing) leave the box where
it is, but in its hit routine subtract two off the x-component of the ray origin.
![Figure [ray-box]: Ray-box intersection with moved ray vs box](../images/fig-2.08-ray-box.jpg)
Instance Translation
---------------------
Whether you think of this as a move or a change of coordinates is up to you. The way to reason about
this is to think of moving the incident ray backwards the offset amount, determining if an
intersection occurs, and then moving that intersection point forward the offset amount.
We need to move the intersection point forward the offset amount so that the intersection is
actually in the path of the incident ray. If we forgot to move the intersection point forward then
the intersection would be in the path of the offset ray, which isn't correct. Let's add the code to
make this happen.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class hittable {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class translate : public hittable {
public:
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
// Move the ray backwards by the offset
ray offset_r(r.origin() - offset, r.direction(), r.time());
// Determine whether an intersection exists along the offset ray (and if so, where)
if (!object->hit(offset_r, ray_t, rec))
return false;
// Move the intersection point forwards by the offset
rec.p += offset;
return true;
}
private:
shared_ptr object;
vec3 offset;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [translate-hit]: [hittable.h] Hittable translation hit function]
... and then flesh out the rest of the `translate` class:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class translate : public hittable {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
translate(shared_ptr object, const vec3& offset)
: object(object), offset(offset)
{
bbox = object->bounding_box() + offset;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
private:
shared_ptr object;
vec3 offset;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [translate-class]: [hittable.h] Hittable translation class]
We also need to remember to offset the bounding box, otherwise the incident ray might be looking in
the wrong place and trivially reject the intersection. The expression `object->bounding_box() +
offset` above requires some additional support.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class aabb {
...
};
const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty);
const aabb aabb::universe = aabb(interval::universe, interval::universe, interval::universe);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb operator+(const aabb& bbox, const vec3& offset) {
return aabb(bbox.x + offset.x(), bbox.y + offset.y(), bbox.z + offset.z());
}
aabb operator+(const vec3& offset, const aabb& bbox) {
return bbox + offset;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb-plus-offset]: [aabb.h] The aabb + offset operator]
Since each dimension of an `aabb` is represented as an interval, we'll need to extend `interval`
with an addition operator as well.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class interval {
...
};
const interval interval::empty = interval(+infinity, -infinity);
const interval interval::universe = interval(-infinity, +infinity);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
interval operator+(const interval& ival, double displacement) {
return interval(ival.min + displacement, ival.max + displacement);
}
interval operator+(double displacement, const interval& ival) {
return ival + displacement;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [interval-plus-displacement]: [interval.h] The interval + displacement operator]
Instance Rotation
------------------
Rotation isn’t quite as easy to understand or generate the formulas for. A common graphics tactic is
to apply all rotations about the x, y, and z axes. These rotations are in some sense axis-aligned.
First, let’s rotate by theta about the z-axis. That will be changing only x and y, and in ways that
don’t depend on z.
![Figure [rot-z]: Rotation about the Z axis](../images/fig-2.09-rot-z.jpg)
This involves some basic trigonometry using formulas that I will not cover here. It’s a little
involved, but it is straightforward, and you can find it in any graphics text and in many lecture
notes. The result for rotating counter-clockwise about z is:
$$ x' = \cos(\theta) \cdot x - \sin(\theta) \cdot y $$
$$ y' = \sin(\theta) \cdot x + \cos(\theta) \cdot y $$
The great thing is that it works for any $\theta$ and doesn’t need any cases for quadrants or
anything like that. The inverse transform is the opposite geometric operation: rotate by $-\theta$.
Here, recall that $\cos(\theta) = \cos(-\theta)$ and $\sin(-\theta) = -\sin(\theta)$, so the
formulas are very simple.
Similarly, for rotating about y (as we want to do for the blocks in the box) the formulas are:
$$ x' = \cos(\theta) \cdot x + \sin(\theta) \cdot z $$
$$ z' = -\sin(\theta) \cdot x + \cos(\theta) \cdot z $$
And if we want to rotate about the x-axis:
$$ y' = \cos(\theta) \cdot y - \sin(\theta) \cdot z $$
$$ z' = \sin(\theta) \cdot y + \cos(\theta) \cdot z $$
Thinking of translation as a simple movement of the initial ray is a fine way to reason about what's
going on. But, for a more complex operation like a rotation, it can be easy to accidentally get your
terms crossed (or forget a negative sign), so it's better to consider a rotation as a change of
coordinates.
The pseudocode for the `translate::hit` function above describes the function in terms of _moving_:
1. Move the ray backwards by the offset
2. Determine whether an intersection exists along the offset ray (and if so, where)
3. Move the intersection point forwards by the offset
But this can also be thought of in terms of a _changing of coordinates_:
1. Change the ray from world space to object space
2. Determine whether an intersection exists in object space (and if so, where)
3. Change the intersection point from object space to world space
Rotating an object will not only change the point of intersection, but will also change the surface
normal vector, which will change the direction of reflections and refractions. So we need to change
the normal as well. Fortunately, the normal will rotate similarly to a vector, so we can use the
same formulas as above. While normals and vectors may appear identical for an object undergoing
rotation and translation, an object undergoing scaling requires special attention to keep the
normals orthogonal to the surface. We won't cover that here, but you should research surface normal
transformations if you implement scaling.
We need to start by changing the ray from world space to object space, which for rotation means
rotating by $-\theta$.
$$ x' = \cos(\theta) \cdot x - \sin(\theta) \cdot z $$
$$ z' = \sin(\theta) \cdot x + \cos(\theta) \cdot z $$
We can now create a class for y-rotation. Let's tackle the hit function first:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class translate : public hittable {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class rotate_y : public hittable {
public:
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
// Transform the ray from world space to object space.
auto origin = point3(
(cos_theta * r.origin().x()) - (sin_theta * r.origin().z()),
r.origin().y(),
(sin_theta * r.origin().x()) + (cos_theta * r.origin().z())
);
auto direction = vec3(
(cos_theta * r.direction().x()) - (sin_theta * r.direction().z()),
r.direction().y(),
(sin_theta * r.direction().x()) + (cos_theta * r.direction().z())
);
ray rotated_r(origin, direction, r.time());
// Determine whether an intersection exists in object space (and if so, where).
if (!object->hit(rotated_r, ray_t, rec))
return false;
// Transform the intersection from object space back to world space.
rec.p = point3(
(cos_theta * rec.p.x()) + (sin_theta * rec.p.z()),
rec.p.y(),
(-sin_theta * rec.p.x()) + (cos_theta * rec.p.z())
);
rec.normal = vec3(
(cos_theta * rec.normal.x()) + (sin_theta * rec.normal.z()),
rec.normal.y(),
(-sin_theta * rec.normal.x()) + (cos_theta * rec.normal.z())
);
return true;
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rot-y-hit]: [hittable.h] Hittable rotate-Y hit function]
... and now for the rest of the class:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class rotate_y : public hittable {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
rotate_y(shared_ptr object, double angle) : object(object) {
auto radians = degrees_to_radians(angle);
sin_theta = std::sin(radians);
cos_theta = std::cos(radians);
bbox = object->bounding_box();
point3 min( infinity, infinity, infinity);
point3 max(-infinity, -infinity, -infinity);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
auto x = i*bbox.x.max + (1-i)*bbox.x.min;
auto y = j*bbox.y.max + (1-j)*bbox.y.min;
auto z = k*bbox.z.max + (1-k)*bbox.z.min;
auto newx = cos_theta*x + sin_theta*z;
auto newz = -sin_theta*x + cos_theta*z;
vec3 tester(newx, y, newz);
for (int c = 0; c < 3; c++) {
min[c] = std::fmin(min[c], tester[c]);
max[c] = std::fmax(max[c], tester[c]);
}
}
}
}
bbox = aabb(min, max);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
private:
shared_ptr object;
double sin_theta;
double cos_theta;
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rot-y]: [hittable.h] Hittable rotate-Y class]
And the changes to Cornell are:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void cornell_box() {
...
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
shared_ptr box1 = box(point3(0,0,0), point3(165,330,165), white);
box1 = make_shared(box1, 15);
box1 = make_shared(box1, vec3(265,0,295));
world.add(box1);
shared_ptr box2 = box(point3(0,0,0), point3(165,165,165), white);
box2 = make_shared(box2, -18);
box2 = make_shared(box2, vec3(130,0,65));
world.add(box2);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-rot-y]: [main.cc] Cornell scene with Y-rotated boxes]
Which yields:
![Image 21: Standard Cornell box scene
](../images/img-2.21-cornell-standard.png class='pixel')
Volumes
====================================================================================================
One thing it’s nice to add to a ray tracer is smoke/fog/mist. These are sometimes called _volumes_
or _participating media_. Another feature that is nice to add is subsurface scattering, which is
sort of like dense fog inside an object. This usually adds software architectural mayhem because
volumes are a different animal than surfaces, but a cute technique is to make a volume a random
surface. A bunch of smoke can be replaced with a surface that probabilistically might or might not
be there at every point in the volume. This will make more sense when you see the code.
Constant Density Mediums
-------------------------
First, let’s start with a volume of constant density. A ray going through there can either scatter
inside the volume, or it can make it all the way through like the middle ray in the figure. More
thin transparent volumes, like a light fog, are more likely to have rays like the middle one. How
far the ray has to travel through the volume also determines how likely it is for the ray to make it
through.
![Figure [ray-vol]: Ray-volume interaction](../images/fig-2.10-ray-vol.jpg)
As the ray passes through the volume, it may scatter at any point. The denser the volume, the more
likely that is. The probability that the ray scatters in any small distance $\Delta L$ is:
$$ \mathit{probability} = C \cdot \Delta L $$
where $C$ is proportional to the optical density of the volume. If you go through all the
differential equations, for a random number you get a distance where the scattering occurs. If that
distance is outside the volume, then there is no “hit”. For a constant volume we just need the
density $C$ and the boundary. I’ll use another hittable for the boundary.
The resulting class is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef CONSTANT_MEDIUM_H
#define CONSTANT_MEDIUM_H
#include "hittable.h"
#include "material.h"
#include "texture.h"
class constant_medium : public hittable {
public:
constant_medium(shared_ptr boundary, double density, shared_ptr tex)
: boundary(boundary), neg_inv_density(-1/density),
phase_function(make_shared(tex))
{}
constant_medium(shared_ptr boundary, double density, const color& albedo)
: boundary(boundary), neg_inv_density(-1/density),
phase_function(make_shared(albedo))
{}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
hit_record rec1, rec2;
if (!boundary->hit(r, interval::universe, rec1))
return false;
if (!boundary->hit(r, interval(rec1.t+0.0001, infinity), rec2))
return false;
if (rec1.t < ray_t.min) rec1.t = ray_t.min;
if (rec2.t > ray_t.max) rec2.t = ray_t.max;
if (rec1.t >= rec2.t)
return false;
if (rec1.t < 0)
rec1.t = 0;
auto ray_length = r.direction().length();
auto distance_inside_boundary = (rec2.t - rec1.t) * ray_length;
auto hit_distance = neg_inv_density * std::log(random_double());
if (hit_distance > distance_inside_boundary)
return false;
rec.t = rec1.t + hit_distance / ray_length;
rec.p = r.at(rec.t);
rec.normal = vec3(1,0,0); // arbitrary
rec.front_face = true; // also arbitrary
rec.mat = phase_function;
return true;
}
aabb bounding_box() const override { return boundary->bounding_box(); }
private:
shared_ptr boundary;
double neg_inv_density;
shared_ptr phase_function;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [const-med-class]: [constant_medium.h] Constant medium class]
The scattering function of isotropic picks a uniform random direction:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class diffuse_light : public material {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class isotropic : public material {
public:
isotropic(const color& albedo) : tex(make_shared(albedo)) {}
isotropic(shared_ptr tex) : tex(tex) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
scattered = ray(rec.p, random_unit_vector(), r_in.time());
attenuation = tex->value(rec.u, rec.v, rec.p);
return true;
}
private:
shared_ptr tex;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [isotropic-class]: [material.h] The isotropic class]
The reason we have to be so careful about the logic around the boundary is we need to make sure this
works for ray origins inside the volume. In clouds, things bounce around a lot so that is a common
case.
In addition, the above code assumes that once a ray exits the constant medium boundary, it will
continue forever outside the boundary. Put another way, it assumes that the boundary shape is
convex. So this particular implementation will work for boundaries like boxes or spheres, but will
not work with toruses or shapes that contain voids. It's possible to write an implementation that
handles arbitrary shapes, but we'll leave that as an exercise for the reader.
Rendering a Cornell Box with Smoke and Fog Boxes
-------------------------------------------------
If we replace the two blocks with smoke and fog (dark and light particles), and make the light
bigger (and dimmer so it doesn’t blow out the scene) for faster convergence:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "bvh.h"
#include "camera.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "constant_medium.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "quad.h"
#include "sphere.h"
#include "texture.h"
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void cornell_smoke() {
hittable_list world;
auto red = make_shared(color(.65, .05, .05));
auto white = make_shared(color(.73, .73, .73));
auto green = make_shared(color(.12, .45, .15));
auto light = make_shared(color(7, 7, 7));
world.add(make_shared(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green));
world.add(make_shared(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red));
world.add(make_shared(point3(113,554,127), vec3(330,0,0), vec3(0,0,305), light));
world.add(make_shared(point3(0,555,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
shared_ptr box1 = box(point3(0,0,0), point3(165,330,165), white);
box1 = make_shared(box1, 15);
box1 = make_shared(box1, vec3(265,0,295));
shared_ptr box2 = box(point3(0,0,0), point3(165,165,165), white);
box2 = make_shared(box2, -18);
box2 = make_shared(box2, vec3(130,0,65));
world.add(make_shared(box1, 0.01, color(0,0,0)));
world.add(make_shared(box2, 0.01, color(1,1,1)));
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 600;
cam.samples_per_pixel = 200;
cam.max_depth = 50;
cam.background = color(0,0,0);
cam.vfov = 40;
cam.lookfrom = point3(278, 278, -800);
cam.lookat = point3(278, 278, 0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (8) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
case 6: simple_light(); break;
case 7: cornell_box(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 8: cornell_smoke(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [cornell-smoke]: [main.cc] Cornell box, with smoke]
We get:
![Image 22: Cornell box with blocks of smoke
](../images/img-2.22-cornell-smoke.png class='pixel')
A Scene Testing All New Features
====================================================================================================
Let’s put it all together, with a big thin mist covering everything, and a blue subsurface
reflection sphere (we didn’t implement that explicitly, but a volume inside a dielectric is what a
subsurface material is). The biggest limitation left in the renderer is no shadow rays, but that is
why we get caustics and subsurface for free. It’s a double-edged design decision.
Also note that we'll parameterize this final scene to support a lower quality render for quick
testing.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void final_scene(int image_width, int samples_per_pixel, int max_depth) {
hittable_list boxes1;
auto ground = make_shared(color(0.48, 0.83, 0.53));
int boxes_per_side = 20;
for (int i = 0; i < boxes_per_side; i++) {
for (int j = 0; j < boxes_per_side; j++) {
auto w = 100.0;
auto x0 = -1000.0 + i*w;
auto z0 = -1000.0 + j*w;
auto y0 = 0.0;
auto x1 = x0 + w;
auto y1 = random_double(1,101);
auto z1 = z0 + w;
boxes1.add(box(point3(x0,y0,z0), point3(x1,y1,z1), ground));
}
}
hittable_list world;
world.add(make_shared(boxes1));
auto light = make_shared(color(7, 7, 7));
world.add(make_shared(point3(123,554,147), vec3(300,0,0), vec3(0,0,265), light));
auto center1 = point3(400, 400, 200);
auto center2 = center1 + vec3(30,0,0);
auto sphere_material = make_shared(color(0.7, 0.3, 0.1));
world.add(make_shared(center1, center2, 50, sphere_material));
world.add(make_shared(point3(260, 150, 45), 50, make_shared(1.5)));
world.add(make_shared(
point3(0, 150, 145), 50, make_shared(color(0.8, 0.8, 0.9), 1.0)
));
auto boundary = make_shared(point3(360,150,145), 70, make_shared(1.5));
world.add(boundary);
world.add(make_shared(boundary, 0.2, color(0.2, 0.4, 0.9)));
boundary = make_shared(point3(0,0,0), 5000, make_shared(1.5));
world.add(make_shared(boundary, .0001, color(1,1,1)));
auto emat = make_shared(make_shared("earthmap.jpg"));
world.add(make_shared(point3(400,200,400), 100, emat));
auto pertext = make_shared(0.2);
world.add(make_shared(point3(220,280,300), 80, make_shared(pertext)));
hittable_list boxes2;
auto white = make_shared(color(.73, .73, .73));
int ns = 1000;
for (int j = 0; j < ns; j++) {
boxes2.add(make_shared(point3::random(0,165), 10, white));
}
world.add(make_shared(
make_shared(
make_shared(boxes2), 15),
vec3(-100,270,395)
)
);
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = image_width;
cam.samples_per_pixel = samples_per_pixel;
cam.max_depth = max_depth;
cam.background = color(0,0,0);
cam.vfov = 40;
cam.lookfrom = point3(478, 278, -600);
cam.lookat = point3(278, 278, 0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (9) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
case 6: simple_light(); break;
case 7: cornell_box(); break;
case 8: cornell_smoke(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 9: final_scene(800, 10000, 40); break;
default: final_scene(400, 250, 4); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-final]: [main.cc] Final scene]
Running it with 10,000 rays per pixel (sweet dreams) yields:
![Image 23: Final scene](../images/img-2.23-book2-final.jpg)
Now go off and make a really cool image of your own!
See [our Further Reading wiki page][wiki-further] for additional project related resources.
Feel free to email questions, comments, and cool images to me at ptrshrl@gmail.com.
(insert acknowledgments.md.html here)
Citing This Book
====================================================================================================
Consistent citations make it easier to identify the source, location and versions of this work. If
you are citing this book, we ask that you try to use one of the following forms if possible.
Basic Data
-----------
- **Title (series)**: “Ray Tracing in One Weekend Series”
- **Title (book)**: “Ray Tracing: The Next Week”
- **Author**: Peter Shirley, Trevor David Black, Steve Hollasch
- **Version/Edition**: v4.0.1
- **Date**: 2024-08-30
- **URL (series)**:
- **URL (book)**:
Snippets
---------
### Markdown
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[_Ray Tracing: The Next Week_](https://raytracing.github.io/books/RayTracingTheNextWeek.html)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### HTML
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ray Tracing: The Next Week
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### LaTeX and BibTex
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~\cite{Shirley2024RTW2}
@misc{Shirley2024RTW2,
title = {Ray Tracing: The Next Week},
author = {Peter Shirley, Trevor David Black, Steve Hollasch},
year = {2024},
month = {August},
note = {\small \texttt{https://raytracing.github.io/books/RayTracingTheNextWeek.html}},
url = {https://raytracing.github.io/books/RayTracingTheNextWeek.html}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### BibLaTeX
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\usepackage{biblatex}
~\cite{Shirley2024RTW2}
@online{Shirley2024RTW2,
title = {Ray Tracing: The Next Week},
author = {Peter Shirley, Trevor David Black, Steve Hollasch},
year = {2024},
month = {August},
url = {https://raytracing.github.io/books/RayTracingTheNextWeek.html}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### IEEE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
“Ray Tracing: The Next Week.” raytracing.github.io/books/RayTracingTheNextWeek.html
(accessed MMM. DD, YYYY)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### MLA
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ray Tracing: The Next Week. raytracing.github.io/books/RayTracingTheNextWeek.html
Accessed DD MMM. YYYY.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Peter Shirley]: https://github.com/petershirley
[Steve Hollasch]: https://github.com/hollasch
[Trevor David Black]: https://github.com/trevordblack
[stb_image]: https://github.com/nothings/stb
[readme]: ../README.md
[releases]: https://github.com/RayTracing/raytracing.github.io/releases/
[wiki-further]: https://github.com/RayTracing/raytracing.github.io/wiki/Further-Readings