同一盏灯照在金属球上,改变粗糙度会让高光展宽;透过玻璃观察时,光线还会改变方向。继续使用 Lambert 的常量 BRDF 无法表达这两种现象。把高光指数调大也不能自动得到正确的反射份额、折射方向和抽样权重。

本篇选择两种明确的模型:各向同性 GGX 单次散射导体反射,以及理想光滑、无吸收的介质界面。前者检查粗糙度和反射能量,后者检查 Fresnel、全反射与离散事件采样。暂不实现粗糙玻璃、体吸收或完整玻璃球的多界面路径追踪。

先固定方向与介质两侧

沿用第 19 篇的约定,ωᵢ 和 ωₒ 都从表面向外。ωᵢ 指向入射光的来源,实际入射传播方向是 −ωᵢ。法线 n 朝向入射介质,因此 cᵢ=n·ωᵢ≥0。程序显式传入入射侧折射率 ηᵢ 和另一侧折射率 ηₜ,不依靠一个含糊的“玻璃折射率”猜测当前在物体内还是外。

令 q=ηᵢ/ηₜ。反射方向 r=2cᵢn−ωᵢ;折射方向为:

ωt=qωi+(qcict)n,ct=1q2(1ci2).\omega_t=-q\omega_i+(q c_i-c_t)n, \qquad c_t=\sqrt{1-q^2(1-c_i^2)}.

切向分量变成原方向切向分量的 −q 倍,法向分量落到另一侧。这个负号来自“ωᵢ 指向来源”的约定,不能只检查方向长度为 1。代码同时检查切向符号、Snell 正弦关系以及旋转到 +Y 的法线。

若 q²(1−cᵢ²)>1,根号没有实数值,发生全反射。ηᵢ=1.5、ηₜ=1 时,临界角为 arcsin(2/3)≈41.8103°。反过来从 1 进入 1.5,不会在这个单界面模型中发生全反射。

精确临界角对应沿界面传播的极限方向,当前实现将它并入无传播透射的分支。折射率相同则单独处理为直通、F=0,包含精确切线,避免把零除零误判为全反射。

Fresnel 决定反射份额

无吸收介质界面的非偏振 Fresnel 反射率为两个偏振分量反射率的平均:

rs=ηiciηtctηici+ηtct,rp=ηtciηictηtci+ηict,F=rs2+rp22.r_s=\frac{\eta_i c_i-\eta_t c_t}{\eta_i c_i+\eta_t c_t},\qquad r_p=\frac{\eta_t c_i-\eta_i c_t}{\eta_t c_i+\eta_i c_t},\qquad F=\frac{r_s^2+r_p^2}{2}.

正常入射时退化为 F=[(ηᵢ−ηₜ)/(ηᵢ+ηₜ)]²。空气近似折射率 1、玻璃示例折射率 1.5,得到 F=0.04;不是所有角度都固定反射 4%。接近掠射时,反射份额通常明显增加。

本实验从公式实际计算得到:

入射侧→另一侧 入射角 F 有传播折射方向
1→1.5 0.040000
1→1.5 60° 0.089187
1→1.5 80° 0.387704
1.5→1 30° 0.055190
1.5→1 40° 0.245291
1.5→1 42° 1.000000

下图由 material-glass.csv 中的计算方向生成,表示单个平面界面的射线方向,不是玻璃物体渲染图。橙色指入射传播,蓝色指反射,绿色指折射;上半区折射率 1.5,下半区为 1。42° 那一格没有绿色射线,是计算结果中的全反射分支。

程序在临界角两侧各取 10⁻⁷ 弧度扰动,分别检查存在和不存在折射方向;另检验两条方向的单位长度、Snell 残差和法向入射的 0.04。边界检查比只画一条看起来弯折的线更能发现介质顺序和符号错误。

金属需要复折射率

导体用复数折射率 η+ik 表达界面响应,k 与介质中的衰减有关。本篇通过 C++17 std::complex 计算复 Fresnel 振幅,再取模平方形成反射率。它没有把金属当成“漫反射颜色加白色高光”。

三个 RGB 通道的合成参数分别为 η=(0.2,0.9,1.5)、k=(3.9,2.5,1.8)。它们只用于固定实验,不对应经测量拟合的铜、金或其他金属,也不是完整光谱模型。

