上一篇讲的是光追画面上的颗粒从哪来、降噪器怎么补。但降噪要补的那个“正确值”到底是什么?对颜色的判断,最终都要落回同一件事:光线碰到表面之后去了哪里。

这一篇回答三个连着的问题:为什么同一束光打在金属、塑料、玻璃上会变成三种不同的东西;游戏里的 roughness 和 metallic 各自在控制什么;以及为什么“有点粗糙的反射”是实时渲染里最难算清的。

一个表面只有两个出口

光线碰到表面,可能发生的事只有两件:被弹回去,或者进入材料内部。哪个发生、各占多少,由界面决定,这个比例叫菲涅耳反射率。

垂直看过去,塑料、玻璃这类非金属只反射百分之四左右(水约 2%,宝石可以到 16%),其余九成多进了材料;金属则相反,光几乎进不去,大部分从表面弹回来,没弹回来的被吸收掉。“光进不去”是理解金属的钥匙,它后面所有特殊表现都从这句推出来 1。

进去的那部分:漫反射

进入非金属的光在材料内部来回散射,一路被颜料吸收掉一部分,最后从表面某个随机方向钻出来。方向既然是随机的,你从正面看、从侧面看亮度都一样——这就是漫反射,哑光的来源。颜色也来自这里:哪种颜色的光被吸收得多,物体就显出剩下的颜色。

有个容易被忽略的结果:漫反射的颜色来自材料内部,而镜面反射那部分光没进过材料,所以不带材料的颜色。红塑料球上的白色高光就是这么来的——高光可以是白的,球本身是红的。

弹回去的那部分:镜面反射

弹回去的光走一条严格的路径,入射角等于反射角,方向完全由表面法线说了算。所以镜面反射给你的不是“材料的样子”,而是环境的像:看到的是它对面那面墙、那片天。

现实中不存在数学意义上完美的平面。把表面放大到微观,是一大片朝向各不相同的小平面。粗糙度描述的就是这些朝向散得多开:接近 0 时小平面几乎同向,等于一面镜子,高光又小又亮、反射清晰;粗糙度变大,朝向散开,每个方向都反射一点,高光摊开变暗,反射糊成一片;接近 1 时,反射已经糊得和漫反射差不多。

在渲染器眼里,这个“糊”有确切含义:一个像素的镜面反射要的,是以理想反射方向为中心、一个锥形范围内所有方向入射光的平均值,锥有多宽由粗糙度决定 3。这句话是理解后半部分全部代价的钥匙。

顺带一个实用细节:游戏里的 roughness 滑杆和锥的实际宽度通常不是线性关系,常见做法是先把数值平方(α=perceptualRoughness2\alpha = \text{perceptualRoughness}^2)再拿去算锥宽 2。所以滑杆从 0.1 拨到 0.2,画面变化远比从 0.8 拨到 0.9 剧烈——“0.2 还像镜子、0.3 就明显糊了”是正常现象。

metallic 这根滑杆在切什么

把两类材料并排看:非金属(塑料、木头、皮肤、玻璃、水)有被材料染色的漫反射,外加一层很弱、无色的镜面反射(4% 上下);金属(金、银、铜、铝)没有漫反射——光进不去,没有内部散射来染色——而镜面反射很强,并且这个镜面反射本身带颜色。

金属的镜面颜色就是金属自己的颜色。金在垂直入射时的反射率大约是 (1.00, 0.85, 0.57)(1.00,\ 0.85,\ 0.57):红光几乎全被弹回,蓝光被吃掉大半,所以金是黄的。银是 (0.97, 0.96, 0.91)(0.97,\ 0.96,\ 0.91),三色几乎全弹回,所以银看起来是白的——“银色”其实是接近无色的高反射,不是某种颜料 1。

所以 metallic 不是“亮不亮”的滑块,而是个开关:它决定 baseColor 这张贴图该被读成“漫反射的颜色”还是“镜面反射的颜色”。设成 0.5 在物理上并不存在,只用来表示锈迹这类过渡 2。

