内容工厂. / 演示空间 / 全局光照 GI · 第 4 课 · 反弹多次 段 1 / 3
全局光照 GI | 第 4 课 | 上一课:反弹一次

反弹多次

读时 约 7 分钟 本课 3 段 · 已上线 3 段 每段 三件套 保真度 示意 · 等价重现(全课) 页面版本 v3
课题骨架(本课题只声明一次)

一个着色点接收到的光,有一部分来自别的物体反射的光;而别的物体,又接收到它反射的光。 要算清这一点,只能在场景里采样 —— 而一旦靠采样,就永远受 1/√N 支配。

这条骨架把 GI 收成三件事:在哪采样(探针 / 像素 / 路径)、采几次(一次反弹 / 多次反弹)、 怎么排(随机 / 分层 / 蓝噪声)。第 1 课量清了"不采样"的那一头, 第 2 课回答了第一件:在哪采样,第 3 课回答了第二件的前半步:采几次(一次)。 本课把"采几次"推到像素上 —— 再也不预先把场景切好,让光路自己走; 而一旦沿光路抽,就立刻要面对第三件事:怎么排。

4.1
路径追踪:随机走一条光路
4.2
收敛:1/√N 又出现了
4.3
采样怎么排:随机 / 分层 / 蓝噪声
4.1

路径追踪:随机走一条光路

示意 · 等价重现

3.3 把整个场景切成面片、把"互相问"的方程解了一遍,于是"多走一步多带回多少"第一次有了数。 可它的两笔账都押在前提上:每个面片只有一个数(所以窄缝里的渐变是分段的), 而那张"谁看得见谁"的形状因子表,只对漫反射成立 —— 换成金属、换成任意形状,那张表就得重做。

换一条完全不同的路:不预先切场景、也不解方程,而是从像素出发,让光路自己走 —— 每一步按材质的分布随机挑一个新方向,撞到东西就继续走,撞到天就把那一份光带回来。 这条路什么形状、什么材质都能走;可它的代价在第一步就露出来了: 一条光路只是无数种走法里的一种 —— 它给出的答案是抽出来的,不是解出来的。 所以本段唯一要装的东西就是这个:每条像素抽几条光路 M。M = 1 时画面是"麻"的, M 一大就干净起来 —— 而 3.3 那本"多走一步多带回多少"的账,搬到光路上照样能算(件三表①)。 三件量出来的事见下:判据②给"抽得越多越接近答案",判据④⑤给"被挡住的地方,间接那一份涨了一个量级", 判据⑥给"两种抽法是同一个积分",判据⑧给"一条光路绝不会凭空发光", 判据⑨给"两条带格数悬殊,那条带的均值自己带多少不确定度"。 保真度如实标「示意 · 等价重现」:整页是一台逐像素自算的 CPU 路径追踪器,不依赖 WebGPU; 件一框里那几行就是件二真正在跑的那份源码。

件一 含代码的数据流示意图 · 框里是件二真正在跑的那几行
本段只加一样东西:每条像素抽几条光路 M 一条光路只是"一种走法" —— 每步随机拐弯,撞到天就带一份光回来 这一段的做法(不再解方程,改成抽光路) ① 相机出发,每个像素一条主射线,打到第一个面 ② 每一"步":先算被打到的太阳(含遮挡),再按余弦分布随机挑一个新方向    撞到天就把那一份带回来 —— 一条光路只给一个答案 ③ 一条不够:抽 M 条求平均,那才是这个像素的答案 左列:这一段吃什么 天空(天光强度) — 每条光路最多走 — 白墙反照率 ρ 0.85 件二每抽一条光路,就是这几行 // ★ 本段唯一改动:沿光路随机走 function walk41(o, d, hmax) { let th = 1, L = [0, 0, 0]; for (let h = 0; h <= hmax; h++) { const p = hit41(o, d); if (!p) return add(L, mul(sky41(d), th)); L = add(L, mul(sun41(p), th * p.rho / PI)); o = off41(p); th *= p.rho; d = cosDir41(p.n, rnd41(), rnd41()); } return L; } 右列:交出什么 一条光路 vs 答案(均方差) — M 条之后剩多少 — 影子里的光,间接占 — 每一步都乘上一次反照率 ρ ⇒ 一条光路带回来的光,永远不超过它出发时的那一份
看什么:左边三格是这一段要吃的输入 —— 天空、每条光路最多走几步(本段固定 4 步), 以及白墙的反照率。中间框里是件二真正在跑的源码(函数 walk41 的正文,逐字未改): 它抽一条光路——从相机出发,每一"步"先问一句"这里晒得到太阳吗", 再按余弦分布随机拐个弯,直到撞到天、把那一份光带回来。 右列是它交出来的三个数:一条光路离答案有多远、抽 M 条之后还剩多少、 以及影子里的光有多少是"间接"来的。
为什么"抽一条"不够:光路上每一步都是一次随机的选择。 同样一个像素,第一次抽可能拐进墙里、第二次可能直奔天 —— 于是同一个像素两次给出不同的数。 这不是 bug,是抽样的本来面目:一条光路是"一个样本",不是"答案"。 答案是这个分布的期望,只能靠抽很多条去逼近(判据②量出:M 从 1 涨到 32,离答案还差的距离掉到 1/8.26)。
为什么这条路值得走:3.3 的装置要先把场景切好、把"谁看得见谁"列成表, 这张表只对漫反射成立,而且每个面片只能存一个数。 光路法不需要那张表:撞到什么就按什么走 —— 什么形状、什么材质都行, 而"多走一步"就是循环多转一圈(hmax 从 1 调到 4 就是件三表①)。
如实标的六处:① 本段是 CPU 路径追踪器(逐像素自己算、写在 canvas 上),不是 WebGPU 产出; ② "直接光"那一份在本页单独写成了一个小函数(direct41:只取第一个可见点自己收到的太阳与天), 再与总账相减。★ 它不能拿 walk41(hmax = 1) 冒充 —— 那一条走完 h=1 还会在邻居身上 再问一次太阳,带回来的其实是"弹过一次"的光(本页第一版正是这么写的,判据④把它抓了出来); ③ 太阳用"每步打一条阴影线"的显式抽样(sun41),所以太阳那一份不带噪声, 噪声全部来自"下一步往哪拐";④ 白墙的 ρ 取 0.85(与 3.3 同一个数), 所以"多走一步"的账能在两个装置之间对上;⑤ 两条带按"第一个可见点晒不晒得到太阳"分, 本页相机朝下看、地面又敞着,所以影带只占 153 格、亮带 2007 格(表②把两带的格数都印了出来)—— 两条带这么悬殊,"影带的平均亮度"这个数就比亮带飘得多,所以表② 底下把这笔不确定度如实算了出来 (影带 ±0.0028、亮带 ±0.0005,判据⑨); ⑥ 每条光路最多走 4 步,再深的部分在画面上已经看不出来。

