### 引言

《Ray Tracing: The Rest of Your Life》（《用余生研究光线追踪》）， 由 Peter Shirley（就是那本图形学虎书的作者）所编写的的软渲光追三部曲第三本。在本卷中，我假设你将追求与光线追踪相关的职业，我们将深入创建一个非常严肃的光线追踪器的数学。当你完成的时候，你应该准备好开始与电影和产品设计行业的许多严肃的商业射线追踪器打交道。

### 概述

In Ray Tracing in One Weekend and Ray Tracing: the Next Week, you built a “real” ray tracer.

In this volume, I assume you will be pursuing a career related to ray tracing, and we will dive into the math of creating a very serious ray tracer. When you are done you should be ready to start messing with the many serious commercial ray tracers underlying the movie and product design industries. There are many many things I do not cover in this short volume; I dive into only one of many ways to write a Monte Carlo rendering program. I don’t do shadow rays (instead I make rays more likely to go toward lights), bidirectional methods, Metropolis methods, or photon mapping. What I do is speak in the language of the field that studies those methods. I think of this book as a deep exposure that can be your first of many, and it will equip you with some of the concepts, math, and terms you will need to study the others.

As before, https://in1weekend.blogspot.com/ will have further readings and references.

Thanks to everyone who lent a hand on this project. You can find them in the acknowledgments section at the end of this book.

### 一个简单的蒙特卡罗程序

Let’s start with one of the simplest Monte Carlo (MC) programs. MC programs give a statistical estimate of an answer, and this estimate gets more and more accurate the longer you run it. This basic characteristic of simple programs producing noisy but ever-better answers is what MC is all about, and it is especially good for applications like graphics where great accuracy is not needed.

#### Estimating Pi

As an example, let’s estimate 𝜋. There are many ways to do this, with the Buffon Needle problem being a classic case study. We’ll do a variation inspired by that. Suppose you have a circle inscribed inside a square:

Now, suppose you pick random points inside the square. The fraction of those random points that end up inside the circle should be proportional to the area of the circle. The exact fraction should in fact be the ratio of the circle area to the square area. Fraction:

$\frac{\pi r^2}{(2r)^2} = \frac{\pi}{4}$

Since the 𝑟 cancels out, we can pick whatever is computationally convenient. Let’s go with 𝑟=1, centered at the origin:

The answer of 𝜋π found will vary from computer to computer based on the initial random seed. On my computer, this gives me the answer Estimate of Pi = 3.0880000000

#### Showing Convergence

If we change the program to run forever and just print out a running estimate:

#### Stratified Samples (Jittering)

We get very quickly near 𝜋, and then more slowly zero in on it. This is an example of the Law of Diminishing Returns, where each sample helps less than the last. This is the worst part of MC. We can mitigate this diminishing return by stratifying the samples (often called jittering), where instead of taking random samples, we take a grid and take one sample within each:

This changes the sample generation, but we need to know how many samples we are taking in advance because we need to know the grid. Let’s take a hundred million and try it both ways:

On my computer, I get:

Interestingly, the stratified method is not only better, it converges with a better asymptotic rate! Unfortunately, this advantage decreases with the dimension of the problem (so for example, with the 3D sphere volume version the gap would be less). This is called the Curse of Dimensionality. We are going to be very high dimensional (each reflection adds two dimensions), so I won’t stratify in this book, but if you are ever doing single-reflection or shadowing or some strictly 2D problem, you definitely want to stratify.

### 一维MC集成

Integration is all about computing areas and volumes, so we could have framed chapter 2 in an integral form if we wanted to make it maximally confusing. But sometimes integration is the most natural and clean way to formulate things. Rendering is often such a problem.

#### Integrating x²

Let’s look at a classic integral:

$I = \int^2_0{x^2dx}$

In computer sciency notation, we might write this as:

$I = area{(x^2, 0, 2)}$

We could also write it as:

$I = 2 \cdot average(x^2, 0, 2)$

This suggests a MC approach:

This, as expected, produces approximately the exact answer we get with algebra, 𝐼=8/3. We could also do it for functions that we can’t analytically integrate like $\log(\sin(𝑥))$. In graphics, we often have functions we can evaluate but can’t write down explicitly, or functions we can only probabilistically evaluate. That is in fact what the ray tracing ray_color() function of the last two books is — we don’t know what color is seen in every direction, but we can statistically estimate it in any given dimension.

One problem with the random program we wrote in the first two books is that small light sources create too much noise. This is because our uniform sampling doesn’t sample these light sources often enough. Light sources are only sampled if a ray scatters toward them, but this can be unlikely for a small light, or a light that is far away. We could lessen this problem if we sent more random samples toward this light, but this will cause the scene to be inaccurately bright. We can remove this inaccuracy by downweighting these samples to adjust for the over-sampling. How we do that adjustment? To do that, we will need the concept of a probability density function.

#### Density Functions

First, what is a density function? It’s just a continuous form of a histogram. Here’s an example from the histogram Wikipedia page:

If we added data for more trees, the histogram would get taller. If we divided the data into more bins, it would get shorter. A discrete density function differs from a histogram in that it normalizes the frequency y-axis to a fraction or percentage (just a fraction times 100). A continuous histogram, where we take the number of bins to infinity, can’t be a fraction because the height of all the bins would drop to zero. A density function is one where we take the bins and adjust them so they don’t get shorter as we add more bins. For the case of the tree histogram above we might try:

