all lessons / computer_graphics / 11 · path tracing lesson 11 / 15

Monte Carlo path tracing蒙特卡洛路径追踪

Whitted ray tracing (lesson 10) only handled perfect mirrors and point lights — every other direction was ignored. But the rendering equation of lesson 07 is a full hemisphere integral, and it is recursive: the light arriving at a surface is the light leaving all other surfaces. That integral has no closed form. This lesson introduces the one general tool that can evaluate it — Monte Carlo integration — and builds it into a path tracer: shoot a ray, bounce it by random sampling, average the paths. We will meet the fundamental cost law (error ∝ 1/√N), the variance-reduction tricks that make it practical, and the denoisers that finish the job. Whitted 光线追踪(第 10 课)只处理了完美镜面与点光源——其余每个方向都被忽略了。但第 07 课的渲染方程是一个完整的半球积分,而且是递归的:到达一个表面的光,正是所有其他表面离开的光。这个积分没有闭式解。这一课引入唯一能求它的通用工具——蒙特卡洛积分——并把它搭成一台路径追踪器:射出一条光线,用随机采样让它反弹,再把路径平均起来。我们会遇到那条根本的代价定律(误差 ∝ 1/√N)、让它变得实用的降方差技巧,以及最终收尾的降噪器。

The plan本课计划

Five moves. (1) Why Monte Carlo: the integral is high-dimensional and recursive, so no analytic answer exists — but a random average (1/N)Σ f(xi)/p(xi) is unbiased and its error falls as 1/√N. (2) The path-tracing loop: trace from the eye, at each hit sample one bounce, attenuate by BRDF·cosθ/pdf, accumulate emission, average many paths per pixel. (3) Importance sampling and next-event estimation: choose samples where the integrand is large, and sample the lights directly instead of hoping to hit them. (4) Russian roulette: end paths without bias. (5) Noise & denoising: variance is grain; kill it with more samples, better sampling, and modern edge-aware / ML denoisers. Then lesson 12 asks how to get any of this in real time.

五步走。(1) 为什么用蒙特卡洛:积分高维且递归,故无解析解——但随机平均 (1/N)Σ f(xi)/p(xi)无偏的,其误差按 1/√N 下降。(2) 路径追踪循环:从眼睛出发追踪,每次命中采样一次反弹,用 BRDF·cosθ/pdf 衰减,累加自发光,把每个像素的许多条路径平均。(3) 重要性采样下一事件估计:在被积函数大的地方取样,并直接采样光源、而不是指望撞上它。(4) 俄罗斯轮盘赌:无偏地终止路径。(5) 噪声与降噪:方差就是颗粒;用更多样本、更好的采样、以及现代的边缘感知 / ML 降噪器去消灭它。随后第 12 课要问:这一切如何实时做到。

1 · Why Monte Carlo — the integral has no closed form为什么用蒙特卡洛——积分没有闭式解

Recall the rendering equation from lesson 07: the light leaving a point x toward the eye is an integral over the whole hemisphere of directions, Lo = Le + ∫Ω fr·Li·(ωi·n) dωi. Two facts make this brutal. First, Li — the light arriving along ωi — is itself the Lo of whatever surface that direction sees, so the equation is recursive: a bounce contains another integral, which contains another. Second, each bounce adds two more dimensions (a direction on a hemisphere). A path of k bounces is a 2k-dimensional integral. There is no antiderivative to write down.

回忆第 07 课的渲染方程:从点 x 射向眼睛的光,是在整个半球方向上的一个积分,Lo = Le + ∫Ω fr·Li·(ωi·n) dωi。两个事实让它极其棘手。其一,Li——沿 ωi 到达的光——本身就是那个方向所看到表面的 Lo,所以方程是递归的:一次反弹里含着又一个积分,那里面又含着一个。其二,每次反弹多出两个维度(半球上的一个方向)。一条 k 次反弹的路径就是一个 2k 维积分。根本写不出原函数。

When you cannot integrate analytically, you estimate. The key identity of Monte Carlo integration: to estimate I = ∫ f(x) dx, draw samples xi from any probability density p(x) (with p>0 wherever f≠0), and average f re-weighted by 1/p:

