Ray Tracing in One Weekend 读书笔记

以下内容整理于Peter Shirley的《Ray Tracing in One Weekend》,加入了一些自己的解读,若有错漏之处欢迎读者指出。原书可在这里免费下载。

第0章 概述

作者已经从事图形学教学多年,常用的教学方法就是光线追踪(ray tracing)。因为这使得我们被迫自己编写代码,并且即使没有API的帮助,我们也可以得到很酷的照片。我们要实现的不是一个特性完备的光线追踪器,但它的确拥有了间接光照。从技术上讲,我们即将实现的是一个路径追踪器,并且是一个很普通的路径追踪器。(光线追踪和路径追踪的区别参考这篇文章

第1章 输出照片

本书生成的照片使用PPM格式。一个PPM文件的示例如下:
在这里插入图片描述
如图中注释所示,第一行“P3”代表颜色值是用ASCII编码的,然后第二行声明了像素的行数和列数。第三行声明了像素值的最大值。然后接下来是很多个RGB颜色向量值。(可以每行写入一个RGB颜色向量,文件会以前面声明的行数和列数进行解析。)
让我们来实践一下:
在这里插入图片描述
以上代码的输出结果
在这里插入图片描述
(我是在win10商店安装了Image Viewer Pro来查看PPM文件的,使用其他软件例如Photoshop也可以。)

第2章 vec3类

我们需要一个类来存储几何向量以及颜色。在很多系统上这些向量被设计成4D的(3D再加上齐次坐标或者RGB加上一个alpha透明通道)。但是我们的系统只需要三维向量即可。为了以更少的代码完成工作,我们将设计一个vec3类,用来存储颜色、位置、方向等等。以下是我们设计的vec3类。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后我们就可以修改我们的main函数来使用这个类了:
在这里插入图片描述

第3章 光线、一个简单的相机和背景

有一个类是所有光线追踪器都有的,那就是ray类。我们把光线认为是一个函数 p ( t ) = A + t ∗ B ⃗ p(t)=A+t*\vec B p(t)=A+tB p p p表示的是三维空间中沿着一条直线的点。 A A A是光线的起点, B ⃗ \vec B B 是光线的方向向量。参数 t t t是一个实数(在代码中用float表示)。当 t t t取不同值得时候, p ( t ) p(t) p(t)表示沿着光线移动的点。当 t t t取所有实数时, p ( t ) p(t) p(t)表示了三维空间中的整条线。当 t > 0 t>0 t>0时, p ( t ) p(t) p(t)仅表示在 A A A沿着 B B B的方向上的部分,这就是我们常说的射线或光线。 C = p ( 2 ) C=p(2) C=p(2)的示例如下。( C C C就是图中 t = 2 t=2 t=2那个点。)
在这里插入图片描述
我们编写的光线类如下:
在这里插入图片描述
类中的point_at_parameter函数即是 p ( t ) p(t) p(t)函数,返回当参数为 t t t A + t ∗ B ⃗ A+t*\vec B A+tB 代表的点。
现在我们可以开始编写光线追踪器了。光线追踪器的核心是从像素发射出光线,然后计算在这些光线的方向上看到了什么颜色。计算方式是,从眼睛发出指向某个像素的射线,然后计算这条射线与什么物体相交,把相交点处的颜色计算出来,便是这个像素的颜色。如下图所示,屏幕上像素点M的颜色值就是光线与球体相交点P的颜色值。
在这里插入图片描述
让我们来编写一个简单的 c o l o r ( r a y ) color(ray) color(ray)函数来返回背景的颜色。
为了让调试的时候不至于混淆 x x x轴和 y y y轴,我们决定生成200x100的照片。我们把“眼睛”(也就是摄像机的中心)放在 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)这个点。从眼睛看向屏幕的方向上,我们让y轴向上,x轴向右。同时,我们采用右手坐标系,所以穿入屏幕的方向是z轴的负方向。我们会从屏幕左下角开始遍历屏幕像素,通过使用两个沿着屏幕边缘的偏置向量,我们可以控制光线穿过屏幕的像素点,如下图所示。
在这里插入图片描述
注意我们并没有把光线的方向向量设定为一个单位向量,因为这样会使得代码更简洁,程序运行速度更快。
在下面的代码中,光线 r r r 会大致穿过像素的中心(我们不用担心这个精确度,因为很快我们会加入抗锯齿的功能)。
在这里插入图片描述
c o l o r ( r a y ) color(ray) color(ray)函数根据y轴的上下程度线性地混合了白色和蓝色。我们先把 r r r 单位化,所以 − 1.0 < y < 1.0 -1.0<y<1.0 1.0<y<1.0。所以 0.0 < 0.5 ∗ ( y + 1 ) < 1.0 0.0<0.5*(y+1)<1.0 0.0<0.5(y+1)<1.0。当t取1时我们得到蓝色,当t取0时我们得到白色。当t取中间值时我们便得到了混合色。这就形成了两种颜色之间的“线性混合”/“线性插值”。运行以上代码,我们得到这个照片:
在这里插入图片描述