$bin-height = \frac{(Fraction \space of \space tree \space between \space height \space H \space and \space H’)}{(H-H’)}$​

That would work! We could interpret that as a statistical predictor of a tree’s height:

$Probability \space a \space random \space tree \space is \space between \space H \space and \space H’ = bin-height \cdot (H - H’)$

If we wanted to know about the chances of being in a span of multiple bins, we would sum.

A probability density function, henceforth PDF, is that fractional histogram made continuous.

#### Constructing a PDF

Let’s make a PDF and use it a bit to understand it more. Suppose I want a random number 𝑟 between 0 and 2 whose probability is proportional to itself: 𝑟. We would expect the PDF 𝑝(𝑟) to look something like the figure below, but how high should it be?

The height is just 𝑝(2). What should that be? We could reasonably make it anything by convention, and we should pick something that is convenient. Just as with histograms we can sum up (integrate) the region to figure out the probability that 𝑟 is in some interval (𝑥0,𝑥1):

$Probability \space 𝑥_0 < 𝑟 < 𝑥_1 = 𝐶⋅area(𝑝(𝑟),𝑥_0,𝑥_1)$

where 𝐶 is a scaling constant. We may as well make 𝐶=1 for cleanliness, and that is exactly what is done in probability. We also know the probability 𝑟 has the value 1 somewhere, so for this case

$area(p(r), 0, 2) = 1$

Since 𝑝(𝑟) is proportional to 𝑟r, i.e., 𝑝=𝐶′⋅𝑟 for some other constant 𝐶′

$area(C’,r,0,2) = \int^2_0{C’rdr} = \frac{C’r^2}{2}|^{r=2}_{r=0} = \frac{C’ \cdot 2^2}{2} - \frac{C’ \cdot 0^2}{2} = 2C’$

So 𝑝(𝑟)=𝑟/2.

How do we generate a random number with that PDF $𝑝(𝑟)$? For that we will need some more machinery. Don’t worry this doesn’t go on forever!

Given a random number from d = random_double() that is uniform and between 0 and 1, we should be able to find some function $𝑓(𝑑)$ that gives us what we want. Suppose $𝑒=𝑓(𝑑)=𝑑^2$. This is no longer a uniform PDF. The PDF of $𝑒$ will be bigger near 1 than it is near 0 (squaring a number between 0 and 1 makes it smaller). To convert this general observation to a function, we need the cumulative probability distribution function $𝑃(𝑥)$:

$P(x) = area(p, -\infin, x)$

Note that for 𝑥 where we didn’t define 𝑝(𝑥), 𝑝(𝑥)=0, i.e., the probability of an 𝑥 there is zero. For our example PDF 𝑝(𝑟)=𝑟/2, the 𝑃(𝑥) is:

$P(x) = 0:x < 0$​

$P(x) = \frac{x^2}{4}: 0 < x < 2$

$P(x) = 1 : x > 2$

One question is, what’s up with 𝑥 versus 𝑟? They are dummy variables — analogous to the function arguments in a program. If we evaluate 𝑃 at 𝑥=1.0, we get:

$P(1.0) = \frac{1}{4}$​

This says the probability that a random variable with our PDF is less than one is 25%. This gives rise to a clever observation that underlies many methods to generate non-uniform random numbers.

$f(P(x)) = x$

That means 𝑓 just undoes whatever 𝑃 does. So,

$f(x) = P^{-1}(x)$

The −1 means “inverse function”. Ugly notation, but standard. For our purposes, if we have PDF $𝑝()$ and cumulative distribution function $𝑃()$, we can use this “inverse function” with a random number to get what we want:

−1表示“逆函数”。很难看的符号，但很标准。对于我们的目的，如果我们有PDF $p()$ 和累积分布函数 $P()$，我们可以使用这个“逆函数”与一个随机数来得到我们想要的:

$e = P^{-1}(random_double())$

For our PDF 𝑝(𝑥)=𝑥/2, and corresponding 𝑃(𝑥), we need to compute the inverse of 𝑃. If we have

$y = \frac{x^2}{4}$

we get the inverse by solving for 𝑥 in terms of 𝑦:

$x = \sqrt{4y}$

Thus our random number with density 𝑝 is found with:

$e = \sqrt{4 \cdot random_double()}$

Note that this ranges from 0 to 2 as hoped, and if we check our work by replacing random_double() with $\frac{1}{4}$ we get 1 as expected.

We can now sample our old integral

$I = \int_0^2x^2$

We need to account for the non-uniformity of the PDF of 𝑥. Where we sample too much we should down-weight. The PDF is a perfect measure of how much or little sampling is being done. So the weighting function should be proportional to 1/𝑝𝑑𝑓. In fact it is exactly 1/𝑝𝑑𝑓:

#### Importance Sampling

Since we are sampling more where the integrand is big, we might expect less noise and thus faster convergence. In effect, we are steering our samples toward the parts of the distribution that are more important. This is why using a carefully chosen non-uniform PDF is usually called importance sampling.

If we take that same code with uniform samples so the PDF = 1/2 over the range [0,2] we can use the machinery to get x = random_double(0,2), and the code is:

