all lessons / computer_graphics / 10 · ray tracing & BVH lesson 10 / 15

Ray tracing & acceleration structures光线追踪与加速结构

Rasterization (lesson 04) only ever answers one question well: what does the eye see? Its loop starts every ray at the camera. But shadows start at a surface and point toward a lamp; reflections start at a surface and point into the mirror; the recursive rendering equation of lesson 01 needs rays from arbitrary origins in arbitrary directions. So we switch to lesson 01's transposed loop — one ray per pixel, then follow rays wherever physics sends them — and spend the rest of the lesson making that loop fast enough to actually use, because the naive version tests every object against every ray. 光栅化(第 04 课)真正答得好的只有一个问题:眼睛看到什么?它的循环让每条光线都从相机出发。但阴影从表面出发、指向一盏灯;反射从表面出发、指进镜子里;第 01 课那个递归的渲染方程,需要从任意原点、朝任意方向发出的光线。于是我们切换到第 01 课那个转置的循环——每个像素一条光线,然后让光线随物理去往任何地方——并用这一课余下的篇幅,把这个循环变得足够快、真能用起来,因为它的朴素版本要拿每个物体去和每条光线都测一遍。

The plan本课计划

Five moves. (1) Write the ray as r(t) = o + t·d and generate one primary ray per pixel through the image plane, reusing lesson 02's camera. (2) Do the geometry: ray–sphere (a quadratic in t) and ray–triangle via Möller–Trumbore (barycentric u,v and t from cross/dot products); the nearest positive t is the visible hit. (3) Follow the rays: the Whitted recursive tracer casts shadow, reflection, and refraction rays at each hit, weights reflect/refract by Fresnel, and caps recursion depth. (4) Confront the cost: naive tracing is O(N) per ray, O(P·N) for the frame — hopeless at millions of triangles. (5) Fix it with acceleration structures: bound primitives in AABBs, build a BVH, and descend only into boxes a ray hits → about O(log N) per ray. The widget makes the "skip whole subtrees" idea literal. Then lesson 11 turns the one reflection ray into a spray of random ones.

五步走。(1) 把光线写成 r(t) = o + t·d,并沿成像平面为每个像素生成一条主光线,复用第 02 课的相机。(2) 做几何:光线–球(关于 t 的二次方程)与经 Möller–Trumbore 的光线–三角形(由叉积/点积解出重心坐标 u,vt);最小的正 t 就是可见命中。(3) 让光线继续走:Whitted 递归追踪器在每个命中处投出阴影反射折射光线,用 Fresnel 给反射/折射加权,并给递归深度设上限。(4) 正视开销:朴素追踪每条光线是 O(N),一帧是 O(P·N)——面对数百万三角形时毫无希望。(5) 用加速结构解决它:把图元装进 AABB,建一棵 BVH,光线只往它命中的盒子里下降 → 每条光线约 O(log N)。控件会把“跳过整棵子树”这件事画得明明白白。随后第 11 课把那一条反射光线,变成一把随机的光线。

1 · A ray is r(t) = o + t·d — and one per pixel光线就是 r(t) = o + t·d——每像素一条

A ray is the simplest object in the whole field: a starting point plus a direction, parameterized by a single scalar t.

光线是整个领域里最简单的对象:一个起点加一个方向,用单个标量 t 参数化。

r(t) = o + t·d ,    t > 0 .

Here o is the origin, d the direction (we keep it unit-length so t reads as true distance), and the constraint t > 0 means "in front of the origin" — points behind, at t < 0, are not on the ray. That tiny t > 0 guard is where half of all ray-tracing bugs hide: forget it and a surface can shadow or reflect itself from geometry sitting behind the ray's start.

这里 o 是原点,d 是方向(我们让它单位长,这样 t 就读作真实距离),而约束 t > 0 表示“在原点前方”——位于 t < 0 的、原点后方的点不在光线上。就是这个不起眼的 t > 0 守卫,藏着一半的光线追踪 bug:忘了它,一个表面就可能被落在光线起点后方的几何体自己投影或反射。

