内容工厂. / 演示空间 / PBR 高级效果 · 第 5 课 · 材质与形状 段 1 / 3
PBR 高级效果 | 第 5 课 | 上一课:阴影:加一个可见性

材质与形状

读时 约 6 分钟 本课 3 段 · 已上线 3 段 每段 三件套 保真度 真机可测
课题骨架(本课题只声明一次)

着色不是「给表面涂颜色」,而是解一个积分:对每个着色点,把所有入射方向的贡献按 BRDF 加权累加。 PBR 的全部内容就是——怎么把这一个积分算得既接近物理、又跑得动。 这个课题沿着一代代模型走:每一代都是对同一个积分更好的一次近似。

5.1
各向异性:高光成一条线
5.2
清漆:双层表面
5.3
参数表:Disney
5.1

各向异性:高光成一条线

真机可测 · 本机渲染

本节点在骨架的下一环 —— 前四课把「光从哪来、这一点怎么反射、怎么把被挡的光扣掉」一路算到了能出图。可这四课里有一件事从头到尾没被问过:表面朝各个方向反射得一样吗。

到上一课为止,那个积分里能调的只剩下一根滑块:粗糙度 σ。它有两个特点 —— ① 它是一个数;② 它不分方向:把 σ 调小,高光瓣就四面均匀地收紧成一个圆点。 于是四课下来,画出来的东西永远是「像塑料」:一颗球、一个圆亮点,翻来覆去都一样。 可真实的表面不是这样:拉丝金属的高光是一条横着的亮线、缎面是一道斜的光带、 唱片和 CD 的彩虹环、被拉的头发与兽毛 —— 它们的高光都不是圆的。 原因很朴素:这些表面的微观起伏有走向 —— 顺着一个方向划痕很长、横着很密。 所以「粗糙度」根本不该是一个数,它沿两个方向可以是两个数。 本段只增加一样东西:把粗糙度从一个数拆成沿两个正交方向的两个数(αT 与 αB)。 它要两档控件才说得完 —— 各向异性强度(两个数差多远)与拉丝方向 φ(这两根轴朝哪)—— 但它们是同一个变量的两半,不是两个变量。 其余控件与第 3 课那一组完全相同:光照项、光源方向、粗糙度 σ、高光怎么算、高光瓣归一。 第 4 课那三档(可见性、半影角、深度图分辨率)本段整个收起来 —— 那是「挡不挡」的事, 与本段的正题(被积函数自己长什么样)无关;只把 4.1 那档硬射线的可见性原样留着, 让场景与上一课同框(球 + 地板 + 球投下的那块影),读者不必重新认路。
有一处必须说清楚,否则本段的数字会被误读:σ 一个数没变,它管的还是「瓣整体有多大」; 本段的 a 只管「瓣被拉成多扁」。做法是让两个方向上的指数乘起来恒等于 σ 那个指数 (几何平均分账,见件三 ②),于是把 a 从 0 拖到最大,瓣的总面积一点不变、只换形状 —— 峰值跟着下降,因为同一份能量摊到了更大的面积上。这条「面积不变」是本段的主判据。

件一 含代码的数据流示意图 · 框里是件二真正在跑的那段 WGSL
左列 → 中间真源码 → 右列。框里是件二里真正在执行的那段 WGSL(逐字抄出,页面对它做了自检); 右列三个数与件三、与画面同源。跟第 3 课比,中间那几行只做了一处改动:指数不再是 n,而是 ne / m。 m 是这一点的方位角 cos²φ 在两个方向上的加权: c2 = tt²/(tt²+bb²)(tt、bb 是半向量 h 在切线系里的两个分量), 于是 h 落在拉丝方向 T 上时 m = ax、落在垂直方向 B 上时 m = ay, 其余方位取两者的加权平均。因为 ax · ay ≡ 1(两个方向上的指数乘起来 = ne²), 把 a 拧大只是把 m 从一个 1 摊成一对倒数,指数在几何上一样大。 框外还有两行:fn tanDir(n)(把世界方向 X(φ) 投影到这一点的切平面上,得到 T; n 与 X 平行时退化成 cross(n, 上))与 let wgt = select(1.0, u.anis.z, u.view.w > 0.5); —— u.anis.z 就是这一档的归一因子 Cn(件三 ③ 给出它为什么不再等于各向同性那个 (ne+8)/(8π) 了)。 右列第三个「Cn」是这张图上真正的结果:瓣被拉长以后,同一个白炉口径要在更大的面积上摊开, 峰值只能降下去(不带权重的那条面积五档恒为一个常数,见件三 ③)。框里最后一行 let vis = shadowVis(wp, l); 是第 4 课那条可见性 —— 它在框里原样出现,就是为了让读者看见它一行都没动。