① 先看左块(M = 1,只抽了一条光路):场景的形状是对的,但整块满是麻点 —— 相邻两个像素可能差好几倍。这就是"一条光路 = 一个样本"的样子。

② 把「每条像素抽几条光路」从 1 拖到 32:中块一点点干净起来, 右块(与左块的差)也跟着淡下去 —— 右列那个"均方差"就是它的读数。

③ 盯住右块最亮的地方:麻点不是均匀撒的 —— 越暗的地方麻得越厉害 (M = 1 时,影带里样本的相对散布是 29.9%,亮带只有 10.7%,差 2.8 倍)。

④ 看件三表②:它把画面按"第一个可见点晒不晒得到太阳"分成两条带。 影带收到的光里间接占 45.6%(0.4568 里有 0.2085 是弹出来的),而亮带里只有 3.9% —— 相差 11.6 倍。这就是这一课真正要说的那句:被挡住的地方,光几乎全是"走出来的"。
如实标一句:影带只有 153 格,这个 0.4568 自己带着 ±0.0028 的不确定度(亮带 ±0.0005)—— "相差 11.6 倍"这句不受影响,但影带那串小数只该读到第 3 位。表② 底下写着这笔账是怎么算出来的。

⑤ 把「天光强度」拖到 0(只剩太阳):影带几乎全黑(平均亮度从 0.457 掉到 0.053), 可它还亮着一点 —— 而那一点100% 是间接来的(判据④实测:直接那一份正好是 0)。 太阳被盒子挡住了,它能拿到的每一份光,都得先在墙上弹一次。

件二 可操作活例 · 本页自算(示意 · 等价重现) · 三块并排

左:只抽一条光路(M = 1)| 中:抽 M 条求平均 | 右:两者之差(灰阶)

启动中…

与上一段相同的四个控件 + 本段的一个

M = 1 是只抽一条(画面会麻); M 越大越接近答案,但每加一倍只是把麻点压小一点 —— 这条曲线的形状是 4.2 的事。

控件与上一段只多不少:天光、太阳方位、眼位、盒子数逐字相同。 上一段那三个开关(粗糙度 α、层号、间接反弹次数 N)在本段不再出现 —— 因为装置换成了"抽光路":3.3 那个 N 是解方程的遍数,本段的 M 是抽样的条数, 前者决定"算多深",后者决定"算多准"。本段唯一的新旋钮就是 M。

定点观察(三步):① 看左块(M = 1)那一脸麻点; ② 把 M 拖到 32,看中块怎么一点点变干净; ③ 再看右块最亮处 —— 麻点在暗处最厉害。 结论:一条光路是一个样本,不是一个答案;答案只能靠"多抽几条"去逼近。

看什么:三块并排,用的是同一个场景、同一台相机、同一批主射线, 只有"这个像素抽了几条光路"这一样不同 —— 左 = M = 1(只抽一条), 中 = M 条求平均,右 = 两者的色差(灰阶)。
为什么 M = 1 时画面是麻的:光路上的每一步都是一次随机选择, 所以同一个像素每抽一次就换一条"走法"。左边那一块画的就是其中一次走法的结果—— 它没有错,它只是一个样本。判据②把"抽得越多越接近答案"这条曲线量了出来: 从 1 条到 32 条,均方差掉到 1/8.26。
为什么暗处更麻:一个像素拿到的光 = 很多条路径各自带回来的和。 亮处(晒得到太阳、看得见大片天)每条路径几乎都能带回一份,样本之间的差别小; 暗处(太阳被挡、天也被挡)十条里可能只有一条侥幸拐到了亮处, 于是样本之间的差别极大 —— 实测 M = 1 时影带里样本的相对散布 29.9%, 亮带只有 10.7%:这就是右块上"越暗越麻"的道理。
如实标的两处:① 右块是逐像素色差的灰阶(满量程 0.25),不区分正负; ② 中块与左块共用同一批随机数(固定种子),所以"左块"是那一串里的第一条, 不是随手重抽的 —— 判据①验的就是"同一套配置跑两遍,逐字节相同"。
件三 中间量数值表 · 与件一、件二同源
① 每条光路多走一步,多带回多少光(hmax 扫描)
最多走几步全场影带(晒不到太阳)亮带(晒得到)影带相对 1 步涨了
② 光从哪来:直接(太阳 + 看得见的天)与间接(弹过至少一次)
带平均亮度直接那一份间接那一份间接占
③ 白炉:环境处处 L = 1、ρ = 1 时一条光路能带回多少
配置画面最大 / 最小恰好 = 1 的格子判

两条带的均值有多少是噪声:建表后填在这里

① 固定住抽样条数,只把"每条光路最多走几步"从 1 扫到 4:第一列是"只走 1 步"时拿到的光 (它等于直接光:太阳+这个点自己看得见的天),后面两列是按带分开的平均亮度。 末列是影带相对"只走 1 步"涨了几倍 —— 多走一步,涨的全在被挡住的那条带上。 这张表就是 3.3 那本账在光路上的版本:同一件事,两个装置各算一遍。
② 换个问法:一个像素拿到的光,是"直接看到的"还是"弹出来的"? 两行是同一批像素,只按"第一个可见点晒不晒得到太阳"分开。 影带收到的光里,间接那一份占 45.6%(0.4568 里有 0.2085 是弹出来的),而亮带里只有 3.9% —— 相差 11.6 倍。注意影带剩下的那 54.4% 是"直接"的,而这里的"直接"包含它自己看得见的那片天: 把天光关掉,那 54.4% 立刻归零,间接就成了 100%(判据④的后半段量的就是这个)。 第 1 课那句"AO 讲不出缝隙是亮的",正面答案就在这里:缝隙的光不是没有,是走出来的。
③ 最后一条是不创造能量的证据:把环境换成"处处 L = 1"、ρ 换成 1、太阳关掉, 那么一条光路要么原样带回一份 1、要么一份也带不回(th 一直是 1)—— 所以画面每一格只能落在 {0, 1} 上,绝不可能超过 1。空地上(没有盒子)每一条都会一步出界, 于是整块画面逐位恒等于 1。这可以拿笔算。
三处如实标:① 表里的数是像素上的平均值(不是面片),两条带按"第一个可见点晒不晒得到太阳"分 —— 本页影带 153 格 / 亮带 2007 格(相机朝下看、地面又敞着,所以影带小;表②把两带的格数都印了出来); ② 表里的数不随画面上的滑块变(整张表按页首那套配置算一遍;判据另在固定的那套配置上评估); ③ ★ 两条带这么悬殊,"这条带的均值"自己就带着不确定度 —— 表② 下面那一段把它算了出来 (影带 153 格 ±0.61%、亮带 2007 格 ±0.07%,判据⑨):所以表里影带那串小数只读到第 3 位, 亮带能读到第 4 位。这一条不影响任何结论("相差 11.6 倍"远大于这个抖动),但表里的位数不能多写。
本节获得

