Ray Tracing the Rest of Your Life

引言

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

原文源自 《Ray Tracing: The Rest of Your Life》

概述

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

在Ray Tracing In One Weekend和Ray Tracing: the Next Week中,你构建了一个“真正的”Ray Tracing。

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.

和之前一样,https://in1weekend.blogspot.com/将提供更多的阅读材料和参考资料。

感谢每一个为这个项目提供帮助的人。你可以在本书末尾的致谢部分找到它们。

一个简单的蒙特卡罗程序

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.

让我们从一个最简单的蒙特卡洛(Monte Carlo, MC)程序开始。MC程序对一个答案给出一个统计估计,并且这个估计会随着你运行时间的延长而变得越来越准确。这种简单程序产生嘈杂但更好的答案的基本特征就是MC的全部,它尤其适用于不需要很高精确度的图形等应用程序。

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:

作为一个例子,让我们估算 pi。有很多方法可以做到这一点,布丰针问题就是一个经典的案例研究。我们会受启发做一个变奏。假设在一个正方形中有一个内圆:

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:

因为 r 消掉了,我们可以选择计算上方便的值。让我们使用 r=1 ,在原点居中:

#include "rtweekend.h"

#include <iostream>
#include <iomanip>
#include <math.h>
#include <stdlib.h>

void estimatePI() {
int inside_circle = 0;
int runs = 0;
std::cout << std::fixed << std::setprecision(12);

runs++;
auto x = random_double(-1, 1);
auto y = random_double(-1, 1);

if (x * x + y * y < 1)
inside_circle++;

if (runs % 100000 == 0)
std::cout << "Estimate of Pi = "
<< 4 * double(inside_circle) / runs
<< '\n';
}

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

所找到的 π 的答案将根据初始的随机种子而因计算机而异。在我的电脑上,我的答案估计 Estimate of Pi = 3.0880000000

Showing Convergence

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

如果我们将程序更改为永远运行,并打印出一个运行估计:

#include "rtweekend.h"

#include <iostream>
#include <iomanip>
#include <math.h>
#include <stdlib.h>

void estimatePI() {
int inside_circle = 0;
int runs = 0;
std::cout << std::fixed << std::setprecision(12);

+ while (true) {
runs++;
auto x = random_double(-1, 1);
auto y = random_double(-1, 1);

if (x * x + y * y < 1)
inside_circle++;

if (runs % 100000 == 0)
std::cout << "Estimate of Pi = "
<< 4 * double(inside_circle) / runs
<< '\n';
+ }
}

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:

我们很快地接近 $\pi$,然后慢慢地瞄准它。这是收益递减定律的一个例子,每个样本的帮助都小于前一个样本。这是MC中最糟糕的部分。我们可以通过分层样本(通常称为抖动)来缓解这种递减的收益,这里我们不是随机抽取样本,而是取一个网格,在每个网格中抽取一个样本:

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:

这改变了样本生成,但我们需要提前知道我们取了多少样本,因为我们需要知道网格。让我们以1亿为例,两种方法都试一下:

#include "rtweekend.h"

#include <iostream>
#include <iomanip>
#include <math.h>
#include <stdlib.h>

void estimatePI() {
int inside_circle = 0;
int inside_circle_stratified = 0;
int sqrt_N = 10000;
std::cout << std::fixed << std::setprecision(12);

for (int i = 0; i < sqrt_N; ++i) {
for (int j = 0; j < sqrt_N; ++j) {
auto x = random_double(-1, 1);
auto y = random_double(-1, 1);

if (x * x + y * y < 1)
inside_circle++;

x = 2 * ((i + random_double()) / sqrt_N) - 1;
y = 2 * ((j + random_double()) / sqrt_N) - 1;

if (x * x + y * y < 1)
inside_circle_stratified++;
}
}

auto N = static_cast<double>(sqrt_N) * sqrt_N;
std::cout << std::fixed << std::setprecision(12);
std::cout
<< "Regular Estimate of Pi = "
<< 4*double(inside_circle) / (sqrt_N*sqrt_N) << '\n'
<< "Stratified Estimate of Pi = "
<< 4*double(inside_circle_stratified) / (sqrt_N*sqrt_N) << '\n';
}

On my computer, I get:

在我的电脑上,我得到了:

Regular    Estimate of Pi = 3.14151480
Stratified Estimate of Pi = 3.14158948

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.

有趣的是,分层方法不仅更好,而且它具有更好的渐近收敛率!不幸的是,这种优势会随着问题的大小而减小(例如,在3D球体体积版本中,差距会更小)。这被称为维度诅咒。我们将会是非常高维的(每个反射都增加两个维度),所以我不会在这本书中分层,但如果你做过单反射或阴影或一些严格的二维问题,你肯定想分层。

一维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:

这就提出了MC方法:

#include "rtweekend.h"

#include <iostream>
#include <iomanip>

int main() {
int N = 100000;
auto sum = 0.0;
for (int i = 0; i < N; ++i) {
auto x = random_double(0, 2);
sum += x * x;
}
std::cout << std::fixed << std::setprecision(12);
std::cout << "I = " << 2 * sum / N << '\n';
}

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.

正如预期的那样,这产生了我们用代数得到的大致准确答案,I = 8/3。对于不能解析积分的函数也可以这样做,比如$\log(\sin(x))$。在图形学中,我们经常有可以求值但不能显式地写出来的函数,或者只能按概率求值的函数。这实际上是射线追踪的 ray_color() 函数的最后两本书-我们不知道在每个方向上看到什么颜色,但我们可以统计估计它在任何给定的维度。

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:

如果我们为更多的树添加数据,直方图会变得更高。如果我们把数据分成更多的箱子,它就会变短。离散密度函数不同于直方图,它将频率y轴归一化为一个分数或百分比(只是分数乘以100)。一个连续的直方图,我们把箱子的数量取到无穷大,不可能是一个分数,因为所有箱子的高度会降为零。密度函数是指我们对箱子进行调整使它们不会随着箱子的增加而变短。对于上面的树直方图,我们可以尝试:

$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.

如果我们想知道在多个箱子张成的空间里的概率,我们要求和。

概率密度函数,即以后的PDF,是使分数直方图连续的。

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?

让我们制作一个PDF,并使用它来更好地理解它。假设我想要一个0到2之间的随机数 $r$,其概率与自身成正比: $r$。我们希望PDF $p(r)$看起来像下面的数字,但它应该有多高?

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):

高度就是 $p(2)$。应该是什么呢?我们可以按照惯例把它写成任何形式,我们应该选一个方便的形式。就像用直方图一样,我们可以对区域求和(积分)来计算出 $r$ 在某个区间内的概率(x0,x1):