当你无法解析地积分时,你就去估计。蒙特卡洛积分的核心恒等式:要估计 I = ∫ f(x) dx,从任意概率密度 p(x) 抽取样本 xi(只要在 f≠0 处都有 p>0),再对用 1/p 重新加权后的 f 求平均:

I ≈ <I>N = (1/N) Σi=1..N f(xi) / p(xi)

Why is this legitimate? Because on average it lands on the right answer. The expected value of a single term is E[f(x)/p(x)] = ∫ (f(x)/p(x))·p(x) dx = ∫ f(x) dx = I — the p cancels exactly. So the estimator is unbiased: E[<I>N] = I for any N, even N=1. It is not "approximately right for large N"; it is centered on the truth for every N. What changes with N is not the center but the spread.

这为什么合理?因为它平均而言正好落在正确答案上。单项的期望值是 E[f(x)/p(x)] = ∫ (f(x)/p(x))·p(x) dx = ∫ f(x) dx = I——p 恰好抵消。所以这个估计量是无偏的:对任意 N(哪怕 N=1)都有 E[<I>N] = I。它不是“N 大时才近似正确”;对每个 N 它都以真值为中心。随 N 变化的不是中心,而是散布

Averaging N independent samples divides the variance by N. Variance is in squared units; the error we actually see is the standard deviation, its square root. So:

N 个独立样本平均,会把方差除以 N。方差是平方量纲;我们实际看到的误差是标准差,即它的平方根。所以:

Var[<I>N] = σ² / N   ⟹   stderr = σ / √N   ∝   1 / √N

This is the fundamental cost law of path tracing, and it is unforgiving. To halve the noise you must quarter the error's square — no — to halve the standard deviation you need the samples, because 1/√(4N) = (1/2)·(1/√N). Worked number: if 100 samples give a noise level of σ/10, reaching σ/20 (half) needs 400 samples, σ/40 needs 1600, σ/80 needs 6400. The last visible bit of grain is the most expensive. Every technique in the rest of this lesson exists to shrink the constant σ so you need fewer samples for the same quality.

这就是路径追踪的根本代价定律,而且它毫不留情。要把噪声减半,你需要 的样本,因为 1/√(4N) = (1/2)·(1/√N)。算个数:若 100 个样本给出 σ/10 的噪声水平,要达到 σ/20(一半)需 400 个样本,σ/40 需 1600 个,σ/80 需 6400 个。最后一丁点可见的颗粒最贵。本课其余的每一种技术,都是为了缩小常数 σ,好让同等质量所需的样本更少。

Bias vs variance — keep them straight偏差 vs 方差——别搞混

Variance is noise: run the same render twice with different random seeds and the pixels differ; average enough and it vanishes. Bias is a systematic offset: the estimator converges to the wrong value no matter how many samples you throw at it. Plain Monte Carlo path tracing is unbiased, so more samples always help. The dangerous shortcuts — clamping fireflies, biased denoisers, capping bounce depth by force — trade a little bias for a lot less variance. Sometimes worth it; always worth knowing you did it.

方差就是噪声:用不同的随机种子把同一画面渲染两遍,像素会不同;平均得足够多,它就消失。偏差是系统性的偏移:无论你投入多少样本,估计量都收敛到错误的值。朴素的蒙特卡洛路径追踪是无偏的,所以样本越多总是越好。那些危险的捷径——钳制萤火虫噪点、有偏的去噪器、强行限制反弹深度——都是用一点点偏差换取大幅更低的方差。有时值得;但知道自己这么做了,永远值得。

2 · The path-tracing loop — the recursion, sampled路径追踪循环——把递归采样出来

Path tracing is the rendering equation with its recursion unrolled by sampling. Start at the eye and shoot a ray through a pixel (lesson 10 gave us the ray, and the BVH gave us fast intersection). At the first hit, we want Lo — an integral. Instead of integrating, sample one incoming direction ωi from some pdf, follow the ray that way, and let the recursion continue at the next surface. Each term fr·Li·cosθ gets divided by the pdf of the direction we chose — the Monte Carlo re-weighting from section 1.