Camera ray generation. We reuse lesson 02's camera exactly, just run backwards. Instead of projecting a 3D point onto the image plane, we take each pixel center on the image plane and shoot the ray from the eye through it into the scene — one primary ray per pixel. Put the eye at e looking down −z, image plane at focal length f; for pixel (i, j) on a W×H grid, map to normalized screen coordinates (s_x, s_y) ∈ [−1, 1] (accounting for aspect ratio and the field of view), and:

相机光线生成。我们原样复用第 02 课的相机,只是倒着跑。不是把一个三维点投影到成像平面,而是取成像平面上每个像素的中心,从眼睛穿过它把光线射入场景——每像素一条主光线。把眼睛放在 e 处、朝 −z 看,成像平面在焦距 f 处;对 W×H 网格上的像素 (i, j),映射到归一化屏幕坐标 (s_x, s_y) ∈ [−1, 1](计入宽高比与视场角),于是:

o = e ,    d = normalize( sx·right + sy·up − f·forward ) .

This is exactly lesson 01's picture — "a ray per pixel, fanning out from the eye through the grid" — now written as code. Notice the loop has already flipped: rasterization's outer loop was triangles, here the outer loop is pixels. That flip is the whole reason arbitrary rays (shadow, reflection) come for free: once "trace a ray" is a subroutine, it does not care whether the origin is the eye or a mirror.

恰恰就是第 01 课那幅画面——“每像素一条光线,从眼睛出发穿过网格扇形散开”——如今写成了代码。注意循环已经翻转:光栅化的外层循环是三角形,这里外层循环是像素。正是这个翻转,让任意光线(阴影、反射)唾手可得:一旦“追一条光线”成了子程序,它就不在乎原点是眼睛还是镜子。

2 · Ray–primitive intersection: sphere and triangle光线–图元求交:球与三角形

Tracing a ray means solving, for each primitive, "at what t does r(t) touch this surface?" — then keeping the smallest positive t across all primitives. Two primitives cover almost everything.

追一条光线,就是对每个图元解“r(t) 在哪个 t 触到这个表面?”——然后在所有图元里保留最小的正 t。两种图元几乎涵盖了一切。

Ray–sphere. A sphere of center c, radius R is the set of points with |p − c|² = R². Substitute p = r(t) = o + t·d and expand — because |d| = 1, you get a clean quadratic in t:

光线–球。圆心 c、半径 R 的球,是满足 |p − c|² = R² 的点集。代入 p = r(t) = o + t·d 展开——因为 |d| = 1,你会得到一个关于 t 的干净二次方程:

t² + 2(d·(o−c)) t + (|o−c|² − R²) = 0 ,    t = −b ± √(b² − c0) .

The discriminant tells the story: negative → the ray misses; zero → it grazes (tangent); positive → two roots, entry and exit. Take the smallest positive root — the nearer intersection in front of the origin. If both roots are negative the sphere is entirely behind the eye. Worked case: eye at origin, d = (0,0,−1), sphere at c = (0,0,−5), R = 1. Then o−c = (0,0,5), b = d·(o−c) = −5, c_0 = 25 − 1 = 24, discriminant = 25 − 24 = 1, roots t = 5 ± 1 = {4, 6} — front face at 4, back face at 6. We keep t = 4.

判别式道出全部:负 → 光线错过;零 → 相切(掠过);正 → 两个根,入点与出点。取最小的正根——原点前方更近的那个交点。若两根都为负,球整个在眼睛后方。算一例:眼睛在原点,d = (0,0,−1),球心 c = (0,0,−5)R = 1。则 o−c = (0,0,5)b = d·(o−c) = −5c_0 = 25 − 1 = 24,判别式 = 25 − 24 = 1,根 t = 5 ± 1 = {4, 6}——正面在 4,背面在 6。我们保留 t = 4