$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

其中 C 是缩放常数。我们也可以用 C = 1 来表示整洁,这正是概率所做的。我们也知道概率 r 在某个地方的值是1,所以在这种情况下:

$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.

所以 $p(r) = r / 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 $𝑃(𝑥)$:

我们如何生成一个随机数与 PDF $p(r)$?为此,我们需要更多的机器。别担心,这不会永远持续下去!

给定 d = random_double() 中的一个均匀且介于 0 和 1 之间的随机数,我们应该能够找到一些函数(f(d)),从而得到我们想要的结果。假设 $e = f(d) = d^2$。这不再是一个统一的PDF文件。PDF的 $e$ 在接近1时会比接近 0 时更大(将 0 和 1 之间的数字平方会使它更小)。为了将这个一般的观察结果转化为一个函数,我们需要累积概率分布函数 $P(x)$:

$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)$,$p(x) = 0$,即存在一个 $x$ 的概率为 0。对于我们的示例PDF $p (r) = r / 2$, $P(x)$ 是:

$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:

一个问题是,x 和 r 是什么关系?它们是虚拟变量—类似于程序中的函数参数。如果在 x = 1.0 处求 P 值,则得到:

$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.

也就是说,PDF值小于1的随机变量的概率是25%这就产生了一个聪明的观察结果:

$f(P(x)) = x$

That means 𝑓 just undoes whatever 𝑃 does. So,

这意味着函数 f 只是撤消函数 P 所做的任何事情。所以,

$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

对于我们的 PDFp(x)=x/2,以及相应的P(x),我们需要计算P的逆。如果我们有

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

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

我们通过用 $y$ 求出 $x$ 来求逆:

$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

注意,这个范围如预期的从 0 到 2,如果我们用 1/4 替换 random_double() 来检查我们的工作,我们会得到预期的1。

我们可以对旧的积分进行抽样

$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/𝑝𝑑𝑓:

#include "rtweekend.h"

#include <iostream>
#include <iomanip>

inline double pdf(double x) {
return 0.5*x;
}

int main() {
int N = 100000;
auto sum = 0.0;
for (int i = 0; i < N; ++i) {
- auto x = random_double(0, 2);
- sum += x * x;
+ auto x = sqrt(random_double(0,4));
+ sum += x*x / pdf(x);
}
std::cout << std::fixed << std::setprecision(12);
std::cout << "I = " << 2 * sum / N << '\n';
}

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.

由于我们在被积函数较大的地方进行更多采样,我们可能会期望更少的噪声,从而更快地收敛。实际上,我们正在将样本转向分布中更重要的部分。这就是为什么使用精心选择的非均匀PDF通常被称为重要抽样。

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:

如果我们使用统一样本的相同代码,那么在[0,2]范围内的PDF = 1/2 ,我们可以使用 x = random_double(0,2) 获得,代码是:

+inline double pdf(double x) {
+ return 0.5;
+}

int main() {
int N = 1000000;
auto sum = 0.0;
for (int i = 0; i < N; i++) {
+ auto x = random_double(0,2);
sum += x*x / pdf(x);
}
std::cout << std::fixed << std::setprecision(12);
std::cout << "I = " << sum/N << '\n';
}

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:

注意,我们不再需要 2*sum/N 中的 2,它是由PDF处理的,当你除以它时,它是 2。您会注意到,重要抽样的作用不大。我们可以让PDF完全遵循被积函数:

$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:

只有当我们已经知道答案时(我们通过对 $P$ 进行分析来得到 $p$),这个完美的重要抽样才可能实现,但这是确保代码工作的一个很好的练习。对于一个样本,我们得到:

+inline double pdf(double x) {
+ return 3*x*x/8;
+}

int main() {
+ int N = 1;
auto sum = 0.0;
for (int i = 0; i < N; i++) {
+ auto x = pow(random_double(0,8), 1./3.);
sum += x*x / pdf(x);
}
std::cout << std::fixed << std::setprecision(12);
std::cout << "I = " << sum/N << '\n';
}

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.

它总是返回准确的答案。

现在让我们回顾一下,因为这是MC射线示踪仪的基本概念。

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

任何 PDF(p) 的选择总是收敛到正确的答案,但是,p 与 f的近距越近,它收敛的速度就越快

方向球上的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:

在我们的射线追踪器中,我们选择随机的方向,方向可以用单位球上的点表示。与之前的方法相同,但是现在我们需要一个在2D上定义的PDF。

假设我们在所有方向上积分

$\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.

通过MC积分,我们应该能够采样 $\cos^2(theta) / p(direction)$,但方向在这种情况下是什么?我们可以让它基于极坐标,所以 p 是用 (theta, phi) 表示的。不管你怎么做,记住PDF必须积分到1,并表示该方向被采样的相对概率。回想一下,我们使用vec3函数在(random_in_unit_sphere())或(random_unit_vector())单位球体上进行均匀随机采样。

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:

#include <iostream>
#include <iomanip>

#include "vec3.h"

inline double pdf(const vec3& p) {
return 1 / (4*pi);
}

int main() {
int N = 1000000;
auto sum = 0.0;
for (int i = 0; i < N; i++) {
vec3 d = random_unit_vector();
auto cosine_squared = d.z()*d.z();
sum += cosine_squared / pdf(d);
}
std::cout << std::fixed << std::setprecision(12);
std::cout << "I = " << sum/N << '\n';
}

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.

解析答案(如果你还记得足够的高级计算,请检查我!)是 $\frac{4}{3} \pi$,上面的代码产生了这个结果。接下来,我们准备在光线追踪中应用它!

这里的关键点是所有的积分和概率,所有的都在单位球上。单位球上的面积就是测量方向的方法。叫它方向,立体角,或者面积,都是一样的。立体角是常用的术语。如果你觉得没问题,那太好了!如果没有,就像我做的一样,想象单位球上一系列方向经过的面积。实心角omega 和单位球上的投影面积 A 是一样的。

光散射

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−𝐴

光散射概率: A

光被吸收的概率: 1-A

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.

这里 A 代表反照率(拉丁语中白色的意思)。反照率在某些学科中是一个精确的技术术语,但在所有情况下,它被用来定义某种形式的分数反射率。这部分反射率(或反照率)会随着颜色的变化而变化(正如我们在书一中为我们的玻璃实现的那样),可以随着入射方向而变化。

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.

在大多数基于物理的渲染器中,我们会使用一组波长的光颜色,而不是RGB。我们可以扩展我们的直觉,把R、G和B看作是长、中、短波长的特殊代数混合物。

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.