这一段把"采几次"从解方程推到了抽光路:装置换了,代价也换了。

  • 一条光路是一个样本,不是一个答案(判据①②):光路上每一步都是随机的, 所以同一个像素抽两次得到两个数。把"M 条求平均"当答案,误差才随抽样条数往下走 —— M 从 1 涨到 32,离答案还差的距离掉到 1/8.26,而且是单调降的。
  • "多走一步"的账,两个装置都能算(判据⑤ + 件三表①):3.3 用面片解了一遍, 本段用光路抽了一遍 —— 结论一样:多走一步,涨的全在被挡住的那条带上 (影带 0.2865 → 0.4522,涨 1.58×;亮带 0.6792 → 0.7030,只涨 1.04×)。 同一件事有两条路可走,这就是"装置可以换、物理只有一套"。
  • 被挡住的地方,间接那一份涨了一个量级(判据④,本段主判据):按"第一个可见点晒不晒得到太阳"分带, 影带收到的光里间接占 45.6%,亮带里只有 3.9% —— 相差 11.6 倍; 再把天光整个关掉(只剩太阳),影带里那一点光100% 是间接来的(直接那一份正好是 0)。 第 1 课说过"AO 讲不出缝隙是亮的"—— 这里给出了正面答案:缝隙的光是走出来的。
  • 两条带悬殊时,"均值"这个数自己也要标不确定度(判据⑨):分带之后影带只有 153 格、 亮带 2007 格,于是"影带的平均亮度"这个数比亮带飘得多 —— 影带 ±0.0028(±0.61%)、 亮带 ±0.0005(±0.07%),相差 9.2 倍。它有两个来源(拿 n 个格子代表整条带的格间起伏、 每格自己那份抽样噪声),表② 底下都算了。所以表里影带那串小数只该读到第 3 位。 结论稳不稳,先看这个数够不够大:11.6 倍远大于 0.61% 的抖动,所以结论不受影响。
  • 两种抽法是同一个积分(判据⑥):按余弦分布抽(权重只乘 ρ)与按均匀分布抽(权重乘 2ρcosθ), 给出的是同一个期望 —— 本页各抽 32 条,整块画面的平均分别是 0.68584 与 0.68498 (相对差 0.13%)。这是两条路可以拿笔对上的地方 (∫(ρ/π)·L·cosθ dω 与 (ρ/π)L·cos/(1/2π) 是同一件事的两种写法)。 工程上选前者,因为它把 cos 抽掉了,方差小。
  • 一条光路绝不会凭空发光(判据⑧):每走一步都乘一次 ρ ≤ 1, 而"撞到天"那一份就是环境本身。所以把环境设成"处处 L = 1"、ρ 设成 1、太阳关掉, 一条光路要么带回 1、要么带回 0 —— 永远到不了 1 以上。实测:空地(没有盒子)2160/2160 格恰好是 1, 两个盒子时最大也正好是 1.000000000。件三表③可以拿笔验。
  • 下一段(4.2)要接的:M 从 1 加到 32,画面确实干净了 —— 可它不是线性地干净: 每把 M 翻一倍,误差只掉一点点(件三表①那一列,或者判据②那串数)。这个"一点点"是多少, 正是下一段要量的东西;而它的形状就是整条主线里那句话:1/√N。
4.2

收敛:1/√N 又出现了

示意 · 等价重现

4.1 量出了一条曲线:M 从 1 涨到 32,"离答案还差的距离"掉到 1/8.26。 可那条曲线是拿什么量出来的?—— 拿"与参考解的差"。而参考解是上帝视角: 它由同一台程序另跑一遍、只为了给你当靶子。真机上你手上没有它。

这就让 4.1 那条曲线能看、不能用来做决定:画面还是麻的,你不知道是"再抽一倍就够"还是"再抽一千倍"。 出路其实就在同一个像素里:那 M 条光路彼此之间就差着,而这个"自己之间的差"恰好就是对 "均值离真值多远"的估计 —— 标准差除以 √M(标准误)。有了它,"还要抽多少"第一次变成一个能算的问题。 本段只加一样东西:一个不用参考解就能读出来的误差。 于是 4.1 那条曲线终于变成一条律:判据②量出"误差 × √M 是个常数"(也就是 误差 ∝ 1/√M), 判据④量出"自估的误差与上帝视角算出来的对得上",判据⑤量出那句最反直觉的话 —— 噪声减半要抽 4 倍,因为半是 √4 的一半,不是 2 的一半。 保真度如实标「示意 · 等价重现」:整页仍是那台逐像素自算的 CPU 路径追踪器,件一框里那几行就是本段真正在跑的。

件一 含代码的数据流示意图 · 框里是件二真正在跑的那几行
本段只加一样东西:一个不用参考解就能读出的误差 那 M 条光路彼此之间就差着 —— 这个"自己之间的差"就是对"离真值多远"的估计 这一段的做法(误差自己估) ① 抽光路那一套照旧(4.1 的 walk41,M 条求平均) ② 只是每像素多记一个数:这 M 条样本的平方和(原先只记和) ③ 于是方差能反解出来:方差 =(Σx² − M·均值²)/(M − 1) ④ 标准误 = 标准差 ÷ √M —— 它估的就是"均值离真值多远" 左列:这一段吃什么 每条像素的光路数 — 每像素记两个数 — 参考解(另跑) — 件二每格跑的就是这几行 // ★ 本段唯一改动:误差自己估 function se41(sum, sq, m) { const mu = sum / m; const d2 = (sq - m * mu * mu) / (m - 1); return Math.sqrt(Math.max(0, d2) / m); } 右列:交出什么 自估误差(M 条自己估) — 真误差(与参考解比) — 两者之比(应接近 1) — 误差 ∝ 1/√M:想要噪声减半,M 要乘 4 —— 因为半是 √4 的一半
看什么:左边三格是这一段要吃的输入 —— 光路数 M、每像素记的那两个数(Σx 与 Σx²)、 以及一个只用来对答案的参考解(另一套种子另跑一遍,真机上没有它)。 中间框里是件二真正在跑的源码(函数 se41 的正文,逐字未改):它不碰参考解, 只用这个像素自己那 M 条样本的和与平方和,把标准差反解出来、再除以 √M。 右列是它交出来的三个数:自己估的误差、拿参考解算出来的真误差、以及两者的比。
为什么"自己之间的差"能当误差用:均值 x̄ 是把 M 个独立样本平均出来的, 所以 x̄ 的方差 = 单个样本方差 ÷ M,也就是 x̄ 的标准差 = σ/√M。 而 σ 可以拿这 M 条自己反解(d2 那一行就是样本方差)。 于是"这个像素的答案有多不准"变成一件就地可算的事 —— 不需要知道答案。
为什么这条律这么要紧:它把"再多抽一点"从一个感觉变成一道算术题。 误差 ∝ 1/√M ⇒ 误差减半要 M ×4、减到 1/4 要 ×16。 这就是为什么实时渲染里"再加一点点采样"几乎从不解决问题 —— 想要明显变化,采样数得按平方涨。 它也解释了 4.1 那串数为什么会那样:M 每翻一倍,误差只掉到 1/√2 = 70.7%。
如实标的五处:① 本段是 CPU 路径追踪器(逐像素自算),不是 WebGPU 产出; ② 自估误差要用逐像素的样本散布,所以本段在 4.1 的累加器上多记了一个平方和(RSQ41)—— 这是本段唯一的实现改动; ③ M = 1 时估不出来(一条样本没有"彼此之间的差"),本页那一格按 0 画并注明; ④ "真误差"要一个参考解,本页用另一套种子另跑 M = 256(在样本网格上)—— 参考解自己也在收敛,所以它给的真误差在最大那几档会略微偏高(表①的"误差 × √M"那一列照实列出来); ⑤ 表里的数不随画面上的滑块变(整张表按页首那套配置算一遍;判据另在固定的那套配置上评估)。

① 先看右块(自估误差):它是一张亮度图 —— 亮的地方 = "这个像素的误差大"。 它不需要任何参考解,只用像素自己那 M 条样本。

② 把「每条像素抽几条光路」从 4 拖到 32:中块越来越干净, 而右块整块一起变暗 —— 自估误差从 0.0397 掉到 0.0142,暗了 2.79 倍 (条数翻了 8 倍,而 √8 = 2.83 —— 这就是 1/√M 的指纹)。

③ 把 M 拖回 1:右块整块黑掉 —— 不是"误差是 0",是估不出来: 一条样本没有"彼此之间的差"。要看误差,至少得两条。

④ 看件三表②:它把"要几条"直接摊开 —— 想让噪声减半,M 要乘 4; 想减到 1/4,要乘 16。这可以拿笔算:误差 ∝ 1/√M。

⑤ 看件三表①的最后一列:把每一档的误差都乘上 √M,得到的几乎是一条水平线 —— 这就是"1/√M 这条律"的样子。而表②告诉你同一件事的另一面:M 只翻一倍,误差只掉 29%。

件二 可操作活例 · 本页自算(示意 · 等价重现) · 三块并排

左:只抽一条光路(M = 1)| 中:抽 M 条求平均 | 右:这个像素自己估的误差(灰阶)

启动中…

与上一段逐字相同的五个控件

同一个滑块,本段盯的不再是"画面干净不干净", 而是右边那块有多亮:它 = 这个像素自己估出来的误差。M = 1 时估不出来(一条样本没有散布)。

控件与上一段完全相同(一个不多、一个不少):光路数 M、天光、太阳方位、眼位、盒子数。 本段真正加的不是一个旋钮,而是一个读数 —— 右块那个不用参考解就能读出来的误差, 以及由它反推出来的"要几条"(件三表②)。

定点观察(三步):① 把 M 从 4 拖到 32,盯右块; ② 看它整块变暗了 2.79 倍(条数翻了 8 倍,而 √8 = 2.83); ③ 再拖回 M = 1 —— 右块全黑,但那是"估不出来"而不是"没有误差"。 结论:误差 ∝ 1/√M,而这条律只用像素自己的样本就能读出来。

看什么:三块并排,用的是同一个场景、同一台相机、同一批主射线、同一批光路 —— 左 = M = 1(只抽一条)/ 中 = M 条求平均/ 右 = 这个像素自己估的误差(灰阶,满量程 0.30)。
右块为什么能"自己估":它画的是每个像素的标准误 = 这 M 条样本的标准差 ÷ √M。 标准差是"样本之间彼此差多少",而均值把 M 条一分摊,误差就只剩 1/√M —— 所以右块读的就是 "中块那个数离真值大概多远"。它从头到尾没有用过参考解。
为什么 M 一翻倍只暗 29%:1 − 1/√2 = 0.293。所以"把滑块往右拖一倍"这件事, 在误差上是很不划算的 —— 想让它暗一半,条数得翻 4 倍(件三表②)。
如实标的两处:① 右块的灰阶满量程是 0.30(不区分通道,取三通道平均); ② 中块与左块共用同一批随机数(固定种子),所以"左块"是那一串里的第一条,不是随手重抽的。
件三 中间量数值表 · 与件一、件二同源
① M 扫描:误差怎么落(真误差 vs 自己估的)
M真误差自估误差自估 ÷ 真误差 × √M比上一档掉了
② "要几条":目标噪声 ↔ 需要的采样数(可笔算)
目标噪声(相对 M = 1)要几条(= 目标²)滑块到得了吗实测误差比
③ 参考解够不够:两份独立参考解给出的误差曲线差多少
M用 M = 256 的参考解用 M = 64 的参考解相对差
① 把 M 从 1 扫到 32:第二列是与参考解比出来的真误差(上帝视角),第三列是只用像素自己那 M 条估的, 第四列是两者的比(应当接近 1,判据④量的就是它)。第五列是误差 × √M —— 这一列近乎水平就是"误差 ∝ 1/√M"这条律。末列是相对上一档掉了多少:每翻一倍只掉到 70.7%。
② 换个问法:想让噪声减半,要多抽几条? 由 误差 ∝ 1/√M 直接反解: 目标 = 1/2 ⇒ M ×4;目标 = 1/4 ⇒ M ×16;目标 = 1/8 ⇒ M ×64。这张表可以拿笔验: 把"要几条"开平方再取倒数,正好等于第一列那个目标。末列是扫描里真能用滑块走到的那些档的实测比值。
③ 最后一条是自检:比出来的"真误差"里,也混着参考解自己的那一份。 换一份更粗的独立参考解(M = 64,另一套种子)再算一遍,看曲线差多少 —— 差得越小,说明参考解越不是瓶颈(判据⑧)。
两处如实标:① 表里的数是在步长 2 的样本网格(与画面同一批像素)上算的,参考解与两条带同一批像素; ② "真误差"那一列在最大几档会略微偏高,因为参考解自己也在收敛 —— 那一列照实印出来、不做修正。
本节获得