下面这张小图只说一件事:瓣被拉成什么样。 它把瓣本身画出来(不是画面上的光斑):极角 = 偏离中心多少度,极径方向 = 方位角, 那两条曲线是「亮度掉到峰值一半」的等值线 —— 也就是把 cos θ½ = 0.5m/ne 对每个方位角解一遍得到的轮廓(与件三 ② 同一份算式)。 虚线圆是各向同性档(a = 0):它的半径处处等于 14.93°(只随 σ 变)。 实线是本档(a = 0.85):沿 T 半角涨到 27.91°、沿 B 缩到 7.90° —— 两条半轴的比值 3.54(1/aspect = 3.60)。 图上的面积别去比:极径画的是半角、不是瓣的密度,这两条曲线围出来的极坐标面积本来就不该相等; 「形状换了、总量没换」那本账在件三 ③ 的 ∫lobe dωh 那一列(五档恒为同一个常数 0.3326)。 图上那两个小方块就是 T 与 B 上的半角刻度 —— 拖着 a 或 σ 动,它们会一起动。

注意看

右列三个数是同一件事的三段:沿 T 的指数、沿 B 的指数、归一因子 Cn。 前两个就是形状因子 m 取两个极端值时的指数(m = ax ⇒ ne/ax;m = ay ⇒ ne/ay), 中间那几行 tt、bb 是半向量 h 在切线系里的两个分量(平方和 = 1 − (n·h)²), c2 把它们的方位角压成一个数,mm 再按 ax / ay 加权 —— 指数就是 ne/mm。 默认档(σ = 0.30、a = 0.85、φ = 30°)三个数是:5.605 / 72.784 / Cn = 0.6222, 而 m 本身随 h 的方位在 0.278 ~ 3.604 之间连续滑动; 把「各向异性」切回关:两个指数立刻并成同一个 20.1976(= ne)、Cn 回到 1.1219(= (ne+8)/(8π), 与第 3 课那个常数逐位相同)。同一个 a,Cn 从 1.1219 掉到 0.6222,就是「瓣更宽了、峰值只能更低」。 而它的账是平的:各向异性只把瓣摊开 —— 不带权重的面积五档恒为 0.3326, 带权重的白炉口径只降不升(各向同性 1.0604 → 各向异性 0.8159,见 ① 表那一行)。

件二 可操作活例 · 纯 WebGPU · 控件与第 3 课相同,只多两档「各向异性」
拖画面 → 转视角(看球面上那条亮线怎么走)
启动中…
光照项(与第 3 课相同)
光源方向(与第 3 课相同)
粗糙度 σ(与第 3 课相同 · 它管瓣整体多大)
高光怎么算(与第 3 课相同)
高光瓣要不要归一(与第 3 课相同)
本段新增 · 粗糙度是一个数还是两个数

先切到「各向同性」。整颗球与你第 3 课看到的逐像素相同:球面上一个圆高光点, 右下读数给出「指数 20.2 · Cn 1.1219 · 比值 1.00」。 再切回「各向异性」(a = 0.85):那个圆点被拉成一条短亮线 —— 这不是换了一种颜色,是同一份能量换了形状:读数的「比值」跳到 3.54、 Cn 从 1.1219 掉到 0.6222(降 45%), 球上高光像素从 10423 涨到 17074;唯一一动不动的是件三 ③ 那一列 ∫lobe dω_h —— 五档都是 0.3326。面积守恒、峰值下降、斑变大 —— 三件事是同一件事。

