首发于Milo的编程

用 C 语言画光(六):菲涅耳方程

上一篇我们实现了折射,也假设了反射和折射的比重是恒定的。但现实上,这个比重与材质和入射角相关。

题图是我 2006 年在九寨沟拍摄的相片。相片底部,视角较接近垂直,可以看到较清晰的水下景物,倒影较暗;而在远处,视角较与水面平行,则几乎只看见倒影,难以看到水下景物。

你现在也可以把一个马克杯装满水,放在屏幕前观察水面。如果眼睛接近水平面,就可以清楚地看到屏幕倒影;如果视点往上移,那么屏幕倒影变暗,更多看到杯里的颜色。

这个现象称为菲涅耳反射(Fresnel reflectance)。奥古斯丁·菲涅耳(Augustin Fresnel, 1788-1827)为法国物理学家,注意 Fresnel 为法语姓氏,s 是不发音的。

1. 菲涅耳方程

菲涅耳方程(Fresnel equation)描述了光线经过两个介质的界面时,反射和透射的光强比重。参考下图的符号:

当中 R \in [0, 1] 为反射比,因能量守恒,透射比为 T=1-R

对于电介质(dielectric)而言,菲涅耳方程为:

\begin{align} R_s &= \left(\frac{\eta_1\cos\theta_i-\eta_2\cos\theta_t}{\eta_1\cos\theta_i+\eta_2\cos\theta_t}\right)^2\\ R_p &=\left(\frac{\eta_1\cos\theta_t-\eta_2\cos\theta_i}{\eta_1\cos\theta_t+\eta_2\cos\theta_i}\right)^2 \end{align}\tag{1}

R_sR_p 分别对应入射光的 s 偏振(senkrecht polarized)和 p 偏振(parallel polarized)所造成的反射比。图形学中通常考虑光是无偏振的(unpolarized),也就是两种偏振是等量的,所以可以取其平均值:

R = \frac{R_s + R_p}{2}\tag{2}

那么,给定入射角、透射角的余弦,以及两个介质的折射率,就可简单实现:

float fresnel(float cosi, float cost, float etai, float etat) {
    float rs = (etat * cosi - etai * cost) / (etat * cosi + etai * cost);
    float rp = (etai * cosi - etat * cost) / (etai * cosi + etat * cost);
    return (rs * rs + rp * rp) * 0.5f;
}

透射角实际上是使用斯涅尔定律从入射角得出的,所以假设折射率是常数,菲涅耳反射比是入射角的函数。下图展示了光线从空气射向不同材质时的菲涅耳反射比(来自[1] P. 233,R_F为菲涅耳反射比):

在趋向 90^\circ 入射角时,不论什么材质反射比都趋向 1。

另外,导体(conductor)和半导体(semiconductor)的菲涅耳方程会更复杂一些,而且不同对波长的影响较大(上图中红色和紫色曲线分别对应 RGB 三种波长),我们稍后再讨论。

2. 修改实现

这个函数用于取代原来恒定的 \texttt{reflectivity} ,我们原来的追踪代码是这样的:

if (depth < MAX_DEPTH && (r.reflectivity > 0.0f || r.eta > 0.0f)) {
    /* ... */
    if (r.eta > 0.0f) {
        if (refract(dx, dy, nx, ny, sign < 0.0f ? r.eta : 1.0f / r.eta, &rx, &ry))
            sum += (1.0f - refl) * trace(x - nx * BIAS, y - ny * BIAS, rx, ry, depth + 1);
        else
            refl = 1.0f; // Total internal reflection
    }
    /* ... */
}

当有折射发生时,我们利用点积计算入射角和透射角的余弦,然后用 \texttt{fresnel()} 获取反射比。和调用 \texttt{refract()} 时相似,我们要考虑光是从空气(\eta=1)进入物体,还是相反:

if (depth < MAX_DEPTH && (r.reflectivity > 0.0f || r.eta > 0.0f)) {
    /* ... */
    if (r.eta > 0.0f) {
        if (refract(dx, dy, nx, ny, sign < 0.0f ? r.eta : 1.0f / r.eta, &rx, &ry)) {
            float cosi = -(dx * nx + dy * ny);
            float cost = -(rx * nx + ry * ny);
            refl = sign < 0.0f ? fresnel(cosi, cost, r.eta, 1.0f) : fresnel(cosi, cost, 1.0f, r.eta);
            sum += (1.0f - refl) * trace(x - nx * BIAS, y - ny * BIAS, rx, ry, depth + 1);
        }
        else
            refl = 1.0f; // Total internal reflection
    }
    /* ... */
}

我们比较一下,上一篇使用恒定反射比和本篇使用菲涅耳方程的渲染结果:

恒定 20% 的反射比
使用菲涅耳方程

比较明显的区别是,光线垂直射向凸透镜和凹透镜时,使用恒定反比射会产生很亮的反射光斑,而使用菲涅耳方程的反射光斑就会暗得多,大部分光能穿过了透镜。

3. Schlick 近似

鉴于导体的菲涅耳方程较复杂,Schlick [2] 提供了一个近似的函数:

R(\theta_i) \approx R(0)+(1-R(0))(1-\cos\theta_i)^5\tag{3}

此函数只需要对材质提供光线垂直反射时的 R(0) 值,就能近似地计算出不同入射角旳菲涅耳反射比。下图展示此近似函数(虚线)与菲涅耳方程(实线)的比较(自 [1] P. 234):

对于电介质,我们可以使用 (2) 计算出 R(0)

R(0)=\left(\frac{\eta_i-\eta_t}{\eta_i+\eta_t}\right)^2\tag{4}

对于导体,我们就需要提供 R(0) 的值(可能需要多个频率,如 RGB)。另一点要注意的事,从低折射率材质到高折射率材质时,要使用 \theta_t

float schlick(float cosi, float cost, float etai, float etat) {
    float r0 = (etai - etat) / (etai + etat);
    r0 *= r0;
    float a = 1.0f - (etai < etat ? cosi : cost);
    float aa = a * a;
    return r0 + (1.0f - r0) * aa * aa * a;
}

这个函数采用和 \texttt{fresnel()} 相同的接口,可以在例子中替换。但如果要支持导体,则要改变接口。

这个例子中用 Schlick 近似并没有肉眼能分辨的差异,就不显示渲染结果了。

4. 结语

我们依据菲涅耳方程,以入射角及材质特性推断出光线的反射比和透射比,这样更接近真实世界的情况。

Schlick 近似被广泛应用在实时的基于物理渲染(physically based rendering, PBR)中,除了运算简单快捷,以 R(0) 作为参数更容易让美术理解──白光直射材质所反射的颜色。用物理量(如本文未提及的波阻抗和磁导率)作为参数的话就很不友好。

本文的代码位于 fresnel.c

参考

[1] Akenine-Möller, Tomas, Eric Haines, and Naty Hoffman. Real-time rendering, Third Edition. CRC Press, 2008.

[2] Schlick, Christophe. "An Inexpensive BRDF Model for Physically‐based Rendering." Computer graphics forum. Vol. 13. No. 3. Blackwell Science Ltd, 1994.


更新1:谢 @codeworm96 指出,公式 (4) 右侧需平方,已修正。

编辑于 2018-01-03

文章被以下专栏收录