路径追踪就是把递归用采样展开的渲染方程。从眼睛出发,射一条光线穿过某个像素(第 10 课给了我们光线,BVH 给了我们快速求交)。在第一个命中点,我们想要 Lo——一个积分。不去积分,而是从某个 pdf 采样一个入射方向 ωi,沿那条光线走下去,让递归在下一个表面继续。每一项 fr·Li·cosθ 都要除以我们所选方向的 pdf——正是第 1 节的蒙特卡洛重新加权。

Rather than literal recursion, we carry a running throughput β — the product of all the fr·cosθ/pdf factors so far — and whenever we hit an emitter, add β·Le to the pixel. One camera ray traces one full path; a pixel's color is the average of many such paths (its samples per pixel, spp):

与其真的递归,我们携带一个不断累乘的通量 β——到目前为止所有 fr·cosθ/pdf 因子的乘积——每当命中一个发光体,就把 β·Le 加到该像素上。一条相机光线追踪一条完整路径;一个像素的颜色是许多这样路径的平均(它的每像素样本数,spp):

radiance_estimate(ray): L = 0 # accumulated radiance for this path β = 1 # throughput (product of BRDF·cos/pdf so far) for bounce in 0 .. maxDepth: hit = intersect(scene, ray) # BVH from lesson 10 if no hit: L += β · sky(ray); break L += β · hit.emission # emitter reached along this path ωi, pdf = sample_direction(hit.brdf, hit.normal) # ONE random bounce β *= brdf(hit, ωi) · dot(hit.normal, ωi) / pdf # re-weight ray = new_ray(hit.point, ωi) # continue from the hit return L # pixel = average over spp independent calls of radiance_estimate(camera_ray)

Read the loop against lesson 07's equation. The Le term is the hit.emission line. The integral ∫ frLicosθ becomes "sample one ωi, multiply β by frcosθ/pdf, recurse into Li." Because a random bounce eventually finds light sources on its own, this one loop produces effects Whitted ray tracing had to fake or skip entirely:

把这个循环对照第 07 课的方程读。Le 项就是 hit.emission 那行。积分 ∫ frLicosθ 变成了“采样一个 ωi,把 β 乘以 frcosθ/pdf,递归进 Li”。因为随机反弹最终会自己找到光源,这一个循环就产生了 Whitted 光线追踪只能伪造或干脆跳过的效果:

EffectWhy it emerges for free
Soft shadowsAn area light is partly visible from the penumbra; the fraction of bounce rays that reach it varies smoothly.
Colour bleedingA bounce off a red wall multiplies β by a red BRDF, tinting whatever light it carries onward.
Glossy reflectionThe BRDF's lobe is wide, not a mirror spike, so sampled bounce directions spread — a blurred reflection.
Indirect lightLight reaching a surface after ≥2 bounces is exactly the recursion the loop unrolls.
效果为什么它自然出现
软阴影面光源从半影区部分可见;反弹光线中到达它的比例平滑变化。
颜色渗透从红墙反弹会把 β 乘上一个红色 BRDF,给它此后携带的光染色。
光泽反射BRDF 的波瓣是宽的、而非镜面尖峰,故采样的反弹方向散开——一次模糊的反射。
间接光经 ≥2 次反弹才到达表面的光,正是这个循环展开的递归。

3 · Importance sampling & next-event estimation重要性采样与下一事件估计

Section 1 said we may draw samples from any pdf p. The choice does not change the answer (still unbiased) but it dramatically changes the variance σ. This is importance sampling: put samples where the integrand is big. The variance of f/p is zero when p ∝ f — if you could sample exactly proportional to the integrand, one sample would give the exact answer. You never can (that would require knowing the integral), but you can match part of the integrand.

第 1 节说过,我们可以从任意 pdf p 抽样。这个选择不改变答案(仍无偏),却会剧烈改变方差 σ。这就是重要性采样:把样本放在被积函数大的地方。当 p ∝ ff/p 的方差为零——若你能精确地按被积函数成比例采样,一个样本就给出精确答案。你永远做不到(那要求已知积分),但你能匹配被积函数的一部分