Note that we don’t need that 2 in the 2*sum/N anymore — that is handled by the PDF, which is 2 when you divide by it. You’ll note that importance sampling helps a little, but not a ton. We could make the PDF follow the integrand exactly:

$p(x) = \frac{3}{8}x^2$

And we get the corresponding

$P(x) = \frac{x^3}{8}$

and

$P^{-1}(x) = 8x^{\frac{1}{3}}$

This perfect importance sampling is only possible when we already know the answer (we got 𝑃 by integrating 𝑝 analytically), but it’s a good exercise to make sure our code works. For just 1 sample we get:

Which always returns the exact answer.

Let’s review now because that was most of the concepts that underlie MC ray tracers.

1. You have an integral of 𝑓(𝑥) over some domain [𝑎,𝑏]
2. You pick a PDF 𝑝 that is non-zero over [𝑎,𝑏]
3. You average a whole ton of $\frac{𝑓(𝑟)}{𝑝(𝑟)}$ where 𝑟r is a random number with PDF 𝑝.

Any choice of PDF 𝑝 will always converge to the right answer, but the closer that 𝑝p approximates 𝑓, the faster that it will converge.

1. 你有一个f(x)在某个定义域 [a,b] 上的积分
2. 你选择一个非0 [a,b] 的 PDF(p)
3. 你平均一整吨的 f(r)/p(r)，其中 r 是PDF(p)的一个随机数。

### 方向球上的MC集成

In our ray tracer we pick random directions, and directions can be represented as points on the unit sphere. The same methodology as before applies, but now we need to have a PDF defined over 2D.

Suppose we have this integral over all directions:

$\int{\cos^2(\theta)}$

By MC integration, we should just be able to sample $\cos^2(𝜃)/𝑝(direction)$, but what is direction in that context? We could make it based on polar coordinates, so 𝑝 would be in terms of $(𝜃,𝜙)$. However you do it, remember that a PDF has to integrate to 1 and represent the relative probability of that direction being sampled. Recall that we have vec3 functions to take uniform random samples in (random_in_unit_sphere()) or on (random_unit_vector()) a unit sphere.

Now what is the PDF of these uniform points? As a density on the unit sphere, it is 1/area of the sphere or 1/(4𝜋). If the integrand is $cos^2(𝜃)$, and 𝜃 is the angle with the z axis:

The analytic answer (if you remember enough advanced calc, check me!) is $\frac{4}{3}𝜋$, and the code above produces that. Next, we are ready to apply that in ray tracing!

The key point here is that all the integrals and probability and all that are over the unit sphere. The area on the unit sphere is how you measure the directions. Call it direction, solid angle, or area — it’s all the same thing. Solid angle is the term usually used. If you are comfortable with that, great! If not, do what I do and imagine the area on the unit sphere that a set of directions goes through. The solid angle 𝜔 and the projected area 𝐴 on the unit sphere are the same thing.

### 光散射

In this chapter we won’t actually program anything. We will set up for a big lighting change in the next chapter.

#### Albedo

Our program from the last books already scatters rays from a surface or volume. This is the commonly used model for light interacting with a surface. One natural way to model this is with probability. First, is the light absorbed?

Probability of light scattering: 𝐴

Probability of light being absorbed: 1−𝐴

Here 𝐴 stands for albedo (latin for whiteness). Albedo is a precise technical term in some disciplines, but in all cases it is used to define some form of fractional reflectance. This fractional reflectance (or albedo) will vary with color and (as we implemented for our glass in book one) can vary with incident direction.

#### Scattering

In most physically based renderers, we would use a set of wavelengths for the light color rather than RGB. We can extend our intuition by thinking of R, G, and B as specific algebraic mixtures of long, medium, and short wavelengths.

If the light does scatter, it will have a directional distribution that we can describe as a PDF over solid angle. I will refer to this as its scattering PDF: 𝑠(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛). The scattering PDF can also vary with incident direction, which is the direction of the incoming ray. You can see this varying with incident direction when you look at reflections off a road — they become mirror-like as your viewing angle (incident angle) approaches grazing.

The color of a surface in terms of these quantities is:

$Color = \int{A \cdot s(direction) \cdot color(direction)}$

Note that 𝐴 and 𝑠() may depend on the view direction or the scattering position (position on a surface or position within a volume). Therefore, the output color may also vary with view direction or scattering position.

#### The Scattering PDF

If we apply the MC basic formula we get the following statistical estimate:

$Color = \frac{A \cdot s(direction) \cdot color(direction)}{p(direction)}$

where 𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛) is the PDF of whatever direction we randomly generate.

