9.1. 创建一个抽象的材质类
如果我们想要有着不同材质的不同物体,还需要做进一步的优化。我们将创建一个有着众多参数的通用材质类,如果不需要某项参数,我们可以将它令为0。这是一个不错的设计。或者我们还可以创建一个封装了所以方法的抽象类。我比较欣赏第二种。在我们的程序中,材质类需要注意以下两点:
1. 可以产生散射的光线(或者说吸收入射光线)
2. 如果散射完成,需要明确有多少光线应该衰减。
遵循这些建议的抽象类如下:
代码41:[Material.h]材质类
9.2. 创建一个描述光线物体相交的结构体
之前的HitRecord结构体忽略了许多的参数,现在我们将我们想要的添加到它里面。你可以在它实例化后使用这些参数;这是风格的问题。可被击中的物体和材质都需要可以互相访问对方,这里提供一些关键的参考。在C++中,你只需要在提醒编译器,可被击中类中有指向“材质类”的指针,如下所示:
代码42:[Hittable.h] 在HitRecord结构体中添加了指向Material的指针
我们这么设置的原因是,材质类将告诉我们光线如何与物体表面交互。HieRecord类只是一种将一组参数填充到结构中的方法,以便我们可以将它们作为一个组发送。当一条光线击中表面(比如说一个小球),在main函数中,我们设置HitRecord中需要指向的特定材质指针,当ray_color获取HitRecord时,它可以调用Material的成员方法来获得散射光线。
我们必须设置一个对材质类的引用,以便HitRecord类。代码如下:
代码43:[Sphere.h和Sphere.cpp] 添加了材质信息的球体类
9.3. 对灯光散射和反射建模
我们已经有了Lambertian(漫反射)模型,它可以总是散射,也可以通过反射系数R来减弱,或者可以不减弱来散射,但是1-R的光线。或者混合这两种策略。对于Lambertian材质,我们可以得到以下简单类:
代码44:[material.h和material]Lambertian模型的材质类
注意,我们也可以仅以某种概率p进行散射,衰减为albedo/p。如果你愿意的话。
如果您仔细阅读上面的代码,您会注意到有一个小小的危害。如果我们生成的随机单位向量总是和法向量相反,那么它们相加结果可能是0,这就意味着我们的散射向量为0。这是一个糟糕的情况(还比如无穷和NaNs),所以我们要在它活动前拦住它。
综上所述,我们需要创造一个新的向量方法——near_zero()——如果向量在所有维度上都非常接近零,它将返回true。
代码45:[RTWeekend.h]near_zero函数
代码46:[Material.h]Lambertian散射模型
9.4. 灯光的镜面反射
对于光滑金属,光线不会随机散射。关键的数学问题是:光线如何从金属镜反射?向量数学可以帮助我们:
图11:光线反射
反射光线的方向是v+2b,如红色所示。按我们的想法来说,n为单位向量,但是v可能并不是。B的长度应该是v*n。因为v指向内部,方向相反,我们需要为其添加一个负号。如下:
代码47:[RTWeekend.h]Vec3反射函数
反射材质类的定义如下:
代码48:[material.h]反射材质
我们需要向下面这样修改ray_color函数:
代码49:散射和反射的光颜色
9.5. 多金属球的场景
现在让我们在场景中多加入一些金属球,将代码添加到main方法中:
代码50:[main.cpp]添加一些金属球到场景中
之后我们就会得到:
结果11:闪亮的金属球们
9.6. 模糊(fuzzy)反射
我们也可以通过一个小球选择一个随机的反射方向,并且为光线选择一个新的终止点:
图12:生成模糊反射的光线
球体越大,反射越模糊。这里建议添加一个模糊度参数,该参数正好是球体的半径(因此0也可以)。问题是,对于大球体或掠射射线,我们可能会散射到表面以下。我们可以设置一个表面来吸收这些:
代码51:[Material.h]使用模糊反射的金属材质
然后我们将场景左侧的球模糊系数设置为0.3,右侧的球设置为1。
代码52:[main.cpp]设置模糊系数
结果12:含有模糊系数的金属
水、玻璃和钻石等透明材料是电介质。当一束光线击中它们时,同时被反射和折射(透射)。我们通过在反射和折射之间随机选择来处理它们,并且每次交互仅生成一条散射光线。
10.1. 折射
最难处理的是折射光线。如果有折射光线的话,我通常首先让所有的光折射。在这个工程中,我们试着将两个玻璃球放入场景,然后我们将得到如下结果(之后再讲它正确与否,这里原文作者没有给出代码):
结果13:玻璃材质
这样做是正确的吗?玻璃球看起来和显示生活中的不太一样啊。这样做是不对的。玻璃球中的景象应该是颠倒过来的,并且也不应该有奇怪的黑色。我们只是直接从图像中间展示出光线,这显然是错误的。这个错误经常犯。
10.2. 斯涅尔定律(Snell's Law)
遵循斯涅尔定律的折射方程为:
这里 θ 和 θ′ 是角度与法线的夹角,η 和 η′ (发音为“eta”和“eta prime”) 是折射率(空气=1.0,玻璃=1.3-1.7,钻石=2.4)。图示为:
图13:光线折射
为了确定折射光线的方向,我们可以解出sinθ′:
在曲面的折射侧有一条折射光线 R′ 和法线 n′ ,它们之间的夹角为 θ′。我们可以将R′分别看成是与n′垂直的部分和与n′平行的部分:
将R′⊥和R′∥分解我们可以得到:
如果你愿意,你可以自己来证明以下,但这里我们会将其视为事实并继续前进。本文的其余部分不要求你证明。
我们需要解出cosθ。众所周知,两个向量点乘可以求出它们夹角的余弦值。
当a和b均为单位向量时:
让我们在此基础上重新定义R′⊥:
我们将其组合在一起,并编写一个函数用来计算R′ :
代码53:[RTWeekend.h]折射函数
始终会发生折射现象的介电材质定义如下:
代码54:[Material.h和Materal.cpp]介电材质
马上在我们的场景添加两个介电材质,看看效果:
代码55:[main.cpp]将场景中间和左边的球材质变成介电材质
结果如下:
结果14:玻璃球总是折射
10.3. 全反射
这看起来绝对不对。一个棘手的实际问题是,当光线位于折射率较高的材料中时,斯涅尔定律没有真实的解,以至于没有体现出折射。如果我们回到涅尔定律和sinθ′的推导:
当光线在玻璃内部和外部时:(η = 1.5,η′= 1.0)
sinθ′的值是不可能超过1的,所以,如果:
方程两边的等式被打破了,并且结果是不存在的。这样,玻璃便无法进行折射,因此必须反射光线:
代码57: [Material.cpp]分情况讨论
事实是在固体的内部通常会被全部反射,这种现象,我们称之为全反射。这也就是,当你潜水时,水和空气的边界看起来就和镜子一样的原因。我们可以通过三角函数求出sinθ:
然后:
代码57:[Material.cpp]明确cosθ和sinθ
然后我们的完整代码介电材质定义如下:
代码58:[Material.cpp]介电材质类中的散射方法
衰减始终为1——玻璃表面不吸收任何东西。如果我们用这些参数进行试验:
代码59:添加介电材质和光滑球体
我们将得到:
结果15:不会一直折射的玻璃球体
10.4. Schlick近似方法(Schlick Approximation)
现在,真正的玻璃具有随角度变化的反射率——以较小的角度观察窗户,它就会成为一面镜子。这里有一个丑陋的方程,但它被广泛使用,克里斯托夫·施利克(Christophe Schlick)的简单多项式确出乎意料的模拟出了这种现象。这里是我们的全玻璃材质:
代码60:[main.cpp]全玻璃材质
10.5. 对空心玻璃球体建模
介电材质球体的一个有趣且简单的技巧是,如果使用负半径,几何体不会受到影响,但曲面法线指向内部。这点可以看作用气泡来制作空心玻璃球:
代码61:[main.cpp]在场景中添加了一个空心玻璃球体
运行代码我们将会得到:
结果16:一个空心的玻璃球