如果光散射,它将有一个方向分布,我们可以描述为PDF立体角。我将把它称为散射PDF: s(direction)。散射PDF也可以随入射方向而变化,入射方向是入射射线的方向。当你观察道路上的反射时,你可以看到这随着入射方向的变化而变化——当你的视角(入射角)接近入射时,它们变成了镜子一样。

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.

注意 A 和 s()可能取决于视图方向或散射位置(表面上的位置或体积内的位置)。因此,输出颜色也可能随着视图方向或散射位置的变化而变化。

The Scattering PDF

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

如果我们应用MC基本公式,我们得到以下统计估计:

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

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

其中p(direction)是我们随机生成的任意方向的PDF。

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 𝜋.

对于朗伯曲面,我们已经隐式地实现了这种特殊情况下的公式,其中 $p()$​ 是一个余弦密度。朗伯曲面的 $s()$​ 正比于 $cos(\theta)$​,其中 $\theta$​ 是相对于曲面法线的角度。记住,所有的PDF都需要集成到一个。对于$cos(\theta) < 0$​ 我们有 $s(direction) = 0$, cos在半球上的积分是 $\pi$

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:

所以对于朗伯曲面散射PDF是:

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

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

如果我们使用一个等于散射PDF的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.

这正是我们在原来的ray_color()函数中所使用的!然而,我们需要一般化,这样我们才能在重要的方向发送额外的光线,比如向光的方向。

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)描述的。它与我们的术语非常简单:

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

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

以朗伯曲面为例,BRDF=A/π 我们的术语和BRDF之间的转换很容易。

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

对于参与介质(体积),我们的反照率通常称为散射反照率,我们的散射PDF通常称为相位函数。

重要采样材质

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:

我们的目标在接下来的两章是仪器我们的程序发送一束额外的射线到光源,使我们的图片噪音更小。让我们假设我们可以使用PDF (光照(方向)) 向光源发送一束光线。我们还假设有一个与 $s$​ 相关的 PDF,并将其命名为 *pSurface(direction)*。关于pdf 的一个很棒的地方是你可以使用它们的线性混合来形成混合密度也是 pdf。例如,最简单的是:

$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.

只要权值是正的并且加起来是1,任何这样的 PDF 混合都是 PDF。记住,我们可以使用任何 PDF: 所有的 PDF 最终都会收敛到正确的答案。所以,游戏是要找出如何使 PDF 更大的产品 s(direction)⋅ color(direction) 是大。对于漫反射表面,这主要是猜测哪里 color(direction) 高的问题。

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.

对于镜子来说,只在一个方向上,$s()$ 是巨大的,所以它更重要。事实上,大多数渲染器都将镜像作为一种特殊情况,并且只隐式地使用 s/p——我们的代码目前就是这样做的。

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.

让我们做一个简单的重构,暂时删除所有不属于 Lambertian 的材料。我们可以再次使用 Cornell Box 场景,让我们在生成模型的函数中生成摄像机。

...
color ray_color(...) {
...
}

hittable_list cornell_box() {
hittable_list objects;

auto red = make_shared<lambertian>(color(.65, .05, .05));
auto white = make_shared<lambertian>(color(.73, .73, .73));
auto green = make_shared<lambertian>(color(.12, .45, .15));
auto light = make_shared<diffuse_light>(color(15, 15, 15));

objects.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red));
objects.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 555, white));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white));
objects.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white));

shared_ptr<hittable> box1 = make_shared<box>(point3(0,0,0), point3(165,330,165), white);
box1 = make_shared<rotate_y>(box1, 15);
box1 = make_shared<translate>(box1, vec3(265,0,295));
objects.add(box1);

shared_ptr<hittable> box2 = make_shared<box>(point3(0,0,0), point3(165,165,165), white);
box2 = make_shared<rotate_y>(box2, -18);
box2 = make_shared<translate>(box2, vec3(130,0,65));
objects.add(box2);

return objects;
}

int main() {
// Image

const auto aspect_ratio = 1.0 / 1.0;
const int image_width = 600;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;

// World

auto world = cornell_box();

color background(0,0,0);

// Camera

point3 lookfrom(278, 278, -800);
point3 lookat(278, 278, 0);
vec3 vup(0, 1, 0);
auto dist_to_focus = 10.0;
auto aperture = 0.0;
auto vfov = 40.0;
auto time0 = 0.0;
auto time1 = 1.0;

camera cam(lookfrom, lookat, vup, vfov, aspect_ratio, aperture, dist_to_focus, time0, time1);

// Render

std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

for (int j = image_height-1; j >= 0; --j) {
...
}

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.

减少噪音是我们的目标。我们将通过构建一个向光线发射更多射线的PDF来实现这一点。

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(𝜃)/𝜋$​.

首先,让我们对代码进行测试,以便它显式地对一些PDF进行采样,然后对其进行规范化。记住MC基础知识:$∫𝑓(𝑥)≈𝑓(𝑟)/𝑝(𝑟)$​。对于朗伯材料,让我们像现在一样采样: $p(direction)=\cos(θ)/π$​​。

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

我们修改基类 material 来启用这个重要抽样:

class material {
public:

virtual bool scatter(
const ray& r_in, const hit_record& rec, color& albedo, ray& scattered, double& pdf
+ ) const {
+ return false;
+ }

+ virtual double scattering_pdf(
+ const ray& r_in, const hit_record& rec, const ray& scattered
+ ) const {
+ return 0;
+ }

virtual vec3 emitted(double u, double v, const point3& p) const {
return vec3(0,0,0);
}
};

And Lambertian material becomes:

class material {
public:
virtual vec3 emitted(double u, double v, const vec3& p) const {
return vec3(0, 0, 0);
}

virtual bool scatter(
const ray& r_in,const hit_record& rec,vec3& attenuation,ray& scattered, double& pdf
+ ) const {
+ return false;
+ };

+ virtual double scattering_pdf (
+ const ray& r_in, const hit_record& rec, const ray& scattered
+ ) const {
+ return 0;
+ }
};

And the ray_color function gets a minor modification:

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

vec3 ray_color(const ray& r, const color& background, const hittable& world, int depth) {
hit_record rec;

// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);

// If the ray hits nothing, return the background color.
if (!world.hit(r, 0.001, infinity, rec))
return background;

ray scattered;
vec3 attenuation;
vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
+ double pdf;
+ vec3 albedo;

+ if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf))
+ return emitted;

return emitted
+ + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
+ * ray_color(scattered, background, world, depth-1) / pdf;
}

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𝜋}$.

