跳到主要内容

11. 介电质

像水、玻璃和钻石这样的透明材料都是介电质。当一条光线打到它们时,光线会分成一条反射光线和一条折射(透射)光线。我们会通过在反射和折射之间随机选择来处理这件事,也就是每次相互作用只生成一条散射光线。

先快速复习一下术语:反射光线会打到表面,然后“弹开”到新的方向。

折射光线则会在从材料周围环境进入材料本身时发生弯折,例如进入玻璃或水中。这就是为什么铅笔部分插入水中时,看起来会像是弯掉了一样。

折射光线弯折的程度由材料的折射率决定。一般来说,折射率是一个单一数值,用来描述光从真空进入某种材料时会弯折多少。玻璃的折射率大约是 1.5 到 1.7,钻石大约是 2.4,而空气的折射率很小,约为 1.000293。

当一个透明材料被放在另一个不同的透明材料中时,可以用相对折射率来描述折射:也就是物体材料的折射率除以周围材料的折射率。例如,如果你想渲染水中的玻璃球,那么玻璃球的有效折射率会是 1.125。这是由玻璃的折射率 1.5 除以水的折射率 1.333 得到的。

你可以通过快速的网络搜索,找到大多数常见材料的折射率。

11.1 折射

最难调试的部分是折射光线。我通常会先让所有光线都发生折射,只要存在折射光线就这么做。对于这个项目,我试着在场景中放入两个玻璃球,然后得到了下面这样的结果(我还没告诉你这样做是对还是错,不过很快就会讲到!):

这样对吗?玻璃球在现实生活中看起来确实有点怪。但不,这并不对。整个世界应该上下颠倒,而且不应该有那些奇怪的黑色东西。我只是把穿过图像正中央的光线打印出来看了一下,结果明显是错的。这招通常就能抓出问题。

11.2 斯涅尔定律

折射可以用斯涅尔定律来描述:

ηsinθ=ηsinθ\eta \cdot \sin\theta = \eta' \cdot \sin\theta'

其中,θ\thetaθ\theta' 是相对于法线的角度,而 η\etaη\eta' 读作 “eta” 和 “eta prime”,表示折射率。几何关系如下:

为了确定折射光线的方向,我们需要解出 sinθ\sin\theta'

sinθ=ηηsinθ\sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta

在表面的折射侧,有一条折射光线 R\mathbf{R}' 和一条法线 n\mathbf{n}',它们之间存在一个角度 θ\theta'。我们可以把 R\mathbf{R}' 分解成垂直于 n\mathbf{n}' 和平行于 n\mathbf{n}' 的两个部分:

R=R+R\mathbf{R}' = \mathbf{R}'*\perp + \mathbf{R}'*\parallel

如果我们解出 R\mathbf{R}'*\perpR\mathbf{R}'*\parallel,就会得到:

R=ηη(R+Rcos(θ)n)\mathbf{R}'_\perp = \frac{\eta}{\eta'}(\mathbf{R} + |\mathbf{R}|\cos(\theta)\mathbf{n}) R=1R2n\mathbf{R}'*\parallel = -\sqrt{1 - |\mathbf{R}'*\perp|^2}\mathbf{n}

如果你愿意,可以自己继续证明这个式子,但我们会先把它当成事实继续往下走。本书后面的内容不要求你理解这个证明。

我们知道右边每一项的值,除了 cosθ\cos\theta 之外。众所周知,两个向量的点积可以用它们之间夹角的余弦来解释:

ab=abcosθ\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}||\mathbf{b}|\cos\theta

如果我们限制 a\mathbf{a}b\mathbf{b} 都是单位向量:

ab=cosθ\mathbf{a} \cdot \mathbf{b} = \cos\theta

现在我们可以用已知量重写 R\mathbf{R}'_\perp

R=ηη(R+(Rn)n)\mathbf{R}'_\perp = \frac{\eta}{\eta'}(\mathbf{R} + (-\mathbf{R} \cdot \mathbf{n})\mathbf{n})

当我们把这些部分重新组合起来,就可以写一个函数来计算 R\mathbf{R}'

...

inline vec3 reflect(const vec3& v, const vec3& n) {
return v - 2*dot(v,n)*n;
}

inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
auto cos_theta = std::fmin(dot(-uv, n), 1.0);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n);
vec3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
}

而总是折射的介电材质如下:

...

class metal : public material {
...
};

class dielectric : public material {
public:
dielectric(double refraction_index) : refraction_index(refraction_index) {}

bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;

vec3 unit_direction = unit_vector(r_in.direction());
vec3 refracted = refract(unit_direction, rec.normal, ri);

scattered = ray(rec.p, refracted);
return true;
}

private:
// 真空或空气中的折射率,或者材料折射率与包围介质折射率的比值
double refraction_index;
};

现在,我们将更新场景来展示折射效果:把左边的球改成玻璃材质,玻璃的折射率大约是 1.5。

auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.50);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);

其给出以下结果:

11.3 全内反射

折射中有一个比较麻烦的实际问题:对于某些光线角度,用斯涅尔定律是得不到解的。当一条光线以足够贴近表面的掠射角进入折射率较低的介质时,它可能会以大于 9090^\circ 的角度发生折射。如果我们回到斯涅尔定律以及 sinθ\sin\theta' 的推导:

sinθ=ηηsinθ\sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta

如果光线在玻璃内部,而外部是空气,也就是 η=1.5\eta = 1.5η=1.0\eta' = 1.0

sinθ=1.51.0sinθ\sin\theta' = \frac{1.5}{1.0} \cdot \sin\theta

sinθ\sin\theta' 的值不可能大于 1。所以,如果

1.51.0sinθ>1.0\frac{1.5}{1.0} \cdot \sin\theta > 1.0

方程两边的相等关系就被破坏了,也就不存在解。如果不存在解,玻璃就不能发生折射,因此必须反射这条光线:

if (ri * sin_theta > 1.0) {
// 必须折射
...
} else {
// 可以折射
...
}

这里所有光线都会被反射,而且因为在实际中这种情况通常发生在实体物体内部,所以它被称为全内反射 (total internal relfection)。这就是为什么有时候,当你潜在水下时,水和空气的交界面会像一面完美的镜子:如果你在水下向上看,可以看到水面上的东西;但当你靠近水面并侧向看时,水面看起来就像镜子一样。

我们可以用三角恒等式求出 sin_theta

sinθ=1cos2θ\sin\theta = \sqrt{1 - \cos^2\theta}

以及:

cosθ=Rn\cos\theta = \mathbf{R} \cdot \mathbf{n}
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);

if (ri * sin_theta > 1.0) {
// 必须折射
...
} else {
// 可以折射
...
}

且这个总是会发生折射的介电材质(在可能折射的情况下)为:

class dielectric : public material {
public:
dielectric(double refraction_index) : refraction_index(refraction_index) {}

bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;

vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);

bool cannot_refract = ri * sin_theta > 1.0;
vec3 direction;

if (cannot_refract)
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, ri);

scattered = ray(rec.p, direction);
return true;
}

private:
// 真空或空气中的折射率,或者材料折射率与包围介质折射率的比值
double refraction_index;
};

衰减始终为 1 —— 玻璃表面不会吸收任何东西。

如果我们用新的 dielectric::scatter() 函数渲染之前的场景,会发现……没有变化。嗯?

其实原因是:对于一个折射率大于空气的球体材料来说,不存在任何入射角会产生全内反射——无论是在光线进入球体的位置,还是在光线离开球体的位置都不会。这是由球体的几何形状导致的,因为一条掠射入射的光线总会先被弯折到一个更小的角度,然后在离开时又被弯回原本的角度。

那么我们要怎样展示全内反射呢?如果球体的折射率小于它所在介质的折射率,那么我们就可以用较浅的掠射角打到它,从而得到全外反射。这样应该足够观察到这个效果了。

我们会建模一个充满水的世界,水的折射率大约是 1.33,然后把球体材质改成空气,空气的折射率是 1.00 —— 也就是一个气泡!为此,需要把左边球体材质的折射率改成:

空气的折射率水的折射率\frac{\text{空气的折射率}}{\text{水的折射率}}
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.00 / 1.33);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);

这个改变给出以下渲染图:

在这里可以看到,或多或少接近直射的光线会折射,而擦着表面掠过的光线会反射。

11.4 Schlick 近似

现实中的玻璃具有随角度变化的反射率——以很陡的角度看窗户时,它就会变得像一面镜子。描述这个现象有一个很大、很丑的方程,但几乎所有人都会使用 Christophe Schlick 提出的一个开销较小且出奇准确的多项式近似。这样我们就得到了完整的玻璃材质:

class dielectric : public material {
public:
dielectric(double refraction_index) : refraction_index(refraction_index) {}

bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;

vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);

bool cannot_refract = ri * sin_theta > 1.0;
vec3 direction;

if (cannot_refract || reflectance(cos_theta, ri) > random_double())
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, ri);

scattered = ray(rec.p, direction);
return true;
}

private:
// 真空或空气中的折射率,或者材料折射率与包围介质折射率的比值
double refraction_index;

static double reflectance(double cosine, double refraction_index) {
// 使用 Schlick 近似来计算折射率
auto r0 = (1 - refraction_index) / (1 + refraction_index);
r0 = r0*r0;
return r0 + (1-r0)*std::pow((1 - cosine),5);
}
};

11.5 建模空心玻璃球

我们来建模一个空心玻璃球。它是一个具有一定厚度的球体,内部还有另一个由空气组成的球。如果你思考一条光线穿过这种物体的路径,它会先打到外层球体,发生折射;然后打到内层球体(假设确实打到了),再次折射,并在内部空气中传播。接着它会继续前进,打到内层球体的内侧表面,折射回去,然后打到外层球体的内侧表面,最后再次折射,退出并回到场景的大气中。

外层球体可以直接用标准的玻璃球建模,折射率大约为 1.50,也就是模拟光线从外部空气进入玻璃。内层球体稍微有点不同,因为它的折射率应该相对于周围外层球体的材料来定义,也就是模拟从玻璃进入内部空气的过渡。

这其实很容易指定,因为介电材质中的 refraction_index 参数可以被解释为:物体材料的折射率除以包围介质的折射率。在这个例子中,内层球体的折射率就是空气的折射率(内层球体材料)除以玻璃的折射率(包围介质),也就是:1.00/1.50=0.671.00 / 1.50 = 0.67

代码如下:

...
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.50);
auto material_bubble = make_shared<dielectric>(1.00 / 1.50);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0);

world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.2), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.4, material_bubble));
world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right));
...

以下是结果: