一个着色点接收到的光,有一部分来自别的物体反射的光;而别的物体,又接收到它反射的光。 要算清这一点,只能在场景里采样 —— 而一旦靠采样,就永远受 1/√N 支配。
这条骨架把 GI 收成三件事:在哪采样(探针 / 像素 / 路径)、采几次(一次反弹 / 多次反弹)、 怎么排(随机 / 分层 / 蓝噪声)。第 1 课量清了"不采样"的那一头, 第 2 课回答了第一件:在哪采样(探针 + 球谐,顺带量了它的三种错)。 本课回答第二件:采几次 —— 从一次反弹开始,而它有一个精确的入口:镜子。
前两课问的都是"光从哪儿来":1.1 把它压成一次乘法、1.3 换成眼前一屏的深度、 2.1–2.3 把它装进探针。它们全都在算入射。可骨架说的是另一件事 —— 光打在物体上会再飞出去,那个"再飞出去"的光,才是 GI 的本体。
要从"只算入射"跨到"光会再飞出去",最干净的入口是镜子:它把这件事做成了一条精确的几何命题 —— 镜面上每一点,只用一条反射线就够了(大学光学第一节课的内容)。 于是"一次反弹"在这里第一次有了一个能对答案的版本:参数化一支反射线, 逐点求交算出画面;再拿工程上真在用的做法(把相机搬到地板下面)去对 —— 两条路应当一模一样。 本段只加一样东西:一个能把地面法线扳歪的旋钮(镜面波纹 κ)。 κ = 0 就是一块平镜 —— 此时两条路逐像素相同(判据①); 而 κ 一旦不为 0,那句"把相机搬过去就够了"就开始说错话,错多少正是本段要量的东西。 三件量出来的事见下:判据①②给"平镜时是恒等式",判据④⑤给"弯了差多少", 判据⑥给"一次"这两个字的确切含义 —— 反射线只追一次,所以镜子里看不到"镜子里的镜子"。 保真度如实标「示意 · 等价重现」:整页是一台逐像素自算的光线投射器(CPU),不依赖 WebGPU; 件一框里那几行就是件二真正在跑的。
mirrorDir31 的正文,逐字未改):
它给出"打到地面的射线该往哪走",而它内部有两条路 ——
r1 是教科书里的按法线反射,r2 是工程上真在用的"把眼睛搬到地板下面"。
右列是它交出来的两个数:两条路差多少度、以及画面差多少。
① 先看左块(真值:逐点按法线反射):地面是一面镜子,盒子的侧面倒映在里面, 远处映出的是天。这就是一次反弹 —— 反射线跑到盒子上就停了。
② 把「镜面波纹 κ」从 0 拖到 0.30:左块里镜面开始起波纹,倒影被扯歪; 而中块(把相机搬过去的做法)还是那么干净 —— 两块开始不一样,右块亮起来。 结论:"搬相机"这句话的精确性,完全押在"地面是平的"上。
③ 把 κ 拖回 0:右块整个黑掉(判据①:两条路逐像素相同、一个格子都不差)—— 这不是"差不多",是恒等式。
④ 转「眼位方位」:相机绕着场景走,倒影跟着走 —— 因为镜面里看到的世界 等于"从 E′ 看到的世界",而 E′ 是 E 的镜像,它跟着动。
⑤ 把「天光强度」拖到 0:镜子里那片天黑了,可盒子的倒影还在(它靠的是太阳)—— 说明镜面只是把已经有的光搬了个位置,它自己不发光(判据⑧)。
左:逐点按法线反射(真值)| 中:把相机搬到地板下面 | 右:两者之差(灰阶)
启动中…
κ = 0 是一块平镜(两条路逐位相同); κ 越大,地面法线的倾角上限越大(几何仍是平的 —— 这是"水面"的模型)。
控件与前两课只多不少:两个滑块与"盒子数"逐字相同; 另加本课新装的两样装置各一个旋钮 —— 相机(眼位方位)与镜面(波纹 κ)。 本段真正的新变量只有一个:κ。
定点观察(三步):① 看左块地面里盒子的倒影; ② 把 κ 从 0 拖到 0.30,看右块从全黑变成一片亮; ③ 结论:"把相机搬到地板下面"只在平面镜上成立 —— 镜面一弯,它给出的方向就与逐点反射分开了,而左块(逐点反射)才是对的。
r1(按法线反射,逐点求交),
中 = r2(把眼睛搬到地板下面,再看那个方向),右 = 两者的色差(灰阶)。
P − E′ 与
D − 2(D·n)n 正好平行(件三表②逐点列出来,夹角一栏恒为 0)——
所以两条路走的是同一条线,追到的是同一个物体。
| κ | 方向夹角 平均 / 最大(度) | 右块平均色差 | 右块最大色差 | 法线最大倾角(度) |
|---|
| 地面点 P | 法线 N | r₁ 按法线反射 | r₂ 从 E′ 看 P | 夹角 |
|---|
P − E′ 与反射方向 平行(因为 E′ 就是 E 关于镜面的镜像)——
表里"夹角"那一栏因此恒为 0。第三列与第四列看起来是两个不同的向量,
但它们是同一个方向的两种写法(只差一个正的长度因子)。
acos 算,
所以 κ = 0 那一行显示 0.0000° 而不是"逐位相同"——
逐位相同那一句是画面的(判据①数的是格子),两者不要混;
② 表里的数不随画面上的滑块变(判据在固定的那套配置上评估),免得读者随手一拖就以为判据错了。
这一段第一次让"光会再飞出去"变成一件能对答案的事,入口是一面镜子。
P − E′ 与按法线反射的方向正好平行。所以工程上画平面反射的办法是
—— 把场景再渲染一遍(相机搬到 E′),把结果贴到镜面上。3.1 的地面是一面完美的镜子:每个像素一条反射线,方向精确 —— 所以才有"把相机搬过去就够了"这条恒等式。可真实材质没有一面是完美的:打磨过的金属、水波、塑料, 反射方向总要糊开一片。
一旦糊开,"一条线"就不够了:那个方向上的答案只是这一片里一个点的抽样。 最直白的做法是多抽几条—— 但那就把 3.1 好不容易省下的"一条线"又赔回去了。 工程上的做法是另一条路:先把"环境"这张图按模糊程度预先糊好几份,再照旧只查一条线, 而糊到哪一份,按粗糙度选 —— 这正是 mip 那一套(连"多花 1/3 存储"这笔账都一样)。 本段只加一样东西:粗糙度 α。(α = 0 就是 3.1 那面完美镜。) 三件量出来的事见下:判据①②给"α = 0 时两条路逐位相同、α 一大就分开", 判据④⑤给"预先糊好的那张图能顶上多少、层级该怎么选",判据③⑥给画面与这笔账的价钱。 保真度如实标「示意 · 等价重现」:环境图、预滤波链、粗糙度分布都按定义在本页算出来; 为了把"糊"这件事单独拎出来,本段把"反射线上打到什么"换成了一张预先拍好的全景图 —— 也就是一枚反射探针(真机上这一步就是预先烤好的):一次拍完整个球,之后每个像素只查表。 代价是视差(一枚探针要服务横跨 2.5 × 2.4 的一整片地面,本页实测平均视差 0.94)—— 换来的是"糊"可以预先做掉。
roughColor32 的正文,逐字未改):
先按法线算出那一条反射方向,再按粗糙度选一层、只查一次。右列是它交出来的两个数:
不糊会有多错、以及选对层级之后还剩多少错。
null,
盒子会整块"消失"而画面照样有内容、门禁也照样绿 —— 所以这一条写成了判据③里的哨兵;
④ 全景图拍的是整个球(真探针就是拍整个球),所以"地平线"处没有突变;但反射瓣只统计朝上的样本
(地面只接从上面来的光)—— α 一大就有一部分样本落到水平面以下被切掉(判据⑤报了这个数),那属于"又弹回地面",本段不追;
⑤ 多采样那一路用的是分层采样(不是随机),所以左边那块几乎是干净的 —— 随机采样会带来噪声,那是第 4 课的事。
① 先看左块(多采样,N = 6 × 6 = 36 条线):α 小时地面是锐利的倒影;把 α 拖大, 倒影糊成一片。这一块是"老实做法",代价是每像素 36 条线。
② 看中块(一条线 + 预滤波):它跟着一起糊,而且几乎与左块一样 —— 可它每像素只查了一条。右列两个读数就是这笔交易的价钱:不糊硬查那条线,误差有多大; 选对层级之后,误差还剩多少。
③ 把 α 一档一档拖过去:右块会在跨过某一档时轻轻跳一下(因为层号是整数)—— 这就是 mip 那个老现象:层内误差随 α 偏离而增大,一跨层就跳回去。
④ 看件三表②:它把"每个 α 该选哪一层"直接摊开(每个 α 一行,六层各一个误差, 最小的那个加粗)。该选哪层不是猜的,是量出来的 —— 表里那一条斜线就是"越粗糙、层越糊"。
⑤ 把「粗糙度」拖回 0:三块逐像素相同(判据①)—— α = 0 时那"一片"缩成了一个方向, 一条线又够了。
左:按分布抽 36 条线求平均(参照)| 中:一条线 + 预滤波 | 右:两者之差(灰阶)
启动中…
α = 0 是完美镜(一条线就够); α = 1 时反射方向糊开一大片,锐利的倒影全糊掉。
控件与上一段只多不少:天光、太阳方位、眼位、盒子数逐字相同 (上一段的"镜面波纹 κ"在本段固定为 0 —— 地面是平的,这一段只问"糊"), 另加本段的新变量(粗糙度 α)与一个用来"故意选错一层"的开关。
定点观察(三步):① 看左块地面里那片倒影; ② 把 α 从 0 拖到 1,看它糊成一片; ③ 再看中块 —— 它也糊了,而且只花了一条线。结论:"糊"可以预算好, 不必每个像素多抽几条线;要糊到哪一层,按粗糙度选(件三表②那一条斜线)。
| α | 只用一条线(不糊) | 选对层级后 | 误差压到 1/N | 层号 | 错一层最少涨 |
|---|
| α | 层 0 | 层 1 | 层 2 | 层 3 | 层 4 | 层 5 |
|---|
∞ = 选错就完全不对)。
α = 0 那一行前两列都是 0(判据①)。
这一段把"反射"从一条线变成一片方向,并量出了它的价钱。
null,全景图里盒子整块消失,
而画面照样有内容、门禁也照样绿(这类错只能靠"量全景图里各拍到多少"抓出来);
第二版放在地面上、盒子外面,但离得太远 —— 反射全是天,"糊不糊"几乎没区别;
最后选中 (0.60, 0.40):反射方向里有 44% 打到盒子(既有结构、又不是一堵墙)。
而它的价钱也照实报:探针要服务的地面点平均在 0.94 开外,跟 3.1 那条精确反射线逐格比平均差 0.20
(判据①)—— 这就是"从逐像素追线退到查一枚探针"换来的东西。3.1 的镜子和 3.2 的粗糙镜子,都是"光撞到东西之后再走一步"。 3.1 甚至把"再走一步"数清楚了:有 5.8% 的格子反射方向指回地面 —— 一次反弹没有第二段可追,那一份光就被扔了。可真实的房间呢?光撞到墙、墙再撞到墙, 同一份光可以走很多步。
更要紧的是:被扔掉的往往不是"边角料"。房间里最暗的角落恰恰是被挡得最多的地方 ——
它看不见天、也常常晒不到太阳,可它的四面八方都是墙,而墙自己也在发亮。
"一次"只把光从邻居搬一趟;现实里这一趟要走很多次,而每多走一次,暗角落就再亮一点。
本段只加一样东西:间接反弹次数 N。
装置也随之换了一处(如实声明):地面与盒子从 3.1 / 3.2 的镜面换成白墙(漫反射,ρ = 0.85)
—— 镜子把光原样弹走、不留一点"多次"的证据;白墙收到光会自己再发出去,"多次"这才有意义。
做法也彻底换了:把场景切成一片片面片,算出每片"看得见谁"(形状因子),
然后把"发出去的"接回"收进来的",一遍收集就是多一次反弹 —— 旋钮 N 就是收集的遍数。
保真度如实标「示意 · 等价重现」:面片常数辐射度、方向是分层抽出来的,两条边界都写在判据里。
bounce33 与 once33 的正文,逐字未改):
每一遍都把"邻居发出来的光"收一遍,再乘 ρ 发出去;跑 N 遍就是"多 N 次反弹"。
右列是它交出来的两个数:"一次"在总账里给的份额、以及"两次及以上"给的份额。
① 先看左块(N = 0,只有直接光):太阳照得到的地方是亮的,影子是纯黑的 —— 那里既没有太阳,也没有任何人给它光。这就是"一次都没弹"的世界。
② 把 N 从 0 拖到 4:中块从影子开始亮起来,而太阳照得到的地方几乎一动不动—— 一眼就能看出"多出来的光落在哪儿"。
③ 盯住右块(两者的差):那层"补回来的光"在贴着盒子脚那一圈最亮 —— 墙脚、两块盒子之间那条缝。开阔的地面上,这一层几乎是空的。
④ 看件三表②:它把"反弹在总账里占多少"直接摊开 —— 缝隙带收到的光里只有 17.8% 来自"一次",另有 24.4% 要两次以上才到得了; 而离墙远的那条带,这两个数是 6.9% 与 9.6%,全场面片平均只有 2.5% 与 1.1%。 多弹几次,涨得最狠的正是被挡得最多的地方(缝隙带整体涨 1.73×,全场只有 1.04×)。
⑤ 把「天光强度」拖到 0(只剩太阳):影子在 N = 0 时是 0,N ≥ 1 才有值 —— 因为这时"被挡掉的方向"上不再有天,只剩墙;而墙的那份光,正是"一次反弹漏掉"的那份。
左:只有直接光(N = 0)| 中:跑 N 遍 | 右:两者之差(灰阶)
启动中…
N = 0 是只有直接光(影子纯黑); N = 4 之后画面基本不动了(判据④给出了"每多一遍只剩多少")。
控件与上一段只多不少:天光、太阳方位、眼位、盒子数逐字相同。 上一段那两个开关(粗糙度 α、层号差一档)在本段不再有意义 —— 因为装置换成了白墙,"镜面"这件事整段都不见了;本段唯一的变量是那个新旋钮:间接反弹次数 N。
定点观察(三步):① 看右块(差)里最亮的那一圈; ② 把 N 从 1 拖到 4,看那一圈怎么一点点被填平; ③ 再把天光拖到 0 —— 影子在 N = 0 时是 0,N ≥ 1 才有值。 结论:"一次"漏掉的光,几乎全落在被挡得最多的墙脚与缝里, 而那里恰恰是 AO 判它最暗的地方。
ρ·E/π 比过:逐位相同。
| N | 全场 | 缝隙带 | 缝隙带涨了 |
|---|
| 带 | 天可见度 | 一次给的 | 两次及以上给的 | 多弹涨了 |
|---|
| 由 N 到 N+1 | Δ 平均 | 与上一遍之比 |
|---|
这一段把"一次反弹"与"多次反弹"并排,量出了一次到底漏掉了什么。
bounce33 里那次循环。本段的活例由本页自己计算(示意 · 等价重现,非硬件产出):不依赖 WebGPU,任何设备都能跑。同一页的示意图与数值表照常可读。
页面版本 v3(课题四第 3 课「反弹一次」:三段全部上线 ——
3.1 镜面反射 / 3.2 粗糙反射 / 3.3 一次反弹漏掉了什么,本课收束)。
3.1 这一段只加一样东西:镜面波纹 κ。世界是前两课那个世界,但第一次给它装上相机与镜子:
地面成了一面镜面,每个像素的主射线打到地面上要按法线反射,再往前追一次就停。
量出来的四件事:
① 平镜时"搬相机"是恒等式:κ = 0 时"按法线反射"与"把眼睛搬到地板下面(E′)"两条路
在整块画面上逐像素相同(差 0 格),方向夹角平均 0.0000°;
② 弯了就分开,而且分开的量与 κ 成正比:κ 从 0 扫到 0.30,平均色差 0 → 0.0228 → 0.0446 → 0.0679 → 0.0892
("色差 ÷ κ"是一个常数,最大偏离 5% 以内),而两条路的方向夹角正好是法线倾角的 2 倍
(判据⑤实测比值 1.997)—— 所以"镜面反射 = 镜像相机"只能用在平面镜上;
③ "一次"是可以数的:反射线只测盒子与天、从不测地面 ——
所以"镜子里看不到镜子里的镜子"不是形容词,是个数得出来的事实(反射线穿到地面以下的条数恒为 0);
而这条边界本身也会自己露头:κ 拖到最大时有约 5% 的格子"反射方向指回地面"(一次反弹没有第二段可追,
本页按"映到天"处理 —— 那个数判据⑥也照实报出来了);
④ 镜面只搬光、不发光:把天光关掉,镜子里的天黑了,可盒子的倒影还在。
判据八条:① 零位(κ = 0 时两条路逐像素相同)/ ② 恒等式(逐点验 P − E′ ∥ r₁)/
③ 画面(写入格子数、主射线数、非法值 0)/ ④ 两边路分开且随 κ 单调增/
⑤ 夹角 = 2 × 法线倾角(实测比值 1.997,可笔算一例)/ ⑥ 一次反弹(地面从不出现在反射线里,并照实报出边界)/
⑦ 同源(件一框里那几行就是 mirrorDir31 的正文,重算逐位相同)/
⑧ 天光关掉后镜子里的天也黑(镜面不发光)。
3.2 这一段只加一样东西:粗糙度 α。3.1 的镜子是完美的(一条反射线就够),
真实材质要糊开一片;本段把"反射线上打到什么"从逐像素追线换成一枚预先烤好的反射探针
(128 × 64 的全景图,从地面上一个固定点拍出去),然后把它逐层糊成一条链、按 α 选一层,
像素那边永远只查一条线。量出来的四件事:
① α = 0 时"一条线"与"36 条线的答案"逐格相同(那一片缩成一个方向,是恒等式);
而同时它和 3.1 那条精确反射线并不逐位相同 —— 平均差 0.20,那就是"改用一枚探针"的价钱
(探针要服务的地面点平均在 0.94 开外,视差如实报出);
② α 一大,"一条线"就分开,而且单调变坏:只用一条线的平均色差 0 → 0.0302 → 0.0840 → 0.1329 → 0.1775 → 0.2192
(α 从 0.2 到 1.0 涨 7.3 倍);
③ 求平均可以预先做:把环境糊成一条链(总存储 ×4/3 = 1.3330),按 α 选一层后误差压到
1/1.69 ~ 1/7.74(随 α 单调变大),而多采样是每像素 ×36;
④ "该选哪层"是量出来的:α 从 0 走到 1,层号 0 → 3 → 4 → 5,每个 α 的"误差 vs 层号"都是
单谷的;选偏一层最少涨 1.19×,α = 1 时涨 3.79×(α = 0 时选错就完全不对)。
判据八条:① 零位 + 探针的价钱/ ② 单条线误差随 α 单调增/ ③ 画面(含"探针不在盒子里""反射方向全朝上"两条哨兵)/
④ 层号单调加深、误差-层单谷、选错一层有代价/ ⑤ 预滤波把误差压下来且随 α 单调变大/ ⑥ 存储 ×4/3 vs 每像素 ×36(可笔算)/
⑦ 同源(件一框里那几行 = roughColor32 的正文,重算逐位相同)/ ⑧ 探针只搬光、不发光。
3.3 这一段只加一样东西:间接反弹次数 N。装置换了一处(如实声明):地面与盒子从"镜面"换成
白墙(漫反射 ρ = 0.85)—— 镜面把光原样弹走、不留一点"多次"的证据;白墙收到光会自己再发出去,
"多次"才有意义。做法是把场景切成 2876 个面片、算出每片"看得见谁"(形状因子,每片 24 条方向),
然后把"发出去的"接回"收进来的"——一遍收集就是多一次反弹。量出来的四件事:
① N = 0 就是"每个面片的出射 = 直接光 × ρ"(对 2876 × 3 个通道逐位与解析式相同);
② "一次"漏掉的光几乎全落在影子里:多弹几次,缝隙带整体涨 1.73×,而全场面片平均只涨 1.04×;
③ 反弹在总账里的份额:缝隙带收到的光里 17.8% 来自"一次"、另有 24.4% 要两次以上才到得了;
离墙远的那条带是 6.9% / 9.6%,全场面片平均只有 2.5% / 1.1%;
④ AO 错在哪,被量化了:AO 把"被挡掉的那 46.5% 个方向"当成没有光,于是说缝隙只有外圈的
0.661 倍亮 —— 而真实收敛值是 0.963 倍,判暗了 0.30(相对 46%)。
判据八条:① 零位(N = 0 = 直接光 × ρ)/ ② "一次"让影子不再等于直接光/
③ 画面(含"打到东西的格数""两遍逐字节相同")/ ④ 一遍比一遍小且都在上一遍的 0.36 以下/
⑤ ★ AO 与真实的对比(主判据)/ ⑥ 白炉(环境处处 L = 1、ρ = 1 ⇒ 恒等于 1,可笔算)/
⑦ 同源(件一框里那几行 = bounce33/once33 的正文,重算逐位相同)/
⑧ 抽样误差怎么落:本页"分层 + 低差异"≈ 1/M(实测 1/4.15、1/3.28),
伪随机才是 1/√M(1/1.96、1/1.80)—— 下一课那句 1/√N 就落在这里。
★ 与前两课共享同一个场景(同一组盒子坐标与高度、同一个太阳高度角与方位约定)——
所以"这是同一个世界,只是第一次从别处看它"这句话是能对上的。
★ 如实标:① 本页是 CPU 光线投射器(逐像素自算),不依赖 WebGPU;② κ 只改地面法线、
不改地面形状(几何仍是 z = 0)—— 这是"水面"的模型;③ 反射线只追一次,不递归;
④ 3.2 的反射瓣只统计朝上的样本(地面只接从上面来的光),α = 1 时约 34% 的样本被水平面切掉;
⑤ 3.2 的全景图只有 128 × 64,最糊那层是 4 × 2;
⑥ 3.3 的每个面片只有一个数(常数辐射度),所以窄缝里的渐变是分段的,而且只算到"撞到东西"为止。
| 看不到新内容时请硬刷新(Ctrl+Shift+R),或给地址加 ?t=N —— 本站的 HTML 不带缓存指令。