现在,为了体验,尝试一个不同的抽样策略。和第一本书一样,让我们从表面上的半球中随机选择。这将是 $p(direction)=1/2π$

virtual bool scatter(
const ray& r_in, const hit_record& rec, color& alb, ray& scattered, double& pdf
) const override {
auto direction = random_in_hemisphere(rec.normal);
scattered = ray(rec.p, unit_vector(direction), r_in.time());
alb = albedo->value(rec.u, rec.v, rec.p);
pdf = 0.5 / pi;
return true;
}

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.

它和我们的旧图片很接近,但是有一些区别不是噪音。高盒子的正面颜色更加统一。所以我有一个在蒙特卡罗程序中最难找到的错误——一个产生合理外观图像的错误。我也不知道这个bug是程序的第一个版本,还是第二个版本,或者两者都有!

让我们构建一些基础设施来解决这个问题。

生成随机方向

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

在这两章和接下来的两章中,让我们加强我们的理解和工具,并找出哪个Cornell Box是正确的。

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.

让我们先弄清楚如何生成随机方向。为了简化,我们假设z轴是表面法线,而 $θ$ 是与法线的角度。我们将在下一章中让它们指向曲面法向量。我们只讨论关于 z 旋转对称的分布。所以 $p(direction)=f(θ)$ 如果你学过高等微积分,你可能会记得,在球坐标下 $dA=sin(θ)⋅dθ⋅dϕ$。如果你还没学过,下一步你就得听我的了,但等你上高等微积分的时候你就会明白了。

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

给定一个定向PDF,$p(direction) = f(θ)$ 在球体上,在 $\theta$ 和 $\phi$ 上的1D PDF是:

$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$ 和 $r_2$,一维MC集成一章的内容如下:

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

Solving for $𝜙$ we get:

求解 $\phi$ 我们得到:

$\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.

这里,$t$ 是一个哑变量。让我们尝试一些不同的 $f()$ 函数。首先让我们在球体上尝试均匀密度。单位球的面积是 $4π$​,所以单位球上的一个均匀的$p(direction)=\frac{1}{4π}$。
$$
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(θ)$ 给出:

$\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.

我们不求解,因为我们可能只需要知道 $\cos(θ)$,而不希望不必要的 $\arccos()$ 调用到处运行。

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)$:

使用恒等式 $\cos^2 + \sin^2 = 1$,我们得到以下随机的 $(r_1,r_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:

简化一下,$(1−2𝑟_2)^2=1−4𝑟_2+4𝑟_2^2$​,所以:
$$
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:

我们可以输出其中一些:

int main() {
for (int i = 0; i < 200; i++) {
auto r1 = random_double();
auto r2 = random_double();
auto x = cos(2*pi*r1)*2*sqrt(r2*(1-r2));
auto y = sin(2*pi*r1)*2*sqrt(r2*(1-r2));
auto z = 1 - 2*r2;
std::cout << x << " " << y << " " << z << '\n';
}
}

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

然后在地图上免费标出它们。ly(一个很棒的网站,支持3D散点图):

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:

现在我们来推导半球上的均匀性。密度在半球上是均匀的,意味着 $p(direction)= \frac{1}{2\pi}$​。只要改变方程中的常数就能得到:
$$
\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:

下面是重要抽样的集成。$p(direction)=\frac{1}{2π}$,所以我们取 $f/p$ 的平均值,即 $cos^3(𝜃)/(1/2𝜋)$,我们可以测试这个:

int main() {
int N = 1000000;
auto sum = 0.0;
for (int i = 0; i < N; i++) {
auto r1 = random_double();
auto r2 = random_double();
auto x = cos(2*pi*r1)*2*sqrt(r2*(1-r2));
auto y = sin(2*pi*r1)*2*sqrt(r2*(1-r2));
auto z = 1 - r2;
sum += z*z*z / (1.0/(2.0*pi));
}
std::cout << std::fixed << std::setprecision(12);
std::cout << "Pi/2 = " << pi/2 << '\n';
std::cout << "Estimate = " << sum/N << '\n';
}

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

现在让我们用 p(directions)=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:

让我们开始以随机向量的形式生成它们:

#include "rtweekend.h"

#include <iostream>
#include <math.h>

inline vec3 random_cosine_direction() {
auto r1 = random_double();
auto r2 = random_double();
auto z = sqrt(1-r2);

auto phi = 2*pi*r1;
auto x = cos(phi)*sqrt(r2);
auto y = sin(phi)*sqrt(r2);

return vec3(x, y, z);
}

int main() {
int N = 1000000;

auto sum = 0.0;
for (int i = 0; i < N; i++) {
auto v = random_cosine_direction();
sum += v.z()*v.z()*v.z() / (v.z()/pi);
}

std::cout << std::fixed << std::setprecision(12);
std::cout << "Pi/2 = " << pi/2 << '\n';
std::cout << "Estimate = " << sum/N << '\n';
}

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.

在最后一章中,我们开发了生成相对于z轴的随机方向的方法。我们希望能够相对于曲面法向量来做这个。

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.

一个标准正交基(ONB)是三个相互正交的单位向量的集合。笛卡尔的XYZ轴就是这样一个ONB,我有时会忘记它必须位于真实的位置,具有真实的方向,才能在现实世界中有意义,在虚拟世界中也有虚拟的位置和方向。图片是相机和场景的相对位置/方向的结果,所以只要相机和场景在同一个坐标系中描述,一切都好。

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

假设我们有一个原点 O 和笛卡子单位向量 xyz。当我们说一个位置是(3,-2,7)时,我们实际上是在说:
$$
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:

如果我们想测量另一个以原点 $\mathbf{O}’$ 和基向量 $\mathbf{u}$、$\mathbf{v}$ 和 $\mathbf{w}$ 为原点的坐标系中的坐标,我们可以找到数字 $(u,v,w)$,这样:
$$
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.

如果你上了一门图形入门课程,你会花很多时间学习坐标系统和 4×4 坐标变换矩阵。注意了,这是图像中的重要内容!但我们不需要它。我们需要的是用相对于 $\mathbf{n}$ 的集合分布生成随机方向。我们不需要原点,因为方向相对于没有特定的原点。我们确实需要两个余切向量,它们相互垂直于 $\mathbf{n}$ 并且彼此垂直。

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 𝐝:

有些模型会带有一个或多个余切向量。如果我们的模型只有一个余切向量,那么生成ONB的过程就不是简单的了。假设有任何长度非零且不平行于 $\mathbf{n}$ 的向量 $\mathbf{a}$。通过使用 $\mathbf{c}$ 乘以 $\mathbf{d}$ 同时垂直于 $\mathbf{c}$ 和 $\mathbf{d}$​ 的叉积属性,我们可以得到垂直于 $\mathbf{s}$ 和 $\mathbf{t}$ 的向量:
$$
\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.

这一切都很好,但问题是,在加载模型时,可能不会给我们一个 a,而且在现有的程序中也没有 a。如果我们继续并选择一个任意的 a 作为初始向量,我们可能会得到一个与 n 并行的 a。一个常见的方法是使用if语句来确定 n 是否是一个特定的轴,如果不是,就使用这个轴。

if absolute(n.x > 0.9)
a ← (0, 1, 0)
else
a ← (1, 0, 0)

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

一旦我们有了一个 st,和 n 的ONB,并且有了一个相对于z轴的random(x,y,z),我们就可以得到相对于 n 的向量:
$$
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:

我们应该为onb创建一个类,还是实用函数就足够了?我不确定,但让我们创建一个类,因为它不会比实用函数更复杂:

#ifndef ONB_H
#define ONB_H

class onb {
public:
onb() {}

inline vec3 operator[](int i) const { return axis[i]; }

vec3 u() const { return axis[0]; }
vec3 v() const { return axis[1]; }
vec3 w() const { return axis[2]; }

vec3 local(double a, double b, double c) const {
return a*u() + b*v() + c*w();
}

vec3 local(const vec3& a) const {
return a.x()*u() + a.y()*v() + a.z()*w();
}

void build_from_w(const vec3&);

public:
vec3 axis[3];
};


void onb::build_from_w(const vec3& n) {
axis[2] = unit_vector(n);
vec3 a = (fabs(w().x()) > 0.9) ? vec3(0,1,0) : vec3(1,0,0);
axis[1] = unit_vector(cross(w(), a));
axis[0] = cross(w(), v());
}

#endif

We can rewrite our Lambertian material using this to get:

我们可以用这个改写朗伯公式得到

virtual bool scatter(
const ray& r_in, const hit_record& rec, color& alb, ray& scattered, double& pdf
) const override {
+ onb uvw;
+ uvw.build_from_w(rec.normal);
+ auto direction = uvw.local(random_cosine_direction());
scattered = ray(rec.p, unit_vector(direction), r_in.time());
alb = albedo->value(rec.u, rec.v, rec.p);
+ pdf = dot(uvw.w(), scattered.direction()) / pi;
return true;
}

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.

是这样吗?我们还不确定。在缺乏可靠的参考解决方案的情况下,追踪bug是很困难的。我们先把它放在桌子上,去掉一些杂音。

直接光源采样

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?

对方向进行几乎一致的采样的问题是,光的采样并不比不重要的方向多。我们可以使用阴影光线来分离直接照明。相反,我将向光发射更多的射线。然后我们可以用它向任何我们想要的方向发送更多的射线。

我们很容易选择一个随机的朝向光的方向;在光上随便选一个点,然后往那个方向发送一条射线。我们还需要知道PDF, $p(direction)$。那是什么?

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:

对于一个区域(a)的光,如果我们在该光上均匀采样,光表面的PDF是 $\frac{1}{a}$。在单位球的面积上是什么定义了方向?幸运的是,有一个简单的对应关系,如图所示:

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):