这一段把 4.1 那条"抽得越多越接近答案"的曲线,量成了一条能用来做决定的律。

  • 误差可以就地估出来(判据④):每像素多记一个平方和,就能把标准差反解出来、 再除以 √M 得到标准误。它与上帝视角算出来的真误差对得上(比值接近 1)—— 所以真机上"我还差多少"是一个可以当场回答的问题,不必等答案。
  • 误差 ∝ 1/√M(判据②):把每一档的误差都乘上 √M,得到的近乎是一条水平线。 这条律与场景、光源、深度都无关,只要你在做独立采样的平均,它就成立 —— 这就是整条主线里那句话的来历。
  • 噪声减半要抽 4 倍(判据⑤):半是 √4 的一半。所以"再把滑块往右拖一点"几乎永远不够; 想要明显变化,采样数得按平方涨。这也解释了 4.1 那串数:M 每翻一倍,误差只掉到 70.7%。
  • 同一条律,在明处和暗处都成立(判据⑥):两条带各自的"误差 × √M"都是水平线, 只是暗处那条整体更高 —— 这正是 4.1 量到的"越暗越麻":暗处的光要靠一次次反弹才凑得出来, 单个样本的方差本来就大,同样的 M 下它更难收敛。
  • 下一段(4.3)要接的:既然"抽多少"这条路回报这么差,那就换一条 —— 不增加条数,只改"往哪儿抽"。3.3 判据⑧已经量过一次:同样的预算, 分层比随机快一档(≈ 1/M 对 1/√M)。4.3 把它搬到光路上。
4.3

采样怎么排:随机 / 分层 / 蓝噪声

示意 · 等价重现

4.2 把"抽多少"这条路量到了底:误差 ∝ 1/√M —— M 翻一倍只掉 29%, 想让噪声减半得把采样数乘 4。也就是说,靠"再多抽一点"这条路,回报是平方级地差。

那就换一条:不增加条数,只改"往哪儿抽"。同一笔预算 M,如果把这 M 条光路的第一步方向 在半球上铺开(仰角分层 + 方位低差异),而不是随便撒,它们就"覆盖得更均匀", 答案也就更准。可这里立刻冒出一个新问题:如果每个像素都用同一套铺法, 相邻像素就会一起错 —— 噪声变成块状的,而块状噪声在人眼里比细颗粒噪声难看得多。 工程上的解法是给每个像素整体转一个角度(用像素号当种子),铺法不变、但像素之间不再相关 —— 这就是蓝噪声。本段只加一样东西:采样怎么排(三档并排,同一个 M)。 三件量出来的事见下:判据②给"铺开比随便撒准多少",判据④给"分层准但成块、蓝噪声准且不成块", 判据⑧给一件更值得记的事 —— 3.3 那个"分层能快到 1/M"在光路上拿不到, 因为光路积分是十几维的,把第一步铺开只买到一部分。 保真度如实标「示意 · 等价重现」:整页仍是那台逐像素自算的 CPU 路径追踪器,件一框里那几行就是本段真正在跑的。

件一 含代码的数据流示意图 · 框里是件二真正在跑的那几行
本段只加一样东西:采样怎么排(同一个 M,三种排法) 铺开的第一步方向,比随便撒覆盖得更均匀 —— 但同一套铺法给每个像素就会成块 这一段的做法(换掉光路的头两个随机数) ① 光路本身一字不改(还是 4.1 的 walk41),只换"头两个随机数"从哪来 ② 随机档:照旧走伪随机流 —— 这是基准 ③ 分层档:第 s 条的仰角取 (s + 0.5) / M,方位取 van der Corput 序列 ④ 蓝噪声档:同一套铺法,只把方位整体转一个角度(用像素号当种子) 左列:这一段吃什么 每条像素的光路数 — 三套排法 — 参考解(另跑) — 件三每档跑的就是这几行 // ★ 本段唯一改动:把第一步的两个随机数排开 function sch43(p, s, m) { if (SCHED43 === 0) { _pi43 = 2; return; } const o = SCHED43 === 2 ? hash43(p) : 0; _pre43[0] = frac43((s + 0.5) / m); _pre43[1] = frac43(rv2_43(s + 1) + o); _pi43 = 0; } 右列:交出什么 随机档的误差(M 条) — 分层档的误差(同一 M) — 相邻像素相关:分层 / 蓝噪声 — 三档的期望是同一个数 —— 换排法只动"误差有多大、长什么样",不动"答案是多少"
看什么:左边三格是这一段要吃的输入 —— 光路数 M、三套排法、 以及一个只用来对答案的参考解。中间框里是件二真正在跑的源码(函数 sch43 的正文,逐字未改): 它不改光路本身,只把"这一步往哪拐"的头两个随机数换成排开的那一份。 右列是它交出来的三个数:随机档的误差、同一 M 下分层档的误差、 以及三档的相邻像素相关系数。
为什么"铺开"会更准:随便撒 M 个方向,总会有的地方挤、有的地方空 —— 而"挤"和"空"带来的误差是同一量级的。铺开之后每个小格恰好落一个,覆盖的起伏被压掉, 于是同样的 M 条,答案更接近真值。
为什么要"每像素转一个角度":如果所有像素共用同一套铺法,那么"哪里偏亮、哪里偏暗" 在每个像素上都朝同一个方向 —— 于是误差在两三个像素的尺度上结成块。 加一个以像素号为种子的整体旋转,铺法(因而误差大小)一点没变, 但每个像素偏的方向换成了各自独立的 —— 噪声从"块"变成"粒"。这就是蓝噪声在做的事。
如实标的五处:① 本段是 CPU 路径追踪器(逐像素自算),不是 WebGPU 产出; ② 本段只把头两个随机数换成排开的版本:光路后面还有十几维(每次弹射的方向、路径长度), 那些仍然随机 —— 这正是判据⑧要讲的事,也是"1/M 拿不到"的原因; ③ 三档各自用独立的随机流(种子按档号与样本号派生),保证只有"排法"这一个变量在变; ④ 参考解是 4.2 那份(M = 256、种子 7777、与三档都不同源),三档共用同一个靶子; ⑤ 表里的数不随画面上的滑块变(整张表按页首那套配置算一遍;判据另在固定的那套配置上评估)。