For a Lambertian surface we already implicitly implemented this formula for the special case where 𝑝() is a cosine density. The 𝑠() of a Lambertian surface is proportional to cos(𝜃), where 𝜃 is the angle relative to the surface normal. Remember that all PDF need to integrate to one. For cos(𝜃)<0 we have 𝑠(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=0, and the integral of cos over the hemisphere is 𝜋.

To see that, remember that in spherical coordinates:

$dA = \sin(\theta)d\theta d\phi$

So:

$Area = \int_0^{2\pi}\int_0^{\pi/2}{\cos{\theta} \sin{\theta} d\theta d\phi} = 2 \pi \frac{1}{2} = \pi$

So for a Lambertian surface the scattering PDF is:

$s(direction) = \frac{\cos{\theta}}{\pi}$

If we sample using a PDF that equals the scattering PDF:

$p(direction) = s(direction) = \frac{\cos{\theta}}{\pi}$

The numerator and denominator cancel out, and we get:

$Color = A \cdot color(direction)$​

This is exactly what we had in our original ray_color() function! However, we need to generalize so we can send extra rays in important directions, such as toward the lights.

The treatment above is slightly non-standard because I want the same math to work for surfaces and volumes. To do otherwise will make some ugly code.

If you read the literature, you’ll see reflection described by the bidirectional reflectance distribution function (BRDF). It relates pretty simply to our terms:

$BRDF = \frac{A \cdot s(direction)}{\cos{\theta}}$

So for a Lambertian surface for example, 𝐵𝑅𝐷𝐹=𝐴/𝜋. Translation between our terms and BRDF is easy.

For participation media (volumes), our albedo is usually called scattering albedo, and our scattering PDF is usually called phase function.

### 重要采样材质

Our goal over the next two chapters is to instrument our program to send a bunch of extra rays toward light sources so that our picture is less noisy. Let’s assume we can send a bunch of rays toward the light source using a PDF 𝑝𝐿𝑖𝑔ℎ𝑡(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛). Let’s also assume we have a PDF related to 𝑠, and let’s call that 𝑝𝑆𝑢𝑟𝑓𝑎𝑐𝑒(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛). A great thing about PDFs is that you can just use linear mixtures of them to form mixture densities that are also PDFs. For example, the simplest would be:

$p(direction) = \frac{1}{2}\cdot Light(direction) + \frac{1}{2} \cdot pSurface(dirction)$

As long as the weights are positive and add up to one, any such mixture of PDFs is a PDF. Remember, we can use any PDF: all PDFs eventually converge to the correct answer. So, the game is to figure out how to make the PDF larger where the product 𝑠(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)⋅𝑐𝑜𝑙𝑜𝑟(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛) is large. For diffuse surfaces, this is mainly a matter of guessing where 𝑐𝑜𝑙𝑜𝑟(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛) is high.

For a mirror, 𝑠() is huge only near one direction, so it matters a lot more. Most renderers in fact make mirrors a special case, and just make the 𝑠/𝑝 implicit — our code currently does that.

#### Returning to the Cornell Box

Let’s do a simple refactoring and temporarily remove all materials that aren’t Lambertian. We can use our Cornell Box scene again, and let’s generate the camera in the function that generates the model.

At 500×500 my code produces this image in 10min on 1 core of my Macbook:

Reducing that noise is our goal. We’ll do that by constructing a PDF that sends more rays to the light.

First, let’s instrument the code so that it explicitly samples some PDF and then normalizes for that. Remember MC basics: $∫𝑓(𝑥)≈𝑓(𝑟)/𝑝(𝑟)$​​. For the Lambertian material, let’s sample like we do now: $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=\cos(𝜃)/𝜋$​.

We modify the base-class material to enable this importance sampling:

And Lambertian material becomes:

And the ray_color function gets a minor modification:

ray_color 函数得到了一个小修改:

You should get exactly the same picture.

#### Random Hemisphere Sampling

Now, just for the experience, try a different sampling strategy. As in the first book, Let’s choose randomly from the hemisphere above the surface. This would be $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=\frac{1}{2𝜋}$.

And again I should get the same picture except with different variance, but I don’t!

It’s pretty close to our old picture, but there are differences that are not noise. The front of the tall box is much more uniform in color. So I have the most difficult kind of bug to find in a Monte Carlo program — a bug that produces a reasonable looking image. I also don’t know if the bug is the first version of the program, or the second, or both!

Let’s build some infrastructure to address this.

### 生成随机方向

In this and the next two chapters, let’s harden our understanding and tools and figure out which Cornell Box is right.

#### Random Directions Relative to the Z Axis

Let’s first figure out how to generate random directions. To simplify things, let’s assume the z-axis is the surface normal, and $𝜃$ is the angle from the normal. We’ll get them oriented to the surface normal vector in the next chapter. We will only deal with distributions that are rotationally symmetric about $𝑧$. So $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=𝑓(𝜃)$. If you have had advanced calculus, you may recall that on the sphere in spherical coordinates $𝑑𝐴=sin(𝜃)⋅𝑑𝜃⋅𝑑𝜙$. If you haven’t, you’ll have to take my word for the next step, but you’ll get it when you take advanced calculus.

Given a directional PDF, $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=𝑓(𝜃)$ on the sphere, the 1D PDFs on $𝜃$ and $𝜙$ are:

$a(\phi) = \frac{1}{2\pi}$

(uniform)

$b(\theta) = 2 \pi f(\theta)\sin(\theta)$

For uniform random numbers $𝑟_1$ and $𝑟_2$, the material presented in the One Dimensional MC Integration chapter leads to:

$r_1 = \int_0^{\phi}{\frac{1}{2\pi}dt} = \frac{\phi}{2\pi}$

Solving for $𝜙$ we get:

$\phi = 2\pi \cdot r_1$

For $𝜃$ we have:

$r_2 = \int_0^{\theta}2\pi f(t)\sin(t)dt$

Here, $𝑡$ is a dummy variable. Let’s try some different functions for $𝑓()$. Let’s first try a uniform density on the sphere. The area of the unit sphere is $4𝜋$, so a uniform $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=\frac{1}{4𝜋}$ on the unit sphere.

$$r_2 = \int_0^{\theta}{2\pi \frac{1}{4 \pi} \sin(t)dt} \newline = \int_0^{\theta}{\frac{1}{2} \sin(t)dt} \newline = \frac{-\cos(\theta)}{2} - \frac{-\cos(0)}{2} \newline = \frac{1 - \cos(\theta)}{2}$$

Solving for $\cos(𝜃)$ gives:

$\cos(\theta) = 1 - 2r_2$

We don’t solve for theta because we probably only need to know $\cos(𝜃)$ anyway, and don’t want needless $\arccos()$ calls running around.

To generate a unit vector direction toward $(𝜃,𝜙)$ we convert to Cartesian coordinates:

$$x = \cos(\phi) \cdot \sin(\theta) \space y = \sin(\theta) \cdot \sin(\theta) \space z = \cos(\theta)$$

And using the identity that $\cos^2+\sin^2=1$, we get the following in terms of random $(𝑟_1,𝑟_2)$:

$$x = \cos(2\pi \cdot r_1) \sqrt{1 - (1 - 2r_2)^2} \space y = \sin(2\pi \cdot r_1) \sqrt{1 - (1 - 2r_2)^2} \space z = 1 - 2r_2$$

Simplifying a little, $(1−2𝑟_2)^2=1−4𝑟_2+4𝑟_2^2$, so:

$$x = \cos(2\pi r_1) \cdot 2\sqrt{r_2(1-r_2)} \space y - \sin(2\pi r_1) \cdot 2\sqrt{r_2(1-r_2)} \space z = 1 - 2r_2$$

We can output some of these:

And plot them for free on plot.ly (a great site with 3D scatterplot support):

On the plot.ly website you can rotate that around and see that it appears uniform.

#### Uniform Sampling a Hemisphere

Now let’s derive uniform on the hemisphere. The density being uniform on the hemisphere means $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=\frac{1}{2𝜋}$​. Just changing the constant in the theta equations yields:

$$\cos(\theta) = 1 - r^2$$

It is comforting that $\cos(𝜃)$ will vary from 1 to 0, and thus theta will vary from 0 to 𝜋/2. Rather than plot it, let’s do a 2D integral with a known solution. Let’s integrate cosine cubed over the hemisphere (just picking something arbitrary with a known solution). First let’s do it by hand:

$$\int{\cos^3(\theta)dA} = \int_0^{2\pi} \int_0^{\pi/2}{\cos^3(\theta) \sin(\theta)d\theta d\phi} = 2\pi \int_0^{\pi/2}{\cos^3(\theta})\sin(\theta) = \frac{\pi}{2}$$

Now for integration with importance sampling. $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=\frac{1}{2𝜋}$, so we average $𝑓/𝑝$ which is $cos^3(𝜃)/(1/2𝜋)$, and we can test this:

Now let’s generate directions with 𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠)=cos(𝜃)/𝜋.