vec3 ray_color(const ray& r, const vec3& background, const hittable& world,int depth) {
hit_record rec;

// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return vec3(0,0,0);

// If the ray hits nothing, return the background color.
if (!world.hit(r,0.001,infinity,rec))
return background;

ray scattered;
vec3 attenuation;
vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
double pdf;
vec3 albedo;

if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf))
return emitted;

+ auto on_light = vec3(random_double(213, 343), 554, random_double(227, 332));
+ auto to_light = on_light - rec.p;
+ auto distance_squared = to_light.length_squared();
+ to_light = unit_vector(to_light);

+ if (dot(to_light, rec.normal) < 0)
+ return emitted;

+ double light_area = (343 - 213) * (332 - 227);
+ auto light_cosine = fabs(to_light.y());
+ if (light_cosine < 0.000001)
+ return emitted;

+ pdf = distance_squared / (light_cosine * light_area);
+ scattered = ray(rec.p, to_light, r.time());

return emitted
+ albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
* ray_color(scattered, background, world, depth - 1) / pdf;
}

With 10 samples per pixel this yields:

如果每像素10个样本,就会产生:

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:

天花板上的灯周围嘈杂的砰砰声是因为灯是双面的,灯和天花板之间有一个很小的空间。我们可能想让光发射下去。我们可以通过让hittable发出的成员函数获取额外的信息来做到这一点:

virtual vec3 emitted(const ray&r_in, const hit_record& rec, double u, double v, const vec3& p) const {
if (rec.front_face)
return emit->value(u, v, p);
else
return vec3(0,0,0);
}

Making sure to call this in our world definition:

确保在我们的世界定义中调用它:

hittable_list cornell_box_plus() {
hittable_list objects;

auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05)));
auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73)));
auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15)));
auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(15, 15, 15)));

objects.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red));
+ objects.add(make_shared<flip_face>(make_shared<xz_rect>(213, 343, 227, 332, 554, light)));
objects.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 555, white));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white));
objects.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white));
......

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.

我们使用了一个与 $\cos(\theta)$ 有关的PDF,以及一个与光线采样有关的PDF。我们想要一个结合这些的PDF。

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:

概率论中的一个常用工具是将密度混合,形成一个混合密度。任何PDF的加权平均值都是PDF。例如,我们可以取两个密度的平均值:
$$
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:

我们该如何编写代码来实现这一点呢?有一个非常重要的细节使得这并不像人们想象的那么容易。选择随机方向很简单:

if (random_double() < 0.5)
pick direction according to pdf_reflection
else
pick direction according to pdf_light

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$.

但是对 mixture_pdf 的计算稍微微妙一些。我们需要同时计算 reflection_pdf 和 light_pdf,因为有一些方向,其中任何一个pdf都可以生成方向。例如,我们可以使用 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.

退一步来看,PDF需要支持两个功能:

  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:

这是如何做的细节是不同的 reflection_pdf 和 light_pdf 和两者的混合密度,但这正是类层次结构被发明的!抽象类中有什么是不明显的,所以我的方法是贪婪的,希望一个最小的接口工作,对于PDF这意味着:

#include "vec3.h"

class pdf {
public:
virtual ~pdf() {}

virtual double value(const vec3& direction) const = 0;
virtual vec3 generate() const = 0;
};

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:

我们将通过充实子类来看看它是否有效。为了对光线进行采样,我们需要hittable来回答一些它没有接口的查询。我们可能也需要弄乱它,但我们可以先看看是否可以在hittable中放入一些东西,包括采样与所有子类一起工作的边界框。

首先,我们来试试余弦密度:

class cosine_pdf : public pdf {
public:
cosine_pdf(const vec3& w) { uvw.build_from_w(w); }

virtual double value(const vec3& direction) const override {
auto cosine = dot(unit_vector(direction), uvw.w());
return (cosine <= 0) ? 0 : cosine/pi;
}

virtual vec3 generate() const override {
return uvw.local(random_cosine_direction());
}

public:
onb uvw;
};

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.

我们可以在 ray_color() 函数中尝试一下,突出显示主要的变化。我们还需要将变量 pdf 更改为其他变量名,以避免与新 pdf 类的名称冲突。

vec3 ray_color(const ray& r, const vec3& background, const hittable& world,int depth) {
hit_record rec;

// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return vec3(0,0,0);

// If the ray hits nothing, return the background color.
if (!world.hit(r,0.001,infinity,rec))
return background;

ray scattered;
vec3 attenuation;
vec3 emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
+ double pdf_val;
vec3 albedo;

+ if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val))
return emitted;

- auto on_light = vec3(random_double(213, 343), 554, random_double(227, 332));
- auto to_light = on_light - rec.p;
- auto distance_squared = to_light.length_squared();
- to_light = unit_vector(to_light);

- if (dot(to_light, rec.normal) < 0)
- return emitted;

- double light_area = (343 - 213) * (332 - 227);
- auto light_cosine = fabs(to_light.y());
- if (light_cosine < 0.000001)
- return emitted;

- pdf = distance_squared / (light_cosine * light_area);
- scattered = ray(rec.p, to_light, r.time());

+ cosine_pdf p(rec.normal);
+ scattered = ray(rec.p, p.generate(), r.time());
+ pdf_val = p.value(scattered.direction());

return emitted
+ albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
+ * ray_color(scattered, background, world, depth - 1) / pdf_val;
}

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

这产生一个明显匹配的结果,所以我们所做的一切,到目前为止是重构的pdf计算:

Sampling Directions towards a Hittable

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

现在我们可以试着朝着可击中物体的方向取样,比如光。

class hittable_pdf : public pdf {
public:
hittable_pdf(shared_ptr<hittable> p, const vec3& origin) : ptr(p), o(origin) {}

virtual double value(const vec3& direction) const {
return ptr->pdf_value(o, direction);
}

virtual vec3 generate() const {
return ptr->random(o);
}

public:
vec3 o;
shared_ptr<hittable> ptr;
};

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:

这假设在hittable类中有两个尚未实现的函数。为了避免向所有hittable子类添加插装,我们将向hittable类添加两个虚拟函数:

class hittable {
public:
virtual bool hit(const ray& r,double t_min,double t_max,hit_record& rec) const = 0;
virtual bool bounding_box(double t0, double t1, aabb& output_box) const = 0;

+ virtual double pdf_value(const vec3& o, const vec3& v) {
+ return 0.0;
+ }

+ virtual vec3 random(const vec3& o) const {
+ return vec3(1, 0, 0);
+ }
};

And we change xz_rect to implement those functions:

我们改变 xz_rect 来实现这些函数:

class xz_rect : public hittable {
public:
......
virtual double pdf_value(const vec3& origin, const vec3& v) const {
hit_record rec;
if (!this->hit(ray(origin, v), 0.001, infinity, rec))
return 0;

auto area = (x1-x0)*(z1-z0);
auto distance_squared = rec.t * rec.t * v.length_squared();
auto cosine = fabs(dot(v, rec.normal) / v.length());

return distance_squared / (cosine * area);
}

virtual vec3 random(const vec3& origin) const override {
auto random_point = vec3(random_double(x0,x1), k, random_double(z0,z1));
return random_point - origin;
}

And then change ray_color():

vec3 ray_color(
const ray& r, const color& background, const hittable& world,
+ shared_ptr<hittable>& lights, int depth
) {
...

ray scattered;
color attenuation;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
double pdf_val;
color albedo;
if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val))
return emitted;

+ hittable_pdf light_pdf(lights, rec.p);
+ scattered = ray(rec.p, light_pdf.generate(), r.time());
+ pdf_val = light_pdf.value(scattered.direction());

return emitted
+ albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
+ * ray_color(scattered, background, world, lights, depth-1) / pdf_val;
}

...
int main() {
...
// World

auto world = cornell_box();
+ shared_ptr<hittable> lights =
+ make_shared<xz_rect>(213, 343, 227, 332, 554, shared_ptr<material>());

...
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
...
+ pixel_color += ray_color(r, background, world, lights, max_depth);
...

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:

现在我们要做余弦的混合密度和光采样。混合密度类很简单:

class mixture_pdf : public pdf {
public:
mixture_pdf(shared_ptr<pdf> p0, shared_ptr<pdf> p1) {
p[0] = p0;
p[1] = p1;
}

virtual double value(const vec3& direction) const override {
return 0.5 * p[0]->value(direction) + 0.5 *p[1]->value(direction);
}

virtual vec3 generate() const override {
if (random_double() < 0.5)
return p[0]->generate();
else
return p[1]->generate();
}

public:
shared_ptr<pdf> p[2];
};

And plugging it into ray_color():

并将其插入 ray_color() 中:

color ray_color(
const ray& r, const color& background, const hittable& world,
shared_ptr<hittable>& lights, int depth
) {
...

ray scattered;
color attenuation;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
double pdf_val;
color albedo;
if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val))
return emitted;
+ auto p0 = make_shared<hittable_pdf>(lights, rec.p);
+ auto p1 = make_shared<cosine_pdf>(rec.normal);
+ mixture_pdf mixed_pdf(p0, p1);

+ scattered = ray(rec.p, mixed_pdf.generate(), r.time());
+ pdf_val = mixed_pdf.value(scattered.direction());

...
}

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.

镜面光线(玻璃和金属)不再被支持。如果我们把它们的散射函数设为脉冲函数,数学就能算出来。但那将是浮点灾难。我们可以将镜面反射分离出来,或者让表面粗糙度永远不为零,拥有看起来完美光滑但不会产生 NaN的准镜面。我对这种方法没有意见(我都试过了,他们都有他们的优势),但我们有光滑的金属和玻璃代码,所以我添加了完美的镜面,不做显式的f()/p()计算。

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.

如果我们想要添加环境映射或更有趣的功能背景,我们还缺少真正的后台功能基础设施。有些环境映射是HDR (RGB组件是浮点数,而不是0-255字节,通常被解释为0-1)。我们的输出一直是HDR;我们只是在截短它。

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.