读数里那个「长宽比」要留意量在哪儿。件三 ① 的那一行把它量了两次: 曲面角坐标(把斑摊回球面自己)与屏幕坐标。默认档两个数是 各向同性 1.022 / 1.385、各向异性 2.216 / 2.372。 各向同性档的真值明明是 1.000 —— 曲面那个只差 2.2%,屏幕那个差了 38%: 那 0.363 全是球面投影与透视的账,换到曲面坐标就被抵掉了。 剩下的一截(2.216 对瓣本身的 3.535)来自两件球上躲不掉的事 —— 球面切线场在斑内会转、n·l 在斑内不恒定。要瓣本身那个数,看件三 ②③。

再拖两个角度。强度 a 只管「扁到什么程度」:从 0 拖到 0.95, θ½(T) 从 14.93° 涨到 33.21°、θ½(B) 从 14.93° 缩到 6.58°,比值从 1.0000 一路到 5.0475; 但 σ 一个数没动 —— 所以「瓣整体多大」没变,只有形状在变。 拉丝方向 φ 只让那条线转方向:φ 从 0° 转到 90°, 量到的主轴角从 7.10° 转到 74.16°(单调跟着转;但屏幕上量到的角度 不等于 φ 本身 —— 球面切线场与透视一起把它挪了,见件三 ① 那一行)。 把 a 拖回 0,或切到「各向同性」,画面与第 3 课逐像素相同 —— 这是本段的还原性判据。

最后拖「粗糙度 σ」。它会同时改变两个方向上的粗糙度(两个指数一起变大)—— 所以 σ 管的是瓣整体多大,a 管的是瓣多扁。两件事分开, 这正是本段把粗糙度从一个数拆成两个数要换来的东西。

动手

先在「本段新增」那一档来回切:注意看球面上那个高光点 —— 各向同性时是圆的(读数「瓣比值 1.00 · Cn 1.1219 · ∫lobe dω_h 0.3326」), 各向异性时被拉成一条短亮线(比值 3.54 · Cn 0.6222 · ∫lobe dω_h还是 0.3326)。 再把「各向异性强度 a」从 0 慢慢拖到 0.95:线越来越扁,而 Cn 一直往下掉 —— 掉多少,斑就摊开多少。最后拖「拉丝方向 φ」走一圈:线转方向,长短不变。 还要看件三 ① 的那一行:同一个斑的长宽比被量了两次(曲面角坐标 / 屏幕坐标), 各向同性档是 1.022 / 1.385 而真值是 1.000 —— 差的那些全是"量在哪儿量"的账。 想核数的话,件三 ② 是沿 T 与沿 B 两条剖面的逐格扫描,件三 ③ 是五档 a 的正交对照。