But importance sampling by the BRDF alone has a blind spot: a small, bright light. A random cosine-weighted bounce from a diffuse surface almost never happens to point straight at a tiny lamp, so the light is found rarely — and each rare hit carries huge energy divided by a tiny pdf, producing bright speckles ("fireflies"). The fix is next-event estimation (NEE), also called direct light sampling: at every bounce, also sample a point on the light sources directly and add its (shadow-ray-tested) contribution, using a pdf defined over the light's area. Now the direct lighting term has low variance because we sample the lights on purpose instead of hoping to hit them.

但只靠 BRDF 做重要性采样有个盲点:又小又亮的光源。从漫反射面随机的余弦加权反弹,几乎从不会正好指向一盏小灯,所以光源很少被找到——而每次罕见的命中都带着巨大能量除以极小的 pdf,产生明亮的斑点(“萤火虫”)。解法是下一事件估计(NEE),也叫直接光照采样:在每次反弹时,额外直接在光源上采样一个点,用定义在光源面积上的 pdf,加上它(经阴影光线检验)的贡献。现在直接光照项方差很低,因为我们是故意采样光源、而非指望撞上它。

Two good strategies → combine them with MIS两个好策略 → 用 MIS 结合

Now we have two ways to estimate the same lighting: sample the BRDF (great for glossy surfaces reflecting a big light) and sample the light (great for diffuse surfaces under a small light). Each is bad exactly where the other is good — a glossy surface under a large light makes light-sampling noisy, and vice versa. Multiple importance sampling (MIS) takes both sets of samples and weights each by a heuristic (the balance or power heuristic) that automatically down-weights whichever strategy had the low pdf for that sample. You get the best of both with no bias — the workhorse combiner in every production path tracer.

现在我们有两种方式来估计同一处光照:对 BRDF 采样(很适合反射一个大光源的光泽表面),以及对光源采样(很适合小光源下的漫反射表面)。每一种恰恰在另一种擅长的地方表现糟糕——光泽表面在大光源下会让光源采样很吵,反之亦然。多重重要性采样(MIS)同时取两组样本,并用一个启发式(balancepower 启发式)为每个样本加权,自动压低那个对该样本 pdf 偏低的策略。你就无偏地兼得两者之长——这是每个生产级路径追踪器里的主力组合器。

4 · Russian roulette — ending paths without bias俄罗斯轮盘赌——无偏地终止路径

The loop in section 2 needs to stop. A path could in principle bounce forever, and after many bounces the throughput β is tiny — those paths cost intersection tests but barely change the image. The naive fix is a hard depth cap ("stop after 8 bounces"), but that throws away all the energy of deeper paths and darkens the image: it introduces bias. We want to stop paths on average without losing energy on average.

第 2 节的循环需要停下来。原则上一条路径可以永远反弹下去,而在多次反弹后通量 β 已很小——这些路径耗费求交测试却几乎不改变图像。朴素的解法是硬性的深度上限(“8 次反弹后就停”),但那会丢掉更深路径的全部能量并让图像变暗:它引入了偏差。我们想要平均而言停下路径、又平均而言不损失能量。

Russian roulette does exactly that. At a bounce, pick a survival probability q (often tied to β, e.g. its max channel). Draw a uniform sample: with probability 1−q, kill the path (contribute nothing further); otherwise it survives, and we divide its throughput by q to compensate. The division is the whole trick — it makes the estimator unbiased:

俄罗斯轮盘赌正是这么做的。在一次反弹处,取一个存活概率 q(常与 β 挂钩,例如取其最大通道)。抽一个均匀样本:以概率 1−q 杀死路径(此后不再贡献);否则它存活,我们把它的通量除以 q 来补偿。这个除法就是全部诀窍——它让估计量无偏:

E[contribution] = q · (β / q)·Lrest + (1−q) · 0 = β · Lrest

The killed paths contribute zero; the survivors are boosted by 1/q to carry the load of the ones that died. In expectation the total energy is unchanged — same answer, fewer average bounces, no darkening. The cost is a little extra variance (survivors are noisier because they are scaled up), but it is a good trade: you spend samples on paths that still matter. Worked number: with q=0.9 per bounce, the expected path length is 1/(1−0.9) = 10 bounces, but a stubborn path can still go deeper occasionally — so a faint caustic that needs 15 bounces is not silently clipped to black.