最后,我们的渲染器是RGB,一个更基于物理的渲染器-就像汽车制造商可能使用的-可能需要使用光谱颜色,甚至极化。对于电影渲染器,您可能需要RGB。你可以制作一个混合渲染器,它有两种模式,但这当然比较困难。现在我将坚持RGB模式,但是我会在本书的末尾重新讨论这个问题。

优化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.

到目前为止,我的 ray_color() 函数创建两个硬编码的 PDF:

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

我们可以将关于光的信息(或者任何我们想要采样的 hittable)传递到 ray_color() 函数中,并且我们可以要求材质函数提供一个PDF文件(我们将不得不为此设置它)。我们也可以询问 hit 函数或 material 类是否有一个高光矢量。

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.

我们想要允许的一件事是一个材料,如涂漆的木材,部分是理想的镜面反射(抛光)和部分扩散(木材)。一些渲染器让材质产生两条光线:一条高光,一条漫反射。我不喜欢分支,所以我宁愿让材料随机决定是漫反射还是镜面反射。这种方法的陷阱是,当我们要求PDF值时,我们需要小心,并知道对于 ray_color() 的评估,它是漫反射还是镜面反射。幸运的是,我们知道只有当 pdf_value() 是diffuse时才应该调用它,这样我们就可以隐式地处理它。

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

struct scatter_record {
ray specular_ray;
bool is_specular;
vec3 attenuation;
shared_ptr<pdf> pdf_ptr;
};

class material {
public:
virtual vec3 emitted(const ray& r_in, const hit_record& rec, double u, double v, const vec3& p) const {
return vec3(0, 0, 0);
}

virtual bool scatter(
const ray& r_in,const hit_record& rec, scatter_record& srec
) const {
return false;
};

virtual double scattering_pdf (
const ray& r_in, const hit_record& rec, scatter_record& srec
) const {
return 0;
}
};

The Lambertian material becomes simpler:

class lambertian : public material {
public:
lambertian(const vec3& a) : albedo(make_shared<constant_texture>(a)) {}
lambertian(shared_ptr<texture> a) : albedo(a) {}

virtual bool scatter(
const ray& r_in, const hit_record& rec, scatter_record& srec
) const {
srec.is_specular = false;
srec.attenuation = albedo->value(rec.u, rec.v, rec.p);
srec.pdf_ptr = make_shared<cosine_pdf>(rec.normal);
return true;
}

double scattering_pdf(
const ray& r_in, const hit_record& rec, const ray& scattered
) const {
auto cosine = dot(rec.normal, unit_vector(scattered.direction()));
return 0 ? 0 : cosine / pi;
}
public:
shared_ptr<texture> albedo;
};

And ray_color() changes are small:

color ray_color(
const ray& r,
const color& background,
const hittable& world,
+ shared_ptr<hittable>& lights,
int depth
) {
hit_record rec;

// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);

// If the ray hits nothing, return the background color.
if (!world.hit(r, 0.001, infinity, rec))
return background;
+ scatter_record srec;
+ color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
+ if (!rec.mat_ptr->scatter(r, rec, srec))
+ return emitted;

+ auto light_ptr = make_shared<hittable_pdf>(lights, rec.p);
+ mixture_pdf p(light_ptr, srec.pdf_ptr);

+ ray scattered = ray(rec.p, p.generate(), r.time());
+ auto pdf_val = p.value(scattered.direction());

return emitted
+ + srec.attenuation * rec.mat_ptr->scattering_pdf(r, rec, scattered)
* ray_color(scattered, background, world, lights, depth-1) / pdf_val;
}

...

int main() {
...
// World

auto world = cornell_box();
+ auto lights = make_shared<hittable_list>();
+ lights->add(make_shared<xz_rect>(213, 343, 227, 332, 554, shared_ptr<material>()));
+ lights->add(make_shared<sphere>(point3(190, 90, 190), 90, shared_ptr<material>()));
...

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.

我们还没有处理过镜面,也没有处理过破坏表面法线的情况。但这个设计总体上是干净的,而且这些都是可以修复的。现在,我只修复镜面反射。金属和介电材料很容易固定。

class metal : public material {
public:
metal(const color& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {}
virtual bool scatter(
+ const ray& r_in, const hit_record& rec, scatter_record& srec
) const override {
+ vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
+ srec.specular_ray = ray(rec.p, reflected+fuzz*random_in_unit_sphere());
+ srec.attenuation = albedo;
+ srec.is_specular = true;
+ srec.pdf_ptr = 0;
+ return true;
}
public:
vec3 albedo;
double fuzz;
};

...

class dielectric : public material {
public:
...
virtual bool scatter(
const ray& r_in, const hit_record& rec, scatter_record& srec
) const override {
+ srec.is_specular = true;
+ srec.pdf_ptr = nullptr;
+ srec.attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0/ir) : ir;
...
+ srec.specular_ray = ray(rec.p, direction, r_in.time());
return true;
}
...
};

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() 只需要一个新的情况来生成一个隐式采样射线:

vec3 ray_color(
const ray& r,
const vec3& background,
const hittable& world,
shared_ptr<hittable>& lights,
int depth
) {
...

scatter_record srec;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
if (!rec.mat_ptr->scatter(r, rec, srec))
return emitted;

+ if (srec.is_specular) {
+ return srec.attenuation
+ * ray_color(srec.specular_ray, background, world, lights, depth-1);
}

...
}

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

我们还需要把积木换成金属。我们也会把短块换成玻璃球。

hittable_list cornell_box_plus() {
hittable_list objects;

auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05)));
auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73)));
auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15)));
auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(15, 15, 15)));

objects.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green));
objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red));
objects.add(make_shared<flip_face>(make_shared<xz_rect>(213, 343, 227, 332, 554, light)));
objects.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 555, white));
objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white));
objects.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white));

+ shared_ptr<material> aluminum = make_shared<metal>(vec3(0.8, 0.85, 0.88), 0.0);
+ shared_ptr<hittable> box1 = make_shared<box>(vec3(0,0,0), vec3(165,330,165), aluminum);
box1 = make_shared<rotate_y>(box1, 15);
box1 = make_shared<translate>(box1, vec3(265,0,295));
objects.add(box1);

shared_ptr<hittable> box2 = make_shared<box>(vec3(0,0,0), vec3(165,165,165), white);
box2 = make_shared<rotate_y>(box2, -18);
box2 = make_shared<translate>(box2, vec3(130,0,65));
objects.add(box2);