① 先看三块画面(同一个 M):三块的明暗分布是一样的 —— 它们要算的是同一个积分。 不同的只是"麻"的样子。

② 把 M 从 4 拖到 32:三块一起变干净,但左边(随机)始终比中间和右边差一点 —— 右列那两个误差读数就是它的量。

③ 凑近看噪声的"颗粒":左块(随机)是细沙;中块(分层)看起来有一片一片的块; 右块(蓝噪声)又回到细沙 —— 而中块与右块的误差读数几乎一样。 也就是说:蓝噪声换的不是"误差多大",而是"误差长什么样"(判据④)。

④ 看件三表③:它把上面那句话变成一个数 —— 相邻像素误差的相关系数: 分层档明显为正(一起错),随机档与蓝噪声档接近 0。

⑤ 看件三表②:三档的拟合斜率几乎一样(−0.498 / −0.487 / −0.482,都压在 −0.5 上)—— 铺开买到的是常数折扣(误差压到 0.61 倍,等效省下约 2.7 倍采样数), 而不是更快的收敛速度。3.3 在面片上量到的那条 1/M,在光路上拿不到 —— 原因写在判据⑧里。

件二 可操作活例 · 本页自算(示意 · 等价重现) · 三块并排

左:随机 | 中:分层(每像素同一套铺法)| 右:蓝噪声(每像素转一个角度)

启动中…

与上一段逐字相同的五个控件

同一个滑块,三块画面一起动。本段不增加条数 —— 换的是"往哪儿抽",所以请凑近看颗粒,而不只是看干净不干净。 拖的时候三块会一条一条地铺开:先看到的是前几条的均值,越铺越干净,不用等它铺完。

控件与上一段完全相同(一个不多、一个不少)。本段的"唯一新东西"是三档排法 —— 它不是旋钮,而是三块并排:随机 / 分层 / 蓝噪声,同一个 M 下同时看。

定点观察(三步):① 把 M 拖到 16,凑近看三块的颗粒; ② 注意中块有片状的块,而左块与右块是细沙; ③ 再看右列的误差读数 —— 中块与右块几乎一样准。 结论:分层让人更准,蓝噪声让噪声"看起来"更细;两者误差一样,只是颗粒不同。

看什么:三块并排,用的是同一个场景、同一台相机、同一批主射线、同一个 M —— 只有"这 M 条的第一步方向怎么排"这一样不同 —— 左 = 随机(伪随机流)/ 中 = 分层(第 s 条仰角取 (s + 0.5)/M、方位取低差异序列)/ 右 = 蓝噪声(同一套铺法,每像素整体转一个角度)。
为什么"铺开"能降误差:把 M 条的第一步方向在半球上排成网格, 每一个小格恰好落一条 —— 于是"哪里挤、哪里空"这种起伏被压掉了。 而随机撒下的 M 个点,总有些格子挤、有些空着,这份起伏就是误差的来源之一。
为什么还要"每像素转一个角度":铺法是确定的。如果所有像素都照同一套铺, 那么"第 3 条偏亮"这件事会在每个像素上重演 —— 误差于是跨像素同向,结成块 (实测:分层的相邻像素相关 0.392)。 把整套铺法按像素号把方位旋转一个角度之后,每个像素仍然"铺得很均匀",但 它们偏的方向彼此独立(相关被打到 −0.032)—— 块被打散成粒,而误差大小几乎没变。
如实标的两处:① 三块的灰阶映射与 4.1 相同(sqrt),所以可以直接比明暗; ② 三档各自用独立随机流,保证"排法"是唯一变量。
件三 中间量数值表 · 与件一、件二同源
① 三档误差随 M 怎么落(同一个靶子)
M随机分层蓝噪声分层 ÷ 随机
② 拟合斜率与"等效省下的采样数"
档误差 ∝ Mk 的 kM = 32 时相对随机的误差比等效省下的采样数
③ 噪声长什么样:相邻像素误差的相关系数
档相关系数(水平相邻)读作
① 三档各扫一遍 M(同一个靶子):分层与蓝噪声的误差始终低于随机,而分层与蓝噪声几乎相等 (它们是同一套点集,只差一个整体旋转)。末列是"分层把随机压缩到几成"。
② 把每一档的误差对 M 做对数坐标上的直线拟合,斜率 k 就是"误差 ∝ Mk"里的指数: k = −0.5 就是 1/√M(4.2 量到的那条),k = −1 才是 1/M(3.3 在面片上量到的那条)。 ★ 实测结果是三条几乎都压在 −0.5 上(−0.498 / −0.487 / −0.482)—— 也就是说铺开第一步没有换来更陡的收敛曲线,只换来一个常数折扣(误差压到 0.61 / 0.55 倍)。 末列把这份折扣折成采样数:按随机档自己的 1/√M 算, 要抹平这 0.61 倍,随机档得再多抽 ×1/0.61² ≈ ×2.7 条 —— 这一列可以拿笔验。
③ 最后一条是本段真正的新东西:把每个像素的误差与它右边那个像素的误差求相关。 分层档明显为正(误差跨像素同向 ⇒ 块状),随机档与蓝噪声档接近 0(各错各的 ⇒ 细粒)。
两处如实标:① 相关系数是在样本网格上算的,"相邻"= 相距 2 像素,用的是 M = 32 那一档; ② 三档的误差都是与同一个参考解比出来的(4.2 那份,M = 256、另一套种子)。
本节获得