第4章 增加一个球

让我们来向我们的光线追踪器加入一个物体。在编写光线追踪器的时候,人们常常使用球体作为被观察物体,因为计算光线光线是否与球体相交是很简单的。回忆一下,球心在坐标原点,半径为R的球体方程为 x 2 + y 2 + z 2 = R 2 x^2+y^2+z^2=R^2 x2+y2+z2=R2。我们知道,对于一个点 P ( x 0 , y 0 , z 0 ) P(x_0,y_0,z_0) P(x0,y0,z0),若 x 0 2 + y 0 2 + z 0 2 = R 2 x_0^2+y_0^2+z_0^2=R^2 x02+y02+z02=R2,那么 P P P点就在这个球体上,否则不在这个球体上。当球体的圆心位于 ( c x , c y , c z ) (cx,cy,cz) (cx,cy,cz)时,球体方程化为 ( x − c x ) 2 + ( y − c y ) 2 + ( z − c z ) 2 = R 2 (x-cx)^2+(y-cy)^2+(z-cz)^2=R^2 (xcx)2+(ycy)2+(zcz)2=R2。在图形学中,我们总希望方程是以向量的形式给出的。因为这样 x / y / z x/y/z x/y/z就可以用 v e c 3 vec3 vec3类的形式来表示。我们知道从圆心 C = ( c x , c y , c z ) C=(cx,cy,cz) C=(cx,cy,cz)指向 p = ( x , y , z ) p=(x,y,z) p=(x,y,z)的向量是 ( p − C ) (p-C) (pC)。由向量点乘公式有 ( p − C ) ⋅ ( p − C ) = ( x − c x ) 2 + ( y − c y ) 2 + ( z − c z ) 2 (p-C)·(p-C)=(x-cx)^2+(y-cy)^2+(z-cz)^2 (pC)(pC)=(xcx)2+(ycy)2+(zcz)2,所以球体方程用向量的形式来表示就是 ( p − C ) ⋅ ( p − C ) = R 2 (p-C)·(p-C)=R^2 (pC)(pC)=R2,任何满足这个方程的点 p p p都在这个球体上。所以我们为了知道光线 p ( t ) = A + t ∗ B ⃗ p(t)=A+t*\vec B p(t)=A+tB 是否与球体相交于某一点,我们只需把光线方程代入球体的向量方程,如果它们有交点,那么必定存在一个t使得方程 ( p ( t ) − C ) ⋅ ( p ( t ) − C ) = R 2 (p(t)-C)·(p(t)-C)=R^2 (p(t)C)(p(t)C)=R2成立。把光线方程展开就得到 ( A + t ∗ B ⃗ − C ) ⋅ ( A + t ∗ B ⃗ − C ) = R 2 (A+t*\vec B-C)·(A+t*\vec B-C)=R^2 (A+tB C)(A+tB C)=R2。为了解这个t的方程,我们整理一下得到 B ⃗ ⋅ B ⃗ ∗ t 2 + 2 ∗ B ⃗ ⋅ ( A − C ) ∗ t + ( A − C ) ⋅ ( A − C ) − R 2 = 0 \vec B·\vec B*t^2+2*\vec B·(A-C)*t+(A-C)·(A-C)-R^2=0 B B t2+2B (AC)t+(AC)(AC)R2=0。方程中的向量以及 R R R都是已知的常量,解这个关于 t t t的一元二次方程,我们就可以知道光线和球体是否相交,如下图所示。
在这里插入图片描述
让我们把这些数学公式编码到程序里,我们可以这样测试:我们在 z = − 1 z=-1 z=1处放置一个小球,如果光线和这个小球相交,那么我们把发出这条光线的像素渲染为红色。代码如下:
在这里插入图片描述
运行代码,我们得到这张图:
在这里插入图片描述
现在这张图缺少阴影、反射光等等,并且只有一个物体,但这是一个非常好的开始了。需要注意的是现在光线参数 t t t的范围是全体实数,也就是 t t t可以为负数。所以若你把上述代码中球体的圆心移动到 z = + 1 z=+1 z=+1处,那么你会得到相同的结果。这不是一个特性(这是一个bug…)!我们接下来会修复这个问题的。