Ray–triangle (Möller–Trumbore). Triangles are the universal primitive (lesson 03), so this test is the workhorse. A point inside triangle (v_0, v_1, v_2) is v_0 + u·(v_1−v_0) + v·(v_2−v_0) in barycentric coordinates. Set that equal to o + t·d — three equations, three unknowns (t, u, v) — and solve with Cramer's rule expressed as cross and dot products (no matrix inverse needed):

光线–三角形(Möller–Trumbore)。三角形是通用图元(第 03 课),所以这个测试是主力。三角形 (v_0, v_1, v_2) 内一点,用重心坐标写作 v_0 + u·(v_1−v_0) + v·(v_2−v_0)。令它等于 o + t·d——三个方程、三个未知数 (t, u, v)——用克拉默法则、以叉积与点积表达来解(无需求矩阵逆):

e1 = v1−v0,   e2 = v2−v0,   p = d×e2,   det = e1·p
u = (o−v0)·p / det ,   q = (o−v0)×e1 ,   v = d·q / det ,   t = e2·q / det

The hit is valid iff u ≥ 0, v ≥ 0, u + v ≤ 1 (inside the triangle), and t > 0 (in front). If det ≈ 0 the ray is parallel to the triangle's plane — no hit. The pay-off of barycentrics: the same (u, v) that decide "inside?" also interpolate per-vertex normals, UVs, and colors (lesson 05) at the hit point, for free.

当且仅当 u ≥ 0v ≥ 0u + v ≤ 1(在三角形内)且 t > 0(在前方)时,命中有效。若 det ≈ 0,光线平行于三角形所在平面——不命中。重心坐标的红利:那对判定“在内吗?”的 (u, v),还能在命中点免费插值逐顶点的法线、UV 与颜色(第 05 课)。

The nearest positive t wins最小的正 t 获胜

Visibility for a ray is just a running minimum. Loop over primitives; for each, compute its t (if any, with t > 0); keep the primitive with the smallest t seen so far. When the loop ends, that primitive is what the ray sees first — the ray-tracing analogue of rasterization's z-buffer, but resolved per ray instead of per pixel-stamp. This is exactly the inner loop of lesson 01's ray-tracing side, and section 4 is about how expensive that innocent "loop over primitives" becomes.

对一条光线而言,可见性不过是一次"实时求最小值"。遍历图元;对每个图元计算它的 t(若相交,且 t > 0);保留至今见过的最小 t 的那个图元。循环结束时,那个图元就是光线最先看到的东西——它是光栅化深度缓冲在光线追踪里的对应物,只是按每条光线、而非每次像素盖章来求解。这正是第 01 课光线追踪那一侧的内层循环,而第 4 节要讲的,就是这句看似无害的"遍历图元"会变得多么昂贵。

3 · The Whitted recursive ray tracerWhitted 递归光线追踪器

Finding the nearest hit only answers visibility. To shade it we ask what light reaches it — and Turner Whitted's 1980 insight was that the same "trace a ray" routine answers that too. At every hit point, cast three kinds of secondary ray:

找到最近命中只回答了可见性。要给它着色,得问有多少光到达它——而 Turner Whitted 在 1980 年的洞见是:同一个“追一条光线”的例程也能回答这个。在每个命中点,投出三种次级光线:

Fresnel equations set how much energy goes each way: at grazing angles a glass surface is almost a perfect mirror; head-on it is mostly transparent. So the shaded color at a hit is (direct light, gated by shadow rays) + F·(reflected color) + (1−F)·(refracted color), where F is the Fresnel reflectance. Because reflection and refraction recurse, we must cap the recursion depth (say 4–8 bounces) or two facing mirrors would loop forever; each bounce also carries less energy, so deep bounces contribute little and are cheap to drop.