+ auto glass = make_shared<dielectric>(1.5);
+ objects.add(make_shared<sphere>(vec3(190,90,190), 90 , glass));

return objects;
}

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:

当我们从球外一点均匀地采样一个球的立体角时,我们实际上是在均匀地采样一个锥(锥与球相切)。假设代码有 theta_max。回想一下生成随机方向那一章,我们有:
$$
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 𝐶:

我们知道对于 $r_2 = 1$ 我们应该得到 $\theta_{max}$,因此我们可以求解 $C$:
$$
\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

我们还需要评估说明书的PDF。对于朝向球体的方向,这是 1/𝑠𝑜𝑙𝑖𝑑𝑎𝑛𝑔𝑙𝑒。球的立体角是多少?它与上面的 C 有关。根据定义,它是单位球上的面积,所以积分是
$$
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.

在所有这样的计算中检查数学是很好的。我通常会插入一些极端的例子(谢谢你的概念,霍顿先生,我的高中物理老师)。对于一个零半径的球体$\cos \theta_{max} = 0$,这是可行的。对于一个球面,正切在 $\mathbf{p}$, $\cos \theta_{max} = 0$,并且 $2\pi$ 是一个半球的面积,所以也可以这样做。

Updating the Sphere Code

The sphere class needs the two PDF-related functions:

球体类需要两个pdf相关的函数:

double sphere::pdf_value(const vec3& o, const vec3& v) const {
hit_record rec;
if (!this->hit(ray(o, v), 0.001, infinity, rec))
return 0;

auto cos_theta_max = sqrt(1 - radius*radius/(center-o).length_squared());
auto solid_angle = 2*pi*(1-cos_theta_max);

return 1 / solid_angle;
}

vec3 sphere::random(const vec3& o) const {
vec3 direction = center - o;
auto distance_squared = direction.length_squared();
onb uvw;
uvw.build_from_w(direction);
return uvw.local(random_to_sphere(radius, distance_squared));
}

With the utility function:

inline vec3 random_to_sphere(double radius, double distance_squared) {
auto r1 = random_double();
auto r2 = random_double();
auto z = 1 + r2*(sqrt(1-radius*radius/distance_squared) - 1);

auto phi = 2*pi*r1;
auto x = cos(phi)*sqrt(1-z*z);
auto y = sin(phi)*sqrt(1-z*z);

return vec3(x, y, z);
}

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

int main() {
...
// World

auto world = cornell_box();
+ shared_ptr<hittable> lights =
+ // make_shared<xz_rect>(213, 343, 227, 332, 554, shared_ptr<material>());
+ make_shared<sphere>(point3(190, 90, 190), 90, shared_ptr<material>());
...

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!

这产生了一个有噪声的盒子,但球下面的焦散是好的。它所花费的时间是我代码中灯光采样时间的5倍。这可能是因为撞击玻璃的光线代价很昂贵!

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.

我们应该同时对球体和光进行采样。我们可以通过创建它们两个密度的混合密度来做到这一点。我们可以通过在 ray_color() 函数中传递一个 hittables 列表并构建一个混合 PDF 来做到这一点,或者我们可以在 hittable_list 中添加PDF函数。我认为这两种策略都可以,但我还是选择 hittable_list 工具。

double hittable_list::pdf_value(const vec3& o, const vec3& v) const {
auto weight = 1.0/objects.size();
auto sum = 0.0;

for (const auto& object : objects)
sum += weight * object->pdf_value(o, v);

return sum;
}

vec3 hittable_list::random(const vec3& o) const {
auto int_size = static_cast<int>(objects.size());
return objects[random_int(0, int_size-1)]->random(o);
}

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

我们组装了一个列表,从 main() 传递给 ray_color():

hittable_list lights;
lights.add(make_shared<xz_rect>(213, 343, 227, 332, 554, 0));
lights.add(make_shared<sphere>(point3(190, 90, 190), 90, 0));

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

和之前一样,我们用1000个样本得到了一个不错的图像:

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:

一位精明的读者指出,上图中有一些黑点。所有蒙特卡罗射线追踪器都有这样一个主循环:

pixel_color = average(many many samples)

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.

如果你发现自己在图像中出现了某种形式的痤疮,而且这个痤疮是白色或黑色的,所以一个“坏”样本似乎会毁掉整个像素,这个样本可能是一个巨大的数字或 NaN(不是一个数字)。这个特殊的痤疮可能是 NaN。我的光似乎每1000万到1亿次左右就会出现一次。

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:

所以这是一个重大的决定:把这个bug掩盖起来,检查 NaN,或者干脆消灭NaN,希望它以后不会再来咬我们。我总是选择惰性策略,特别是当我知道浮点数很难的时候。首先,我们如何检查 NaN?对于 NaN,我总是记得的一件事是 NaN 不等于它自己。使用这个技巧,我们更新 write_color() 函数,将任何 NaN 组件替换为0:

void write_color(std::ostream &out, vec3 pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();

+ // Replace NaN components with zero. See explanation in Ray Tracing: The Rest of Your Life.
+ if (r != r) r = 0.0;
+ if (g != g) g = 0.0;
+ if (b != b) b = 0.0;

// Divide the color by the number of samples and gamma-correct for gamma=2.0.
auto scale = 1.0 / samples_per_pixel;
r = sqrt(scale * r);
g = sqrt(scale * g);
b = sqrt(scale * b);

// Write the translated [0,255] value of each color component.
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}

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

这本书的目的是展示如何用一种方法组织基于物理的渲染器的采样方法来点画所有的数学i的细节。现在你可以探索许多不同的潜在路径。

如果你想探索蒙特卡罗方法,看看双向和路径间隔的方法,如Metropolis。你的概率空间不会在立体角上,而是在路径空间上,路径是高维空间中的一个多维点。不要被这吓倒——如果你能用一组数字来描述一个对象,数学家们就称它为空间中所有这些点的可能数组中的一个点。这不是作秀。一旦你得到了这样一个清晰的抽象,你的代码也可以变得清晰。干净的抽象就是编程的全部!

如果你想做电影渲染,看看工作室和立体角的文件。他们对自己的手艺出奇地开放。

如果你想做高性能的光线追踪,首先看一下来自Intel和NVIDIA的论文。同样,它们出人意料地开放。

如果你想做硬核的基于物理的渲染器,把你的渲染器从RGB转换成光谱。我非常喜欢每个射线都有一个随机的波长,几乎所有的rgb在你的程序变成浮动。这听起来效率很低,但事实并非如此!

无论你选择什么方向,添加一个光滑的BRDF模型。有很多选择,而且每种都有其优点。

玩得开心!