这一段把"采几次"之外的另一半也量了:同一笔预算,排法本身就是一种预算。

  • 铺开比随便撒准(判据②):同样 M 条,把第一步方向排成网格,误差就比随机低一截 —— 实测每一档都低,到 M = 32 时压到 0.61 倍。排得越好、覆盖面越匀,这一截越大。
  • 但"准"不等于"好看"(判据④,本段主判据):如果所有像素共用同一套铺法, 误差会跨像素同向、结成块(相邻像素相关系数 0.392)。给每个像素把方位整体转一个角度之后, 相关被打回 −0.032,而误差一点没变(判据⑤:五档平均只差 6.3%)—— 颗粒从"块"变成"粒"。这就是蓝噪声的全部内容, 也是实时渲染里"同样噪声量、观感差一大截"的来源。
  • 排法买到的是"常数",不是"指数"(判据⑧):三档的拟合斜率几乎一样 (−0.498 / −0.487 / −0.482,都压在 4.2 那条 1/√M 上)—— 铺开第一步把误差压到 0.61 倍 (等效省下约 2.7 倍采样数),但没有让曲线变陡。原因:3.3 抽形状因子时积分只有 2 维, 铺开就能接近 1/M;而光路积分有 十几维(每次弹射的方向都得抽),只铺开头两维, 误差仍由后面那些维度按 1/√M 决定。这就是路径追踪真正难的地方: 采样数省不掉,排法只能把常数往下压一点。
  • 换排法不动答案(判据⑥):三档在 M = 32 时的平均亮度是 0.68561 / 0.68563 / 0.68633,最大相对差 0.10% —— 排法只改"误差多大、长什么样", 不改"期望是多少"。这也是一条可以拿笔验的守恒。
  • 下一课(第 5 课)要接的:到这里,"在哪采样 / 采几次 / 怎么排"三件事都量过了, 结论是采样这条路本身很贵。所以工程上真正的出路不在采样,而在 把光搬出运行时 —— 下一课讲烘焙、探针网格与硬件光追这些取舍。
本课三段 · 全部上线
4.3 (本课已收束) 第 4 课三段全部上线:4.1 路径追踪(让光路自己走,一条光路只是一个样本)· 4.2 收敛(误差 ∝ 1/√M,减半要抽 4 倍)· 4.3 采样怎么排(铺开比随便撒准,蓝噪声把块打成粒)。 三段落在一起,把骨架第二、三件事都量到了底:采几次与怎么排。 已收束
运行时自检 等待脚本…

本段的活例由本页自己计算(示意 · 等价重现,非硬件产出):不依赖 WebGPU,任何设备都能跑。同一页的示意图与数值表照常可读。


  