第5章 表面法向量和多物体

首先,为了得到阴影,我们需要得到球体表面的法向量。这个向量与球体表面垂直并且指向外。为了更方便地求得阴影,我们让这些法向量为单位向量。对于一个圆心为 C C C的球体来说,球体表面 P P P点处的法向量是 ( P − C ) (P-C) (PC),如下图所示:
在这里插入图片描述
我们现在还没有光线,所以让我们通过颜色图的形式把法向量可视化。一个常用的可视化法向量的技巧是把法向量的x/y/z各分量值映射到0和1之间,然后对应地转化为颜色的r/g/b值。为了得到法向量,我们必须知道光线与球体相交的点的坐标,而不是仅仅知道是否相交。让我们来求离眼睛最近的相交点(对应的t最小)。代码如下:
在这里插入图片描述
运行代码,我们得到这个照片:
在这里插入图片描述
现在让我们尝试实现多个球体。我们设计一个抽象类来描述光线可能与之相交的物体。让我们把它称之为"hitable"类。hitable类会有一个hit函数,以ray作为输入参数。大多数光线追踪器都会为了方便而设置 t m i n tmin tmin t m a x tmax tmax,只有当相交点的 t t t 满足 t m i n < t < t m a x tmin<t<tmax tmin<t<tmax 的时候我们才计算这次相交。有一个设计问题是当我们的光线与某个物体相交时,是否需要计算该交点的法向量。我们只需要最近的相交物体的法线。让我们把相交处的信息计算出来并存入一个结构体hit_record中。我知道我们在某些时候需要景深效果,所以我之后会增加一个时间的输入变量。这是抽象类hitable的设计:
在这里插入图片描述
这是球体类sphere的设计(注意在求解方程时对计算进行了化简):
在这里插入图片描述
在这里插入图片描述
在计算相交点的时候,我们先计算可能存在的较小的 t t t的解 − B − B 2 − 4 ∗ A ∗ C 2 ∗ A \frac{-B-\sqrt{B^2-4*A*C}}{2*A} 2ABB24AC (注意代码进行了变量的带入与化简)。若该解存在并满足 t t t的范围则我们把这个点的信息记录下来并返回true。否则计算可能存在的另一个较大的解 − B + B 2 − 4 ∗ A ∗ C 2 ∗ A \frac{-B+\sqrt{B^2-4*A*C}}{2*A} 2AB+B24AC ,同理若该解存在并满足 t t t的范围则把这个点的信息记录下来并返回true。若上述两个解均不存在则返回false。
然后我们把一系列可相交的物体也抽象为一个类hitable_list:
在这里插入图片描述在这里插入图片描述
对于一系列物体,我们设置一个光线是否与任何物体相交的布尔变量hit_anything,变量closest_so_far记录的是目前为止t允许的最大值。我们遍历全部物体,若找到第一个碰撞的物体,我们就把hit_anything设置为true并更新closest_so_far为当前相交点的t值,然后记录碰撞的信息。意思就是接下来我们只考虑比当前碰撞点离眼睛更近的碰撞点,并不断更新最近碰撞点的信息。
接着我们更新一下main函数:
在这里插入图片描述
在这里插入图片描述
color函数通过调用hit函数来求最近的相交点然后对法向量进行可视化,若没有相交点则根据绘制背景色。把法向量可视化是一个查看模型是否有缺陷的好方法。生成的图片如下:
在这里插入图片描述