件三 中间量数值表 · 与件二同源
① 本档的瓣 · 随控件变
项值
② 瓣的剖面 · 手算可核
β沿 T:m / lobe沿 B:m / lobe
③ 五档 a 的正交对照 · 面积账
aaspectCnθ½(T)θ½(B)比值∫lobe dωh
② 每一行都能拿笔算:取一个偏离瓣心的角 β(度),base = cos β, 沿 T 时 m = ax、沿 B 时 m = ay,于是 lobe = Cn · basene/m。这个指数是要命的地方: 本档 σ = 0.30 ⇒ ne = 2/(σ²+1e-4) − 2 = 20.1976; a = 0.85 ⇒ aspect = 1 − 0.85·a = 0.2775、ax = 1/aspect = 3.6036、ay = aspect = 0.2775。 于是沿 T 的指数是 ne/ax = 5.605(很平、亮得远),沿 B 是 ne/ay = 72.79(很陡、掉得快)—— 同样偏离 12°,沿 T 还剩峰值的 88.3%、沿 B 只剩 20.0%(差 4.4 倍);偏离 24° 时是 60.2% 对 0.14%(差 430 倍)—— 两条曲线越往外差得越离谱,「一条线」这件事就没有别的解释了。 表尾那一行给出半角:把 cos θ½ = 0.5m/ne 解出来就是 θ½(T) = 27.912°、θ½(B) = 7.895°,比值 3.5354。
③ 是本段的主判据,五行都手算可核。∫lobe dωh 那一列五档恒等,都是 0.3326 —— 瓣被拉扁以后,面积一分没变(构造出来的:ax·ay ≡ 1,加上归一因子按面积闭式的比值等比压低: Cn = (ne+8)/(8π) · Jiso/J,其中 J = 1 − ne/√((ne+ax)(ne+ay))、 Jiso = 1/(ne+1) 是各向同性档那个值,见下)。 于是 Cn 那一列只能往下走(1.1219 → 0.4711):面积守恒的意思就是峰值必须让路。 有两条容易被读错的地方,说清楚: ① 不带权重那条面积恒等的常数不再是 1,而是 (ne+8)/(4(ne+1)) = 0.3326 —— 因为归一因子已经挂在白炉口径上(a = 0 时逐位等于第 3 课那个 (ne+8)/(8π)), 第 3 课那个常数也含在这个乘积里;恒等的是「五档同一个数」,这才叫「只换形状」。 ② 带权重的白炉那条口径随 a 只降不升(默认档 各向同性 1.0604 / 各向异性 0.8159)—— 权重把靠近峰值的部分看得更重,而各向异性正好把峰值摊低了。方向是变小: 各向异性不会凭空多发能量(验收判据 ① 把这一条也钉住了)。 比值那一列与 1/aspect 的偏差随 a 增大(−0.21% → −2.84%)—— 因为 θ½ 与瓣宽不是严格成正比的(θ½ ≈ √(2ln2·m/ne) 只是小角度近似), 偏差本身也是这个近似带来的,不是算式写错了。
① 里另有一行是画面实测:把球面上 sp ≥ ½ 峰值 的像素取出来算二阶矩, 而且量两次 —— 屏幕坐标与曲面角坐标(以瓣心 h* 为极点,把斑摊回球面自己)。 各向同性档给出一对最有说服力的数:曲面 1.022 / 屏幕 1.385 —— 真值明明是 1.000, 曲面那个只差 2.2%,屏幕那个多了 38%:那 0.363 全是球面投影与透视的账, 换坐标系就抵掉了。各向异性档是 曲面 2.216 / 屏幕 2.372, 与瓣本身的 3.535 还差一截 —— 那一截来自两件在球上躲不掉的事: ① 球面切线场在斑内会旋转(hairy ball:球面上不存在处处非零的连续切向量场), ② n·l 在斑内不恒定,把斑在背光一侧压小。 所以「球上量到的比值」只能当相对读数用,与瓣本身不声称相等(验收判据 ⑤ 就是这么钉的); 要瓣本身那个数,看 ② ③。贴边像素(落在球面轮廓上、会让二阶矩偏一点的那些)也在读数里如实报出。
① 里最后还有一行是量法对照:把光源临时摆到与视线同向(β = 0,只重算这一次读数,不动画面)。 此时斑心处 n·l 与 n·v 同时取到极值、两个余弦梯度都为零,投影也退化成各向同性 —— 屏幕坐标那个读数从 1.385 掉回 1.002。同一个斑、同一份算式,只是把光挪了个位置: 那 0.363 当场兑现成「投影与余弦梯度的账」。反过来,把 φ 从 0° 扫到 160°, 各向同性档一动不动恒为 1.023(φ 不该有影响),而各向异性档在 1.61~2.18 之间摆 —— 这就说明那个读数被切线场的几何主导,不是被瓣主导。
五档 a 的还原性:a = 0 时 m ≡ 1、Cn = (ne+8)/(8π) = 1.1219, 与第 3 课那条算式逐位相同(离线复算:两式的逐点最大差为 0,Cn 完全相同)。
本节获得