被杀死的路径贡献零;存活者被放大 1/q 倍,替死去的那些扛起负担。期望上总能量不变——同样的答案、更少的平均反弹、不变暗。代价是一点额外方差(存活者因被放大而更吵),但这是笔好交易:你把样本花在仍然要紧的路径上。算个数:每次反弹取 q=0.9,期望路径长度是 1/(1−0.9) = 10 次反弹,但顽固的路径偶尔仍能走得更深——所以一处需要 15 次反弹的微弱焦散不会被悄悄裁成全黑。

5 · Noise & denoising — from grain to clean frames噪声与降噪——从颗粒到干净画面

Everything above still leaves some variance, and in a path-traced image variance looks like grain: pixel-to-pixel speckle that fades as spp climbs. There are two ways to fight it, and they compose. First, reduce the variance at the source: more spp (the brute-force 1/√N path), stratification (split the pixel/hemisphere into cells and put one sample in each, so samples cannot clump — this beats pure random slightly, giving closer to 1/N for smooth integrands), and the importance sampling / NEE / MIS of sections 3. Together these shrink the constant σ.

上面这一切仍会留下一些方差,而在路径追踪图像里方差看起来就是颗粒:随 spp 攀升而消退的逐像素斑点。有两条路对抗它,且能叠加。其一,从源头降方差:更多 spp(暴力的 1/√N 路线)、分层(把像素 / 半球切成格子、每格放一个样本,样本便无法扎堆——这比纯随机略好,对光滑被积函数更接近 1/N),以及第 3 节的重要性采样 / NEE / MIS。它们合力缩小常数 σ

Second, denoise the result. Even with every trick, a real-time budget affords only 1–8 spp — a grainy image. A denoiser is an image-space filter that removes the grain while preserving edges, guided by cheap auxiliary buffers the renderer already has: per-pixel albedo, surface normal, and depth (the G-buffer, lesson 12). Classic edge-aware filters (à-trous / joint-bilateral) blur only across pixels with similar normals and depth. Modern ML denoisers (e.g. those shipped in film and GPU drivers) are trained to turn 1–8 spp into results that look converged. Denoising is what makes real-time path tracing (lesson 12) viable at all — and it is a form of controlled bias, so it must be used with care, which is exactly the balance the next lessons strike.

其二,对结果降噪。哪怕用尽所有技巧,实时预算也只负担得起 1–8 spp——一张有颗粒的图。降噪器是一个图像空间滤波器,它在保留边缘的同时去除颗粒,由渲染器本就有的廉价辅助缓冲引导:逐像素的反照率、表面法线与深度(G-buffer,第 12 课)。经典的边缘感知滤波器(à-trous / 联合双边)只在法线与深度相近的像素间模糊。现代的 ML 降噪器(例如电影与 GPU 驱动里附带的那些)经训练能把 1–8 spp 变成看起来已收敛的结果。降噪正是让实时路径追踪(第 12 课)根本可行的东西——而它是一种受控的偏差,故须谨慎使用,这恰是接下来几课要拿捏的平衡。

Monte Carlo convergence: noise falls as 1/√N蒙特卡洛收敛:噪声按 1/√N 下降
We estimate a known integral so you can watch the error shrink. Throw N points into the unit square; the fraction landing inside the quarter unit circle (radius 1) approximates its area π/4, so π ≈ 4·inside/N. Drag N (log scale). The dashed band is the ±k/√N error envelope around the true π — the estimate should stay inside it and the band should tighten as 1/√N. Toggle the sequence: a low-discrepancy Halton sequence covers the square evenly (low variance); a clumping sequence leaves gaps and clusters (high variance) — same N, worse estimate. All sampling is deterministic; no Math.random.我们估计一个已知的积分,好让你看着误差缩小。往单位正方形里撒 N 个点;落在四分之一单位圆(半径 1)内的比例近似其面积 π/4,故 π ≈ 4·内部/N。拖动 N(对数刻度)。虚线带是真值 π 周围的 ±k/√N 误差包络——估计值应停在带内,且带应按 1/√N 收紧。切换序列:低差异的 Halton 序列均匀铺满正方形(低方差);扎堆的序列留下空隙与簇(高方差)——同样的 N,更差的估计。所有采样都是确定性的;没有 Math.random
N samples
estimate of π
|error| = |est − π|
1/√N reference
Show the core JS查看核心 JS
// Deterministic low-discrepancy sampling — NO Math.random.
// van der Corput radical inverse: mirror i's digits (base b) about the point.
function radicalInverse(i, base){
  var f = 1, r = 0;
  while (i > 0){ f /= base; r += f * (i % base); i = Math.floor(i / base); }
  return r;                       // in [0,1)
}
// Halton 2D: coordinate x from base 2, y from base 3 — evenly covers the square.
function halton2(i){ return [ radicalInverse(i,2), radicalInverse(i,3) ]; }