第6章 抗锯齿

一个真正的相机拍出来的照片在物体边缘处经常是没有锯齿的,因为边缘处的像素是一些前景和一些背景的混合色。我们可以通过在一个像素内采集多个样本颜色并求平均值来得到这样的效果。(“我们不会被分层问题困扰,这个问题是有争议性的但对我们的程序来说这是正常的。这对有些光线追踪器非常重要,但我们正在编写的是一个很普通的光线追踪器,所以这并不会对我们的光线追踪器有好处,而且会让我们的代码变得更丑。” 注:这句话我不理解啥意思…)我们会把相机类抽象出来然后我们之后就可以写一个更酷的相机了。
另外,我们需要一个生成真正的随机数生成器来生成 [ 0 , 1 ) [0,1) [0,1)范围内的随机实数。注意是左闭右开区间。原文作者使用了drand48()函数,然而我使用的是visual c++编译器,所以改用了C++的标准库来编写了get_random()函数,代码如下:
在这里插入图片描述
对于一个给定的像素,我们会有很多个样本在这个像素中并且会发送光线通过每个样本。这个像素的颜色就是这些样本颜色的平均值。如图所示:
在这里插入图片描述
综上所述我们可以封装出一个简单的相机类:
在这里插入图片描述
main函数也要改一下:
在这里插入图片描述
放大生成的图片就会发现,物体边缘的像素是前景和背景的混合色:
在这里插入图片描述

第7章 漫反射材质