Fresnel 方程决定各方向分走多少能量:掠射角下玻璃面几乎是完美镜子;正对时则大多透明。于是命中处的着色颜色是 (直接光,受阴影光线门控) + F·(反射色) + (1−F)·(折射色),其中 F 是 Fresnel 反射率。因为反射与折射会递归,我们必须给递归深度设上限(比如 4–8 次弹跳),否则两面对着的镜子会永远循环下去;每次弹跳还携带更少能量,所以深层弹跳贡献甚微、丢掉也不心疼。

trace(ray, depth): hit = nearest_hit(ray) # section 2: loop primitives, smallest t>0 if no hit: return background color = 0 for each light L: # SHADOW ray — is L visible from hit? s = ray(hit.p -> L) if not nearest_hit(s) before L: color += direct_shade(hit, L) if depth < MAX and hit.reflective: # REFLECTION ray, recurse color += F * trace(reflect(ray.d, hit.n), depth+1) if depth < MAX and hit.transparent: # REFRACTION ray (Snell), recurse color += (1-F) * trace(refract(ray.d, hit.n, eta), depth+1) return color

This is the whole classic ray tracer. Notice every line either calls nearest_hit or recurses into trace, which itself calls nearest_hit. The cost of the entire renderer therefore collapses onto one subroutine — and that subroutine's naive form is the problem section 4 confronts.

这就是整个经典光线追踪器。注意每一行要么调用 nearest_hit、要么递归进 trace,而后者本身又调用 nearest_hit。于是整台渲染器的开销全都坍缩到一个子程序上——而这个子程序的朴素形式,正是第 4 节要面对的问题。

4 · The cost problem — O(N) per ray, O(P·N) per frame开销问题——每光线 O(N),每帧 O(P·N)

The naive nearest_hit tests every primitive against every ray. With N primitives that is O(N) intersection tests per ray. Now count the rays. A P-pixel image casts P primary rays; each hit spawns shadow rays (one per light) and reflection/refraction rays, and those recurse — so the real ray count is several times P. Even counting only primary rays, the frame is O(P·N).

朴素的 nearest_hit每个图元去和每条光线都测。N 个图元,就是每条光线 O(N) 次求交测试。再数光线数量。一幅 P 像素的图像投出 P 条主光线;每个命中又派生阴影光线(每光源一条)与反射/折射光线,它们还递归——所以真实光线数是 P 的好几倍。哪怕只数主光线,一帧也是 O(P·N)

Put numbers on it. A 1080p frame is P ≈ 2×10⁶ pixels; a modest scene has N ≈ 10⁶ triangles. Naive primary-ray visibility alone is P·N ≈ 2×10¹² ray–triangle tests per frame — trillions, before a single shadow or reflection. At even a billion tests per second that is over half an hour for one frame. This is why the plausible-sounding "just shoot rays" was, for decades, a non-starter for anything but tiny scenes. The primitive count grows; the linear scan does not scale. We need to stop testing primitives that a ray obviously cannot hit.

给它填上数字。一帧 1080p 是 P ≈ 2×10⁶ 像素;一个不算大的场景有 N ≈ 10⁶ 三角形。仅朴素的主光线可见性就是每帧 P·N ≈ 2×10¹² 次光线–三角形测试——数万亿次,还没算一条阴影或反射。哪怕每秒十亿次测试,一帧也要半个多钟头。这就是为什么听着挺靠谱的“直接发光线”,几十年来对除小场景以外的一切都是行不通的。图元数在涨;线性扫描扛不住。我们必须停止去测那些光线明显不可能命中的图元。

5 · Acceleration structures — the BVH and ~O(log N) rays加速结构——BVH 与约 O(log N) 的光线