正常入射的独立检查使用 F₀=[(η−1)²+k²]/[(η+1)²+k²]。同时把 k 设成 0、η 设成 1.5,与实数介质 Fresnel 在三个入射余弦处比较。这样既检查复数计算,也检查它退化到已知模型的结果。

一个容易漏掉的边界是 η=1、k=0、余弦=0。介质完全匹配,应为 F=0;不能先套“掠射反射率为 1”的分支。代码为匹配界面单独返回零,其余参数仍要求有限、η 为正、k 非负。

GGX 怎样把微小镜面组成粗糙表面

微表面模型把宏观表面看作具有不同法线的小镜面。若两条反射方向为 ωᵢ、ωₒ,能把它们连接起来的微表面法线为 h=normalize(ωᵢ+ωₒ)。不是所有微表面都能同时看见光与观察方向,因此还需要遮蔽和阴影项。

本篇采用各向同性 GGX 法线分布,以 α 直接作为参数:

D(h)=α2π[(nh)2(α21)+1]2.D(h)=\frac{\alpha^2}{\pi\left[(n\cdot h)^2(\alpha^2-1)+1\right]^2}.

α 越大,法线分布越宽。这里没有使用某个引擎将 UI roughness 平方或开方后的映射,正文和代码中的 α 就是公式里的 α。连续模型要求 α>0;理想光滑表面是单独的离散模型,不能把 α=0 直接代进这个表达式。

D 的归一化是 ∫D(h)(n·h)dωₕ=1,带有投影余弦。D 本身不是每球面度的法线采样 PDF。若按完整 NDF 采法线,密度是 D(h)(n·h),再变成反射方向密度还要除以 4|ωₒ·h|;可见法线采样使用另一种密度,不能混搭。

遮蔽项使用 PBRT 第四版的高度相关 Smith 形式:Λ(ω)=[√(1+α²tan²θ)−1]/2,G=1/[1+Λ(ωᵢ)+Λ(ωₒ)]。它不是两个 G₁ 的简单乘积。最终反射函数为:

fr(ωi,ωo)=F(ωoh)D(h)G(ωi,ωo)4(nωi)(nωo).f_r(\omega_i,\omega_o)= \frac{F(|\omega_o\cdot h|)D(h)G(\omega_i,\omega_o)} {4(n\cdot\omega_i)(n\cdot\omega_o)}.

两条方向都必须在正半球。精确切线先返回零,避免分母为零;这是一条零测边界约定,不是把接近切线的有限响应都删掉。反射函数可能在窄峰处大于 1 sr⁻¹,是否产生多余能量仍需检查积分。

同一光照下的粗糙度图

下面三张球面图均为 128×128,正交观察方向为 +Z,光来自 normalize(−0.5,0.6,1)。这是一束固定方向光,垂直于光束的辐照度设为 4;每个表面点计算 Lₒ=4fᵣ max(0,n·ωᵢ)。它不是有限面积灯的随机采样,也没有环境光或间接反射。

球面法线从可见单位半球解析计算,相机、光照、合成导体参数与曝光完全不变。没有随机数;像素中心一次着色,不做边缘抗锯齿。显示仍先用 L/(1+L),再经 sRGB 编码。

α=0.15:

GGX alpha为0.15的球面结果

α=0.35:

GGX alpha为0.35的球面结果

α=0.70:

GGX alpha为0.70的球面结果

高光随 α 增大而展宽、峰值降低。三图看起来合理仍不足以证明能量正确,尤其显示映射压缩了亮部。程序另保存全部 49,152 个像素的线性 RGB,压缩文件可用于重新显示,不能从 PNG 倒推高精度能量。

单次散射模型为什么会损失能量

固定出射方向,在入射半球积分 fᵣcosθᵢdωᵢ;利用互易性,它也对应固定入射时的反射份额。程序在 μ=cosθ 和 φ 上做中点求积,dω=dμdφ,无需再乘 sinθ。

每档 α=0.15、0.35、0.7,观察角 0°、45°、75°,分别计算三个合成颜色通道和一个 F 恒为 1 的控制。每组先用 256×512 网格,再用 512×1024 网格,共 72 次积分,固定网格结束,不含随机停止。

正常观察方向的细网格结果如下:

α R 反射积分 G 反射积分 B 反射积分 F≡1 控制
0.15 0.925026 0.616924 0.357561 0.971765
0.35 0.793537 0.529221 0.307124 0.833726
0.70 0.479379 0.319701 0.185789 0.503717

F≡1 时,每次微表面反射本身都不吸收,积分却仍小于 1。原因是该模型只保留微表面的一次散射,未计算被遮挡后再次撞击其他微表面的光。粗糙度较高时,漏掉的多次微表面散射更明显。这不能与场景中“球反射到墙再回来”的多次反射混为一谈。

所有指定参数的反射积分都通过 [0,1.002] 范围检查,加密差小于 0.005;阈值是本组网格的诊断容差,不是全参数证明。α=0.15、法向观察、F≡1 的加密差约为 0.000459,说明窄峰仍有求积误差,不能把表中六位小数都视为准确位。

NDF 另用 65,536 个 μ 中点检查投影归一化,三档结果为 0.999999849、0.999999995、0.9999999998。互易性检查交换两条指定方向,要求结果差小于 10⁻¹²。归一化、互易性与反射积分分别约束不同性质,单独通过其中一个不能代替其余检查。

理想镜面是概率质量,不是很大的普通 PDF

光滑玻璃给定入射方向后,只有一个反射方向和一个折射方向。程序按 F 选择反射,按 1−F 选择折射。这两个数是离散事件的概率质量,不是相对于立体角的连续密度。

若用连续余弦分布在整个半球抽方向,恰好命中唯一理想镜面方向的概率在连续模型中为零。把镜面写成某个有限的巨大 BRDF,再沿用普通方向 PDF,不等同于正确处理 Dirac 分布。第 22 篇的连续密度 MIS 也不能直接套在这两个概率质量上。

sample_dielectric 返回方向、概率质量、是否反射和辐亮度传输权重。按 Fresnel 概率选择后,反射分支的 F 被概率抵消,权重为 1;透射分支在 Radiance 传输约定下还保留 (ηᵢ/ηₜ)²。无吸收界面的功率透射份额是 1−F,但这不意味着辐亮度只需乘 1−F。

程序检查从 1 进入 1.5、再从 1.5 离开的两个 η² 因子乘积为 1。这仅检查折射率因子,不表示整个双界面的最终透射必为 1:各界面的反射概率仍然存在,几何与多次内部反射也尚未追踪。

对前表扩展后的 12 个介质/角度配置,每个使用种子 1–8,各抽 65,536 次实际事件,固定次数结束,共 96 次运行。1→1.5 法向反射频率均值为 0.0400982,跨种子均值标准误为 0.0002911;1.5→1 的 40° 为 0.2461433±0.0005566,理论值为 0.2452912。42°、60°、80° 的全反射配置每次都选反射,频率为 1。

复跑、练习与资料

1
2
3
make -C examples/computer-graphics check
examples/computer-graphics/build/material_check examples/computer-graphics/build
python3 examples/computer-graphics/summarize_material.py examples/computer-graphics/build

实现位于 material.hppmaterial_check.cpp,复用累计向量、随机数、图像和颜色输出。反射积分界面数据原始事件计数事件汇总线性像素压缩CSV保留实际结果。

练习一:光从折射率 1.5 的介质射向 1,入射角 30°。折射角的正弦是多少?45° 时还能得到传播折射方向吗?

答案:sinθₜ=1.5sin30°=0.75,折射角约 48.59°。45° 时 1.5sin45°>1,发生全反射。交换介质顺序会改变这个判断。

练习二:按 F 抽反射事件后,为什么反射权重不再乘 F?若再次乘 F,会估计什么?

答案:该事件的物理系数与抽中概率相除为 F/F=1。再乘一次会把反射期望改为 F² 倍相应入射辐亮度。透射同理抵消 1−F,但仍需保留指定传输约定的折射率平方因子。

练习三:F≡1 的 GGX 单次散射积分为 0.5,是否足以判断代码漏乘了 2?

答案:不能。先检查归一化、遮蔽模型、积分收敛和微表面多次散射是否包含。单次散射模型本来会遗漏一部分反射路径;直接乘 2 不会普遍修复不同粗糙度和观察角的响应。

上一篇:22 小光源为什么难以采样。下一篇:24 怎样判断渲染器算对了系列入口