着色不是「给表面涂颜色」,而是解一个积分:对每个着色点,把所有入射方向的贡献按 BRDF 加权累加。 PBR 的全部内容就是——怎么把这一个积分算得既接近物理、又跑得动。 这个课题沿着一代代模型走:每一代都是对同一个积分更好的一次近似。
本节点在骨架的下一环 —— 上一课的收尾把「经验模型的反射预算越过了 1」摆在了画面上,却把两件事留着没答:它为什么会越过,以及该拿什么挡住它。
上一课只把问题摆了出来:漫反射与高光各拿一个系数(k_d = 0.86、k_s = 0.60),
两个系数各自独立、各自也没有上界,于是它们的和能到 1.46。在一块不发光的表面上,
反射出去的光多于照进来的,这不可能。这一课换掉的就是那条松掉的约束 ——
不再让两个系数各自为政,而是把它们看成同一份预算(预算记账):k_d + k_s = 1
—— 注意漫反射那一份写进积分时要折成 α/π(下面每一处算式都能看见那个 1/π)。
预算只有一份,谁多拿一点另一个就少拿一点,两项之和被算式本身压住。
但记账不是守恒:它只说两个系数怎么分,没说分给谁之后瓣能不能发得完 ——
真判据是白炉 ∫ f_r·cosθ dω ≤ 1:件三 ③ 的「白炉」那一列就是它,本段每一档都还大于 1。
本段只增加一样东西:能量守恒这条约束(把 k_s 与 k_d 绑成 k_d = 1 − k_s 的一份预算),
于是面板里多出一根「镜面占比 k_s」滑块、而 k_d 由它反着定。
光照项、光源方向、粗糙度 σ、高光怎么算,四组控件与上一课收尾完全相同。
k_s 从滑块来、k_d = 预算 − k_s ——
两个系数不再是两个独立旋钮,而是同一份预算的两份,所以右列第三个「合计」
不管 k_s 怎么拖,都停在 1.000。
右列三个数里,第一个是 k_d、第二个是 k_s、第三个是它们的合计。
前两个此消彼长,第三个怎么动都停在 1.000 —— 这不是四舍五入的巧合:
k_d 是「预算减 k_s」算出来的,算式里就没有让它俩相加超过预算的余地。
先只拖「镜面占比 k_s」。它是本段唯一新增的东西:k_d 会被它反着抽走
(k_d = 1 − k_s)。把 k_s 从 0.60 推到 0.90,漫反射那一份变薄、正对光处暗下来,
可整个球不会泛白 —— 两项之和始终是一份预算。
再把「粗糙度 σ」推到 0.8 以上。σ 一高,高光瓣被铺开、正好落在「又亮又有高光」那一片上。 上一课在那里会越界;本段画面右下那行读数的末段会同时给出两个数: 「越界(本段)」纹丝不动地停在 0,「越界(上一课)」给出确切个数。
切到「高光关」,两项只剩漫反射:ρ = (1−k_s)·n·l·括号,远在 1 以下 ——
本段要看的不是这一档。
先拖「粗糙度 σ」到 0.90:注意看画面右下那一行读数 —— 「越界(本段) 0 / 越界(上一课) 约 0.8 万」。
再拖「镜面占比 k_s」到 0.90:越界(上一课) 更大(约 2.6 万),而越界(本段) 仍是 0。
末段那个对照数数的是同一画面、同一根 k_s,只把漫反射系数从 1 − k_s 换回上一课的
0.86 —— 也就是说,越界不是高光变亮造成的,是漫反射那一份没被抽走造成的。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| σ | n(σ) | 括号 | ρ 本段 | ρ 独立 | 白炉 | 越界 |
|---|
n(σ) = 2/σ² − 2(夹在 0.5–512)、
括号 = A + B·s/t,其中 A = 1 − 0.5σ²/(σ²+0.33)、B = 0.45σ²/(σ²+0.09);
s/t = 0.0893 由几何定死(就是 ② 的 s、t 两行),σ 改不动它。
两列预算只差一个系数:ρ 本段 = (1−k_s)·n·l·括号 + k_s·(n·h)^n·n·l、
ρ 独立 = 0.86·n·l·括号 + k_s·(n·h)^n·n·l;
n·l = 0.9440、n·h = 0.7509 都是几何常数。ρ 不含环境光。
两项的高光/漫反射都带出射余弦 max(0, n·l)(高光这一份与 2.4 的 PBR 列同一口径)。
默认光源方向、σ 扫过 0–1 时,③ 那一列(探针)ρ 本段的最大值是 0.748(从不过 1);
全屏所有像素的最大值是 0.826(σ = 0.02 处),同样从不过 1
—— 给高光补上余弦之前这两个数是 0.777 / 0.9301(页面原来写的 0.928 是错的,它只对应 σ≈0.05 一带)。
而 ρ 独立在 σ ≥ 0.9 处越过 1。k_d = 1 − k_s 是本段的约束;ρ 独立 那一行只把 k_d 换回上一课的 0.86,
其余一项都不动 —— 它俩的差就是这条约束省下来的量。
知道能量守恒在这条链上可以落成一条很朴素的预算约束:两个系数不再各自独立,
而是同一份预算的两份(k_d + k_s = 1),谁多拿另一个就少拿,两项之和因此天生封在 1 以内 ——
但这是「记账」的恒等式,不是能量守恒:件三 ③ 的「白炉」那一列还大于 1,说明瓣(尤其没归一的高光)
还没把该发的能量发完。记账管系数怎么分,白炉管瓣发不发得完,两件事。
上一课那片泛白的像素在这一档下是 0。也看到它仍然很粗:这份预算怎么分,本段一个字没说
(k_s 是滑块硬给的,还跟视角无关)。真实表面按视角分这份预算,那是下一段要换的约束。
本节点在骨架的下一环 —— 上一段把两个系数绑成了同一份预算(k_d + k_s = 1),可它把「这份预算怎么分」整个留给了读者手里那根滑块。
上一段只解决了「两项之和不能超过入射」:k_s 由滑块给、k_d = 1 − k_s 反着定,
于是两项怎么加都封在 1 以内。可它的分法是死的 —— 不管光从哪来、你站在哪儿看,
镜面那一份永远停在上一次拖到的那个数上。真实表面不是这样:你越斜着看它,它越像一面镜子。
正对着看(视线几乎垂直打在表面上、cosθ 接近 1),玻璃几乎透明,只反射几个百分点;
快贴着看时(cosθ 逼近 0),连水面都能照出倒影。
这个「随角度变的反射率」就是菲涅尔反射率 F,它的自变量正是
视线与法线的夹角余弦 cosθ。
本段只增加一样东西:把 k_s 从滑块硬给、换成由视角算出的菲涅尔反射率 F(掠射角下高光拿得更多),
k_d 仍是「预算 − F」。
光照项、光源方向、粗糙度 σ、高光怎么算四组控件与上一段完全相同;
上一段那根「镜面占比 k_s」滑块撤掉了 —— 它不再是旋钮,而是算出来的量。
k_s 不再是一根滑块,而是 F0 + (1−F0)·(1−cosθ)^5,
而 k_d 仍是「预算 − k_s」——视角一进式子,这份预算的分法就跟着视角走:
cosθ 越小(越掠射),F 越大、k_d 越小。
那个余弦写的是 clamp(n·v, 0, 1):v(eyeDir)指向眼睛,
球朝向观察者的这一面上 n·v > 0,它就是「视线与法线的夹角余弦」。
拖画面就是在改它,右列三个数会一起动。
右列三个数是一条链:cosθ 是输入,F(= k_s) 是它的函数,k_d = 1 − F 是剩下的。
在这颗球看得见的那一半上,cosθ 从球心的 1 一路走到贴边轮廓的 ≈0.26
(相机离球心约 3.89 个球半径,轮廓处的法线离视线约 75°):
cosθ = 1 那点 F 只有 0.040,预算几乎全归漫反射;贴边那一圈 F 爬到 ≈0.26,
高光拿走的份额翻了约六倍、k_d 相应从 0.96 掉到约 0.75。
中间不是直线:(1 − cosθ)^5 前段几乎不动、末段陡起,所以这份变化压在贴边的一圈上,不是铺满半个球——
件二的绘制读数里,F ≥ 0.2 的像素只占球的约 0.25%,全落在轮廓带上。
真掠射(cosθ → 0、F → 1)要转过轮廓才够得着,那已经在看不见的那半边上。
先拖画面转视角。白圈钉住那一点在球面上不动,但「指向眼睛的方向」在动:
探针的 cosθ(视线与法线的夹角余弦)会随视角变,
它一变小(越往贴边走),F 就越大——可见面上 cosθ 从正对处的 1 走到贴边的 ≈0.26 时,
F 从 0.040 涨到 ≈0.26。默认这一档探针正贴着球的轮廓(白圈几乎落在球的边缘),
cosθ = 0.296、F = 0.206,已经落在这一面最靠掠射的那一端(再往下拖一点就翻过轮廓了);
往上拖把探针转正对视线,cosθ 升到约 0.91,F 立刻掉回 0.040。
这就是本段唯一的变量 —— 预算按菲涅尔分。
再盯画面贴边那一圈(轮廓带)。那里 cosθ 最小、F 最大(≈0.26),
k_d 被抽到约 0.75:漫反射在那一圈薄了约四分之一,
于是那一圈本该暗下去;可高光也只在这份预算到手的地方才兑现 ——
它仍钉在光源的反射方向附近,没有跟着菲涅尔铺满整圈。
预算被算出来了,却还没被送到该去的地方:高光瓣怎么把这份能量铺开,是下一段。
最后拖「光源方向」。F 一动不动 —— 它只认视线与法线,不认光源;
动的是高光的底数 n·h。这也说明本段的变量只有一个:视角。
切到「只环境光」或「高光关」,高光那一份就没有可见的去处,只剩菲涅尔在抽 k_d。
把「粗糙度 σ」推到 0.90,再把画面向上拖:注意看面板里的 cosθ 一点点升起来、
F 随之从 0.206 掉向 0.040;反过来往下拖,探针掠过轮廓、白圈变暗
(那一点翻到了看不见的那半边),可见面上能读到的 cosθ 最小只到 ≈0.26。
全程盯件三 ③ 的 ρ 那一列 —— 它始终在 1 以下。
最后拖「光源方向」绕一圈:F 一动不动,动的是 n·h 与高光那一项。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| pitch | cosθ | F = k_s | k_d | 漫反射 | 高光 | ρ |
|---|
n·v,
再取 cosθ = clamp(n·v, 0, 1)(就是那一列);
然后 F = 0.040 + 0.960·(1 − cosθ)^5、k_d = 1 − F
(Schlick 近似,θ 取视线与法线的夹角,把「越斜看越反射」直接对应到屏幕上)。
漫反射与高光各自是 k_d·n·l·括号 与 k_s·(n·h)^n·n·l
(两项都带出射余弦 max(0, n·l),与上一段同一处),ρ 是两者之和。
只有 cosθ 那一列由视角驱动,其余每个数都从它派生,ρ 整列都在 1 以下 —— 上一段那条预算一寸没松。1 − cosθ 与它的五次方是本段新增的中间量:菲涅尔不是线性的,
它就是靠这个五次方把这份变化压成贴边的一圈。默认这一档探针贴在最外圈,
n·v ≈ +0.296、cosθ = 0.296、F = 0.206、k_d = 0.794;
把画面往上拖把它转正对视线,就落进 ③ 上半段那些行。
本段不动高光瓣:(n·h)^n 还是上一段那个没归一的样子。
知道一份预算的「分法」本身也是一个物理量:镜面那一份不是常数,而是视角的函数 ——
正对视线那一点(cosθ = 1)拿到 F0(本例 0.040,几乎全是漫反射),
越往贴边走 cosθ 越小、F 越大:这颗球看得见的那一半上 F 最多到 ≈0.26,高光拿走的份额翻了约六倍
(真掠射 cosθ → 0、F → 1 落在看不见的那半边,凸球面看不见)。
菲涅尔让「你站在哪儿」第一次直接决定能量怎么切,而 k_d + k_s = 1 那条约束原样保留
(实测 ρ 整列都没过 1)。
也看到这一步只改了「谁拿多少」:那份划给高光的预算还没被送到该去的地方 ——
贴边那一圈漫反射被抽薄、本该暗下去,而高光瓣仍钉在光源的反射方向上、没有跟着铺满整圈。
那份能量怎么随高光瓣铺开,是下一段要换的约束。
本节点在骨架的最后一环 —— 上一段把一份预算按菲涅尔分给了两项(高光拿 k_s = F),可它把「高光那一份拿到手之后怎么发出去」留着没答:高光瓣还是 3.1 那个 (n·h)^n,从头到尾没做过归一。
上一段只解决了「谁拿多少」:k_s 由视角定、k_d = 1 − k_s 反着定,两项之和封在 1 以内。
可这份划给高光的预算,是直接乘在瓣上发出去的,而那个瓣从来没被归一过:
(n·h)^n 在正中心(n·h = 1)恒等于 1,与粗糙度毫无关系。
粗糙度一推高,n 变小、瓣被铺开 —— 峰值一动不动,面积却变大,总量就跟着涨。
要归一,先得答一个上一段没问过的问题:这个「总量」到底是对哪个 dω 积的。
页面原来写 ∫(n·h)^n dω = 2π/(n+1) —— 那个 dω 是半程向量自己的半球 dω_h;
白炉要守的却是 对入射方向 dω_l 的积分,两者差一个 Jacobian(法向附近 ≈ 4):
n = 6 时真值是 2.947,不是 0.898。分母认错,因子就归错了地方。
本段只增加一样东西:把归一因子的分母换成你要守的那个积分 —— 取工业标准 C = (n+8)/(8π) ——
乘上它,瓣在白炉下就钉在 1 附近:粗糙度只决定「摊多开」,不决定「发多少」。
光照项、光源方向、粗糙度 σ、高光怎么算四组控件与上一段完全相同;
另加一档「高光瓣 未归一 / 已归一」开关,好把两种口径摆在同一颗球上对比。
u.sp.y(就是 n)
先折成归一因子 cn = (n+8)/(8π),开关打开时 wgt = cn,瓣 (n·h)^n 乘上它才发出去。
注意第一行不是 2π/(n+1) —— 那是对另一个 dω(半程向量的半球)积的:
白炉要守的是瓣带着出射余弦、对入射方向积出来的 ∫(n·h)^n·cosθ dω_l;
未归一那一档发出去的就是它,σ 一拖它就漂(0.69 → 3.00 —— 窄瓣这头是漏、宽瓣那头是爆),
乘上因子后回到 1.01–1.07(工业标准,法向附近误差 ≤ 7%;要精确 1,因子该取这个积分自己的倒数)。
右列三个数是同一条链的两端:未归一是「不乘因子时瓣发出去多少」
(∫(n·h)^n·cosθ dω_l),归一因子是补上的那一份,已归一是两者相乘。
σ 从 0.25 推到 0.90:前一个从 0.69 漂到 3.00(宽瓣那头是爆、窄瓣那头的 0.69 反而是漏),
因子从 1.51 掉到 0.34,第三个却在 1.01–1.07 之间几乎不动。
这不是巧合,也不是「2π/(n+1) 与它互为倒数」那种同义反复 ——
是分母认对了(对 dω_l 的那个白炉积分),总量才守得住。
先把「粗糙度 σ」推到 0.90。瓣这时被铺得很开(半角从 27° 张到 75.5°)。 盯球面那条高光带:已归一这一档,它又宽又暗;再切到未归一,同一条带子立刻亮一大截。 右下读数跟着跳:未归一 3.00、已归一 1.01 —— 未归一时瓣的峰值本来就与粗糙度无关(恒等于 1),是铺开之后总量发到了白炉的 3 倍在亮。
再盯右下读数里的「白炉」。它给的就是「这一刻的瓣发出去多少」。未归一那一档, σ 越大它越大(0.25 时 0.69、0.5 时 1.93、0.9 时 3.00);切回已归一,它几乎不动(1.01–1.07)。 这行数不是屏幕上数出来的,是把瓣带着出射余弦对入射方向积出来的(与件三 ③ 同源)—— 它说的正是画面里正在发生的事:宽了就摊薄,总量不该跟着涨。
把 σ 拖回 0.5 附近再看。两档的差在这里最小 —— 那里 n ≈ 6,白炉两档分别是
1.93 与 1.07,因子 0.56。因子在瓣窄(σ 小)时大于 1、在瓣宽(σ 大)时远小于 1,
两头差别大、中间小;σ 超过 0.894 后 n 被夹在下限 0.5,白炉封顶在 1.01 不再降。
最后拖「光源方向」与「高光怎么算」。它们只挪动高光带的位置与形状
(n·h 变、瓣的朝向变),碰不到瓣宽 n:归一因子与白炉读数一动不动。
这也说明本段的变量只有一个 —— 粗糙度定瓣宽,归一守住总量。
先把「粗糙度 σ」推到 0.90,再在「本段新增」那一档来回切:注意看球面那条高光带, 未归一时又宽又亮、已归一时同样宽却暗下去;而右下读数里的「白炉」, 未归一时是 3.00、已归一时是 1.01 —— 切来切去只有这一个数在跳。 再拖「光源方向」绕一圈:高光带跟着走,可白炉两档都纹丝不动 —— 归一守住的是总量,不是某一处的亮度。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| σ | n(σ) | θ½ | 归一因子 C | ∫(n·h)n·cosθ dωl | 白炉 = C × 左列 |
|---|
n(σ) = 2/σ² − 2(夹在 0.5–512);
瓣半角 θ½ = arccos(0.5^(1/n)),就是 (n·h)^n 掉到一半处的夹角(「铺多开」);
归一因子 C = (n+8)/(8π)(工业标准 —— 本段新取的,不再是 (n+1)/(2π));
它乘的是「瓣带出射余弦、对入射方向积」出来的那一列 ∫(n·h)^n·cosθ dω_l,
乘完就是最后一列的白炉。σ 从 0.25 推到 0.90,未归一那一列从 0.694 涨到 2.996
(宽瓣多发四倍多),白炉那一列却守在 1.01–1.07(工业标准法向附近 ≤7%;
要精确 1,因子该取这个积分自己的倒数)。σ ≥ 0.894 之后 n 被夹到下限 0.5,两列都封顶不动。dω:
2π/(n+1) 对半程向量的半球 dω_h(页面原来误拿它当分母)、
∫(n·h)^n dω_l 才是对入射方向的真值(n = 6 时 2.947,不是 0.898)、
∫(n·h)^n·cosθ dω_l 是白炉真正要守的那个;
而 n 是当前 σ 的瓣宽、C 是同一行的归一因子、θ½ 是同一行的半角。
「高光项」两行分别是 k_s·(n·h)^n·n·l 与 k_s·(n·h)^n·C·n·l ——
只差一个 C(两项都带出射余弦 max(0, n·l)),所以同一个 n·h 上能直接看出因子的大小。
n·h、k_s 都随视角变,拖画面时它们会动,瓣宽 n 只随 σ 动。
本段不动别的:括号、n·l、
菲涅尔那一段与上一段逐字相同。
知道粗糙度对高光的作用到这里才完整:它只改瓣的形状,不改瓣的总量 ——
前提是把瓣对着你要守的那个积分归一。
未归一时 (n·h)^n 的峰值恒为 1(与粗糙度无关),瓣一铺开,发出去的总量就跟着漂:
白炉口径下 σ 从 0.25 推到 0.90,它从 0.694 漂到 2.996 —— 窄瓣那头是漏、宽瓣那头是爆,
都是对不上的能量。补一个因子把分母认对,白炉就守在 1.01–1.07;
宽了就摊薄、窄了就聚亮,总量一寸不动 —— 能量只是从中心搬到了铺开的部分。
(顺带认清「瓣的总量」这个说法得有主:2π/(n+1) 是对半程向量那个 dω_h 的积分,
白炉要守的是对入射方向 dω_l 的积分,两者差一个 Jacobian。)
也看到这一步仍很粗:它只算「有光打过来时这一点怎么反射」,从没问过「这一点看不看得见光源」——
一颗被挡住一半的球,和一颗完全没被挡的球,在这个算式里长得一模一样。
光照积分里那个「有没有东西挡在中间」的可见性,是下一课要加的东西。
跑不起来先看设备要求:活例由真 GPU 出画面 —— 手机需 Android 12+ 且 Chrome 121+,或 iOS / iPadOS 26+;电脑需 Chrome / Edge 113+ 或 Safari 26+。Android 上取不到适配器多是设备不支持 Vulkan 1.1+,会自动改走兼容模式(需 Chrome 146+)。不满足时画面区会写出原因与自查步骤,同一页的示意图与数值表照常可读。