The key move is a bounding volume: wrap a group of primitives in a cheap shape, and test the ray against the cheap shape first. If the ray misses the box, it misses everything inside — one test rejects the whole group. The favorite box is the axis-aligned bounding box (AABB), tested by the slab method: an AABB is the intersection of three "slabs" (one per axis, between a min and max plane); intersect the ray with each slab to get a [t_{near}, t_{far}] interval, and the ray hits the box iff the three intervals overlap, i.e. max(t_{near}) ≤ min(t_{far}). It is a handful of subtractions, multiplies, and min/max — no square roots.

关键一招是包围体:把一组图元裹进一个廉价形状,先拿光线去测那个廉价形状。若光线错过盒子,它就错过里面的一切——一次测试就否掉整组。最受欢迎的盒子是轴对齐包围盒(AABB),用slab(板)法测:一个 AABB 是三块“板”的交(每轴一块,夹在一个 min 面与 max 面之间);把光线与每块板求交得到一个 [t_{near}, t_{far}] 区间,当且仅当三个区间重叠——即 max(t_{near}) ≤ min(t_{far})——光线命中盒子。它只是几次减、乘和 min/max——没有平方根。

// Ray–AABB slab test. box = {min:[x,y,z], max:[x,y,z]}.
// invd = 1/d precomputed per ray; handles d=0 via ±Infinity cleanly.
function hitAABB(o, invd, box){
  var tmin = -Infinity, tmax = Infinity;
  for (var a = 0; a < 3; a++){          // three axes / three slabs
    var t1 = (box.min[a] - o[a]) * invd[a];
    var t2 = (box.max[a] - o[a]) * invd[a];
    if (t1 > t2){ var tmp = t1; t1 = t2; t2 = tmp; }  // order near/far
    tmin = Math.max(tmin, t1);
    tmax = Math.min(tmax, t2);
  }
  return tmax >= Math.max(tmin, 0);     // overlap AND in front (t > 0)
}

One box helps a little; a tree of boxes is the win. A Bounding Volume Hierarchy (BVH) is a binary tree whose every node stores an AABB enclosing all primitives beneath it; leaves hold a few actual primitives. To trace a ray: test the root box; if hit, recurse into both children; if a box is missed, prune its entire subtree. A ray thus descends only through the handful of boxes it actually pierces, touching O(\text{depth}) ≈ O(\log N) nodes and intersecting only the primitives in the few leaves it reaches — instead of all N. The trillions of section 4 collapse toward millions.

一个盒子帮不上多少;一盒子树才是胜负手。包围体层次结构(BVH)是一棵二叉树,每个节点存一个包住其下方所有图元的 AABB;叶子里放几个真实图元。追一条光线:测根盒子;命中就递归进两个孩子;错过某个盒子,就剪掉它整棵子树。于是光线只穿过它真正刺穿的那几个盒子往下走,触及 O(\text{深度}) ≈ O(\log N) 个节点,只与它到达的少数叶子里的图元求交——而不是全部 N 个。第 4 节的万亿,坍缩向百万。

BVH: why a ray needn't test every objectBVH:为什么一条光线不必测试每个物体
24 fixed circles, deterministically placed and partitioned into four AABBs (a 2-level BVH: a root box over four leaf boxes). Aim the ray with the sliders. In BRUTE FORCE every circle is tested — all 24 light up. In BVH the ray first runs the slab test on each box; only circles inside boxes the ray actually enters get tested (highlighted), the rest are pruned (dimmed). The nearest hit is ringed. Watch the two test counts: BVH ≪ brute, and the harder the scene, the wider the gap.24 个固定圆,确定性地摆放并划入四个 AABB(一棵两层 BVH:一个根盒罩住四个叶盒)。用滑块瞄准光线。在暴力模式下每个圆都被测试——全部 24 个亮起。在 BVH 模式下,光线先对每个盒子跑 slab 测试;只有落在光线真正进入的盒子里的圆才被测试(高亮),其余被剪除(变暗)。最近命中被圈出。看这两个测试计数:BVH ≪ 暴力,且场景越难,差距越大。
Brute tests
BVH tests
Boxes entered
Nearest hit?
Show the core JS查看核心 JS
// BVH trace: box test gates the circle tests. box test is cheap; circle test isn't.
var tests = 0, boxesEntered = 0, best = Infinity, hit = null;
for (var b = 0; b < boxes.length; b++){
  if (!hitAABB(o, invd, boxes[b])) continue;   // MISS box -> prune all its circles
  boxesEntered++;
  for (var k = 0; k < boxes[b].items.length; k++){
    tests++;                                    // only now do the expensive test
    var t = hitCircle(o, d, circles[boxes[b].items[k]]);
    if (t > 0 && t < best){ best = t; hit = boxes[b].items[k]; }
  }
}
// BRUTE FORCE for comparison: test all N circles, no boxes.
var bruteTests = circles.length;               // always N