$$r_2 = \int_0^{\theta}{2\pi \frac{\cos(t)}{\pi} \sin(t) = 1 - \cos^2(\theta)}$$

So,

$$\cos(\theta) = \sqrt{1 - r_2}$$

We can save a little algebra on specific cases by noting

$$z = \cos(\theta) = \sqrt{1 - r_2}$$

$$x = \cos(\phi)\sin(\theta) = \cos(2\pi r_1)\sqrt{1 - z^2} = \cos(2\pi r_1)\sqrt{r_2}$$

$$y = \sin(\phi)\sin(\theta) = \sin(2\pi r_1)\sqrt{1 - z^2} = \sin(2 \pi r_2)$$

Let’s also start generating them as random vectors:

We can generate other densities later as we need them. In the next chapter we’ll get them aligned to the surface normal vector.

### 标准正交基

In the last chapter we developed methods to generate random directions relative to the Z-axis. We’d like to be able to do that relative to a surface normal vector.

#### Relative Coordinates

An orthonormal basis (ONB) is a collection of three mutually orthogonal unit vectors. The Cartesian XYZ axes are one such ONB, and I sometimes forget that it has to sit in some real place with real orientation to have meaning in the real world, and some virtual place and orientation in the virtual world. A picture is a result of the relative positions/orientations of the camera and scene, so as long as the camera and scene are described in the same coordinate system, all is well.

Suppose we have an origin 𝐎 and cartesian unit vectors 𝐱, 𝐲, and 𝐳. When we say a location is (3,-2,7), we really are saying:

$$Location \space is \space O + 3x - 2y + 7z$$

If we want to measure coordinates in another coordinate system with origin 𝐎′ and basis vectors 𝐮, 𝐯, and 𝐰, we can just find the numbers (𝑢,𝑣,𝑤) such that:

$$Location \space is \space O’ + 𝑢\mathbf{u} + v\mathbf{v} + w\mathbf{w}$$

#### Generating an Orthonormal Basis

If you take an intro graphics course, there will be a lot of time spent on coordinate systems and 4×4 coordinate transformation matrices. Pay attention, it’s important stuff in graphics! But we won’t need it. What we need to is generate random directions with a set distribution relative to 𝐧. We don’t need an origin because a direction is relative to no specified origin. We do need two cotangent vectors that are mutually perpendicular to 𝐧 and to each other.

