LOFTER for ipad —— 让兴趣,更有趣

点击下载 关闭
一周学会光线追踪:理论和实战(三)

9. 材质

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. 介点材质(Dielectrics)

       水、玻璃和钻石等透明材料是电介质。当一束光线击中它们时,同时被反射和折射(透射)。我们通过在反射和折射之间随机选择来处理它们,并且每次交互仅生成一条散射光线。

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:一个空心的玻璃球

推荐文章
评论(0)
分享到
转载我的主页