知道「各向异性」在这条链上可以落成一个很克制的改动:把高光指数从一个数换成一个随方位角变的数 (ne → ne/m,m = cos²φ·ax + sin²φ·ay),别的什么都不用动。 默认档(σ = 0.30、a = 0.85、φ = 30°)球面上那个圆高光点被拉成一条短亮线: 沿 T 半角 27.91°、沿 B 7.90°、比值 3.54。 最值钱的一条其实不在各向异性本身,而在「量在哪儿量」:把同一个斑量两次 —— 屏幕坐标与曲面角坐标(摊回球面自己)—— 各向同性档得到 1.385 / 1.022, 而真值是 1.000:曲面那个只差 2.2%,屏幕那个多了 38%。同一个斑,换个坐标系, 多出来的那 0.363 就没了 —— 那是球面投影与透视的账,不是材质的账。 各向异性档是 2.216 / 2.372,与瓣本身的 3.535 还差一截,那一截来自 球面切线场在斑内会转(hairy ball)与 n·l 在斑内不恒定 —— 球这个载体的固有代价,本段如实报出、不声称相等。 还看到最有用的那条账:把 a 从 0 拖到 0.95,∫lobe dωh 五档恒为同一个常数 0.3326, 而 Cn 从 1.1219 掉到 0.4711、θ½ 的比值从 1.0000 涨到 5.0475 —— 形状换、总量不换,代价全记在峰值上(降 45% 那一步就在默认档)。 这里的「总量不换」说准一点:恒等的是不带权重的那条面积(∫lobe dωh), 它的常数不再是 1 —— 因为归一因子已经挂在白炉口径上(a = 0 时逐位等于第 3 课那个 (ne+8)/(8π))。而带权重的白炉那条口径随 a 只降不升 (只测高光那一项:各向同性 1.0604、各向异性 0.8159),方向是变小 —— 各向异性不会凭空多发能量。这条也被验收判据 ① 钉住了。 也才知道 σ 与 a 必须分家:σ 是一个数、管瓣整体多大;a 是一对倒数、管瓣多扁。 本段把这两件事分开的做法是让两个方向的指数乘起来恒等于 ne²(几何平均分账)—— 这样 σ 一根滑块的行为与第 3 课完全一样,a 才有资格单独说「我只改形状」。 还看到一个边界:本段只把粗糙度拆成了两个正交方向上的两个数, 两个方向之间还没有任何耦合 —— 真实拉丝金属顺着划痕方向还会有一道反向的拖尾 (「顺着光看更亮」的那种),本段答不了。 那件事要等叠第二层:表面之上再来一层薄薄的、自己带高光的膜 —— 清漆。 那是下一段的事。

5.2

清漆:一层膜把高光分成两枚

真机可测 · 本机渲染

本节点在骨架的下一环 —— 这一段是全书唯一一次把两个被积函数摞在一起:上一段的瓣算的是「一个表面怎么反光」,可最亮的那类漆面(汽车、钢琴、手机背板)根本不是一个表面。

到上一段为止,「表面」始终被当成一样东西:一个法线、一份粗糙度、一枚高光。 可你只要摸过一次车漆就知道不是这样:它有两层性格 —— 正面看是沉闷的深色(光透进颜料层, 被底层的粗糙反射糊成一片),换个角度掠着看,边缘却突然变成一面镜子(光来不及进去, 先被最上面那层光滑的膜弹回来)。同一个表面、同一个光源,两枚高光在打架。 本段只增加一样东西:在表面之上再叠一层薄而光滑的膜(清漆),两层各自反射、菲涅尔各算一次。 控件与上一段完全相同(光照项、光源方向、σ、高光模型、归一、a、φ), 只多一档「清漆强度 c」。膜的粗糙度 σc 固定成常量 0.10 —— 那不是一个变量,是这一层膜的「材质」:它就是薄而光的。
账要一笔一笔走,否则会被误读:光先过膜 —— 膜这次反射掉 Fc, 剩下的 (1 − Fc) 才透进底层;底层照旧算自己的漫反射 + 自己的高光, 但要乘 (1 − Fc);膜自己那枚高光按膜自己的粗糙度铺开,所以它又紧又亮。 因为 Fc + (1 − Fc) ≡ 1,这条分法逐方向不超预算 —— 白炉门禁钉的就是这一条。 把 c 拖回 0,这一页必须逐像素回到上一段:这是本段的还原性判据。

件一 含代码的数据流示意图 · 框里是件二真正在跑的那段 WGSL(插在 5.1 那一行之后)
左列 → 中间真源码 → 右列。框里是件二里真正在执行的那段 WGSL: WGSL52 并不是另抄一份,而是把这段插进 WGSL51 的高光收尾那一行之后拼出来的 —— 公共部分(几何、可见性、漫反射、菲涅尔)永远只有一份,改一处两段同时生效 (lesson-04 当年整份拷贝,改一处要改三遍,是踩过的坑)。所以「框里与件二逐字一致」这件事 在这里不是靠人盯,而是构造上就不可能不一致:框里这 7 行就是拼出 WGSL52 的那个数组 COAT52LINES 本身(有 3 行太长,为放进框里按显示宽度折了行)。 逐行的账:hc 是半向量(和 5.1 里那个 h 是同一个东西,这里再算一遍是为了让框里这段能独立看懂); odc = 1 − l·hc 就是 1 − cosθd;Fc 是膜的菲涅尔 (0.04 + 0.96·(1−cosθd)⁵,介质膜:与波长无关,所以它那枚高光是白的); c = c * (1.0 - Fc) 这一行把已有的全部颜色(环境光 + 漫反射 + 底层高光) 乘上透射率 —— 光要先过膜;最后一行叠上膜自己那枚:u.coat.z 是膜的 Cn、 u.coat.y 是膜的指数(由 σc = 0.10 定,与本档的 σ 无关)。 右列三个数与件三、与画面同源。