现在我们有了物体(球体类)并且每个像素可以发射多条光线了,这样我们就可以编写出看起来很真实的材质。我们先从漫反射材质开始。有一个问题是我们是否应该把物体形状和材质混合并匹配起来,比如把球体设计成一种材质。对于一些几何体和材质互相链接的程序来说这可能是有用的。但正如很多渲染器所做的,我们会把它们分离开开来。但注意这样会有局限性。
漫反射材质的物体是不会发射光线的,它们仅仅反射周围环境的光线而获得颜色。不同颜色的材质会反射不同颜色的光线。当光线从一个漫反射材质表面进行反射时,它的反射光方向是随机的。所以如果我们往两个漫反射材质表面发射三条光线时,它们会有不同的随机的反射行为,如图:
在这里插入图片描述
除了反射,这些光线还有可能被吸收。漫反射材质表面越暗,吸收得越多(这就是为什么它们看起来比较暗的原因!)。事实上,任何能把反射光线方向随机化的算法都可以产生看起来不光滑的表面。其中有一个最简单的方法,这恰好是制作理想的漫反射材质表面的正确的方法。
我们在光线与物体表面相交点 p p p 做一个与表面相切的单位球。从这个球中随机选取一个点 s s s ,然后从 p p p 点发射一条指向 s s s 的光线。显然这个球的球心坐标是 p + N p+N p+N N N N是物体 p p p点上的单位法向量。
这样我们就需要一个方法来从上述球中选取一个随机点。从实现的角度来说,我们可以先从球心在坐标原点的单位球中随机选取一个点,然后再通过向量变换到与物体表面相切的球中。我们使用一个几乎是最简单的算法:首先我们随机生成三个处于 [ − 1 , 1 ] [-1,1] [1,1]的坐标点 x , y , z x,y,z x,y,z。然后如果我们生成的这个点位于原点单位球之外,我们就从新生成一个随机点,直至这个随机点刚好位于原点单位球之内。代码如下:
在这里插入图片描述
random_in_unit_sphere函数比较简单,循环体中的代码就是把get_random()生成的[0,1)上的随机数变换到[-1,1)上。color函数中求 t a r g e t target target向量坐标的图解如下:
在这里插入图片描述
上图中 x x x轴从原点垂直指向纸面外。假设从眼睛位置也就是坐标原点处发射处一条光线 O P OP OP,与待绘制球体 O 1 O_1 O1相交于 P P P点。我们可以得到位于该切点与 O 1 O_1 O1相切的单位球 O 2 O_2 O2。设切点处单位法向量为 N N N,那么球 O 2 O_2 O2的球心坐标即为 O P + N OP+N OP+N。为了求得 O 2 O_2 O2中的一个随机点,我们先求得了原点单位球中的随机点向量坐标 S S S,那么由向量运算的性质我们就知道 O P + N + S OP+N+S OP+N+S即为球 O 2 O_2 O2中与 S S S对应的随机坐标 S ‘ S^` S的坐标。代码中求 t a r g e t target target的坐标的代码就是这个含义。
求得发射光线的目标位置之后,我们就从光线入射点 P P P发射一条反射光线指向 t a r g e t target target。我们让点 P P P的颜色等于反射光线返回的颜色值的一半,这意味着有一半的光线被吸收了。通过这样递归地求解各像素颜色,就能创造出很真实的画面。这一小节与前面小节最大的不同是这里真正使用了光线追踪(ray tracing)的技术(递归求像素颜色)。而前面小节的内容是一个光线投射(ray casting)的过程。
运行上述代码,速度会稍慢,因为我们递归地计算了像素颜色,运算量比较大。最后得到这个结果:
在这里插入图片描述
注意球体下面是有阴影的。这张照片很暗,但我们的球体在光线每次反射的时候只是吸收了50%的能量。如果你看不到阴影也不用担心,我们马上会解决这个问题。实际上这个球应该看起来很亮才对。在实际中应该是一个亮灰色的球。事实上我们在观察所有照片的时候都默认它是已经经过了伽马校正的。关于伽马校正的内容可以参考这里。总之在得到一个颜色值 x x x之后我们应该求 x 1 / 2.2 x^{1/2.2} x1/2.2作为照片中的颜色值。为了方便我们直接利用平方根来求近似,也就是用 x \sqrt{x} x 来代替 x x x即可。代码如下:
在这里插入图片描述
经过伽马校正之后,我们得到了更真实的结果:
在这里插入图片描述
然而现在的代码有一个bug。有一些光线撞击在t=-0.00000001或者t=0.00000001这样的点上,而我们仍然把这些点的颜色计算进去了(不应该计算进去的)。所以我们应该把hit函数的tmin参数设置为稍大于0的数:
在这里插入图片描述
这会解决shadow acne问题(查了些资料还不是很清楚这个问题,待以后接触了再看看)。

第8章 金属

如果我们想要不同物体有不同的材质,我们就面临一个设计问题。我们可以设计一个matrial类,拥有很多的参数,然后不同的材质我们可能会不使用其中一些参数。这个方法还不赖。或者我们可以设计一个抽象的material类,封装了材质的共同行为。我们采取第二种方式。这个抽象的material类需要做两件事:

  1. 能够生成一个发散的光线。
  2. 如果散射出光线,那么会决定这个光线需要衰减多少。

我们的抽象类如下:
在这里插入图片描述
我们传入hit_record参数是因为这样可以避免需要传入的参数过多,这样我们就可以往这个结构体中放如任何想要传入的信息。当然你也可以把这些参数拆开传入,看你自己想怎么设计。hitable类和material类的头文件互相使用了对方,所以这里存在一个循环引用的问题。我们可以通过前向声明来解决这个问题:
在这里插入图片描述
material类会告诉我们光线是如何与物体表面相互作用的。我们通过hit_record来传入一堆参数。当光线撞击到物体表面时,比如与一个球体撞击时,hit_record类中的material类指针会指向所撞击的球体的材质成员。color函数中,如果光线的撞击成功发生,那么我们就可以调用hit_record里面的material类指针的成员函数scatter来查看光线的发射情况。
对于我们已有的漫反射材质来说,它可以始终散射并以反射系数 R R R进行衰减,或者它可以不衰减地进行反射但会吸收光线的 1 − R 1-R 1R,或者混合采取这两种策略。我们编写的漫反射材质类如下:
在这里插入图片描述
注意我们可以只是以某个概率 p p p来散射光线,或者我们只是把衰减率设为 a l b e d o / p albedo/p albedo/p,你可以自己选择一种实现。(原书代码似乎是直接把衰减率设为 a l b e d o albedo albedo,并没有设置一个 p p p)。
对于光滑的金属来说,光线不会随机地散射。光线撞击到金属的时候会遵循反射定律。图解如下:
在这里插入图片描述
由图可知, V 出 = V 入 + 2 B V_出=V_入+2B V=V+2B B B B是入射光线 V 入 V_入 V在法向量方向的分量取反(所以代码中是减号而不是加号)。求反射光线代码如下:
在这里插入图片描述
只反射光线的金属材质会使用这个公式:
在这里插入图片描述
scatter函数中,我们先求出反射光线向量坐标,然后以入射点作为起点,反射光线向量作为方向向量构造一条光线,这就是反射光线。然后我们设置衰减率。由于反射光线必定与表面法向量夹角为锐角(为钝角说明光源位于物体表面下面了,这样的情况不需要计算)。所以我们返回出射光线与表面法向量的点积是否大于0的结果(向量点积大于0说明夹角为锐角)。
我们需要修改color函数来使用上述代码:
在这里插入图片描述
我们还需要修改sphere类,让它拥有一个指向这个球的材质的material指针,修改后的sphere类如下:
在这里插入图片描述
在这里插入图片描述
然后修改一下main函数,添加几个金属球:
在这里插入图片描述
运行代码,得到以下照片:
在这里插入图片描述
我们还可以通过在一个很小的球体中为光线选择一个新的结束点来随机化发射光线。
在这里插入图片描述
这个选择随机点的球体越大,那么反射光线的可失真程度就越大。这启示了我们在金属材质类中添加一个反映可失真程度的变量,就是我们选择随机结束点的小球的半径。当小球半径为0时,金属材质的反射没有失真。需要注意如果这个选择随机结束点的球体太大,我们的反射光线有可能反射到物体表面下方去了,这不合理。所以我们会把这个半径限制为小于1的值。修改后金属材质代码如下:
在这里插入图片描述
让我们尝试生成两个失真程度分别为0.3和1.0的球体,修改一下main函数:
在这里插入图片描述
运行代码得到结果:
在这里插入图片描述

第9章 电介质

干净的材质例如水,玻璃和钻石都是电介质。当一条光线击中它们的时候,这条光线会分裂为反射光线和折射光线。为了实现这个,我们会随机选择当光线击中它们时是反射还是折射,并且每次击中只产生一条散射光线。
最难debug的部分就是折射光线了。我经常会这样做:如果存在一条折射光线,那就让所有光线都折射。假如这个项目,我得到以下这张图:
在这里插入图片描述
这正确吗?这两个玻璃球看起来很奇怪,很明显这是错误的。真实的世界里,玻璃应该会把周围的景象反过来,并且也不会出现黑色的一块。
光线折射遵循Snell定律:
n s i n ( t h e t a ) = n ‘ s i n ( t h e t a ‘ ) nsin(theta)=n^`sin(theta^`) nsin(theta)=nsin(theta)
这里 n n n n ‘ n^` n是折射系数(比如空气的折射系数为1,玻璃是1.3~1.7,钻石是2.4)。图示如下:
在这里插入图片描述
还有一个实践上的问题,就是当光线从较高折射率的介质进入到较低折射率的介质时,如果入射角大于某一临界角θc(光线远离法线)时,折射光线将会消失,snell定律会失效,这个时候没有折射光线。这就是全内反射。这也就是为什么当你在水下的时候,有时候水面和天空的交界处看起来像一块完美的镜子一样。因此折射的代码相比于反射会更复杂:
在这里插入图片描述
求折射光线的代码图解如下:
在这里插入图片描述
n i n_i ni n t n_t nt分别是两种介质的折射率。公式推导如下:
在这里插入图片描述
Snell定律知道 n i n t = s i n θ 2 s i n θ 1 \frac{n_i}{n_t}=\frac{sin\theta_2}{sin\theta_1} ntni=sinθ1sinθ2,所以代码中的 n i _ o v e r _ n t ni\_over\_nt ni_over_nt就是 n i n t \frac{n_i}{n_t} ntni。代码中的 d t dt dt就是 p o ⃗ ⋅ n ⃗ \vec {po}·\vec n po n ,也即是 − c o s θ 1 -cos\theta_1 cosθ1。代码中的 d i s c r i m i n a n t discriminant discriminant就是 ( c o s θ 2 ) 2 (cos\theta_2)^2 (cosθ2)2,所以只有 d i s c r i m i n a n t > 0 discriminant>0 discriminant>0时才会有折射光线。把推导过程中的①②两式代入(*)式就能得到求折射光线的代码了。注意这个过程默认是入射光线和折射光线均为单位向量,相当于我们求出的是折射光线的方向向量。
所以我们可以编写电介质材质类如下:
在这里插入图片描述
代码中的 r e f _ i d x ref\_idx ref_idx就是 n t n i \frac{n_t}{n_i} nint。代码判断了入射光线与法向量之间的夹角是否为锐角。当它们夹角为钝角时就是我们前面的推导过程(从空气入射到介质中)。夹角为锐角的话,就是从介质中折射到空气中,需要相应改一下代码。图解如下(红色的是光线,先从空气折射到介质球中,再从介质球中折射到空气中):
在这里插入图片描述
因为玻璃不会吸收光线,所以衰减率是1。让我们修改一下main函数代码来使用这个类:
在这里插入图片描述
运行结果如下:
在这里插入图片描述
在现实生活中,光线折射时,反射和折射的比重与材质和入射角相关。我们需要计算一个称为菲涅尔反射比的参数来描述这个比重。关于菲涅尔反射比可参考这篇文章。以下方程可以近似计算菲涅尔反射比:
R ( θ i ) ≈ R ( 0 ) + ( 1 − R ( 0 ) ) ( 1 − c o s θ i ) 5 R(\theta_i)≈R(0)+(1-R(0))(1-cos\theta_i)^5 R(θi)R(0)+(1R(0))(1cosθi)5,其中 R ( 0 ) = ( η i − η t η i + η t ) 2 R(0)=(\frac{\eta_i-\eta_t}{\eta_i+\eta_t})^2 R(0)=(ηi+ηtηiηt)2
其中 η i \eta_i ηi η t \eta_t ηt分别是入射前介质和入射后介质的折射率, c o s θ i cos\theta_i cosθi表示折射率较小的一边的光线与法线的夹角的余弦值,原书中给出的代码有误(后来发现作者在github给出的代码已经做了修改):
在这里插入图片描述
这里计算的是光线从玻璃球中折射到空气中的情况,这个 c o s i n e cosine cosine应该计算的是折射光线向量与球的表面法向量的夹角余弦值。如下图:
在这里插入图片描述
也就是 c o s i n e cosine cosine应该计算图中的 c o s θ 3 cos\theta_3 cosθ3。从代码实现的角度来说,我们可以先计算折射光线向量再来计算这个 c o s i n e cosine cosine
在这里插入图片描述
如果要保持原代码的结构,那么需要利用我们前文推导的计算 c o s θ 2 cos\theta_2 cosθ2的公式来计算:
在这里插入图片描述
为了提高计算效率,我们先计算折射光线向量再计算 c o s i n e cosine cosine,完整的电介质材质类如下:
在这里插入图片描述
运行代码生成图片:
在这里插入图片描述
这个照片看起来和我们不加入菲涅尔反射比的结果差不多…
需要注意的是,编写电介质球有一个有趣而且简单的技巧:如果你创建球体的时候使用的半径是负数,那么几何体会不受影响但是表面法向量是指向球内的。所以这可以用来制作一个玻璃球中有一个气泡的效果,我们修改main代码:
在这里插入图片描述
运行代码得到以下结果:
在这里插入图片描述

第10章 可移动的相机

相机类和电介质类一样难debug。所以我一般以逐步递进的方式来编写它们。首先让我们设置一个可调节的视野范围fov。我们使用的是垂直方向上的视野范围,单位使用角度制。在前文我们让光线总是从坐标原点出发射向 z = − 1 z=-1 z=1这个平面的。其实我们也可以让光线射到 z = − 2 z=-2 z=2这个平面或者其他平面,只要我们设置一个是光源到视野平面的距离的一定比例的变量 h h h即可。如下图所示:
在这里插入图片描述
由图可知 h = tan ⁡ ( θ 2 ) h=\tan(\frac{\theta}{2}) h=tan(2θ),现在我们的camera类变成了这样:
在这里插入图片描述
修改一下main函数代码:
在这里插入图片描述
运行代码得到以下结果:
在这里插入图片描述

为了得到任意视角的景象,我们首先声明我们关心的点。我们把摄像机的位置称为 l o o k f r o m lookfrom lookfrom,把摄像机看向的点称为 l o o k a t lookat lookat。(你也可以定义一个方向来观察而不是一个点去观察。)
我们还需要定义一个表示摄像机绕着 l o o k a t − l o o k f r o m lookat-lookfrom lookatlookfrom这个轴旋转的量 r o l l roll roll。比如你从一个固定位置看向另一个固定位置,你仍然可以绕着你的鼻子旋转你的头。我们需要为摄像机指定一个上向量,注意我们很容易知道这个上向量应该处在的平面:与视线方向垂直的平面,如下图:
在这里插入图片描述
我们使用一个在空间中指向上的向量 v u p vup vup投影到上述平面来求摄像机的上向量。如下图: w w w是和 l o o k f r o m − l o o k a t lookfrom-lookat lookfromlookat向量反向的向量。 v , v u p , w v,vup,w v,vup,w三个向量应该处于一个平面上。我们之前的相机是朝向 − z -z z的,现在它应该朝向 − w -w w。记住我们可以利用世界空间中的向上的向量 ( 0 , 1 , 0 ) (0,1,0) (0,1,0)来求 v u p vup vup,但这不是必须的。现在我们的camera类如下:
在这里插入图片描述
现在我们可以修改摄像机视图,main函数改一下:
在这里插入图片描述
运行代码得到结果:
在这里插入图片描述
调整相机观察点可以改变视图:
在这里插入图片描述

第11章 散焦模糊

散焦模糊也被摄影师们称为景深。我们的摄像机会产生景深效果的原因是它需要一个大的洞来收集光线而不是我们目前模拟的这样,只从一个点去发出光线。这会使得所有东西都散焦(不理解…)。但是如果我们往这个洞里放一根棍子,那就会存在一个距离使得所有东西都在焦距之内。到使得所有东西都在焦距内的平面的距离是由镜片和投影屏幕/感受器之间的距离决定的。我们定义一个 a p e t u r e apeture apeture变量来决定镜头的大小。对于真实的相机,如果你想要更多的光线,你会让镜头更大,这样你得到的景深效果更明显。对于我们的虚拟摄像机,我们有一个完美的感受器并且不需要更多的光线,所以当我们需要景深效果时我们只有一个 a p e t u r e apeture apeture变量即可。
真实的相机镜头很复杂,对于我们的代码,我们可以按顺序模拟感受器,镜片,然后是镜头大小,然后决定我们从哪里发射出光线。当照片生成之后再上下翻转。图形学研究者通常使用一个很薄的近似镜片。在这里插入图片描述
另外我们不需要模拟摄像机的内部。Instead I usually start rays
from the surface of the lens, and send them toward a virtual film plane, by finding the projectionof the film on the plane that is in focus (at the distance focus_dist).(这句话不太理解…)
在这里插入图片描述
为了做到这个,我们只需要在lookfrom周围的一个圆盘上随机发射光线而不是只从一个点发射。现在camera类如下:
在这里插入图片描述
使用一个大光圈:
在这里插入图片描述
运行代码,结果如图:
在这里插入图片描述

第12章 下一步?

首先让我们生成本书封面:
在这里插入图片描述
把这个函数添加到main函数前面,main函数如下:
在这里插入图片描述
运行代码,得到:
在这里插入图片描述
有一件有趣的事情是,你可能发现图中没有阴影的玻璃球看起来像是悬浮着的。这不是一个bug!(在现实生活中你没有看过很多玻璃球,在阴天的时候它们看起来的确是有点奇怪的并且像是漂浮着。)

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值