Some models will come with one or more cotangent vectors. If our model has only one cotangent vector, then the process of making an ONB is a nontrivial one. Suppose we have any vector 𝐚 that is of nonzero length and not parallel to 𝐧. We can get vectors 𝐬 and 𝐭t perpendicular to 𝐧 by using the property of the cross product that 𝐜×𝐝c×d is perpendicular to both 𝐜 and 𝐝:

$$\mathbf{t} = unit_vector(\mathbf a \times \mathbf n)$$

$$\mathbf{s} = \mathbf{t} \times \mathbf{n}$$

This is all well and good, but the catch is that we may not be given an 𝐚 when we load a model, and we don’t have an 𝐚 with our existing program. If we went ahead and picked an arbitrary 𝐚 to use as our initial vector we may get an 𝐚a that is parallel to 𝐧. A common method is to use an if-statement to determine whether 𝐧 is a particular axis, and if not, use that axis.

Once we have an ONB of 𝐬, 𝐭, and 𝐧, and we have a 𝑟𝑎𝑛𝑑𝑜𝑚(𝑥,𝑦,𝑧) relative to the Z-axis, we can get the vector relative to 𝐧 as:

$$Random \space vector = x\mathbf s + y\mathbf t + z\mathbf n$$

You may notice we used similar math to get rays from a camera. That could be viewed as a change to the camera’s natural coordinate system.

#### The ONB Class

Should we make a class for ONBs, or are utility functions enough? I’m not sure, but let’s make a class because it won’t really be more complicated than utility functions:

We can rewrite our Lambertian material using this to get:

Which produces:

Is that right? We still don’t know for sure. Tracking down bugs is hard in the absence of reliable reference solutions. Let’s table that for now and get rid of some of that noise.

### 直接光源采样

The problem with sampling almost uniformly over directions is that lights are not sampled any more than unimportant directions. We could use shadow rays and separate out direct lighting. Instead, I’ll just send more rays to the light. We can then use that later to send more rays in whatever direction we want.

It’s really easy to pick a random direction toward the light; just pick a random point on the light and send a ray in that direction. We also need to know the PDF, 𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛). What is that?

#### Getting the PDF of a Light

For a light of area 𝐴, if we sample uniformly on that light, the PDF on the surface of the light is $\frac{1}{𝐴}$. What is it on the area of the unit sphere that defines directions? Fortunately, there is a simple correspondence, as outlined in the diagram:

If we look at a small area 𝑑𝐴 on the light, the probability of sampling it is $𝑝_𝑞(𝑞)⋅𝑑𝐴$. On the sphere, the probability of sampling the small area 𝑑𝑤 on the sphere is $𝑝(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)⋅𝑑𝑤$. There is a geometric relationship between 𝑑𝑤 and 𝑑𝐴:

$$d\omega = \frac{dA \cdot \cos(alpha)}{distance^2(p,q)}$$

Since the probability of sampling dw and dA must be the same, we have

$$p(direction) \cdot \frac{dA \cdot \cos(alpha)}{distance^2(p,q)} = p_q(q) \cdot dA = \frac{dA}{A}$$

So

$$p(direction) = \frac{distance^2(p,q)}{\cos(alpha) \cdot A}$$

#### Light Sampling

If we hack our ray_color() function to sample the light in a very hard-coded fashion just to check that math and get the concept, we can add it (see the highlighted region):

With 10 samples per pixel this yields:

This is about what we would expect from something that samples only the light sources, so this appears to work.

#### Switching to Unidirectional Light

The noisy pops around the light on the ceiling are because the light is two-sided and there is a small space between light and ceiling. We probably want to have the light just emit down. We can do that by letting the emitted member function of hittable take extra information:

Making sure to call this in our world definition:

This gives us:

### 混合密度

We have used a PDF related to cos(𝜃), and a PDF related to sampling the light. We would like a PDF that combines these.

#### An Average of Lighting and Reflection

A common tool in probability is to mix the densities to form a mixture density. Any weighted average of PDFs is a PDF. For example, we could just average the two densities:

$$mixture_{pdf}(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)=\frac{1}{2}reflection_{pdf}(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)+\frac{1}{2}light_{pdf}(𝑑𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛)$$

How would we instrument our code to do that? There is a very important detail that makes this not quite as easy as one might expect. Choosing the random direction is simple:

But evaluating $mixture_{pdf}$ is slightly more subtle. We need to evaluate both $reflection_{pdf}$ and $light_{pdf}$ because there are some directions where either PDF could have generated the direction. For example, we might generate a direction toward the light using $reflection_pdf$.

If we step back a bit, we see that there are two functions a PDF needs to support:

1. What is your value at this location?
2. Return a random number that is distributed appropriately.

1. 你在这里的价值是多少?
2. 返回一个适当分布的随机数。

The details of how this is done under the hood varies for the $reflection_{pdf}$​ and the $light_{pdf}$​ and the mixture density of the two of them, but that is exactly what class hierarchies were invented for! It’s never obvious what goes in an abstract class, so my approach is to be greedy and hope a minimal interface works, and for the PDF this implies:

We’ll see if that works by fleshing out the subclasses. For sampling the light, we will need hittable to answer some queries that it doesn’t have an interface for. We’ll probably need to mess with it too, but we can start by seeing if we can put something in hittable involving sampling the bounding box that works with all its subclasses.

First, let’s try a cosine density:

We can try this in the ray_color() function, with the main changes highlighted. We also need to change variable pdf to some other variable name to avoid a name conflict with the new pdf class.

This yields an apparently matching result so all we’ve done so far is refactor where pdf is computed:

#### Sampling Directions towards a Hittable

Now we can try sampling directions toward a hittable, like the light.

This assumes two as-yet not implemented functions in the hittable class. To avoid having to add instrumentation to all hittable subclasses, we’ll add two dummy functions to the hittable class:

And we change xz_rect to implement those functions:

And then change ray_color():

At 10 samples per pixel we get:

#### The Mixture PDF Class

Now we would like to do a mixture density of the cosine and light sampling. The mixture density class is straightforward:

And plugging it into ray_color():

1000 samples per pixel yields:

We’ve basically gotten this same picture (with different levels of noise) with several different sampling patterns. It looks like the original picture was slightly wrong! Note by “wrong” here I mean not a correct Lambertian picture. Yet Lambertian is just an ideal approximation to matte, so our original picture was some other accidental approximation to matte. I don’t think the new one is any better, but we can at least compare it more easily with other Lambertian renderers.

### 一些架构决策

I won’t write any code in this chapter. We’re at a crossroads where I need to make some architectural decisions. The mixture-density approach is to not have traditional shadow rays, and is something I personally like, because in addition to lights you can sample windows or bright cracks under doors or whatever else you think might be bright. But most programs branch, and send one or more terminal rays to lights explicitly, and one according to the reflective distribution of the surface. This could be a time you want faster convergence on more restricted scenes and add shadow rays; that’s a personal design preference.

There are some other issues with the code.

The PDF construction is hard coded in the ray_color() function. We should clean that up, probably by passing something into color about the lights. Unlike BVH construction, we should be careful about memory leaks as there are an unbounded number of samples.

PDF结构是在 ray_color() 函数中硬编码的。我们应该把它清理一下，也许通过把一些关于灯光的东西变成颜色。与BVH构造不同，我们应该小心内存泄漏，因为样本的数量是无限的。

The specular rays (glass and metal) are no longer supported. The math would work out if we just made their scattering function a delta function. But that would be floating point disaster. We could either separate out specular reflections, or have surface roughness never be zero and have almost-mirrors that look perfectly smooth but don’t generate NaNs. I don’t have an opinion on which way to do it (I have tried both and they both have their advantages), but we have smooth metal and glass code anyway, so I add perfect specular surfaces that do not do explicit f()/p() calculations.

We also lack a real background function infrastructure in case we want to add an environment map or more interesting functional background. Some environment maps are HDR (the RGB components are floats rather than 0–255 bytes usually interpreted as 0-1). Our output has been HDR all along; we’ve just been truncating it.

Finally, our renderer is RGB and a more physically based one — like an automobile manufacturer might use — would probably need to use spectral colors and maybe even polarization. For a movie renderer, you would probably want RGB. You can make a hybrid renderer that has both modes, but that is of course harder. I’m going to stick to RGB for now, but I will revisit this near the end of the book.

### 优化PDF架构

So far I have the ray_color() function create two hard-coded PDFs:

1. p0() related to the shape of the light
2. p1() related to the normal vector and type of surface

We can pass information about the light (or whatever hittable we want to sample) into the ray_color() function, and we can ask the material function for a PDF (we would have to instrument it to do that). We can also either ask hit function or the material class to supply whether there is a specular vector.

1. p0() 与光的形状有关
2. p1() 与曲面的法向量和类型有关

#### Diffuse Versus Specular

One thing we would like to allow for is a material like varnished wood that is partially ideal specular (the polish) and partially diffuse (the wood). Some renderers have the material generate two rays: one specular and one diffuse. I am not fond of branching, so I would rather have the material randomly decide whether it is diffuse or specular. The catch with that approach is that we need to be careful when we ask for the PDF value and be aware of whether for this evaluation of ray_color() it is diffuse or specular. Fortunately, we know that we should only call the pdf_value() if it is diffuse so we can handle that implicitly.

We can redesign material and stuff all the new arguments into a struct like we did for hittable:

The Lambertian material becomes simpler:

And ray_color() changes are small:

#### Handling Specular

We have not yet dealt with specular surfaces, nor instances that mess with the surface normal. But this design is clean overall, and those are all fixable. For now, I will just fix specular. Metal and dielectric materials are easy to fix.

Note that if fuzziness is high, this surface isn’t ideally specular, but the implicit sampling works just like it did before.

ray_color() just needs a new case to generate an implicitly sampled ray:

ray_color() 只需要一个新的情况来生成一个隐式采样射线:

We also need to change the block to metal. We’ll also swap out the short block for a glass sphere.

The resulting image has a noisy reflection on the ceiling because the directions toward the box are not sampled with more density.

We could make the PDF include the block. Let’s do that instead with a glass sphere because it’s easier.

#### Sampling a Sphere Object

When we sample a sphere’s solid angle uniformly from a point outside the sphere, we are really just sampling a cone uniformly (the cone is tangent to the sphere). Let’s say the code has theta_max. Recall from the Generating Random Directions chapter that to sample 𝜃θ we have:

$$r_2 = \int_0^{\theta}{2\pi \cdot f(t) \cdot \sin(t)dt}$$

Here 𝑓(𝑡) is an as yet uncalculated constant 𝐶, so:

$$r_2 = \int_0^{\theta}{2\pi \cdot C \cdot \sin(t)dt}$$

Doing some algebra/calculus this yields:

$$r_2 =2\pi \cdot C \cdot (1-\cos(\theta))$$

So

$$\cos(\theta) = 1 - \frac{r_2}{2\pi \cdot C}$$

We know that for $𝑟_2=1$ we should get $𝜃_𝑚𝑎𝑥$, so we can solve for 𝐶:

$$\cos(\theta) = 1 + r_2 \cdot (\cos(\theta_{max}) - 1)$$

𝜙 we sample like before, so:

$\phi$ 我们像以前一样采样，所以:
$$z = \cos(\theta) = 1 + r_2 \cdot (\cos(\theta_{max}) - 1)$$

$$x = \cos(\phi)\cdot \sin(\theta)=\cos(2\pi \cdot r_1) \cdot \sqrt{1 - z^2}$$

$$y = \sin(\phi) \cdot \sin(\theta) = \sin(2\pi \cdot r_1) \cdot \sqrt{1 - z^2}$$

Now what is 𝜃𝑚𝑎𝑥?

We can see from the figure that sin(𝜃𝑚𝑎𝑥)=𝑅/𝑙𝑒𝑛𝑔𝑡ℎ(𝐜𝐩). So:

$$\cos(\theta_{max}) = \sqrt{1 - \frac{R^2}{length^2(\mathbf c - \mathbf p)}}$$

We also need to evaluate the PDF of directions. For directions toward the sphere this is 1/𝑠𝑜𝑙𝑖𝑑_𝑎𝑛𝑔𝑙𝑒. What is the solid angle of the sphere? It has something to do with the 𝐶 above. It, by definition, is the area on the unit sphere, so the integral is

$$solid_angle = \int_0^{2\pi}\int_0^{\theta {max}}\sin(\theta) = 2\pi \cdot (1 - \cos(\theta_{max}))$$

It’s good to check the math on all such calculations. I usually plug in the extreme cases (thank you for that concept, Mr. Horton — my high school physics teacher). For a zero radius sphere cos(𝜃𝑚𝑎𝑥)=0 and that works. For a sphere tangent at 𝐩, cos(𝜃𝑚𝑎𝑥)=0, and 2𝜋 is the area of a hemisphere, so that works too.

#### Updating the Sphere Code

The sphere class needs the two PDF-related functions:

With the utility function:

We can first try just sampling the sphere rather than the light:

This yields a noisy box, but the caustic under the sphere is good. It took five times as long as sampling the light did for my code. This is probably because those rays that hit the glass are expensive!

#### Adding PDF Functions to Hittable Lists

We should probably just sample both the sphere and the light. We can do that by creating a mixture density of their two densities. We could do that in the ray_color() function by passing a list of hittables in and building a mixture PDF, or we could add PDF functions to hittable_list. I think both tactics would work fine, but I will go with instrumenting hittable_list.

We assemble a list to pass to ray_color() from main():

And we get a decent image with 1000 samples as before:

#### Handling Surface Acne

An astute reader pointed out there are some black specks in the image above. All Monte Carlo Ray Tracers have this as a main loop:

If you find yourself getting some form of acne in the images, and this acne is white or black, so one “bad” sample seems to kill the whole pixel, that sample is probably a huge number or a NaN (Not A Number). This particular acne is probably a NaN. Mine seems to come up once in every 10–100 million rays or so.

So big decision: sweep this bug under the rug and check for NaNs, or just kill NaNs and hope this doesn’t come back to bite us later. I will always opt for the lazy strategy, especially when I know floating point is hard. First, how do we check for a NaN? The one thing I always remember for NaNs is that a NaN does not equal itself. Using this trick, we update the write_color() function to replace any NaN components with zero:

Happily, the black specks are gone:

### 你的余生

The purpose of this book was to show the details of dotting all the i’s of the math on one way of organizing a physically based renderer’s sampling approach. Now you can explore a lot of different potential paths.

If you want to explore Monte Carlo methods, look into bidirectional and path spaced approaches such as Metropolis. Your probability space won’t be over solid angle, but will instead be over path space, where a path is a multidimensional point in a high-dimensional space. Don’t let that scare you — if you can describe an object with an array of numbers, mathematicians call it a point in the space of all possible arrays of such points. That’s not just for show. Once you get a clean abstraction like that, your code can get clean too. Clean abstractions are what programming is all about!

If you want to do movie renderers, look at the papers out of studios and Solid Angle. They are surprisingly open about their craft.

If you want to do high-performance ray tracing, look first at papers from Intel and NVIDIA. Again, they are surprisingly open.

If you want to do hard-core physically based renderers, convert your renderer from RGB to spectral. I am a big fan of each ray having a random wavelength and almost all the RGBs in your program turning into floats. It sounds inefficient, but it isn’t!

Regardless of what direction you take, add a glossy BRDF model. There are many to choose from, and each has its advantages.

Have fun!

Peter Shirley
Salt Lake City, March, 2016