着色不是「给表面涂颜色」,而是解一个积分:对每个着色点,把所有入射方向的贡献按 BRDF 加权累加。
PBR 的全部内容就是——怎么把这一个积分算得既接近物理、又跑得动。
这个课题沿着一代代模型走:每一代都是对同一个积分更好的一次近似。
被解的那个积分本体现在写出来:L_o(p→v) = ∫Ω f_r(l,v) · L_i(p,l) · max(0, n·l) · V(p,l) dωl
—— 被积函数是 BRDF × 入射辐亮度 × 入射余弦 × 可见性 四项之积,积分域是法线那一侧的半球。
本课只用点光源与方向光,所以每一段画面上算的都是它在该光源方向上的单点离散。
单位也对一遍:f_r 是 sr⁻¹,乘上 L_i · dω(radiance × sr)才回到 radiance ——
所以漫反射写出来必须带 1/π(f_r = α/π):少了它,白炉 ∫ f_r·cosθ dω 会正好大 π 倍。
本课从这一版起把这一步显式化,每一页的算式里都能看见那个 1/π。
本节点在骨架的第二步 —— 光已经有了形状,接下来是拿它算一个表面。最早的算法只有两项。
最早的一代着色只有两项。一项是环境光:假装环境里到处都是均匀的散射光,没有方向。 另一项是朗伯漫反射:粗糙表面把光均匀散射开,亮度只取决于光与法线的夹角。 两项合起来就能给出一个"有立体感的球"——但这个立体感的来源值得追问一句:它跟观察者有关系吗? 本段只增加一样东西:环境光与漫反射这两项。
v。
纯环境光那一档,球会变成一个没有层次的平盘——因为它的式子跟位置、跟法线都无关。
先在球上拖一拖。整个球在转、明暗区域在屏幕上移动,可件三的 n·l
和漫反射值一个都不动——因为式子里没有观察方向。
再拖光源那两根滑块。这次 n·l 会立刻跟着变。
两件事的效果差别,就是"与视角有关 / 无关"的实操区别。
球上白圈是在监视的那个点。注意看:转视角时,圈会随着表面一起转、甚至转到背面去,但它的 n·l 始终是同一个值(—)——表面点没变,法线就没变,夹角就没变。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| 视角 | n·l | 漫反射 |
|---|
(α/π) · I_l · max(0, n·l)(α = 0.86 是反照率,
而漫反射是 BRDF、单位 sr⁻¹,所以算式里带 1/π)。
知道最早的一代着色由两项组成,而这两项的性质完全不同:环境光没有方向,所以它不产生任何层次;
漫反射有方向(n·l),所以它能塑形——但它的式子里没有观察方向,
因此漫反射与视角无关。记住这一点:后面高光会引入观察方向,而那正是它和漫反射最根本的区别。
本节点在骨架的第二步 —— 上一段把表面当成「已经散得足够匀」,于是只剩夹角 n·l。可「匀到什么程度」本身就是一个量。
上一段只解决了「光有方向,所以有立体感」,可它偷偷假设了表面是理想粗糙的:
光一进来就被打散得彻彻底底,亮度只由夹角 n·l 决定。真实表面不是这样 ——
它的微面朝向有一圈散布,散布多大,就是粗糙度 σ。σ 一进式子,漫反射就多出两个系数:
一个把整体压低(A),一个在「光与眼同一侧」时补一点回来(B)。
本段只增加一样东西:粗糙度 σ(也就是 Oren–Nayar 的 A、B 这两个系数)。
光照项、光源方向、拖视角三样与上一段完全相同,只多一根 σ 滑块。
v 只乘在回射项 B 上——
σ 拖回 0 时 B = 0,它便从式子里消失,式子退回上一段的朗伯。
右列三个数其实是同一个括号的三级读数,而 A 与 B 的走向相反:σ 越大,A 越小(把整体压低)、 B 越大(在回射方向补一点)。可 σ=1 时 B(0.413)还不到 A(0.624)的量级,所以净效果是变暗,不是变亮。
先拖「粗糙度 σ」。它只改那个括号:正对光源那一点的括号就是 A 本身(那里 s = 0), 越往明暗交界走 B 补得越多。于是 σ 推大时,峰值掉得最狠、交界几乎没被压 —— 整片对比度被摊平,球变得又暗又"粉"。
留意 σ=0 这一档:把滑块拖到最左,括号回到 1.000、A = 1、B = 0, 本节完全退回上一段。粗糙度不是"多加一项",它是把朗伯当成一个极限包含进来。
再拖光源那两根滑块。这次变化比 σ 大得多——光源一动,夹角 n·l 与括号同时变。
两件事的差别就在这儿:σ 只动括号,光源动的是夹角。
先在球上拖一拖:n·l 一动不动(它只认表面与光源,和上一段一样;件三 ② 的 n·l 行可以盯着看)。
再拖「粗糙度 σ」:白圈钉住那一点的读数开始掉,而明暗交界的位置一动不动——
σ 不动 n·l,它只动括号。把 σ 拖回 0,读数回到朗伯值。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| σ | A | B | 括号 | 漫反射 |
|---|
A = 1 − 0.5σ²/(σ²+0.33)、B = 0.45σ²/(σ²+0.09)、
括号 = A + B·(s/t),而 s/t = 0.0893 由几何定死(就是 ② 的 s、t 两行),σ 改不动它;
漫反射 = (α/π)·I_l · n·l · 括号 = (0.86/π) × 0.9440 × 括号——
括号整个属于 BRDF(f_r = (α/π)·括号),所以 1/π 在括号外面;
括号这一列作为"相对朗伯的倍数"不受它影响。
括号这一列同时就是「相对朗伯」的倍数——σ=1 那行的 0.661,意思就是只剩朗伯的 66.1%。知道朗伯只是「粗糙度为零」的那一档:粗糙度不改变明暗交界在哪,它改的是一个括号—— 正对光那点乘 A、越往掠射由 B 补回来,于是峰值掉得最狠、交界几乎没掉,漫反射被摊平。 也顺带看到这个模型的一个毛病:σ 一大,漫反射的总能量就少了(σ=1 时正对光那点只剩 62.4%)—— 那是「换一个约束:能量守恒」那一课要修的事。
本节点在骨架的第二步 —— 上一段给漫反射加了粗糙度,可它改来改去只动一个括号,球面上始终是一片缓坡。真实表面还缺一样东西:那道又小又亮的斑。
上一段只解决了「粗糙的漫反射」,可它有一个改不掉的性子:不管站在哪儿看,那片缓坡都一模一样 ——
因为漫反射的式子里只有夹角 n·l,没有观察者。真实表面还有第二种光:镜面高光。
它像一面镜子 —— 只有当你恰好站在光源的反射方向上,那块亮斑才照进你眼里。
所以高光的式子里必须多出一个量:观察方向 v。v 一动,亮斑就在表面上滑动;
这是它和漫反射最根本的区别。算这块亮斑有两种经典写法:Phong 先算出光源的反射向量 r,
再看 r 与 v 有多重合;Blinn-Phong 不去算 r,改用一个夹在光源与视线中间的半程向量 h。
本段只增加一样东西:镜面高光项(它把观察方向 v 引进来),并在 Phong 与 Blinn-Phong 之间切换。
光照项、光源方向、粗糙度 σ 三样与上一段完全相同,只多一组按钮。
n、l,还多了一个 v ——
它就是高光随视角移动的原因;r 是反射向量(Phong 用它),h 是半程向量(Blinn-Phong 用它)。
最后一行里那个 ct 是出射余弦 max(0, n·l)(定义在片元开头),
高光与漫反射共用它 —— 少乘它,高光就没有对应的入射投影。
右列前两个数是两种算法的底数:r·v 与 n·h。同一几何下它们常常差得很远 ——
在默认这条视线下,r·v 只有 0.20 上下,而 n·h 有 0.75。底数被同一个指数一放大,差距就拉成天壤:
一个几乎为 0,另一个还有 0.1 —— 这就是「半程向量把高光加宽」的算术来源。
先拖画面转视角。球上的亮斑会随之滑动 —— 因为它由观察方向 v 决定;
而明暗交界(由 n·l 定)纹丝不动。件三 ③ 里 n·l 那一列可以盯着看。
再切 Phong / Blinn-Phong:同一条视线、同一个光源,两个亮斑的形状不同 ——
Phong 的斑更窄更硬;Blinn-Phong 的斑更宽,而且在掠射角不会整块消失
(Phong 的 r·v 会掉到 0 以下,一取 max(0,·) 就被掐掉)。
切到「高光关」,画面就退回上一段。
先在球上拖一拖:白圈钉住那一点的高光值在变,而它的 n·l 一动不动 ——
v 进了高光的式子,却进不了漫反射的式子。再把高光切到「关」:亮斑消失,画面与上一段逐像素一样。
最后在两档 Phong / Blinn-Phong 之间来回切:注意看亮斑的大小 —— Blinn-Phong 的大一圈。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| pitch | n·l | r·v | n·h | Phong·n·l | Blinn·n·l |
|---|
r·v、n·h),
两列高光各自是它们的 8 次方、再乘出射余弦 cosθ = max(0, n·l)
(高光指数 n = 8,本段固定,不驱动)—— 漫反射与高光都带这个余弦,后面的白炉才平。
pitch 上下扫的时候,n·l 那一列纹丝不动,而 r·v 与 n·h 一路在动 ——
漫反射的底数不认视角,高光的底数认视角:这就是「观察方向进了式子」。r 是反射向量、h 是半程向量;切换模型只换底数,其余一项都不动。
知道高光与漫反射是两路来头不同的光:漫反射由 n·l 定,跟站在哪儿无关;
高光由观察方向 v 定,所以随视角移动 —— 同一个表面点,视角一转,高光值就变。
也看到两种算法的差别落在同一个底数上:Phong 用反射向量、底数 r·v;
Blinn-Phong 用半程向量、底数 n·h。同一几何下 n·h 通常比 r·v 大,
指数一放大,Blinn 的斑更宽;而 Phong 的 r·v 一旦掉到 0 以下就被 max(0,·) 掐掉,
亮斑会在掠射角整块消失。这一课的下一页才回答:这样拼出来的高光,能量对得上吗。
本节点在骨架的第二步 —— 上一段把漫反射与高光拼到一起、给了一个"看起来对"的球,却留了一句话没答:这样拼出来的高光,能量对得上吗。
上一段只解决了"高光随视角移动",可它是把两条来路完全不同的式子直接相加:
漫反射那一项拿 α/π·I_l,高光那一项拿 k_s,两个系数各自独立、谁也没保证"瓣发得完"。
账面把它们加起来是 0.274 + 0.60 = 0.874,看着没越界 ——
但那只是记账:真判据是白炉(把这一点放进均匀环境光,看它出射多少),而经验那一列当场越过 1。
那么"某个条件"到底是哪个条件?
本段只增加一样东西:把画面换成 PBR 并排对比(同光源、同粗糙度),让左边那一代经验模型自己去撞这条线。
光照项、光源方向、粗糙度、高光怎么算,四组控件与上一段完全相同。
右列三个数:前两个是经验模型的漫反射与高光(各自都到不了 1),第三个是它们的和。
把两项写成辐射度量形(漫反射 α/π、高光再乘出射余弦)之后,
账面和只有 0.274 + 0.60 = 0.874 —— 系数相加不再是判据。
真判据是白炉:把这一点放进均匀环境光(L_i ≡ 1、V ≡ 1)看它出射多少 ——
件三 ③ 的「经验白炉」那一列就是它,每一档都远大于 1,而且粗糙度越大越狠。
越界依旧成立,只是判据从"系数相加"换成了"白炉积分"。右边 PBR 那一列同样有上限,
但它被同一份预算绑在 1 以下(见件三)。
先只盯左边。两边的光源、粗糙度、视角是同一套,差别只在着色口径。 默认 σ = 0.5 时左边球面上就已经有一片反射预算越过 1 的像素(画面右下那行读数给出个数,约 0.7 万)—— 那片就是高光瓣与漫反射叠在一处的地方;把 σ 继续推大,越界的面积还会涨(σ = 0.9 时约 0.8 万)。 右边 PBR 在同一个 σ 下始终没有这种像素。
再拖画面转视角:高光的落点在两边一起移动(两边用的是同一个 v)。 把它拖到球面上"正对光"的那一侧,经验那一边越过 1 的面积会更大 —— 因为"又亮又有高光"的地方,两项的和最容易冲过 1。
切到「高光关」,两边都只剩漫反射:经验那一边是 Oren–Nayar 的括号,PBR 那一边是朗伯乘 (1−F0),
左球比右球略暗一点,但两边都远在 1 以下 —— 那一片的差别不是本段要看的。
把「粗糙度 σ」推到 0.9:左球那片泛白明显变大,右球仍是一颗正常的球;画面右下读数的末段
从 越界 约0.7万 涨到 越界 约0.8万(读数里会给确切值)。
注意看左边那片泛白不是高光变亮,而是高光瓣被铺得很开之后,
与漫反射叠在了同一片区域上 —— 0.86 与 0.60 加到了一处。
| 项 | 值 | |
|---|---|---|
| 项 | 值 | |
|---|---|---|
| σ | n(σ) | 经验 ρ | PBR ρ | 经验白炉 | 越界 |
|---|
n = 2/σ² − 2(经验模型没有"粗糙度"这个量,
只能硬造这一步),再
ρ(经验) = (α/π)·I_l·n·l·括号 + k_s·(n·h)^n·n·l、
ρ(PBR) = (1−F0)·(α/π)·I_l·n·l + F0·(n·h)^n·n·l。
ρ 只算画面里开着的那两项(光照项关掉就没漫反射、高光关掉就没高光);
两边的高光都带出射余弦 n·l,所以这两列可以直接并排比。
括号就是 2.2 那个 A + B·s/t;三个系数 α = 0.86、k_s = 0.60、
F0 = 0.35 本段固定。PBR 的 ρ 从不超过 (1−F0)·α/π + F0 = 0.528;
经验那一列的账面上限是 α/π + k_s = 0.874 —— 它已经不再越过 1。ρ 是记账
(两项系数 × 形状因子),它只说"预算怎么分",不保证瓣发得完;
守恒的真判据是 ∫ f_r·cosθ dω ≤ 1 —— 而经验那一列并不满足它。
② 里 ρ 是不含环境光的反射预算(环境光属于另一路的入射项,混进来会说不清);
括号与高光底数 n·h 都在 ② 里逐项列出,可以对着 ③ 的算式核。
知道经验模型那一代为什么会在某个条件下崩:它的漫反射与高光来自两套互不相干的经验,
两个系数各自独立、直接相加,没有任何机制让这个和停在 1 以下;
而"粗糙度"在它这里根本不存在,只能硬造一个 σ → 高光指数 的换算 ——
粗糙度一推高,高光瓣被铺开,正好落在"又亮又有高光"那一片上,于是和越过 1:
表面反射出的光多于照进去的,不可能。也看到 PBR 在同一光源、同一粗糙度下没有这个问题 ——
它的两个系数被同一个 F0 绑住,和天生封在 1 以下。
为什么它能这样绑、那个 F0 又是什么,是下一课要拆的事。
跑不起来先看设备要求:活例由真 GPU 出画面 —— 手机需 Android 12+ 且 Chrome 121+,或 iOS / iPadOS 26+;电脑需 Chrome / Edge 113+ 或 Safari 26+。Android 上取不到适配器多是设备不支持 Vulkan 1.1+,会自动改走兼容模式(需 Chrome 146+)。不满足时画面区会写出原因与自查步骤,同一页的示意图与数值表照常可读。