先把「清漆强度 c」从 0 拖到 1,来回两遍:注意看球面上那枚高光 —— c = 0 时它是上一段那条被拉长的亮线(拉丝金属);c = 1 时它并没有变宽,反而收紧成一个又小又白的圆点, 而且颜色从底层的暖色变成无色。原因是膜那枚比底层亮 13.1 倍(件三 ① 的「膜峰值」), 而且膜是各向同性的(σc 是一个数,不分方向)—— 所以它把上一段辛苦拉出来的那条线盖回成一个圆点。 这就是真实车漆上「清漆盖过拉丝」的样子。

再盯球的边缘与远处的地板:c = 1 时它们会泛起一层镜面反射。 这是膜的菲涅尔在掠射角上冲向 1 —— 件三 ② 那张表就是它的手算版: 正面 Fc = 0.04000(96% 透进底层)、60° 0.07000、75° 0.25473、85° 0.64849、89° 0.91910 (只剩 8% 进底层)。正面是漆、侧面是镜,两件事同一个公式。

件二 可操作活例 · 纯 WebGPU · 控件与上一段相同,只多一档「清漆强度 c」
拖画面 → 转视角(把球缘掠过来看膜接管)
启动中…
光照项(与上一段相同)
光源方向(与上一段相同)
粗糙度 σ(与上一段相同 · 底层的瓣)
高光怎么算(与上一段相同)
高光瓣要不要归一(与上一段相同)
底层的粗糙度是一个数还是两个数(与上一段相同)
本段新增 · 清漆强度(膜的那一层)
膜的粗糙度 σc = 0.10 固定不动(它就是「薄而光」这一层膜的材质,本段不当变量)。 可见面判据 n·v > 0 的占比与上一段同口径,同样必须是 100.00%。
件三 中间量数值表 · 与件二同源
① 本档的膜与底层 · 随控件变
项值
② 膜的菲涅尔 Fc · 手算可核
θdcosθdFc底层还剩
③ 五档 c 的正交对照 · 分层账
cFc底层峰值膜峰值底层还剩
② 每一行都能拿笔算:取一个 θ_d(半向量与光的夹角),cosθ_d 是它的余弦, F_c = c·(0.04 + 0.96(1−cosθ_d)⁵),最后一列是 1 − F_c。 注意 0° → 85° 之间它才涨了 16 倍(0.04000 → 0.64849), 而 85° → 89° 这 4° 又涨了 42%(0.64849 → 0.91910)—— 掠射那一小段才是膜真正接管的地方。 ③ 是把 c 摆成五档看分层账:两列都随 c 线性走(底层峰值乘 (1 − Fc)、膜峰值乘 Fc), 所以拿笔按比例一算就能核。① 里那一行「还原性」是本节最要紧的一条: c = 0 时必须与上一段逐像素相同(最大差 = 0.000e+0)。
本节获得

本段只加了一层膜,就把「一个表面」变成了两层:

  • 能量要逐方向分账:膜反射 Fc、透射 (1 − Fc),加起来恒等于 1 —— 所以「加了一层膜」在物理上不是从底层偷光,而是多了一次反射的机会。c = 0 时这一页逐像素回到上一段。
  • 膜的那枚高光又紧又白:θ½ 只有 4.82°(底层各向同性是 14.93°)、 峰值却高 7.24 倍;而且因为膜是各向同性的,它会把上一段那条拉丝的光盖回成一个圆点 —— 这就是车漆上清漆盖过拉丝的样子。
  • 掠射处膜接管:85° 时膜已经拿走 65%、89° 时拿走 92%。小图上那两个数(0.64849 / 0.91910) 就是「正面是漆、侧面是镜」的全部原因。
