一个着色点接收到的光,有一部分来自别的物体反射的光;而别的物体,又接收到它反射的光。 要算清这一点,只能在场景里采样 —— 而一旦靠采样,就永远受 1/√N 支配。
这条骨架把 GI 收成三件事:在哪采样(探针 / 像素 / 路径)、采几次(一次反弹 / 多次反弹)、 怎么排(随机 / 分层 / 蓝噪声)。第 1 课量清了"不采样"的那一头(一次乘法、一次反弹、一屏深度); 本课回答第一件:在哪采样 —— 不在像素上采,改在场景里采:插几根探针,把半球装进几个数。
上一课把三件事各自量到了边界:1.1 把间接光压成一次乘法(常数 × 可见度)、 1.2 让它真的跳一次(要全场景几何)、1.3 又把全场景换成只有一屏深度。 可有一件事从头到尾没动过:每条线都是从像素上发出去的 —— 一个着色点 16 条线,一屏 120 × 84 就是 161280 条。
真实画面里这个数字还要乘上分辨率:一帧几百万个着色点,每个都发几十条线,算不动。 于是退一步想:为什么非得在像素上采? 光照在空间里是平滑的 —— 相邻两个像素看到的天几乎一样。那就别在每个像素上算,改在场景里插几根"天线": 一根探针(probe)就是一个固定位置上的采样点,它把收到的那半个球存下来, 像素只是去查表。 本段只加一样东西:采样点从像素搬到探针。 三件事都是量出来的:① 便宜是按"射线数"算的:探针 8 × 8 = 64 个、每个 16 条方位线 = 1024 条, 而逐像素是 161280 条 —— 便宜 157 倍(判据①);但直接光那一份省不掉: 每个像素还得自己发一条影子线,探针在这件事上帮不上忙(判据⑧)。 ② 代价是"糊":查表出来的光照是插值出来的,误差集中在明暗交界(盒子轮廓)那一圈, 而且随探针加密会收敛(判据②、③)。 ③ 探针不认表面、也不认法线:它只知道"我这个位置收到多少光"—— 这一点在高度场里还看不出来(每个 (x,y) 只有一个面),换成真的三维场景就会变成 2.3 那三种错。 保真度如实标「示意 · 等价重现」:探针值与像素值都由本页按同一条算式算出来,件一框里那几行就是件二真正在跑的。
51 后缀省掉了,其余逐字)。
右列是它换来的两个数:省下多少条线、以及为此多出多少误差。
pv(i, j) 读的那个数,就是在探针位置上跑一次
第 1 课那个 aoAt(对每条方位求地平线角、取 cos²、再平均)。
本段没有换算法,只换了算的地方:从"每个着色点都算"改成"只在 PN² 个点上算,其余靠插值"。
所以探针值得来全不费工夫 —— 它和 1.1 的读数逐位相同(判据⑤)。
① 先看左块(逐像素):它就是 1.1 那张 AO 图,每个像素自己算 —— 这是"算得最准"的那一版, 代价是 161280 条线。
② 看中块(探针查表):整体很像,但盒子轮廓那一圈"糊"了 —— 明暗交界不再是一条边, 而是一段渐变。因为像素拿到的是插值:它把旁边两个数值不同的探针混在一起。 右块(差)把这件事画出来了:亮的地方全在轮廓附近,空地上几乎是黑的(判据③)。
③ 拖「探针密度」4 → 32:轮廓那一圈越来越细,误差一路降(判据②)。 但注意降到 32×32 = 1024 个探针时,射线数已经涨到 16384 条 —— 便宜是有代价的: 探针越密越准,也越贵。这条曲线就是这一类方法的全部取舍。
④ 把「插值」从双线性切成最近邻:中块立刻变成一块一块的格子(每个探针管一片), 误差和最大差都变大,但只大 1.2 倍左右(判据④)。真正管用的还是加密: 换插值方式只买到 0.0066,加密一档买到 0.0104 —— 插值只能抹平,补不出没采到的信息。
⑤ 拖「太阳方位」:阴影转,而右块那块"差"不跟着转 —— 探针值(AO)本来就与光无关(1.1 量过),所以插值误差也是几何账。
⑥ 把「间接光强度」拖到 0:三块逐字节变得一模一样(判据⑧)—— 探针只经手间接光那一项;直接光那一份(阴影)两条路完全一样,探针省不掉它。
左:逐像素(自己算)| 中:探针查表 | 右:两者之差(灰阶满量程 0.25)
启动中…
控件与上一段只多不少:两个滑块与两个开关逐字相同(L_amb、太阳方位、方位数、盒子数),
另加探针的两个开关:密度(探针插多密)与插值(查表怎么读)。它们加起来只是"在哪采、怎么读",
不是新概念:本段只加了一样东西,就是采样点从像素搬到了探针。
定点观察(三步):① 看中块盒子轮廓那一圈,与左块同一位置比 —— 那边糊成了渐变; ② 把探针密度从 8 拖到 32:那一圈变细、误差下降(判据②),但右列的射线数同时在涨; ③ 把插值切成最近邻:中块变成一块一块的格子,误差只大 1.2 倍 —— 结论:真正管用的是加密(换插值只买到 0.0066,加密一档买到 0.0104); 插值只能把已知的抹平,补不出没采到的信息。
| 位置 | 逐像素(真值) | 探针查表 | 差 | 离最近探针的距离 |
|---|
| 插值 | 4×4 | 8×8 | 16×16 | 32×32 |
|---|
aoAt,查表 = 四个探针值按 fx, fy 加权。
这一段换了采样点在哪:从"每个像素自己采"换成"在场景里插几根探针,像素去查表"。
2.1 的探针很便宜,但它把整张环境图压成了一个数:"这一点平均收到多少光"。 方向在那次积分里被丢掉了:两个探针可以读出完全相同的数,一个背后是一盏小灯, 另一个背后是一整片天光。
一个数只能喂给漫反射 —— 它就是"总量",没有方向可用;要做高光、要把环境转起来、 要让同一个探针服务各个朝向的表面,就得把方向也存下来。 可又不能把每个方向都存一遍(那又回到 12800 个数)。办法是: 用一族固定的形状函数去逼近整张环境图,只存这些形状的系数—— 这族形状就是球面上的谐波(球谐,SH):阶数越高,形状越细。 本段只加一样东西:把探针里那个数换成几个数(球谐系数)。 四件量出来的事:① 9 个数确实装下了整张图:12800 个方向压成 L=2 的 9 个系数,压缩 1422 倍(判据①)—— 但话要说实:重建出来的那张图并不像,平均差 0.17,而这张图上的天光本身才 0.66; ② 可它真正要拿去做的那件事却算得很准:同一份 9 个系数拿去算漫反射,相对误差最大只有 1.4%(判据⑤)—— "重建得像不像"与"光算得对不对"是两件事,这是整段的骨头; ③ 它的代价压在"又小又亮"上:太阳(6° 的盘)的峰值只留下 11%,加到 L=4 也只有 15%(判据③)—— 球谐装得下"平滑",装不下一个点,这是它的适用范围,不是 bug; ④ 旋转在系数空间里做:把环境绕极轴转一个角度,系数只需做几次 2×2 旋转, 与"把图转过去再重新投影"一致到 浮点误差(判据④)—— 这是 SH 在实际工程里被选中的真正理由。 保真度如实标「示意 · 等价重现」:环境图与球谐都按定义在本页算出来,件一框里那几行就是件二真正在跑的。
shPass52 的正文,逐字未改):
扫过整张图,把每个方向的亮度按形状函数累加到系数(投影);再用这几个系数把图乘回来(重建)。
右列是它交出来的两个数,这两个数就是整段的结论:图重建得并不准、而真正要用的那个积分很准
(上面那个"重建平均差"跟着阶数走;下面那个辐照度误差固定在 L=2,取表②四个方向里最大的那一个
—— 所以它不随阶数变,这正是判据⑤要说的那件事)。
cos(仰角) 加了立体角权重,投影出来是对的);
③ 系数只算到 L=4(25 个),再往上收益很小而代码会长得多;
④ 本段的"探针"只借用了 2.1 的位置概念,不涉及 2.1 的插值;默认站在空地 ——
换成"两盒之间",太阳被盒 A 挡掉,图上就没有那个亮斑了(判据③会自己挑"看得见太阳"的那个探针去量)。
① 先看左块(真值环境图):上面亮的是天,下面黑的是被盒子挡住的方向; 中间那个亮斑是太阳(只有 6° 宽)。注意它有多小、多亮 —— 记住它,它是本段的"反派"。
② 看中块(用 9 个数重建):大块的明暗、地平线的高度都对上了,但太阳不见了 —— 它被糊成了一大片淡淡的亮。右块(差)把这件事画成灰阶:太阳那一块直接画满(差到 7.8,而满量程只有 2.0), 其余地方还压着一层约 0.17 的浅灰 —— 那是九个低阶形状与"一片天光 + 一条硬地平线"之间差出来的余量。 两笔账都在,只是一笔亮得刺眼、一笔淡得几乎看不出。
③ 把「阶数」从 L=2 一路拖到 L=4:地平线慢慢变锐、平均差一路降(判据②报了每一档), 但那个亮斑几乎不动 —— 峰值保留率只从 11% 挪到 15%。 要看清这件事:它永远追不上真值 —— 那是一个 δ 形状,需要无穷阶。球谐的适用面是"平滑的东西"。
④ 把「阶数」调到 L=2 与 L=3:重建的图明显不一样(L=3 更准), 可是右边那个辐照度读数一个数字都不变(判据⑤)—— 因为 cos 那个核的奇数阶系数全是 0。 "重建得准"和"着色用得准"是两件事:我们要的从来不是把图重建出来,而是把那个积分算对。
⑤ 拖「绕极轴旋转」:整张环境图跟着转。旋转是在系数上做的(几次 2×2 旋转), 不是把图转一遍再重新投影 —— 两者结果一致到浮点误差(判据④)。这就是 SH 在工程里被选中的理由: 环境的旋转可以事后做,不用重算。
⑥ 换「探针位置」(默认是空地):移到"两盒之间" —— 地平线抬高、背后一大片被盒子挡住, 最要紧的是那个太阳亮斑整块没了(被盒 A 挡掉)。同一个场景、同一份天光, 探针站哪儿,它就存下哪儿的半球,而"存下来的半球"直接决定这几个系数长什么样。 也正因如此,"低频的代价"那一项只在看得见太阳的位置才量得出来 —— 判据③自己会挑那个位置去量。
左:真值环境图(方位 × 仰角)| 中:用系数重建 | 右:两者之差(灰阶满量程 2.0)
启动中…
控件与上一段只多不少:两个滑块与"盒子数"逐字相同,另加球谐的两个开关 (阶数、绕极轴旋转)与一个探针位置开关 —— 前两个是"存多细"与"怎么转", 最后一个只是"展开哪儿的半球"。不是新概念:本段只加了一样东西,就是把那个数换成几个数。
定点观察(三步):① 先找到左块里那个小亮斑(太阳), ② 把「阶数」在 L=2 与 L=4 之间来回切,盯住那个亮斑和地平线 —— 亮斑被糊成一片、边缘发毛, ③ 结论:球谐装得下"平滑",装不下"又小又亮";而把阶数从 2 调到 3, 右边那个辐照度读数一动不动(判据⑤:cos 核的奇数阶是 0)—— 说明"重建得准"和"着色用得准"根本不是同一件事。
| 阶数 | 系数个数 | 重建平均差 | 最大差 | 太阳峰值保留 |
|---|
| 方向 | 精确积分 | L=0 | L=1 | L=2 | L=2 相对误差 |
|---|
E(n) = ∫ L(ω) max(0, n·ω) dω。左边是逐方向精确积分(12800 个方向),
右边是用系数算的(E(n) = Σ Âl ck Yk(n),每个方向 25 次乘加以内)。
注意 L=2 那一列与 L=3 完全一样:cos 那个核的奇数阶系数全是 0(Â3 = 0),
所以漫反射只需要偶数阶,而 L=2 已经吃掉了几乎全部。
把两张表并排看就是整段的结论:① 表①里 L=2 的平均差 0.17(图不像),表②里同一个 L=2 的相对误差只有 0.1–1.4%(光很准)
—— 差的不是精度,是我们问的问题。表①里"最大差"那一列可以到 7.8,比平均差大 45 倍:
那笔误差几乎全压在"太阳那几格"上,平均下来就被摊薄了。
这一段把 2.1 丢掉的那一维找回来了:方向。
2.1 与 2.2 讲的都是"探针里存什么"(一个数 → 几个系数)。可探针还有一半没碰: 它插在哪儿。前两段的探针都刚好躺在表面上 —— 每一个探针就在它服务的那个点上, 这是理想化。
真实工程里探针是插在场景里的一个规则格子上:格子是自己铺的,与几何无关; 而表面点只是借用附近探针的半球 —— 并不保证那个半球是"从它自己的位置看出去"的。 于是"探针站在哪儿"本身就成了一个自变量,三种错全从它来: ① 错位:探针不在表面上,它看到的半球就不是那个点该看到的 —— 抬高只会让它看得更多, 所以误差是单边的(只能偏亮); ② 漏光:抬高 d 会让附近每个遮挡物挡住的角度变小;而一旦探针站得比某个遮挡物还高, 那个遮挡物就从它的视野里整个消失 —— 被它挡住的那一块于是变亮。★ 但这条曲线不是台阶: 它随 d 越抬越快地涨,涨到 d = 场景里最高的那个遮挡物时彻底饱和(探针的半球里 一点几何都不剩,AO 恒等于 1);矮的遮挡物先"失效",高的后失效。这不是"越来越糊",是一笔有方向的偏差; ③ 无穷远:探针的半球里只记着"某个方向有多亮",不记那个东西离它多远 —— 所以"让表面点离探针更近"(加密探针)治不了前面两样:站错了地方,站得再密也还是错。 本段只加一样东西:让探针沿表面法线抬离表面 d。(d = 0 就是前两段的做法。) 三件量出来的事见下:判据①给"零位",判据④给"饱和平台",判据⑤给"矮的先失效",判据⑥给"加密治不了"。 保真度如实标「示意 · 等价重现」:高度场、地平线、AO 都按定义在本页算出来,件一框里那几行就是件二真正在跑的。
aoZ53 的正文,逐字未改):唯一的改动是
把观察点从表面抬到表面之上 d,然后照旧对每个方位求地平线角、取 cos² 再平均。
右列是它交出来的两个数:误差有多大、以及有没有格子比 d = 0 那一版更暗(那个数应该是 0)。
① 先看左块与中块(d = 0):两块应该几乎一样 —— 中块就是 2.1 那一版。 判据①量的是这条"零位":d = 0 时中块与 2.1 的探针查表逐字节相同。
② 把「探针抬离表面 d」从 0 慢慢拖到 0.5:盯住盒 A 西侧那条窄带(画面上盒 A 左边的那一小条)。 它本来偏暗,越抬越亮,而且越抬涨得越快;到 d = 0.30(盒 A 的高度)时它等于整片天 —— 再往上拖,画面一个字节都不变了(判据④:d = 0.30 时 64 个探针的值全等于 1; d = 0.31 与 0.40 的画面逐位相同)。★ 顺带看一眼盒 B 旁边那片地:它在 d = 0.16 那阵子就已经漏得差不多了 (判据⑤:漏光进度 91%),而盒 A 旁边那片地到那时才 24% —— 矮的遮挡物先"失效"。
③ 看右块(差):把 d 拖大之后它整块变亮,却不会冒出"比 d = 0 那一版更暗"的格子 (右列那个读数恒为 0,判据③)。★ 这里要分清楚:右块画的是"探针 − 真值", 而真值那一栏在 d = 0 时就带着 2.1 的插值误差(两边都会偏);判据③比的是 "抬高后的那一版"对"不抬高的那一版" —— 那才是一笔干净的、只增不减的账。
④ 把「探针密度」从 8×8 一路拖到 32×32:格子间的过渡变细了、误差也降了一点, 但那条亮带还在 —— 加密只是让"借来的半球"更连续,借的位置并没有变对。 判据⑥把这件事量成一句可核的话:最密的一档下,d = 0.4 的误差仍是 d = 0 的好几倍。
⑤ 把 d 拖回 0,再拖上去:反复几次就能看出这不是"越来越糊" —— 它一直在往同一个方向偏(只偏亮),而且涨到某个高度就停了。停在哪儿由场景里最高的遮挡物决定 (这里是盒 A 的 0.30),跟探针多密、画面多大都无关。
左:逐像素(真值)| 中:抬离表面 d 的探针查表 | 右:两者之差(灰阶满量程 0.5)
启动中…
两个盒子的高度:盒 A 0.30 · 盒 B 0.16 —— d 拖过它们时会各有一个台阶。
控件与上一段只多不少:两个滑块与"盒子数"逐字相同, 另加本段的一个新变量(探针抬离表面 d)与一个从 2.1 原样搬来的开关(探针密度,四档逐字相同)。
定点观察(三步):① 看盒 A 西侧那条窄带; ② 把 d 从 0 拖到 0.5,盯住它 —— 一过 0.30 就整块亮起来; ③ 再把密度拉到 32×32,它还是亮的。结论:漏光来自"站在哪儿",加密治不了它; 而 2.1 量到的"糊"来自"离得多远",加密治得了。这两件事必须分开。
| d | 探针值平均增量 | 画面平均差 | 盒 A 西侧 漏光进度 | 盒 B 东侧 漏光进度 |
|---|
| d | 4×4 | 8×8 | 16×16 | 32×32 |
|---|
L_amb = 0.35 / 太阳 135°),免得读者随手一拖就以为判据错了。
这一段把探针的"下半句"说完了:前两段问它存什么,这一段问它站在哪儿。
本段的活例由本页自己计算(示意 · 等价重现,非硬件产出):不依赖 WebGPU,任何设备都能跑。同一页的示意图与数值表照常可读。
页面版本 v3(v1 = 2.1「在场景里插一根"天线"」· v2 = 2.2「九个系数装下整个半球」·
v3 = 2.3「探针的三种错」;本课三段到此全部上线)。
【2.1】只加一样东西:采样点从像素搬到探针 —— 同一个 aoAt(1.1 那一刀,逐字不改),
从"每个着色点都算"改成"只在 PN² 个点上算",其余像素查表。量到:① 便宜按射线数算
(8×8 = 1024 条 vs 逐像素 161280 条,157.5 倍),而直接光那一份省不掉
(判据⑧:L_amb = 0 ⇒ 两块逐字节相同);② 误差随密度收敛(0.0630 → 0.0359 → 0.0255 → 0.0162);
③ 误差有形状(集中在明暗交界,最大档是平移区的 4.0 倍);④ 插值只能"抹平"不能"补信息"
(换插值只买到 0.0066,而加密一档买到 0.0104)。
【2.2】只加一样东西:把探针里那个数换成几个数(球谐系数)。12800 个方向的环境图 → L=2 的 9 个系数
(压缩 1422 倍)。量到:① "重建得像"和"光算得对"是两件事(重建平均差 0.1714、最大差 7.7687,
而同一份系数算漫反射的相对误差只有 1.42%);② 阶数收益递减(0.3534 → 0.1253);
③ 低频的代价压在"又小又亮"上(太阳的峰值保留率 L=2 只有 11.4%);
④ 旋转在系数空间里做(与"转图再投影"最大差 4.7e-10)。
【2.3 这一段】只加一样东西:让探针沿表面法线抬离表面 d(d = 0 就是前两段的做法 ——
此时中块与 2.1 的探针查表逐字节相同)。探针里存的仍是一个数(AO),这一段问的只有"它站在哪儿":
① 错位只偏亮:抬高只会让探针"看得更多" ⇒ AO 只增不减,"比 d = 0 变暗"的格子恒为 0;
② 漏光随 d 越抬越快,且有一条平台:d 高过场景里最高的遮挡物(盒 A 的 0.30)之后,
探针的半球里再没有任何几何(64 个探针的值全等于 1),画面逐位不再变;
而矮的遮挡物先"失效" —— d = 0.16(=盒 B 的高)时,B 旁边那片地的漏光进度已 91%、A 旁边那片才 24%;
③ 无穷远 ⇒ 加密治不了:最密的一档下,d = 0.40 的误差仍是 d = 0 的好几倍 ——
那笔错不在"隔得远",在"站错了"。
判据八条:① 零位(d = 0 与 2.1 逐位 / 逐字节相同)/ ② 探针只增不减(六档逐个体检)/
③ 画面只增不减(变暗格数 0)/ ④ 饱和平台(d = 0.30 时 64 个探针全为 1;0.31 与 0.40 逐位相同)/
⑤ 矮的遮挡物先失效(d = 0.16 时漏光进度 91% vs 24%)/ ⑥ 加密治不了/
⑦ 画面(写入格子数、非黑占比、非法值 0、探针标记)/
⑧ 同源(件一框里那几行就是 aoZ53 的正文;且 horizonZ53 在 z = 表面高度时
与共享基线的 horizonA 逐位相同,aoZ53(p, 0) 与 aoAt 也逐位相同)。
★ 与第 1 课共享同一张高度场 hgt、同一个 horizonA、同一个 aoAt、同一个 byte41/曝光 ——
四份函数一处不差,所以"探针只是换了算的地方"这句话是能验的。
★ 如实标:① 探针网格是规则的(不是按几何自适应摆的);② 高度场是 2.5D ⇒"沿表面法线抬高"在这里就等于
竖直抬高(法线处处朝上);③ 盒子的一次反弹没有算进环境图(那是 1.2 的事)。
| 看不到新内容时请硬刷新(Ctrl+Shift+R),或给地址加 ?t=N —— 本站的 HTML 不带缓存指令。