How you build the tree matters. A BVH is built top-down: take a set of primitives, pick an axis and a split position, partition into two child sets, recurse. The cheap heuristic is a median split — sort along the longest axis, cut at the middle primitive. It is fast to build and gives a balanced tree, but ignores geometry: it can produce fat, overlapping boxes that a ray enters often. The quality heuristic is the Surface Area Heuristic (SAH), which estimates the expected cost of a split as P(\text{hit left})·N_L + P(\text{hit right})·N_R, using the fact that the probability a random ray hits a child box is proportional to its surface area. SAH tries many candidate splits and keeps the cheapest; it builds slower but produces trees that trace markedly faster — the standard trade for offline renderers.

树怎么建,很要紧。BVH 自顶向下建:取一组图元,选一条轴和一个分割位置,划成两个孩子集合,递归。廉价的启发式是中位数分割——沿最长轴排序,从中间那个图元切开。建得快、给出平衡树,但忽略几何:它可能产出又胖又重叠的盒子,光线常常一进就进俩。高质量的启发式是表面积启发式(SAH),它把一次分割的期望开销估为 P(\text{命中左})·N_L + P(\text{命中右})·N_R,用到“随机光线命中某个孩子盒的概率正比于其表面积”这一事实。SAH 试许多候选分割、保留最便宜的;它建得慢,却产出追踪明显更快的树——离线渲染器的标准取舍。

Not the only structure. A kd-tree splits space with axis-aligned planes (tight cells, pricier build, and a primitive can straddle two cells); a uniform grid buckets primitives into equal voxels and marches the ray cell-by-cell (great for evenly-spread scenes, bad for a few dense clumps — the "teapot in a stadium" problem). The BVH's virtue is that it partitions objects, not space, so boxes shrink to fit geometry and it copes with wildly non-uniform density. And the whole descend-a-tree-of-boxes dance is now baked into silicon: modern GPU RT cores do BVH traversal and ray–triangle tests in hardware, which is what makes real-time ray tracing possible — we return to that convergence in lesson 15.

并非唯一结构。kd-tree 用轴对齐平面切空间(格子更紧、建得更贵,且一个图元可能横跨两格);均匀网格把图元分进等大体素、让光线逐格前进(对均匀铺开的场景极好,对少数密集团块很糟——即“体育场里的茶壶”问题)。BVH 的长处在于它划分的是物体而非空间,所以盒子会收缩去贴合几何,也能应付极不均匀的密度。而这整套“下降一棵盒子树”的舞步如今已刻进硅片:现代 GPU 的 RT 核心用硬件做 BVH 遍历与光线–三角形测试,这正是实时光线追踪成为可能的原因——我们在第 15 课再回到这场融合。

StructurePartitionsBuild costBest for
BVH (median split)objectscheap, fastdynamic scenes, quick rebuilds
BVH (SAH)objectsexpensiveoffline, trace-time wins
kd-treespace (planes)expensivetight static scenes
Uniform gridspace (voxels)very cheapevenly-spread geometry
结构划分对象构建开销擅长
BVH(中位数分割)物体便宜、快动态场景、快速重建
BVH(SAH)物体昂贵离线、追踪期取胜
kd-tree空间(平面)昂贵紧凑的静态场景
均匀网格空间(体素)极便宜均匀铺开的几何