5.3

一张表把前面都装进去

真机可测

本节点在骨架的收束环上 —— 上一段给「一个表面」叠上了第二层(膜),可到此为止,这一路的旋钮是按物理量各起各的名字的:σ、a、φ、c、F0。没有一个做材质的人会这么配参。

把前面三段连起来看就发现问题了:1.1 里调的是「光源形状」,2.x 调的是「漫反射/高光模型」,3.x 调的是「预算怎么分」, 5.1 加了「沿两个方向的粗糙度 a 与拉丝方向 φ」,5.2 加了「清漆强度 c」。 这些量都对,可它们的名字是物理学家起的——量纲各不相同(角度、比例、无量纲强度), 而且谁也说不清「把皮肤调得更像皮肤」要动哪几个。工业界的答案是把它们收进一张表: 每个格子一个名字、一个范围、一个直觉含义,而表里每个数最终都落到你已经见过的那一项上。 本段只增加一样东西:一张命名参数表(Disney 的 Principled 那一套),把前面的旋钮改名收编,并补上表里前面没有的三个。 控件与上一段相同的两处(光照项、光源方向)一字不动;上一段那些「按物理量命名」的旋钮(σ、各向异性 a、清漆强度 c) 不是消失,而是换了名字落进这张表:roughness / anisotropy / clearcoat。 表里真正新的只有三个:metallic(把漫反射那一份搬进高光,并让高光带上底色)、 specular(绝缘体高光的绝对大小)、sheen(掠射的边缘光晕)。
判据只有一条,但它最要紧:把表摆回上一段的档,这一页必须逐像素回到 5.2 —— 这张表是同一份被积函数的重新参数化,不是新的物理。而它不守恒这件事也照实写在下面(件三 ②)。

件一 含代码的数据流示意图 · 框里是件二真正在跑的那段 WGSL(插在 5.2 那一行之后)
左列 → 中间真源码 → 右列,与前面几段同一形态。框里这 3 行就是拼出 WGSL53 的那个数组 (COAT53LINES)本身 —— WGSL53 不是另抄一份:它拿 WGSL52 把两行换掉、 再在膜那几行之后插一行得到,所以「框里与件二逐字一致」在这里同样是构造上成立的。 逐行的账:第一行给漫反射乘 (1 − m) —— 金属没有透进内部的漫反射,那一份要退场; 第二行把高光的色调从「高光专用的冷色」stint 混向底色 dtint —— 所以 m = 1 时高光自己带上底色(这就是金与塑料最直观的差别); 第三行是新加的一枚 sheen:(1 − cosθd)⁵ 只在掠射角上抬起,只加不减 —— 这一行就是件三 ② 里「账被往上推」的那一笔。 表里另外四个名字(roughness / specular / clearcoat / clearcoatGloss / anisotropy) 不在这里出现,因为它们只是改名:它们映射到 5.1/5.2 已经有的 uniform 槽位,一个像素都不用改。

把 metallic 从 0 拖到 1,来回两遍:注意看球面正对光源那一片 —— 它整体暗下去(漫反射按 (1 − m) 退场),同时高光那一枚换了颜色: 从 5.2 里那种偏冷的白变成底色的暖。这就是「金属与塑料」最直观的差别 —— 金属没有透进内部的漫反射可用,它能反射的只有自己的底色。件三 ① 里「漫反射份额」与「F0」两行 会随这个滑杆反向走(加起来恒等于 1)。但别把这句读成「总量没变」:搬走的那一份并没有被高光接管 —— 件三 ② 的白炉会因此往下掉,而下面那个 sheen 又会把它往上推,两个方向都没人补。

再把 sheen 从 0 拖到 1,看球的边缘:一圈很薄的灰白晕从轮廓上浮起来, 球心几乎不动 —— 因为它按 (1 − cosθd)⁵ 走,正面是 0、掠射才抬。这是织物(天鹅绒、麂皮)的观感来源。 然后看件三 ② 的白炉那一行:它会往上走一小截(0.3080 → 0.3416,+10.9%)。 这不是本页算错 —— 它是这一段的结论: 这张表的 2012 版并没有把多次散射补回来(Disney 自己在 2015 年补、Imageworks 在 2019 年给出了闭式的补偿项), 所以「参数表是艺术接口,不是物理接口」这句话是可以量出来的 —— 两个新旋钮把账往相反方向拉: 一个搬走 97.7%、另一个只补 10.9%,而 2012 版谁都不补。

件二 可操作活例 · 纯 WebGPU · 控件与上一段相同,只多一张参数表
拖画面 → 转视角(把球缘掠过来看 sheen)
启动中…
光照项(与上一段相同)
光源方向(与上一段相同)
本段新增 · 一张参数表(表里前面没有的三个在最上面)
把表摆回上一段的档(还原性判据)
拉丝方向 φ 固定成 30°、底色固定成前几段那个冷色 —— 它们不是本段的变量。 「摆回 5.2 的档」这一档是硬判据:metallic = 0 · sheen = 0 · clearcoatGloss = 0 · specular = 0.5 · roughness = 0.30 · clearcoat = 1 · anisotropy = 0.85 时, 本页的着色算式与 5.2 的逐方向相同(最大差 = 0.000e+0),画面也必须逐像素一致(件三 ① 的最后一行在报这个数)。
件三 中间量数值表 · 与件二同源
① 这张表怎么落到分量上 · 随控件变
参数它改的是哪一项
② 能量账 · 两个旋钮拉向相反
档白炉 ∫ρ·cos θ dω结论
① 每一行都能对回某一段:roughness 就是 5.1 那根 σ、anisotropy 就是那根 a、 clearcoat 就是 5.2 那根 c;只有前三行是表里新的。 最后一行的「与 5.2 的逐方向最大差」是这一段的还原性判据 —— 它必须是 0.000e+0。 ② 白炉是第 3 课那条账:这一列是本页自己做的半球积分(θ 走半球、φ 走满圈、测度 cosθ·sinθ dθ dφ),算的是同一份着色算式的出射量,所以三行之间可以直接比 —— 但它的取样比 5.1/5.2 那条 rho0 口径粗(16×16),所以看方向与比值,不必对绝对值较真。 要点在这里:metallic 把漫反射那一份搬走, 而高光那项并不会接管它 → 账往下掉(0.3080 → 0.0072,掉 97.7%); sheen 是只加不减的一项 → 账往上走(0.3080 → 0.3416,补回 10.9%)。 两个新旋钮把账往相反方向拉,2012 版里谁都不负责补回来(Disney 2015、Imageworks 2019 才给出补偿项)—— 所以「参数表是艺术接口,不是物理接口」不是一句态度,而是一个可以量出来的缺口: 搬走的那一头补得少近十倍(97.7% 对 10.9%),账是往漏的那一边倒的。
本节获得

本段没有加任何新物理,只把前面四段学到的旋钮收进了一张有名字的表:

  • 改名不收编新东西:roughness / anisotropy / clearcoat / clearcoatGloss 分别就是 5.1 的 σ、a、5.2 的 c 与膜粗糙度;摆回上一段的档,画面逐像素回到 5.2。
  • 真正新的是三个:metallic 把漫反射那一份搬走(高光不接管它)、 specular 决定绝缘体高光的绝对大小、sheen 在掠射角上加一枚只加不减的光晕。
  • 表是接口,不是定律:这张表能表达上面所有分量,却不保证它们的和不超过入射 —— 件三 ② 的白炉那一行就是证据:metallic 把账搬走 97.7%,sheen 只补回 10.9%, 两个旋钮拉向相反、2012 版谁都不补,账最终是往漏的那一边倒的。 这也是下一课(介质与透射)之前的最后一笔账: 要把这张表变成物理上闭合的,得再补一项多次散射,那是另一个话题。
本课其余段落 · 未上线
5.4 (本课已收束) 第 5 课三段全部上线:5.1 各向异性 · 5.2 清漆 · 5.3 参数表。下一页是第 6 课「介质与透射」。 已收束
运行时自检 等待脚本…

跑不起来先看设备要求:活例由真 GPU 出画面 —— 手机需 Android 12+ 且 Chrome 121+,或 iOS / iPadOS 26+;电脑需 Chrome / Edge 113+ 或 Safari 26+。Android 上取不到适配器多是设备不支持 Vulkan 1.1+,会自动改走兼容模式(需 Chrome 146+)。不满足时画面区会写出原因与自查步骤,同一页的示意图与数值表照常可读。