// Estimate pi: fraction of points inside the quarter unit circle, times 4.
var inside = 0;
for (var i = 1; i <= N; i++){
  var p = halton2(i);             // deterministic sample
  if (p[0]*p[0] + p[1]*p[1] <= 1) inside++;
}
var estimate = 4 * inside / N;    // → pi, with error shrinking as 1/sqrt(N)

Where this points next接下来指向何处

We now have a complete, unbiased renderer: the recursive rendering equation of lesson 07, evaluated by Monte Carlo integration, sped up by importance sampling and next-event estimation, bounded by Russian roulette, and cleaned by denoising. Given enough samples it converges to the physically correct image — the offline / film end of lesson 01's budget axis. But "enough samples" is minutes to hours per frame, and a game has 16.7 ms. Lesson 12 — real-time global illumination — asks how to approximate this same integral inside a frame budget: precomputed probes and lightmaps, screen-space tricks, and the low-spp-plus-denoiser pipeline this lesson just set up. Same equation, opposite pole of the budget.

我们现在有了一台完整、无偏的渲染器:第 07 课的递归渲染方程,用蒙特卡洛积分求解,靠重要性采样与下一事件估计加速,用俄罗斯轮盘赌限界,用降噪收尾。给足样本,它就收敛到物理正确的图像——第 01 课预算轴上离线 / 电影那一端。但“足够的样本”意味着每帧数分钟到数小时,而游戏只有 16.7 毫秒。第 12 课——实时全局光照——要问:如何在一帧的预算内近似这同一个积分:预计算的探针与光照贴图、屏幕空间技巧,以及本课刚搭好的“低 spp + 降噪器”流水线。同一个方程,预算的另一端。

Takeaway要点

The rendering equation is a high-dimensional, recursive integral with no closed form, so we estimate it by Monte Carlo: I ≈ (1/N)Σ f(xi)/p(xi), which is unbiased (right on average, for any N) with error ∝ 1/√N — the cost law that makes halving noise cost the samples. Path tracing unrolls the recursion by sampling one bounce per hit, carrying a throughput β·=frcosθ/pdf and accumulating emission — giving soft shadows, colour bleeding, glossy reflection, and indirect light for free. Importance sampling (cosine for diffuse, lobe for specular) and next-event estimation (sample the lights directly) slash σ, and MIS combines them. Russian roulette ends paths unbiasedly by killing with probability 1−q and boosting survivors by 1/q. What variance remains is grain, removed by more spp, stratification, and edge-aware / ML denoisers — the bridge to real-time GI.

渲染方程是一个高维、递归、无闭式解的积分,故我们用蒙特卡洛估计它:I ≈ (1/N)Σ f(xi)/p(xi),它无偏(对任意 N 平均而言正确),误差 ∝ 1/√N——正是这条代价定律让噪声减半要花 样本。路径追踪用“每次命中采样一次反弹”展开递归,携带通量 β·=frcosθ/pdf 并累加自发光——免费给出软阴影、颜色渗透、光泽反射与间接光。重要性采样(漫反射用余弦、镜面用波瓣)与下一事件估计(直接采样光源)大幅削减 σ,而 MIS 把两者结合。俄罗斯轮盘赌以概率 1−q 杀死路径、把存活者放大 1/q,从而无偏地终止路径。剩下的方差就是颗粒,用更多 spp、分层、以及边缘感知 / ML 降噪器去除——通往实时全局光照的桥。

Interview prompts面试题