Where this points next接下来指向何处

We now have a working ray tracer: rays as r(t)=o+t·d, closed-form intersection for spheres and triangles, the Whitted recursion for shadows/reflections/refractions, and a BVH that makes the inner loop affordable. But Whitted's tracer casts exactly one reflection ray, one refraction ray, one shadow ray per light — it captures mirror-perfect reflection and hard, point-light shadows, and nothing softer. Real materials scatter light over a whole lobe (lesson 09's BRDF), real lights have area (soft shadows), and the rendering equation of lesson 01 was an integral over the hemisphere, not a single direction. Lesson 11 replaces the one ray with many random rays and averages them — Monte Carlo path tracing — turning this recursive tracer into an unbiased estimator of the full equation. Every ray it fires still rides on exactly the intersection machinery and BVH built here.

现在我们有了一台能用的光线追踪器:光线 r(t)=o+t·d,球与三角形的闭式求交,处理阴影/反射/折射的 Whitted 递归,以及让内层循环负担得起的 BVH。但 Whitted 追踪器恰好只投条反射光线、条折射光线、每光源一条阴影光线——它捕捉镜面般完美的反射与硬的点光源阴影,仅此而已,再柔一点都没有。真实材质把光散射到整个波瓣上(第 09 课的 BRDF),真实光源有面积(软阴影),而第 01 课的渲染方程是半球上的一个积分、不是单一方向。第 11 课把那一条光线换成许多条随机光线并求平均——蒙特卡洛路径追踪——把这台递归追踪器变成对完整方程的无偏估计器。它发出的每一条光线,仍然完全跑在这里搭起的求交机制与 BVH 之上。

Takeaway要点

A ray is r(t) = o + t·d, t > 0; ray tracing casts one primary ray per pixel (lesson 02's camera, run backwards). Intersection is closed-form: ray–sphere is a quadratic (keep the smallest positive root), ray–triangle is Möller–Trumbore (barycentric u,v,t; hit iff u,v ≥ 0, u+v ≤ 1, t > 0); the nearest positive t across primitives is the visible hit. The Whitted recursion shades a hit by casting shadow rays (hard shadows), a reflection ray, and a refraction ray (Snell), weighted by Fresnel, with a capped depth. Naive tracing is O(N) per ray, O(P·N) per frame — hopeless at scale. An acceleration structure fixes it: bound primitives in AABBs (slab test), build a BVH, and let each ray descend only into boxes it hits, pruning whole subtrees → about O(\log N) per ray. Build with a cheap median split or a costlier, faster-tracing SAH; alternatives are kd-trees and grids, and RT cores now do it in hardware.

光线是 r(t) = o + t·d, t > 0;光线追踪每像素投一条主光线(第 02 课的相机倒着跑)。求交是闭式的:光线–球是二次方程(取最小正根),光线–三角形用 Möller–Trumbore(重心 u,v,t;当且仅当 u,v ≥ 0, u+v ≤ 1, t > 0 命中);所有图元里最小的正 t 是可见命中。Whitted 递归靠投出阴影光线(硬阴影)、一条反射光线、一条折射光线(斯涅尔)来给命中着色,以 Fresnel 加权,并限制深度。朴素追踪每光线 O(N)、每帧 O(P·N)——规模一大就没救。加速结构解决它:把图元装进 AABB(slab 测试),建一棵 BVH,让每条光线只往它命中的盒子里下降、剪掉整棵子树 → 每光线约 O(\log N)。用便宜的中位数分割或更贵、追踪更快的 SAH 来建;备选是 kd-tree 与网格,而 RT 核心如今用硬件来做。

Interview prompts面试题