metallic 从 0 变到 1,同一张贴图被读成漫反射颜色或镜面反射颜色,观感截然不同

掠射角:为什么所有东西在边缘都变反光

菲涅耳反射率还随角度剧烈变化。垂直看塑料只有 4% 被反射,越接近擦着表面看,反射率越趋近 100%:

F=F0+(1−F0)(1−cos⁡θ)5F = F_0 + (1 - F_0)(1 - \cos\theta)^5

F0F_0 是垂直入射时的值,FF 是当前角度下的反射率 1。这一条解释了:下过雨的柏油路顺着路面看出去能拖出长长一条反光,低头看却是黑的;玻璃杯正面看几乎透明,斜着看却能映出整间屋子。

玻璃放进这套框架里很朴素:它就是典型的非金属,垂直入射 4% 反射、96% 透射并折射进去;因为玻璃通常很光滑,那 4% 反射是清晰的像而不是一团高光。有颜色的玻璃,是进去的光在体内走了一段路、被吸收掉一部分的结果。折射、吸收、色散留给下一篇。

显微镜下的表面:平整界面只把一个方向的入射光弹回去,杂乱的小平面把光散到一大片方向上

粗糙的反射为什么最难算

回到那个锥。

完美镜面反而最便宜。 roughness 为 0 时反射方向只有一个,一条光线就给出精确答案,方差为零。它的麻烦在别处——清晰度要够、光线要连贯,但“算准”这件事不费劲。

粗糙表面相反:一条光线只是一个随机样本。 你要的是锥内的平均值,随机抽一条光线只是这个平均的一个样本;锥越宽,样本彼此差别越大,估计越不可靠。按上一篇的结论,误差大致按 1/N1/\sqrt{N} 下降,实时预算下这点采样远远不够,缺口只能交给降噪器去猜 6。

光线会散开。 相邻像素各自在锥里随机取样,落点散得很开,同一束光线打到哪里毫无规律,硬件求交和缓存都失去局部性。AMD 一篇做实时光泽反射的文章说得很直白:表面越粗糙,光线越发散,整体性能越差 4。

采样算法在粗糙端也变钝。 通用的做法是按可见法线采样:先抽一个微平面朝向,再按它算反射方向。当粗糙度拉到 1 且正对观察时,这套采样吐出的反射方向会均匀铺满整个球面,其中一半落在表面背面、只能丢弃——比“在上半球均匀乱扔”还浪费一倍 5。

还有能量的账。 只让光线在小平面之间弹一次,粗糙度越高,被相邻小平面挡住、因而丢失的能量越多,表面越粗糙越暗。这在金属上尤其明显(金属全靠镜面反射),粗糙金属会暗到像深色塑料;要补回来,就得把微表面之间的多次弹射也算进去 1。

所以真正难的不是两端,而是中间。太光滑的一端,一条光线就是答案;太粗糙的一端,反射已经接近漫反射,可以交给低分辨率的探针或光照缓存去糊——很多实时方案确实按一个粗糙度阈值把两边分开处理 4。夹在中间的“半光滑”最难:既糊不掉(一糊就丢掉反射里那个物体的轮廓,一眼假),又必须发够光线才不吵。材质作者多半都撞过这堵墙:车漆、清漆、抛光瓷砖这类表面,一个锥描述不完,所以引擎干脆允许叠第二层反射——一层很光的镜面加一层粗糙的底,各管一段 2。

从画面反推参数

  • 金属看起来像深色塑料:可能缺了菲涅耳,也可能粗糙度太高、单次散射把能量丢光了。
  • 物体边缘没有变亮:菲涅耳缺失,模型还停留在老式的“高光强度”。
  • 反射里该有的东西不见了,或者糊得离谱:反射档位过低,光追反射被换成了探针。