Fundamentals of Ray Tracing: © 2013 by Don Cross. All Rights Reserved
Fundamentals of Ray Tracing: © 2013 by Don Cross. All Rights Reserved
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 Digital Images . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1 Pixels and colors . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 An example . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3 Resolution . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6 Sphere Intersections . . . . . . . . . . . . . . . . . . . . . 47
6.1 General approach for all shapes . . . . . . . . . . . . . . . 47
6.2 Parametric ray equation . . . . . . . . . . . . . . . . . . . 48
6.3 Surface equation . . . . . . . . . . . . . . . . . . . . . . . 49
6.4 Intersection of ray and surface . . . . . . . . . . . . . . . . 50
6.5 Surface normal vector . . . . . . . . . . . . . . . . . . . . 53
6.6 Filling in the Intersection struct . . . . . . . . . . . . . 54
6.7 C++ sphere implementation . . . . . . . . . . . . . . . . 55
6.8 Switching gears for a while . . . . . . . . . . . . . . . . . 56
7 Optical Computation . . . . . . . . . . . . . . . . . . . . . 57
7.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.2 class Optics . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.2.1 Matte color . . . . . . . . . . . . . . . . . . . . . . 59
7.2.2 Gloss color . . . . . . . . . . . . . . . . . . . . . . 59
7.2.3 Avoiding amplification . . . . . . . . . . . . . . . . 60
7.2.4 Opacity . . . . . . . . . . . . . . . . . . . . . . . . 60
7.2.5 Splitting the light’s energy . . . . . . . . . . . . . 61
7.2.6 Surface optics for a solid . . . . . . . . . . . . . . . 62
7.3 Implementation of CalculateLighting . . . . . . . . . . 62
7.3.1 Recursion limits . . . . . . . . . . . . . . . . . . . 62
7.3.2 Ambiguous intersections . . . . . . . . . . . . . . . 64
7.3.3 Debugging support . . . . . . . . . . . . . . . . . . 66
7.3.4 Source code listing . . . . . . . . . . . . . . . . . . 68
8 Matte Reflection . . . . . . . . . . . . . . . . . . . . . . . 75
8.1 Source code listing . . . . . . . . . . . . . . . . . . . . . . 75
8.2 Clear lines of sight . . . . . . . . . . . . . . . . . . . . . . 77
8.3 Brightness of incident light . . . . . . . . . . . . . . . . . 78
8.4 Using CalculateMatte’s return value . . . . . . . . . . . 81
9 Refraction . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
9.1 Why refraction before reflection? . . . . . . . . . . . . . . 83
9.2 Understanding the physics of refraction . . . . . . . . . . 84
9.3 Snell’s Law adapted for vectors . . . . . . . . . . . . . . . 86
9.3.1 Introduction to Snell’s Law . . . . . . . . . . . . . 87
9.3.2 Refractive reflection . . . . . . . . . . . . . . . . . 88
9.3.3 Special case: total internal reflection . . . . . . . . 88
9.3.4 Setting up the vector equations for refraction . . . 89
9.3.5 Folding the three equations into one . . . . . . . . 90
9.3.6 Dealing with the double cone . . . . . . . . . . . . 92
9.3.7 Constraining F to the correct plane . . . . . . . . 94
9.3.8 Reducing to a scalar equation in k . . . . . . . . . 94
9.3.9 Picking the correct solution for k and F . . . . . . 97
9.4 Calculating refractive reflection . . . . . . . . . . . . . . . 98
9.5 Ambient refractive index . . . . . . . . . . . . . . . . . . . 99
9.6 Determining a point’s refractive index . . . . . . . . . . . 99
9.7 CalculateRefraction source code . . . . . . . . . . . . . 101
Introduction
This text and the included C++ source code provide a demonstration
of a 3D graphics technique called ray tracing. Ray tracing mathemat-
ically simulates the behavior of light and its interactions with physical
objects — reflection, refraction, and shadows. Sophisticated ray tracing
techniques make possible realistic special effects for movies and games,
as well as helpful visualizations of organic molecules for biologists and
machinery designs for mechanical engineers. The field of ray tracing
has grown extremely complex over the past three decades, as everyday
computers have grown exponentially in speed and memory capacity.
My goal in this tutorial is to provide a starting point that omits
as much complexity as possible, and assumes only that the reader has
a good understanding of algebra and analytic geometry. I include a
review chapter on vectors for those who need it; vectors are of primary
importance in ray tracing. Some trigonometry will be helpful at times,
but only in small doses, and the necessary parts will be explained. To
follow the programming examples, the reader must also understand the
C++ programming language. By following along with this text and the
C++ code that accompanies it, you will understand core concepts of
computer graphics sufficiently to build your own mathematical models
and see them transformed into 3D images.
To keep things simple, I will limit the covered topics in certain ways.
The shapes we will draw will be simple geometric solids like spheres,
cubes, and cylinders, not complex artistic endeavors like people or ani-
mals. (However, we will be able to combine the simple shapes in inter-
esting and even surprising ways within a single image.) All light sources
will be modeled as perfect points, resulting in unnaturally crisp shadows
— we thus avoid a tremendous amount of complexity for diffuse light
sources and mutual reflections between objects in the scene.
In spite of some limitations, the source code provided with this book
has quite a bit of flexibility in its modeling of optics. Objects can
combine glossy reflection, diffuse scattering, and transparent refraction.
Ray-traced images can thus possess visual appeal from realistic shading,
perspective, lensing, and sense of depth, as shown in Figures 1.1 and
1.2. By default, an object has uniform glossiness, opacity, and color
across its surface, but the programmer has the ability to derive custom
C++ classes that override this behavior, allowing a single object to have
varying features across its surface.
To keep distractions to a minimum, I will limit the programming
examples to the command line. I want to present ideas and algorithms
that you can experiment with whether you are using Microsoft Windows,
Mac OS X, or Linux. All three operating systems have very different
and complicated programming models for their respective graphical user
interfaces (GUIs). But they all have a very simple and similar way of
writing command line programs. Instead of trying to render images
in a GUI window, the source code accompanying this book generates
an image, saves it to an output file, and exits. After the program has
finished, you can use an image viewer or web browser to look at the
output file. You can then modify the source code, build and run the
program again, and see how the output changed.
Overall, my goal is to make the fundamentals of ray tracing under-
standable. There are many places in this book where an expert on the
subject could fairly say, “there is a faster way to do that” or “a more
sophisticated approach is possible,” but in every case where I have had
to make a choice, I have leaned toward making this as gentle an intro-
duction as possible to computer graphics. In some cases, I have left out
Figure 1.1: A ray-traced image.
Digital Images
2.2 An example
In Figure 2.1 we see a photograph of my cat Lucci on the left, and a
magnified view of one of her eyes on the right. Magnifying an ordinary
picture like this reveals a mosaic of colored tiles, each tile being a separate
Figure 2.1: A digital image with cat’s eye zoomed on right to show pixels.
pixel. The arrow points to a pixel whose color components are red=141,
green=147, and blue=115. It is a remarkable fact of this digital age that
photographs can be reduced to a long series of numbers in a computer.
2.3 Resolution
The amount of detail in a digital image, called resolution, increases with
the number of pixels the image contains. Typical digital images have
hundreds or even thousands of pixels on a side. For example, the cat
image on the left side of Figure 2.1 is 395 pixels wide and 308 pixels
high. Some images may have a much smaller pixel count if they are used
for desktop icons, mouse pointers, or image thumbnails. Beyond a few
thousand pixels on a side, images become unwieldy in terms of memory
usage and bandwidth, so width and height dimensions of between 200 to
2000 pixels are the most common compromise of resolution and efficiency.
Chapter 3
3.3 Cameras
In the Renaissance, artists learned to draw landscapes and buildings with
accurate perspective by sitting in a darkened room with a small hole in
one wall. On a bright, sunny day, an upside-down image of the outdoor
scenery would be projected on the wall opposite the hole. The artist
could then trace outlines of the scene on paper or canvas, and simply turn
it right-side-up when finished. In 1604 the famous astronomer Johannes
Kepler used the Latin phrase for “darkened room” to describe this setup:
camera obscura, hence the origin of the word camera.
The discovery of photochemicals in the 1800s, combined with a minia-
turized camera obscura, led to the invention of pinhole cameras. Instead
of an entire room, a small closed box with a pinhole in one side would
form an image on a photographic plate situated on the opposite side of
the box. (See Figure 3.2.)
Before diving into the details of the ray tracing algorithm, the reader
must have a good understanding of vectors and scalars. If you already
feel comfortable with vectors, scalars, and mathematical operations in-
volving them, such as a vector addition and subtraction, multiplying a
vector with a scalar, and vector dot products and cross products, then
you can skip this chapter and still understand the subsequent material.
This chapter is not intended as an exhaustive treatment of vector con-
cepts, but merely as a summary of the topics we need to understand ray
tracing.
4.1 Scalars
1 P
O x
1 1 2 3
Note that we can look at the x, y, and z axes from many different
points of view. For example, we could let x point southeast and y point
northeast. But not only do the x, y, and z axes have to be at right angles
to each other, but the directions that any two of the axes point determine
the direction the third axis must point. An easy way to visualize this
requirement is to imagine your head is at the origin and that you are
looking in the direction of the x axis, with the top of your head pointing
in the direction of the y axis. In this case the z axis must point to
your right; it is not allowed to point to your left. This convention is
arbitrary, but consistently following it is necessary in the mathematics
and algorithms to come; otherwise certain vector operations will give
wrong answers. (See also: Wikipedia’s article Right-hand rule.)
2
B
1 A
C
O x
1 1 2 3 4
First we follow the vector A to move 2 units east, 2 units north, and
1 unit up. Then, starting from that point we move along the vector B
and travel 2 more units east, one unit south (the same as −1 units north)
and 1 unit down (−1 units up). We end up at a location specified by
adding the respective x, y, and z components of the vectors A and B,
namely (2 + 2, 2 + (−1), 1 + (−1)) = (4, 1, 0). (See Figure 4.2.) If we
call our final position C, we can write concisely in vector notation that
A+B=C
or
(2, 2, 1) + (2, −1, −1) = (4, 1, 0).
It would not matter if we reversed the order of the vectors—if we went
from the origin in the direction specified by the B vector first, then from
there in the direction specified by the A vector, we would still end up at
C. So vectors added in either order, A + B or B + A, always produce
the same result, just like scalars can be added in either order to obtain
the same scalar result. Said in more formal mathematical terms, vector
addition and scalar addition are both commutative. In general, if
A = (Ax , Ay , Az )
and
B = (Bx , By , Bz )
then
A + B = (Ax + Bx , Ay + By , Az + Bz ).
A − B = (Ax − Bx , Ay − By , Az − Bz ).
−B
2
B
D 1
A
x
−1 1 2 3 4
y
3
G
2
E
F
1
x
−1 1 2 3 4 5
z
−1
A+A+A
=(Ax , Ay , Az ) + (Ax , Ay , Az ) + (Ax , Ay , Az )
=(Ax + Ax + Ax , Ay + Ay + Ay , Az + Az + Az )
=(3Ax , 3Ay , 3Az )
If we allow ourselves to write the final vector as (3Ax , 3Ay , 3Az ) =
3A, then we have behavior for vectors that matches the familiar rules of
scalar algebra. In general, we define multiplication of a scalar u and a
vector A as
uA = u(Ax , Ay , Az ) = (uAx , uAy , uAz )
where u is any scalar value, whether negative, positive, or zero. If u = 0,
the resulting product uA = (0, 0, 0), no matter what the values of Ax ,
Ay , and Az are. Another way to think of this is that moving zero times
in any direction is the same as not moving at all. Multiplying u = −1
by a vector is the same as moving the same distance as that vector, but
in the opposite direction. We can write (−1)A more concisely as −A.
B A
But if both vectors have positive magnitudes and the angle between
them is somewhere between 0° and 180°, then there is a unique plane
that passes through both vectors. In order for a third vector to point
in a direction perpendicular to both A and B, that vector must be
perpendicular to this plane. In fact, there are only two such directions,
and they lie on opposite sides of the plane. If the vector C points in one
of these perpendicular directions, then −C points in the other, as shown
in Figure 4.6.
Finding a vector like C would answer our original question, regardless
of the magnitude of C: it would point in a direction at right angles to
both A and B.
C
B A
−C
A × B = (Ax , Ay , Az ) × (Bx , By , Bz )
= (Ay Bz − Az By , Az Bx − Ax Bz , Ax By − Ay Bx )
|A × B| = |A||B| sin(θ)
Compare this to the formula relating the dot product and the in-
cluded angle θ. Just as cos(θ) is zero when θ = 90°, sin(θ) is zero when
θ = 0° or 180°.
(0, +1)
Py = sin(θ)
θ
x
(−1, 0) Px = cos(θ) (+1, 0)
(0, −1)
unzip rtsource.zip
...\raytrace\raytrace.sln
1. Open up Xcode.
2. Under the Xcode menu at the top of your screen, choose Prefer-
ences.
4. Look for a “Command Line Tools” entry. Click the Install button
to the right of it.
If you do not have Xcode installed, and you don’t want to download
1.65 GB just to build from the command line, here is a more palatable
alternative. You will need an Apple ID, but you can create one for free.
Once you have an Apple ID, take a look at the Downloads for Apple
Developers page whose URL is listed below and find the Command Line
Tools package appropriate for your version of OS X. The download will
be much smaller (about 115 MB or so) and will cost you nothing.
https://developer.apple.com/downloads/index.action
Once you get g++ installed, you are ready to proceed to the next
section.
cd raytrace/raytrace
The first time before you build the code for Linux, you will need
to grant permission to run the build script as an executable:
chmod +x build
./build
You should see the computer appear to do nothing for a few sec-
onds, then return to the command prompt. A successful build will
not print any warnings or errors, and will result in the executable file
raytrace in the current directory: if you enter the command
ls -l raytrace
Once you get the C++ code built on Windows, Linux, or OS X, running
it is very similar on any of these operating system. On Windows the
executable will be
raytrace\Release\raytrace.exe
On Linux or OS X, it will be
raytrace/raytrace/raytrace
In either case, you should run unit tests to generate some sample
PNG images to validate the built code and see what kinds of images it
is capable of creating. The images generated by the unit tests will also
be an important reference source for many parts of this book.
Go into a command prompt (terminal window) and change to
the appropriate directory for your platform’s executable, as explained
above. Then run the program with the test option. (You can always
run the raytrace program with no command line options to display
help text that explains what options are available.) On Linux or OS X
you would enter
./raytrace test
raytrace test
Wrote donutbite.png
When finished, the program will return you to the command prompt.
Then use your web browser or image viewer software to take a peek at
the generated PNG files. On my Windows 7 laptop, here is the URL I
entered in my web browser to see all the output files.
file:///C:/don/dev/trunk/math/block/raytrace/raytrace/
file:///Users/don/raytrace/raytrace/
Yours will not be exactly the same as these, but hopefully this
gets the idea across. If everything went well, you are now looking at
a variety of image files that have .png extensions. These image files
demonstrate many of the capabilities of the raytrace program. Take a
few moments to look through all these images to get a preview of the
mathematical and programming topics to come.
scene.AddSolidObject(doubleTorus);
scene.AddLightSource(
LightSource(
Vector(+5.0, +90.0, -40.0),
Color(0.5, 0.5, 1.5, 0.5)
)
);
Don’t worry too much about any details you don’t understand yet —
everything will be explained in great detail later in this book. For now,
just notice these key features:
7. We add two point light sources to shine on the double torus, one
bright yellow and located at (−45, 10, 50), the other a dimmer blue
located at (5, 90, −40). As noted in the source code’s comments,
if we forget to add at least one light source, nothing but a solid
black image will be generated (not very interesting).
8. Finally, now that the scene is completely ready, with all vis-
ible objects and light sources positioned as intended, we call
scene.SaveImage() to ray-trace an image and save it to a PNG
file called torus.png. This is where almost all of the run time is:
doing all the ray tracing math.
Likewise, the “-” operator is overloaded for subtraction, and “*” al-
lows us to multiply a scalar and a vector. The inline function DotProduct
accepts two vectors as parameters and returns the scalar dot product of
those vectors. CrossProduct returns the vector-valued cross product of
two vectors.
5.11 SolidObject::AppendAllIntersections
This member function is of key importance to the ray tracing algorithm.
It is a pure virtual method, meaning it must be implemented differently
by any derived class that creates a visible shape in a rendered image.
The caller of AppendAllIntersections passes in two vectors that de-
fine a ray: a vantage point (a position vector that specifies a location in
space from which the ray is drawn) and a direction vector that indicates
which way the ray is aimed. AppendAllIntersections generates zero
or more Intersection structs, each of which describes the locations (if
any) where the ray intersects with the opaque exterior surfaces of the
solid object. The new intersections are appended to the list parame-
ter intersectionList, which may already contain other intersections
before AppendAllIntersections is called.
As an example, the class Sphere, which derives from SolidObject,
contains a method Sphere::AppendAllIntersections that may de-
termine that a particular direction from a given vantage point passes
through the front of the Sphere at one point and emerges from the
back of the Sphere at another point. In this case, it inserts two extra
Intersection structs at the back of intersectionList.
5.13 Scene::SaveImage
We encountered this method earlier when we looked at the function
TorusTest in main.cpp. Now we are ready to explore in detail how it
n̂2
n̂1
P2
P1
E
5.14 Scene::TraceRay
In Scene::SaveImage you will find the following code as the innermost
logic for determining the color of a pixel:
TraceRay embodies the very core of this ray tracer’s optical calcu-
lation engine. Four chapters will be devoted to how it simulates the
physical behavior of light interacting with matter, covering the math-
ematical details of reflection and refraction. For now, all you need to
know is that this function returns a color value that should be assigned
to the given pixel based on light coming from that point on an object’s
surface.
R = round(255 * r / M)
G = round(255 * g / M)
B = round(255 * b / M)
You now have a general idea of how a scene is broken up into pixels
and converted into an image that can be saved as a PNG file. The
code traces rays of light from the camera point toward thousands of
vector directions, each time calling TraceRay. That function figures out
what color to assign to the pixel associated with that direction from
the camera point. In upcoming chapters we will explore TraceRay in
detail to understand how it computes the intricate behavior of light as
it interacts with physical matter. But first we will study an example
of solving the mathematical problem of finding an intersection of a ray
with a simple shape, the sphere.
Chapter 6
Sphere Intersections
1. Write a parametric equation for any point on the given ray. This
means the ray equation will use a scalar parameter u > 0 to select
any point along the ray.
2. Write one or more equations that describe the surfaces of the solid.
intersection.solid = this;
P = D + uE : u>0
where P is the position vector for any point along the ray. Note that
P, D, and E are all vectors, but u is a scalar. Also note the constraint
that u must be a positive real numer. If we let u be zero, P would be
at the same location as D, the vantage point. We want to exclude the
vantage point from any intersections we find, primarily to prevent us
from thinking a point on a surface casts a shadow on itself. We don’t
allow negative values for u because we would then be calculating point
coordinates that are in the opposite direction as intended.
Another way to express the parametric line equation is by explicitly
writing all the vector components in a single equation:
This last form makes it especially clear how, given fixed values for the
vantage point (Dx , Dy , Dz ) and the direction vector (Ex , Ey , Ez ), we
can adjust u to various values to select any point (Px , Py , Pz ) along
the ray. Simply put, the point P = (Px , Py , Pz ) is a function of the
parameter u. Conversely, if P is an arbitrary point along the ray, there
is a unique value of u such that P = D + uE. In order to find all
intersections with a ray and a surface, our goal is to find all positive
values of u such that P is on that surface.
au2 + bu + c = 0
Interestingly, and usefully for our programming, we can express the val-
ues of a, b, c more concisely using vector dot products and magnitudes:
a = |E|2
b = 2E · (D − C)
c = |D − C|2 − R2
In the C++ code for the function Sphere::AppendAllIntersections,
E is the function parameter direction, and D is the parameter vantage.
The center of the sphere is inherited from the base class SolidObject;
we obtain its vector value via the function call Center(). The radius of
the sphere is stored in Sphere’s member variable radius. So we express
the calculation of the quadratic’s coefficients and the resulting radicand
value as:
const Vector displacement = vantage - Center();
const double a = direction.MagnitudeSquared();
const double b = 2.0 * DotProduct(direction, displacement);
const double c = displacement.MagnitudeSquared() - radius*radius;
const double radicand = b*b - 4.0*a*c;
D
P2
P1
If we find positive real solutions for u, we can plug each one back into
the parametric ray equation to obtain the location of the intersection
point:
P = D + uE
Figure 6.2: The surface normal vector n̂ at any point P on the surface
of a sphere points away from the sphere’s center Q.
normal unit vector points exactly away from the center of the sphere, as
shown in Figure 6.2.
If Q is the center of the sphere, P is the intersection point on the
sphere’s surface, and n̂ is the surface normal unit vector, then P − Q
is a vector that points in the same direction as n̂. We can divide the
vector difference P − Q by its magnitude |P − Q| to convert it to the
unit vector n̂:
P−Q
n̂ =
|P − Q|
In C++ code, we can use the method UnitVector in class Vector to
do the same thing:
intersection.surfaceNormal =
(intersection.point - Center()).UnitVector();
intersectionList.push_back(intersection);
intersection.distanceSquared =
vantageToSurface.MagnitudeSquared();
intersection.solid = this;
intersectionList.push_back(intersection);
}
}
}
}
Optical Computation
7.1 Overview
We have already studied how Scene::SaveImage calls Scene::TraceRay
to trace rays from the camera point out into the scene. TraceRay
determines whether the ray intersects with the surface of any of the
objects in the scene. If TraceRay finds one or more intersections
along a given direction, it then picks the closest intersection and
calls Scene::CalculateLighting to determine what color to assign
to the pixel corresponding to that intersection. This is the first of
four chapters that discuss the optical computations implemented by
CalculateLighting and other functions called by it.
CalculateLighting is the heart of the ray tracing algorithm. Given
a point on the surface of some solid, and the direction from which it is
seen, CalculateLighting uses physics equations to decide where that
ray bounces, bends, and splits into multiple rays. It takes care of all
this work so that the different classes derived from SolidObject can
focus on surface equations and point containment. Because instances of
SolidObject provide a common interface for finding intersections with
surfaces of any shape, CalculateLighting can treat all solid objects as
interchangeable parts, applying the same optical concepts in every case.
CalculateLighting is passed an Intersection object, representing
the intersection of a camera ray with the point on the surface that is
closest to the camera in a particular direction from the vantage point.
Its job is to add up the contributions of all light rays sent toward the
camera from that point and to return the result as a Color struct that
contains separate red, green, and blue values. It calls the following
helper functions to determine the contributions to the pixel color caused
by different physical processes:
7.2.4 Opacity
The opacity value is a number in the range 0 to 1 that tells how opaque
or transparent that point on the object’s surface is. If the opacity is 1,
it means that all light shining on that point is prevented from entering
the object: some is matte-reflected, some is gloss-reflected, and the rest
is absorbed by the point. If the opacity is 0, it means that point on
the object’s surface is completely transparent in the sense that it has no
matte or glossy reflection. In this case, the matte color and gloss colors
are completely ignored; their values have no effect on the color of the
pixel when the opacity is 0.
However, there is a potentially confusing aspect of the opacity value.
As mentioned above, there are two types of mirror-like reflection: glossy
reflection and refractive reflection. Even when the opacity is 0, the sur-
face point can still exhibit mirror reflection as a side-effect of refraction.
This is because in the real world, refraction and reflection are physically
intertwined phenomena; they are both continuously variable depending
on the angle between incident light and a line perpendicular to the sur-
face. However, glossy reflection is necessary in addition to refractive
reflection in any complete ray tracing model for two reasons: refractive
reflection does not allow any color tinge to be added to reflective images,
and more importantly, refractive reflection does not provide for surfaces
like silver or polished steel that reflect almost all incident light (at least
not without making light travel faster than it does in a vacuum, which is
physically impossible). In short, the real world has more than one mech-
anism for creating mirror images, and this ray tracing code therefore
emulates them for the sake of generating realistic images.
matte = opacity*matteColor
mirror = opacity*glossColor + (1-opacity)*(1-transmitted)
refract = (1-opacity)*transmitted
if (IsSignificant(rayIntensity))
Change that line to the following and rebuild the entire project to
enable the debugger feature:
#define RAYTRACE_DEBUG_POINTS 1
Then in the code that builds and renders a scene, call the member
function AddDebugPoint for each pixel you are interested in. Just pass
the horizontal and vertical coordinates of each pixel. Here is an example
of what these calls might look like:
scene.AddDebugPoint(419, 300);
scene.AddDebugPoint(420, 300);
scene.AddDebugPoint(421, 300);
Color Scene::TraceRay(
const Vector& vantage,
const Vector& direction,
double refractiveIndex,
Color rayIntensity,
int recursionDepth) const
{
Intersection intersection;
const int numClosest = FindClosestIntersection(
vantage,
direction,
intersection);
switch (numClosest)
{
case 0:
// The ray of light did not hit anything.
// Therefore we see the background color attenuated
// by the incoming ray intensity.
return rayIntensity * backgroundColor;
case 1:
// The ray of light struck exactly one closest surface.
// Determine the lighting using that single intersection.
return CalculateLighting(
intersection,
direction,
refractiveIndex,
rayIntensity,
1 + recursionDepth);
default:
// There is an ambiguity: more than one intersection
// has the same minimum distance. Caller must catch
// this exception and have a backup plan for handling
// this ray of light.
throw AmbiguousIntersectionException();
}
}
#if RAYTRACE_DEBUG_POINTS
if (activeDebugPoint)
{
using namespace std;
Indent(cout, recursionDepth);
cout << "CalculateLighting[" << recursionDepth << "] {" << endl;
Indent(cout, 1+recursionDepth);
cout << intersection << endl;
Indent(cout, 1+recursionDepth);
cout << "direction=" << direction << endl;
Indent(cout, 1+recursionDepth);
cout.precision(4);
cout << "refract=" << fixed << refractiveIndex;
cout << ", intensity=" << rayIntensity << endl;
Indent(cout, recursionDepth);
cout << "}" << endl;
}
#endif
colorSum += matteColor;
#if RAYTRACE_DEBUG_POINTS
if (activeDebugPoint)
{
using namespace std;
Indent(cout, recursionDepth);
cout << "matteColor=" << matteColor;
cout << ", colorSum=" << colorSum;
cout << endl;
}
#endif
}
colorSum += CalculateRefraction(
intersection,
direction,
refractiveIndex,
transparency * rayIntensity,
recursionDepth,
refractiveReflectionFactor // output parameter
);
}
colorSum += matteColor;
}
}
}
#if RAYTRACE_DEBUG_POINTS
if (activeDebugPoint)
{
using namespace std;
Indent(cout, recursionDepth);
cout << "CalculateLighting[" << recursionDepth << "] returning ";
cout << colorSum << endl;
}
#endif
return colorSum;
}
Chapter 8
Matte Reflection
return colorSum;
}
8.2 Clear lines of sight
Because actual matte surfaces scatter incident light in all directions re-
gardless of the source, an ideal ray tracing algorithm would search in
the infinite number of directions from the point for any light that might
end up there from the surroundings. One of the limitations of the C++
code that accompanies this book is that it does not attempt such a thor-
ough search. There are two reasons for this: avoiding code complexity
and greatly reducing the execution time for rendering an image. In our
simplified model CalculateMatte looks for direct routes to point light
sources only. It iterates through all point light sources in the scene,
looking for unblocked lines of sight. To make this line-of-sight determi-
nation, CalculateMatte calls HasClearLineOfSight, a helper function
that uses FindClosestIntersection to figure out whether there is any
blocking point between the two points passed as arguments to it:
bool Scene::HasClearLineOfSight(
const Vector& point1,
const Vector& point2) const
{
// Subtract point2 from point1 to obtain the direction
// from point1 to point2, along with the square of
// the distance between the two points.
const Vector dir = point2 - point1;
const double gapDistanceSquared = dir.MagnitudeSquared();
// We would not find any solid object that blocks the line of sight.
return true;
}
Any light source with a clear line of sight adds to the intensity and
color of the point based on its distance and angle of incidence, as de-
scribed in detail below. We therefore miss light reflected from other
objects or lensed through other objects, but we do see realistic grada-
tions of light and shadow across curved matte surfaces.
λ̂
Q
S
The scaled color value is then added to colorSum using the overloaded
operator +=, which is implemented as the following member function
inside struct Color:
Color& operator += (const Color& other)
8.4 Using CalculateMatte’s return value
The rayIntensity parameter provides the means for the rays getting
weaker each time they are reflected or refracted. The indirectly-recursive
calls take the existing rayIntensity value that was passed to them, and
diminish it even further before calling back into CalculateLighting. It
is important to understand that when CalculateMatte is called, its
return value also needs to be scaled by rayIntensity since the ray may
have ricocheted many times before it arrived at that matte surface in
the first place. Putting all of these multipliers together, we arrive at the
code where CalculateLighting calls CalculateMatte:
if (opacity > 0.0)
{
// This object is at least a little bit opaque,
// so calculate the part of the color caused by
// matte (scattered) reflection.
colorSum +=
opacity *
optics.GetMatteColor() *
rayIntensity *
CalculateMatte(intersection);
}
Chapter 9
Refraction
Because the refractive index is the ratio of two speeds, no matter what
speed units are used to express them (so long as both speeds are mea-
sured using the same units: meters per second, miles per hour, etc.), the
units cancel out by division and the refractive index has same dimen-
sionless numeric value. The refractive index N is inversely related to the
speed of light v passing through the substance. The more the substance
slows down light, the more v gets smaller and therefore the larger N
gets. Nothing can make light travel faster than c (at least according to
Einstein’s theories of relativity), so actual substances in the real world
always have N ≥ 1. Typical values for N are 1.333 for water and 1.5
to 1.6 for common kinds of glass. Toward the lower end of the scale is
air at 1.0003. One of the higher values for refractive index is diamond,
having N = 2.419. The C++ code that accompanies this book allows a
wide range of refractive index values, and the header file imager.h lists
several common values for convenience:
Wikipedia has a more complete list of refractive indices for the curi-
ous reader:
http://en.wikipedia.org/wiki/List_of_refractive_indices
θ1
E n̂ R
N1
N2 P
−n̂ F
θ2
Figure 9.2: Light passing into a substance with a higher refractive index
(N2 > N1 ) is bent toward a line perpendicular to the surface. The light
ray E passes through the top substance (whose refractive index is N1 ),
strikes the boundary between the two substances at intersection point P,
and most of it continues through the bottom substance (whose refractive
index is N2 ) along the direction vector F. A small portion of the light
is reflected along R.
θ1
−n̂
E R
N1
N2 P
n̂ F
θ2
Figure 9.3: Light passing into a substance with a lower refractive index
(N2 < N1 ) is bent away from a line perpendicular to the surface. As
in the previous figure, the light travels from the top along E, strikes
the intersection point P, and bends to travel in a new direction F. (As
before, a small amount reflects in the direction R).
N1 sin(θ1 ) = N2 sin(θ2 )
In both figures, N1 is the refractive index of the substance on the
top and N2 is that of the bottom substance. Both figures show a light
ray E passing from the top substance into the bottom substance. The
light ray strikes the boundary point P between the two substances at an
angle θ1 from the dotted perpendicular line, and is bent to a new angle
θ2 from the perpendicular, thus traveling along a new direction F.
The “arcsin” here is the inverse sine, a function that returns the
angle whose sine is its argument. Because the sine of an angle must be
somewhere between −1 and +1, the inverse sine is defined only when its
argument is between −1 and +1. If N2 > N1 , the arcsin argument goes
−n̂
E R
N1 θc θc
N2 P θ2 F
n̂
E F
ê = f̂ =
|E| |F|
Using the fact that the dot product of two unit vectors is equal to
the cosine of their included angle, in combination with Snell’s Law, we
can write the following system of equations that indirectly relate the
refractive indices N1 and N2 with the unit vectors ê and f̂ via the incident
angle θ1 and the refraction angle θ2 .
N1 sin(θ1 ) = N2 sin(θ2 )
ê · n̂ = ± cos(θ1 )
f̂ · n̂ = ± cos(θ2 )
A quick note on notation: when you see sin2 (x) or cos2 (x), it means
the same thing as (sin(x))2 or (cos(x))2 , namely taking the sine or cosine
of the angle and multiplying the result by itself.
Squaring the equations also lets us further the solution by use of a
trigonometric identity that relates the sine and cosine of any angle x:
We can thus express the squared sines in terms of the squared cosines
by rearranging the trig identity as:
This lets us replace the squared sine terms in the first equation with
squared cosine terms:
2 2 2 2
N1 1 − cos (θ1 ) = N2 1 − cos (θ2 )
(ê · n̂)2 = cos2 (θ1 )
(f̂ · n̂)2 = cos2 (θ2 )
n̂
N1
P
N2
f̂
−n̂
θ2
Figure 9.5: For specific values of n̂ and θ2 , the set of all f̂ such that
(f̂ · n̂)2 = cos2 (θ2 ) sweeps out an infinite number of directions from the
intersection point P, forming a double-cone shape. We want to solve for
the actual refraction vector f̂ as labeled in the figure.
Our original goal was to find the direction vector F or its unit vector
counterpart f̂ . (We aren’t picky here; any vector pointing in the same
direction will be fine, meaning any vector of the form F = pf̂ where p is
a positive scalar.) Clearly we need something extra to further constrain
the solution to provide a specific answer for F itself. Our first clue is
that the vectors E, n̂, and F must all lie in the same plane, because
to do otherwise would mean that the ray of light would be bending in
an asymmetric way. Imagine yourself inside the top substance looking
down at the point P exactly perpendicular to the boundary. Imagine
also that the light ray E is descending onto the surface from the north.
Both of the involved materials are assumed to be completely uniform in
composition, so there is no reason to think they would make the ray veer
even slightly to the east or west; why would one of those directions be
preferred over the other? No, such a ray of light must continue toward
the south, perhaps descending at a steeper or shallower angle depending
on the refractive indices, but due south nonetheless.
Constraining F (and therefore f̂ ) to lie in the same plane as n̂ and
E leads to a finite solution set for f̂ . The double cone intersects with
this plane such that there are now only four possibile directions for f̂ ,
as shown in Figure 9.6. Only one of these four directions is the correct
solution for f̂ . The other three are “phantom” solutions that we must
eliminate somehow. But we are making progress!
E n̂
N1
P
N2
f̂
−n̂
F = ê + kn̂
Here, k is a scalar parameter that determines how far the light ray is
bent from its original direction ê, either toward or away from the normal
vector n̂. Depending on the situation, the as yet unknown value of k
may be negative, positive, or zero.
F
Recalling that f̂ = |F| , we can now write f̂ as
ê + kn̂
f̂ =
|ê + kn̂|
(ê + kn̂) · n̂
= (ex , ey , ez ) + k(nx , ny , nz ) · (nx , ny , nz )
=(ex + knx , ey + kny , ez + knz ) · (nx , ny , nz )
=((ex + knx )nx , (ey + kny )ny , (ez + knz )nz )
=(ex nx + kn2x , ey ny + kn2y , ez nz + kn2z )
=(ex nx , ey ny , ez nz ) + k(n2x , n2y , n2z )
=(ê · n̂) + k(n̂ · n̂)
If you look at the first line and last line of this derivation, it looks
just like what you would expect if you were doing algebra with ordi-
nary multiplication instead of vector dot products. We will use this dot
product distribution trick again soon, so it is important to understand.
We can simplify the expression further by noting that the dot product
of any unit vector by itself is equal to 1. As with any vector, n̂ · n̂ = |n̂|2 ,
but because n̂ is a unit vector, |n̂|2 = 1, by definition. Substituting
n̂ · n̂ = 1, we find that
Now let’s do some work on the squared magnitude term |ê + kn̂|2
we factored out earlier. We take advantage of the fact that the square
of any vector’s magnitude is the same as the dot product of that vector
with itself: for any vector A, we know that |A|2 = A·A = A2x +A2y +A2z .
Applying this concept to |ê + kn̂|2 , and noting again that ê · ê = 1 and
n̂ · n̂ = 1, we have
|ê + kn̂|2
=(ê + kn̂) · (ê + kn̂)
=(ê · ê) + 2k(ê · n̂) + k 2 (n̂ · n̂)
=1 + 2k(ê · n̂) + k 2
2 2
(ê · n̂)2 + 2k(ê · n̂) + k 2
ê + kn̂ ((ê · n̂) + k)
· n̂ = =
|ê + kn̂| 1 + 2k(ê · n̂) + k 2 1 + 2k(ê · n̂) + k 2
This looks a little intimidating, but things become much more pleas-
ant if you note that since both ê and n̂ are known vectors, their dot
product is a fixed scalar value. Let’s make up a new variable α just to
make the math easier to follow:
α = ê · n̂
2
α2 + 2αk + k 2
ê + kn̂
· n̂ =
|ê + kn̂| 1 + 2αk + k 2
2 !
ê + kn̂
N12 1 − (ê · n̂) 2
= N22 1− · n̂
|ê + kn̂|
Using the α substitution and the algebraic work we just did, this
equation transforms into a more managable equation involving only
scalars.
α2 + 2αk + k 2
N12 (1 2
−α )= N22 1−
1 + 2αk + k 2
2
(1 + 2αk + k 2 ) − (α2 + 2αk + k 2 )
N1
(1 − α2 ) =
N2 1 + 2αk + k 2
2
1 − α2
N1
(1 − α2 ) =
N2 1 + 2αk + k 2
2
2 N2
1 + 2αk + k =
N1
Desiring to solve for k, we write the equation in standard quadratic
form:
2 !
2 N2
k + 2αk + 1 − =0
N1
Scene scene;
scene.SetAmbientRefraction(REFRACTION_WATER);
Color Scene::CalculateRefraction(
const Intersection& intersection,
const Vector& direction,
double sourceRefractiveIndex,
Color rayIntensity,
int recursionDepth,
double& outReflectionFactor) const
{
// Convert direction to a unit vector so that
// relation between angle and dot product is simpler.
const Vector dirUnit = direction.UnitVector();
double k[2];
const int numSolutions = Algebra::SolveQuadraticEquation(
1.0,
2.0 * cos_a1,
1.0 - 1.0/(ratio*ratio),
k);
// There are generally 2 solutions for k, but only
// one of them is correct. The right answer is the
// value of k that causes the light ray to bend the
// smallest angle when comparing the direction of the
// refracted ray to the incident ray. This is the
// same as finding the hypothetical refracted ray
// with the largest positive dot product.
// In real refraction, the ray is always bent by less
// than 90 degrees, so all valid dot products are
// positive numbers.
double maxAlignment = -0.0001; // any negative number works as a flag
Vector refractDir;
for (int i=0; i < numSolutions; ++i)
{
Vector refractAttempt = dirUnit + k[i]*intersection.surfaceNormal;
double alignment = DotProduct(dirUnit, refractAttempt);
if (alignment > maxAlignment)
{
maxAlignment = alignment;
refractDir = refractAttempt;
}
}
// Follow the ray in the new direction from the intersection point.
return TraceRay(
intersection.point,
refractDir,
targetRefractiveIndex,
nextRayIntensity,
recursionDepth);
}
double Scene::PolarizedReflection(
double n1, // source material’s index of refraction
double n2, // target material’s index of refraction
double cos_a1, // incident or outgoing ray angle cosine
double cos_a2) const // outgoing or incident ray angle cosine
{
const double left = n1 * cos_a1;
const double right = n2 * cos_a2;
double numer = left - right;
double denom = left + right;
denom *= denom; // square the denominator
if (denom < EPSILON)
{
// Assume complete reflection.
return 1.0;
}
double reflection = (numer*numer) / denom;
if (reflection > 1.0)
{
// Clamp to actual upper limit.
return 1.0;
}
return reflection;
}
return NULL;
}
Chapter 10
Mirror Reflection
Figure 10.1: The incident ray E strikes the boundary between two sub-
stances at point P. The ray reflects in the direction R. The rays E and
R both extend the same angle θ from the surface’s perpendicular.
E=A+B
R=A−B
B = (E · n̂)n̂
To convince you that this equation works in both cases, consider each
case separately. If n̂ points in the opposite direction as B (or upward as
seen in Figure 10.2), then E · n̂ is a negative number, and multiplying
any vector by a negative number results in another vector pointing in
the opposite direction. Thus (E · n̂)n̂ points in the same direction as
B. Alternatively, if n̂ points in the same direction as B (downward as
seen in the figure), the dot product is a positive number, and therefore
(E · n̂)n̂ still points in the same direction as B.
We now substitute the value of B into the equations for E and R.
E = A + (E · n̂)n̂
R = A − (E · n̂)n̂
R = E − 2(E · n̂)n̂
This is exactly what we wanted. We now know how to calculate R
using only E and n̂.
// Follow the ray in the new direction from the intersection point.
return TraceRay(
intersection.point,
reflectDir,
refractiveIndex,
rayIntensity,
recursionDepth);
}
Chapter 11
Reorientable Solid
Objects
y, s
x, r
z, t
If the object is then translated, the (r, s, t) axes are shifted by the
amounts specified by the translation vector, as show in Figure 11.2.
x
z
C
r
t
Figure 11.2: The (r, s, t) axes are shifted after the center of a reorientable
object has been translated by the vector C.
s
r
x 45°
z
C
Figure 11.3: The (r, s, t) axes after having been rotated counterclockwise
45° parallel to the z axis.
The (r, s, t) axes have been rotated about the object’s center, which
is (r, s, t) = (0, 0, 0) or (x, y, z) = (Cx , Cy , Cz ) by 45° counterclockwise
parallel to the z axis, looking into the z axis, where C = (Cx , Cy , Cz ) is
the object’s center as expressed in camera space.
rDir=(1, 0, 0) xDir=(1, 0, 0)
sDir=(0, 1, 0) yDir=(0, 1, 0)
tDir=(0, 0, 1) zDir=(0, 0, 1)
These are direction vectors, not position vectors, so they are not changed
by the object being translated, but only when it is rotated. Translation
affects only the center point position vector as reported by the pro-
tected member function SolidObject::Center. So the rotation prob-
lem breaks down into two questions:
ac + ibd + iad − bd
Collecting the product terms into real and imaginary parts, we end up
with
(a + ib)(c + id) = (ac − bd) + i(bc + ad)
imaginary
(a + ib)(c + id)
(c + id)
α+β
(a + ib)
β
α
real
a = cos(θ)
b = sin(θ)
lectively as the inverse rotation matrix, since they will help us translate
(r, s, t) object space vectors back into (x, y, z) camera space vectors.
Remarkably, there is no need for any calculation effort at all; we merely
need to rearrange the nine numbers from the original rotation matrix
to obtain the inverse rotation matrix. The rows of the original matrix
become the columns of the inverse matrix, and vice versa, as shown in
Table 11.4. This rearrangement procedure is called transposition —
we say that the inverse rotation matrix is the transpose of the original
rotation matrix. So after we update the values of rDir, sDir, and tDir
in RotateX, RotateY, or RotateZ, we just need to transpose the values
into xDir, yDir, and zDir:
xDir = Vector(rDir.x, sDir.x, tDir.x);
yDir = Vector(rDir.y, sDir.y, tDir.y);
zDir = Vector(rDir.z, sDir.z, tDir.z);
This block of code, needed by all three rotation
methods, is encapsulated into the member function
SolidObject Reorientable::UpdateInverseRotation, which is
located in imager.h. The code for RotateX, RotateY, and RotateZ is
all located in reorient.cpp.
A point of clarification is helpful here. Although the vectors xDir,
yDir, and zDir are object space vectors, and therefore contain (r, s, t)
components mathematically, they are implemented using the same C++
class Vector as is used for camera space vectors. This C++ class has
members called x, y, and z, but we must understand that when class
Vector is used for object space vectors, the members are to be inter-
preted as (r, s, t) components. This implicit ambiguity eliminates other-
wise needless duplication of code, avoiding a redundant version of class
Vector (and its associated inline functions and operators) having mem-
bers named r, s, and t.
Now that all the mathematical details of converting back and forth
between camera space and vector space have been taken care of,
it is fairly straightforward to implement SolidObject Reorientable
methods like AppendAllIntersections, shown here as it appears in
reorient.cpp:
void SolidObject_Reorientable::AppendAllIntersections(
const Vector& vantage,
const Vector& direction,
IntersectionList& intersectionList) const
{
const Vector objectVantage = ObjectPointFromCameraPoint(vantage);
const Vector objectRay = ObjectDirFromCameraDir(direction);
ObjectSpace_AppendAllIntersections(
objectVantage,
objectRay,
intersectionList
);
intersection.surfaceNormal =
CameraDirFromObjectDir(intersection.surfaceNormal);
}
}
protected:
virtual size_t ObjectSpace_AppendAllIntersections(
const Vector& vantage,
const Vector& direction,
IntersectionList& intersectionList) const;
private:
const Color color;
const double a; // half of the width
const double b; // half of the length
const double c; // half of the height
};
ObjectSpace AppendAllIntersections
ObjectSpace Contains
and so on.
Coding the method Cuboid::ObjectSpace AppendAllIntersections
requires the same kind of mathematical approach we used in
Sphere::AppendAllIntersections:
face equation
left r = −a
right r = +a
front s = −b
back s = +b
bottom t = −c
top t = +c
s
b
r
The cylinder is aligned along the t axis, with its top disc located at
t = +b and its bottom disc at t = −b. The discs and the lateral tube
have a common radius a. These three surfaces have the equations and
constraints as shown in Table 11.6.
The tube equation derives from taking any cross-section of the cylin-
der parallel to the rs plane and between the top and bottom discs. You
surface equation constraint
top disc t = +b r2 + s2 ≤ a2
bottom disc t = −b r2 + s2 ≤ a2
tube r2 + s2 = a2 −b ≤ t ≤ +b
would be cutting through a perfect circle: the set of points in the cross-
sectional plane that measure a units from the t axis. The value of t
where you cut is irrelevant — you simply have the formula for a circle
of radius a on the rs plane, or r2 + s2 = a2 .
As usual, we find the simultaneous solution of the parametric ray
equation with each of the surface equations, yielding potential intersec-
tion points for the corresponding surfaces. Each potential intersection is
valid only if it satisfies the constraint for that surface.
The equations for the discs are simpler than the tube equation, so we
start with them. We can write both equations with a single expression:
t = ±b
Then we combine the disc surface equations with the parametric ray
equation:
P = D + uE
(r, s, t) = (Dr , Ds , Dt ) + u(Er , Es , Et )
(r, s, t) = (Dr + uEr , Ds + uEs , Dt + uEt )
Dt + uEt = ±b
±b − Dt
u= , Et 6= 0
Et
Assuming u > 0, the potential intersection points for the discs are
±b − Dt ±b − Dt
Dr + Er , Ds + Es , ±b
Et Et
Letting r = Dr + ±b−D
Et
t
Er and s = Ds + ±b−D Et
t
Es , we check the
constraint r2 + s2 ≤ a2 to see if each value of P is within the bounds of
the disc. If so, the surface normal vector is (0, 0, 1) for any point on the
top disc, or (0, 0, −1) for any point on the bottom disc — a unit vector
perpendicular to the disc and outward from the cylinder in either case.
We turn our attention now to the problem of finding intersections of
a ray with the lateral tube. Just as in the case of a sphere, a ray may
intersect a tube in two places, so we should expect to end up having
a quadratic equation. We find that the simultaneous solution of the
parametric ray equation and the surface equation for the tube gives us
exactly that:
(
(r, s, t) = (Dr + uEr , Ds + uEs , Dt + uEt )
r2 + s2 = a2
Au2 + Bu + C = 0
As in the case of the sphere, we try to find real and positive values of u
via
√
−B ± B 2 − 4AC
u=
2A
If B 2 −4AC < 0, we give up immediately. The imaginary result of taking
the square root of a negative number is our signal that the ray does not
intersect with an infinitely long tube of radius r aligned along the t axis,
let alone the finite cylinder. But if B 2 − 4AC ≥ 0, we use the formula
above to find two real values for u. As before, there are two extra hurdles
to confirm that the point P is on the tube: u must be greater than 0
(or EPSILON in the C++ code, to avoid rounding error problems) and
t = Dt + uEt must be in the range −b ≤ t ≤ +b (or |t| ≤ b+ EPSILON,
again to avoid roundoff error problems).
Creating a visible solid like the tetrahedron proceeds with the calling
code following steps like these:
1. Dynamically allocate an instance of TriangleMesh using operator
new.
2. Call the instance’s AddPoint method to define all the vertex points
on the solid (these are the corner points of the triangular faces).
Note that a single vertex can be shared by multiple triangular
faces. For example, each of the vertices in a tetrahedron is shared
by three of the four triangles. Every call to AddPoint must include
an integer index as its first parameter. The first call must pass the
value 0, the second muss pass 1, etc. This parameter is called
pointIndex, and serves as a sanity check for the programmer,
primarily because subsequent code will need to refer to these point
indices. If the wrong value is passed into AddPoint, it will throw
an exception.
E P
D
B
A
P
w
v axis
A v B
Figure 12.3: Any point P on a plane ABC can be specified using pa-
rameters v and w.
that are parallel to the v and w axes, as indicated by the dashed lines
in Figure 12.3. The line passing through P parallel to the v axis will
pass through the w axis somewhere, and the one parallel to the w axis
will pass through the v axis at another location. We can think of these
locations along the v and w axes as “shadows” of the point P falling onto
them. I will define the value v as the v component of P (P’s shadow on
the v axis) and w as the w component of P (P’s shadow on the w axis).
Furthermore, v can be thought of as the fraction of the distance along
the line segment AB that the shadow lies, such that A is at v = 0 and
B is at v = 1. For example, if the shadow of P lies one fourth of the
distance from A to B, we say that v = 0.25. Negative numbers work
fine also: if the shadow of P on the v axis is the same distance from A
as B is, but is in the opposite direction as B, we say that v = −1. If P’s
shadow on the v axis is in the same direction as B from A, but is twice
as far away, we say that v = 2. So v may have any value from −∞ to
+∞, depending on where P is on the infinite plane passing through the
triangle ABC. Analogously, w is defined as the fraction along the line
segment AC that the shadow of P measures along the w axis.
In vector notation, we can think of v and w as scalars that we multiply
with the vectors from A to B and from A to C, respectively, to get
displacement vectors from A along those two axes. Any point P is
the sum of these two displacement vectors along the two axes, plus the
starting point A:
P = A + v(B − A) + w(C − A)
Let’s try some examples to test whether this equation makes sense. If
we choose P to be at the same location as B, we expect v, the fraction
from A to B along the v axis, to be 1. Likewise, we expect w to be 0,
since we don’t travel at all along the w axis. Plugging v = 1, w = 0 into
the equation, we find:
P = A + 1(B − A) + 0(C − A)
P = A + (B − A)
P=B
P = A + 0(B − A) + 1(C − A)
P = A + (C − A)
P=C
That works too. And if both v and w are 0, we have not moved from A
at all:
P = A + 0(B − A) + 0(C − A)
P=A
P = D + uE
P = A + v(B − A) + w(C − A)
describe the set of all points on a ray and the set of all points on a plane,
respectively. If the ray intersects with the plane, both equations must
refer to the same point P where the ray meets the plane, so we can set
the right-hand sides of the equations equal to each other:
D + uE = A + v(B − A) + w(C − A)
At first, this seems insoluble, because there are three unknowns (u, v, w)
but only 1 equation. But this equation is a vector equation in three
spatial dimensions, so we can rewrite it:
We can rearrange the terms to arrive at the usual form for systems of
linear equations, with each unknown term having a known coefficient on
the left and the unknown variable on the right, and a constant term, all
adding up to zero:
0 = Ex u + (Ax − Bx )v + (Ax − Cx )w + (Dx − Ax )
0 = Ey u + (Ay − By )v + (Ay − Cy )w + (Dy − Ay )
0 = Ez u + (Az − Bz )v + (Az − Cz )w + (Dz − Az )
I will not go into the details of solving this system of equations, be-
cause this is a topic in algebra that is covered well by many other au-
thors. If you look for class TriangleMesh in imager.h, you will see
that AttemptPlaneIntersection is one of its member functions. It
tries to solve the above system of equations using the utility method
SolveLinearEquations that is declared in algebra.h and implemented
in algebra.cpp.
I say it tries to solve the system, because there are special cases
where the solver cannot find a solution because it must avoid divid-
ing by zero. This can happen, for example, if the ray is parallel
to either B − A or C − A. In triangle.cpp, you will see that
TriangleMesh::AppendAllIntersections will try the points of a trian-
gle in up to three different orders, (A, B, C), (B, C, A), and (C, A, B),
because sometimes one or two of the orders can fail but another succeeds.
v axis (w = 0)
A B
Figure 12.4: The shaded region is the set of all points where 0 ≤ v ≤ 1
and 0 ≤ w ≤ 1.
inside the triangle, a point must be above the line passing through A
and B (i.e., above the v axis), to the right of the line passing through
A and C (to the right of the w axis) and to the left of the line passing
through B and C.
Let N = (B − A) × (C − B).
N
Then n̂ = .
|N|
12.5 An ambiguity: which perpendicular is
correct?
One problem remains: depending on which order we choose A, B, and
C for the vertices of a triangle, we will end up with a surface normal
unit vector n̂ that points in one of two opposite directions. The correct
direction is the one that points outward from the body of the solid. But
this begs the question: all we know is a triangle’s vertices; it is ambiguous
to consider one perpendicular direction to be its “inside” and the other
its “outside.”
In fact, thinking about the solid’s inside or outside makes sense only
in the context of an entire collection of triangles enclosing a volume of
space. The distinction between the solid’s inside and outside breaks
down if the triangles do not enclose one or more volumes of space, all
in an airtight manner. If there are any leaks or holes due to missing
or misplaced triangular faces, light rays can leak into the interior of
the solid and illuminate faces from the “wrong” side. The dot product
formula in Scene::CalculateLighting will have a negative value, and
therefore the surface will be considered to be in shadow with respect to
the originating light source. Making the ray tracer C++ code enforce
that a TriangleMesh instance always fully enclose its interior is possible,
but it is a very complex problem. I decided the negative consequence
of a leak (an image that doesn’t look right) was not worth the extra
complexity in educational code like this. Instead, it is the responsibility
of the programmer to design a fully-enclosed TriangleMesh by providing
it with a correct series of AddPoint and AddTriangle calls.
Even so, we are left with the problem of choosing which of the two
opposite orientations of a surface normal vector is correct for pointing
outward from the solid’s interior. There is an algorithm that could figure
this out in theory: tracing rays in both directions, counting how many
surfaces are hit; an even number (0, 2, ...) indicates that direction points
outward from the solid, and an odd number of intersections means that
direction points inward. But this is a lot of computational work, and
there are pitfalls that make it difficult to write code that works in all
cases. To make the code much faster and simpler, I put the burden
of selecting the outward normal direction for each triangle on the code
that calls TriangleMesh::AddTriangle. Such code may order the three
point indices in six possible ways. For example, if the indices are 0, 1,
and 2, the following orderings are possible to add the triangle:
0, 1, 2
0, 2, 1
1, 0, 2
2, 0, 1
1, 2, 0
2, 1, 0
The rule is: the caller of AddTriangle chooses the ordering of points
such that they appear in a counterclockwise arrangement as seen from
the intended “outside” of the entire solid. So three of the six orderings
listed above will result in the surface normal pointing in one direction,
and the other three will cause it to point in the opposite direction. Don’t
worry too much about making a mistake here; if you do, the incorrectly-
oriented triangles will be very obviously solid black instead of their in-
tended colors. When this happens, swap any two of the indices in the
AddTriangle calls for those triangles and they will start rendering cor-
rectly.
2
1
A
B 1 2 D
A
r/x
t/z
Let’s define some constant symbols to simplify the algebra. Note that
all of these definitions contain nothing but known constant terms, so the
C++ algorithm will have no difficulty calculating their values.
G = 4A2 (Ex2 + Ey2 )
H = 8A2 (Dx Ex + Dy Ey )
I = 4A2 (D2 + D2 )
x y
Let 2 2 2 2
J = E x + E y + E z = |E|
K = 2(Dx Ex + Dy Ey + Dz Ez ) = 2(D · E)
L = Dx2 + Dy2 + Dz2 + A2 − B 2 = |D|2 + (A2 − B 2 )
The equation becomes much more concise:
(Ju2 + Ku + L)2 = Gu2 + Hu + I
Expanding the squared term on the left is aided by a 3-by-3 grid showing
every term multiplied by every term, as shown in Figure 13.2.
Ju2 Ku L
Ku JKu3 K 2 u2 KLu
L JLu2 KLu L2
u2 u3
E u0 u1
D
O Q P0
Figure 13.4: Surface normal vector n̂ for a point on a torus (edge view).
n̂
Q P
Figure 13.5: Surface normal vector n̂ for a point on a torus (top view).
Q = (Qx , Qy , 0), where Qx and Qy are unknowns to be found.
N=P−Q
R
N = (Px , Py , Pz ) − q (Px , Py , 0)
Px2 + Py2
R
α= q
Px2 + Py2
which gives us
Set Operations
SetUnion
SetIntersection
SetComplement
SetDifference
14.2 SetUnion
This is the simplest set operator to understand. Given two SolidObject
instances, the SetUnion creates a single object that includes both of
them. For example, if you had code that created a sphere and a cube, the
union of the sphere and the cube would act as a single object comprised of
both objects together. The sphere and cube might overlap or they might
have some empty space between them. But in either case, you could
rotate or translate the set union, and the sphere and cube would both
respond to each rotation or translation. But class SetUnion is more
than a mere container; its power increases when used in combination
with other set operators, as we will see later.
14.3 SetIntersection
Like SetUnion, class SetIntersection operates on two solid ob-
jects to produce a new solid object. However, instead of including
all points that are in either of the solid objects like SetUnion does,
SetIntersection includes only the points that are in both of the
objects. Using SetIntersection makes sense only when the two ob-
jects overlap to some extent. For example, let’s take a look at what
happens when we create two overlapping spheres and feed them to
SetIntersection. See Figure 14.1.
SetIntersection
The resulting solid is not a sphere, but a sort of lens shape with a
sharp circular edge. The function SetIntersectionTest in main.cpp
shows exactly how to implement this overlapping sphere intersection
example. If you have run the unit tests, it has already created the image
file intersection.png, reproduced in Figure 14.2.
It may seem odd that the left and right parts of the lens are shaded
differently. If you look at the image file intersection.png you will see
that the left part of the lens is purple and the right part is yellow. Let’s
take a closer look in main.cpp at the source code that creates the two
spheres and their set intersection (the function SetIntersectionTest),
to understand the coloring oddity:
// Display the intersection of two overlapping spheres.
const double radius = 1.0;
14.5 SetComplement
Unlike SetUnion and SetIntersection, which operate on two solids,
class SetComplement operates on a single solid. The complement of a
solid object is the set of points that are not inside that object. If a point
is contained by a solid, it is not contained by the set’s complement, and
if a point is not contained by a solid, it is contained by the complement.
As an example of this idea, imagine an instance of class Sphere. Ray-
tracing an image of it results in what looks like a ball of solid matter
floating in an infinite void. The complement of this sphere would be an
infinite expanse of solid matter in all directions, with a spherical hole
inside it. This does not sound very useful—the camera immersed in an
infinite expanse of opaque substance will not be able to see anything!
The truth is, SetComplement used by itself does not make much sense,
but it is very useful when combined with the other set operators, as we
shall soon see.
As you might expect, when SetComplement::Contains is called, it
calls the Contains method on its inner object and returns the opposite
boolean value. Returning to the same example, if the inner object is a
sphere, and Sphere::Contains reports that a given point is inside the
sphere by returning true, SetComplement::Contains returns false,
indicating that the point resides within the spherical bubble, i.e., outside
the solid matter that comprises the complement.
Interestingly, SetComplement::AppendAllIntersections simply
calls the AppendAllIntersections method on its inner object, because
an object’s surfaces are in exactly the same places as those of the object’s
complement. However, it must modify the surface normal unit vector
for each Intersection struct reported by the inner object, because the
complement operation effectively turns the inner object inside-out. A
sphere with normal vectors pointing outward from the sphere’s solid
matter into empty space becomes an empty bubble with normal vectors
pointing inward, again away from the infinite expanse of solid matter
and into the empty space within the bubble. These implementation de-
tails are crucial to make class SetComplement behave correctly when
combined with the other set operators.
14.6 SetDifference
Like SetUnion and SetIntersection, SetDifference operates on
a pair of inner objects. Unlike those other binary set operators,
SetDifference means different things depending on the order the two
inner objects are passed into its constructor. We will call the first in-
ner object the left object and the second object the right object. The
SetDifference creates a solid object that contains all the points of the
left object except those that belong to the right object. Figuratively
speaking, the left object is like a piece of stone, and the right object
(to the extent that it overlaps in space with the left object) determines
which parts of the left object should be carved away.
Like SetIntersection, SetDifference makes sense only when the
right object overlaps with the left object at least a little bit. If the two
inner objects of a SetIntersection nowhere overlap, the result is no
visible object at all—just complete emptiness. Non-overlapping inner
objects fed to SetDifference result in an object that looks just like
the left object. In both cases, the set operator adds no value, and just
slows down execution of the ray tracer with useless work. The algorithm
will still work correctly in a mathematical sense, but with no practical
benefit.
SetDifference is implemented using SetIntersection and
SetComplement. The difference of the left object L and the right ob-
ject R is defined as the intersection of L and the complement of R:
We can read this as saying that a point is in the set difference only if it
is inside the left object but not inside the right object.
The ray tracer unit test exercises the SetDifference operator with
the function SetDifferenceTest, located in main.cpp. This test func-
tion creates a SetDifference object having a torus as its left object
and a sphere as its right object. The resulting image looks like a
donut with a spherical bite taken out of it, hence the image file’s name,
donutbite.png. See Figure 14.3.
This listing shows A’s center point used as the center for the intersec-
tion object’s center, but any other point is allowed; you are free to choose
any point in space for the intersection object to revolve about. Also, if
you use the Move member function to move the intersection object to
another location, the existing center point will be moved to that new lo-
cation, and all inner objects (A, B, and C in this case) will be shifted by the
same amount in the x, y, and z directions. For example, if A->Center()
and isect->Center() are both (1, 2, 3), B->Center() is (5, 10, 4), and
C->Center() is (0, 6, 2), and we do isect->Move(1,1,1), then all three
objects will by moved by the amount (1, 1, 1) − (1, 2, 3) = (0, −1, −2).
So A and isect will both end up centered at (5, 9, 2) and C will be
centered at (0, 5, 0).
In Figure 14.5 we see that the concrete block shape consists of the
large cuboid shown on the left with two smaller cuboids on the right
subtracted from it.
C
A
B
Figure 14.5: Subtracting the small cuboids from the large cuboid gener-
ates the concrete block.
The header file block.h shows how to create the concrete block
shape with a concise class declaration. The class ConcreteBlock derives
from SetDifference. It creates a large cuboid as the SetDifference’s
left object, and a SetUnion of the two smaller cuboids as the
SetDifference’s right object. Referring to th labels A, B, and C in
the previous diagram, we have the ConcreteBlock defined conceptually
as