页面版本 v3(课题四第 4 课「反弹多次」:三段全部上线 —— 4.1 路径追踪:随机走一条光路 / 4.2 收敛:1/√N 又出现了 / 4.3 采样怎么排:随机 / 分层 / 蓝噪声)。
4.1 这一段只加一样东西:每条像素抽几条光路 M。装置换了(如实声明):3.3 是"把场景切成面片、 把'互相问'的方程解一遍"(确定、无噪声,但每个面片只能存一个数、那张形状因子表只对漫反射成立); 本段是"从像素出发、让光路自己走"——每一步按余弦分布随机拐弯,撞到天就把那一份带回来。 量出来的五件事: ① 一条光路是一个样本,不是一个答案:M = 1 时离参考解的均方差是 0.0816, 抽到 32 条掉成 0.0099(1/8.26,且单调降);而同一套配置跑两遍逐位相同(判据①②)—— 所以"麻"不是随机报错,是抽样本身; ② "多走一步"的账两个装置都能算:把每条光路最多走几步从 1 扫到 4, 影带 0.2865 → 0.4522(涨 1.58×),亮带 0.6792 → 0.7030(只涨 1.04×)—— 涨起来的几乎全在晒不到太阳的那条带上(判据⑤ + 件三表①); ③ ★ 被挡住的地方,间接那一份涨了一个量级:按"第一个可见点晒不晒得到太阳"分带, 影带收到的光里间接占 45.6%,亮带里只有 3.9%(相差 11.6 倍); 再把天光整个关掉(只剩太阳),影带里那一点光100% 是间接来的(直接那一份正好是 0)—— 太阳被盒子挡住之后,它能拿到的每一份光都得先在墙上弹一次(判据④,本段主判据); ④ 两种抽法是同一个积分:按余弦分布抽(只乘 ρ)与按均匀分布抽(乘 2ρcosθ)给出同一个期望 (判据⑥:整块画面平均 0.68584 对 0.68498,相对差 0.13%,可笔算);而白炉 (ρ = 1、环境处处 L = 1、关掉太阳)下,一条光路只能带回 0 或 1 —— 空地上一整块画面逐位恒等于 1(2160/2160 格),两个盒子时最大也正好是 1.000000000(判据⑧ + 件三表③); ⑤ ★ 那两条带悬殊,这笔账要如实标:影带只有 153 格、亮带 2007 格(相机朝下看、地面敞着), 于是"影带的平均亮度"这个数自己带着 ±0.0028(±0.61%),亮带只带 ±0.0005(±0.07%)—— 相差 9.2 倍。表② 底下把这笔不确定度算了出来(判据⑨),影带那串小数因此只读到第 3 位。 判据九条:① 可复现(同种子两遍逐字节相同)/ ② 抽得越多越接近答案(单调降)/ ③ 画面(写入格子数、非黑比例、非法值 0、命中面法线朝向来光的哨兵)/ ④ ★ 直接 vs 间接(主判据)/ ⑤ 多走一步涨在影带(hmax 扫描)/ ⑥ 两种抽法同答案(可笔算)/ ⑦ 同源(件一框里那几行 = walk41 的正文,重算逐位相同)/ ⑧ 白炉(不创造能量)/ ⑨ 两条带的均值按格子数如实标出不确定度(格间起伏 + 每格抽样)。
4.2 这一段只加一样东西:一个不用参考解就能读出的误差。4.1 那条"抽得越多越接近答案"的曲线 是拿"与参考解的差"量的 —— 而参考解是上帝视角,真机上没有。本段把它换成像素自己那 M 条样本的散布: 每像素多记一个平方和(Σx²),就能把标准差反解出来、再除以 √M 得到标准误。 件一框里那几行 = se41 的正文(逐字,含 m < 2 那句守卫)。量出来的四件事: ① ★ 误差 ∝ 1/√M(判据②,主判据):M = 1/2/4/8/16/32 的真误差依次 0.07979 / 0.05766 / 0.04111 / 0.02963 / 0.02111 / 0.01524;把它们各乘上 √M 得到 0.07979 / 0.08154 / 0.08221 / 0.08380 / 0.08445 / 0.08620 —— 最大/最小 = 1.080, 近乎一条水平线。M 每翻一倍,误差只掉到 71~72%(理论 70.7%); ② ★ 自估的误差与上帝视角算出来的对得上(判据④):M = 2/4/8/16/32 上两者的比依次 0.971 / 0.965 / 0.961 / 0.951 / 0.932(理想 1)—— 所以"我还差多少"在真机上当场就能回答; ③ ★ 噪声减半要抽 4 倍(判据⑤):M 1→4 掉 1.941 倍、M 4→16 掉 1.947 倍、 M 1→16 一共掉 3.779 倍(比理想的 4 略小,因为真误差里混着参考解自己那份); ④ 同一条律在明处暗处都成立,只是常数不同(判据⑥):M = 32 时自估误差 亮带 0.01330、影带 0.02293(影带高 1.72 倍)—— 4.1 那句"越暗越麻"在误差上的样子。 判据八条:① 可复现(同种子两遍逐位相同)/ ② ★ 误差 ∝ 1/√M(主判据)/ ③ 画面(写入格子数、非黑、非法值 0、命中面法线朝向来光的哨兵)/ ④ ★ 自估误差可信(主判据)/ ⑤ ★ 噪声减半要 ×4/ ⑥ 明暗两带同律、暗带更高/ ⑦ 同源(件一框里那几行 = se41 的正文,重算逐位相同)/ ⑧ 参考解够不够(换一份更粗的独立参考解:M = 1 处差 0.5%、M = 32 处差 15.1%,随 M 单调变大)。 ★ 如实标:① 4.2 的表在步长 2 的样本网格(与画面同一批像素)上算; ② 两份参考解都是独立的(种子 7777 / 8888,M = 256 / 64)—— 用采样链自己的前缀当参考解会与采样档相关, 那样量出来的曲线是假的;③ 右块灰阶满量程 0.30;④ M = 1 时自估误差估不出来(一条样本没有散布), 本页那一格按 0 画并注明。
4.1 与 4.2 的关系:4.1 的"唯一变量"是 M(滑块),4.2 的"唯一新东西"是一个读数 —— 所以两段的控件逐字相同,读者做同样的动作(拖 M),看到的是新的现象(右块按 1/√M 变暗)。
4.3 这一段只加一样东西:采样怎么排(三档并排,同一个 M)。4.2 把"抽多少"这条路的回报量到了底 (1/√M,翻一倍只掉 29%),于是换一条:不增加条数,只改"往哪儿抽"。 光路本身一字不改(还是 walk41),只换"头两个随机数"从哪来 —— 也就是第一步的方向怎么排: ① 随机(伪随机流,基准)② 分层(第 s 条仰角取 (s + 0.5)/M、方位取 van der Corput 序列) ③ 蓝噪声(同一套铺法,把方位按像素号整体转一个角度)。 件一框里那几行 = sch43 的正文(逐字)。量出来的四件事: ① ★ 铺开比随便撒准(判据②):三档的误差(随机 / 分层 / 蓝噪声)在 M = 2/4/8/16/32 上是 0.05885/0.03661/0.03179 · 0.04119/0.02292/0.02168 · 0.02827/0.01634/0.01639 · 0.02063/0.01292/0.01149 · 0.01483/0.00903/0.00822 —— 每一档分层与蓝噪声都低于随机,到 M = 32 压到 0.61 / 0.55 倍; ② ★ 分层"准但成块",蓝噪声"准且不成块"(判据④⑤,主判据):相邻像素误差的相关系数 随机 0.016 · 分层 0.392 · 蓝噪声 −0.032 —— 而分层与蓝噪声的误差五档平均只差 6.3%。 也就是说蓝噪声换的不是"误差多大",而是"误差长什么样":颗粒从"块"变成"粒"; ③ ★ 排法买到的是"常数",不是"指数"(判据⑧):三档的 log-log 拟合斜率 −0.498 / −0.487 / −0.482 —— 都压在 4.2 量到的那条 1/√M 上。铺开第一步给的是一个 常数折扣(0.61 倍 ≈ 省下 2.7 倍采样数),不是更陡的收敛曲线; ④ 换排法不动答案(判据⑥):三档在 M = 32 时的平均亮度 0.68561 / 0.68563 / 0.68633, 最大相对差 0.10% —— 排法只改"误差多大、长什么样",不改"期望是多少"。 判据八条:① 可复现(三档各跑两遍逐位相同)/ ② ★ 铺开比随便撒准/ ③ 画面(写入格子数、非黑、非法值 0、命中面法线朝向来光的哨兵)/ ④ ★ 分层成块、蓝噪声不成块(主判据)/ ⑤ 分层与蓝噪声误差一样(同一套点集,只差一个方位旋转)/ ⑥ 换排法不动答案/ ⑦ 同源(件一框里那几行 = sch43 的正文,重算逐位相同)/ ⑧ ★ 买到的是常数不是指数。 ★ 如实标:① 本段只把头两个随机数换成排开的版本,光路后面十几维仍然随机 —— 这正是判据⑧的原因; ② 三档各自用独立的随机流(种子按档号与样本号派生),保证"排法"是唯一变量; ③ 相邻像素相关系数在样本网格上算,"相邻"= 相距 2 像素,用 M = 16 那一档; ④ 三档共用同一个靶子(4.2 那份参考解,M = 256、另一套种子)。
4.1 / 4.2 / 4.3 的关系:4.1 的唯一变量是 M(滑块);4.2 的唯一新东西是一个读数(自己估的误差); 4.3 的唯一新东西是三档排法(三块并排)—— 三段控件逐字相同,读者做同样的动作(拖 M), 看到的是三件新事情:一条光路只是样本、误差按 1/√M 落、排法只买常数不买指数。 ★ 与前几课共享同一个场景(同一组盒子坐标与高度、同一个太阳高度角与方位约定、同一套天色)—— 所以"这是同一个世界,只是第一次让光自己走"这句话是能对上的。 ★ 如实标:① 本页是 CPU 路径追踪器(逐像素自算),不依赖 WebGPU; ② "直接光"那一份单独写成了 direct41(只取第一个可见点自己收到的太阳与天)再与总账相减 —— 它不能拿 walk41(hmax = 1) 冒充:那一条还会在邻居身上再问一次太阳, 带回来的是"弹过一次"的光(本页第一版正是这么写的,判据④把它抓了出来); ③ 太阳用显式阴影线抽样,所以太阳那一份不带噪声,噪声全部来自"下一步往哪拐"; ④ 白墙 ρ = 0.85(与 3.3 同一个数);⑤ 每条光路最多走 4 步; ⑥ 两条带按"第一个可见点晒不晒得到太阳"分,本页影带 153 格 / 亮带 2007 格(相机朝下看、地面敞着)。 | 看不到新内容时请硬刷新(Ctrl+Shift+R),或给地址加 ?t=N —— 本站的 HTML 不带缓存指令。