目录

  • 第0章 概述
  • 第1章 输出照片
  • 第2章 vec3类
  • 第3章 光线、一个简单的相机和背景
  • 第4章 增加一个球
  • 第5章 表面法向量和多物体
  • 第6章 抗锯齿
  • 第7章 漫反射材质
  • 第8章 金属
  • 第9章 电介质
  • 第10章 可移动的相机
  • 第11章 散焦模糊
  • 第12章 下一步?

以下内容整理于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 Bp(t)=A+t∗B。ppp表示的是三维空间中沿着一条直线的点。AAA是光线的起点,B⃗\vec BB是光线的方向向量。参数ttt是一个实数(在代码中用float表示)。当ttt取不同值得时候,p(t)p(t)p(t)表示沿着光线移动的点。当ttt取所有实数时,p(t)p(t)p(t)表示了三维空间中的整条线。当t>0t>0t>0时,p(t)p(t)p(t)仅表示在AAA沿着BBB的方向上的部分,这就是我们常说的射线或光线。C=p(2)C=p(2)C=p(2)的示例如下。(CCC就是图中t=2t=2t=2那个点。)

我们编写的光线类如下:

类中的point_at_parameter函数即是p(t)p(t)p(t)函数,返回当参数为 ttt 时A+t∗B⃗A+t*\vec BA+t∗B代表的点。
现在我们可以开始编写光线追踪器了。光线追踪器的核心是从像素发射出光线,然后计算在这些光线的方向上看到了什么颜色。计算方式是,从眼睛发出指向某个像素的射线,然后计算这条射线与什么物体相交,把相交点处的颜色计算出来,便是这个像素的颜色。如下图所示,屏幕上像素点M的颜色值就是光线与球体相交点P的颜色值。

让我们来编写一个简单的color(ray)color(ray)color(ray)函数来返回背景的颜色。
为了让调试的时候不至于混淆xxx轴和yyy轴,我们决定生成200x100的照片。我们把“眼睛”(也就是摄像机的中心)放在(0,0,0)(0,0,0)(0,0,0)这个点。从眼睛看向屏幕的方向上,我们让y轴向上,x轴向右。同时,我们采用右手坐标系,所以穿入屏幕的方向是z轴的负方向。我们会从屏幕左下角开始遍历屏幕像素,通过使用两个沿着屏幕边缘的偏置向量,我们可以控制光线穿过屏幕的像素点,如下图所示。

注意我们并没有把光线的方向向量设定为一个单位向量,因为这样会使得代码更简洁,程序运行速度更快。
在下面的代码中,光线 rrr 会大致穿过像素的中心(我们不用担心这个精确度,因为很快我们会加入抗锯齿的功能)。

color(ray)color(ray)color(ray)函数根据y轴的上下程度线性地混合了白色和蓝色。我们先把 rrr 单位化,所以−1.0<y<1.0-1.0<y<1.0−1.0<y<1.0。所以0.0<0.5∗(y+1)<1.00.0<0.5*(y+1)<1.00.0<0.5∗(y+1)<1.0。当t取1时我们得到蓝色,当t取0时我们得到白色。当t取中间值时我们便得到了混合色。这就形成了两种颜色之间的“线性混合”/“线性插值”。运行以上代码,我们得到这个照片:

第4章 增加一个球

让我们来向我们的光线追踪器加入一个物体。在编写光线追踪器的时候,人们常常使用球体作为被观察物体,因为计算光线光线是否与球体相交是很简单的。回忆一下,球心在坐标原点,半径为R的球体方程为x2+y2+z2=R2x^2+y^2+z^2=R^2x2+y2+z2=R2。我们知道,对于一个点P(x0,y0,z0)P(x_0,y_0,z_0)P(x0​,y0​,z0​),若x02+y02+z02=R2x_0^2+y_0^2+z_0^2=R^2x02​+y02​+z02​=R2,那么PPP点就在这个球体上,否则不在这个球体上。当球体的圆心位于(cx,cy,cz)(cx,cy,cz)(cx,cy,cz)时,球体方程化为(x−cx)2+(y−cy)2+(z−cz)2=R2(x-cx)^2+(y-cy)^2+(z-cz)^2=R^2(x−cx)2+(y−cy)2+(z−cz)2=R2。在图形学中,我们总希望方程是以向量的形式给出的。因为这样x/y/zx/y/zx/y/z就可以用vec3vec3vec3类的形式来表示。我们知道从圆心C=(cx,cy,cz)C=(cx,cy,cz)C=(cx,cy,cz)指向p=(x,y,z)p=(x,y,z)p=(x,y,z)的向量是(p−C)(p-C)(p−C)。由向量点乘公式有(p−C)⋅(p−C)=(x−cx)2+(y−cy)2+(z−cz)2(p-C)·(p-C)=(x-cx)^2+(y-cy)^2+(z-cz)^2(p−C)⋅(p−C)=(x−cx)2+(y−cy)2+(z−cz)2,所以球体方程用向量的形式来表示就是(p−C)⋅(p−C)=R2(p-C)·(p-C)=R^2(p−C)⋅(p−C)=R2,任何满足这个方程的点ppp都在这个球体上。所以我们为了知道光线p(t)=A+t∗B⃗p(t)=A+t*\vec Bp(t)=A+t∗B是否与球体相交于某一点,我们只需把光线方程代入球体的向量方程,如果它们有交点,那么必定存在一个t使得方程(p(t)−C)⋅(p(t)−C)=R2(p(t)-C)·(p(t)-C)=R^2(p(t)−C)⋅(p(t)−C)=R2成立。把光线方程展开就得到(A+t∗B⃗−C)⋅(A+t∗B⃗−C)=R2(A+t*\vec B-C)·(A+t*\vec B-C)=R^2(A+t∗B−C)⋅(A+t∗B−C)=R2。为了解这个t的方程,我们整理一下得到B⃗⋅B⃗∗t2+2∗B⃗⋅(A−C)∗t+(A−C)⋅(A−C)−R2=0\vec B·\vec B*t^2+2*\vec B·(A-C)*t+(A-C)·(A-C)-R^2=0B⋅B∗t2+2∗B⋅(A−C)∗t+(A−C)⋅(A−C)−R2=0。方程中的向量以及RRR都是已知的常量,解这个关于ttt的一元二次方程,我们就可以知道光线和球体是否相交,如下图所示。

让我们把这些数学公式编码到程序里,我们可以这样测试:我们在z=−1z=-1z=−1处放置一个小球,如果光线和这个小球相交,那么我们把发出这条光线的像素渲染为红色。代码如下:

运行代码,我们得到这张图:

现在这张图缺少阴影、反射光等等,并且只有一个物体,但这是一个非常好的开始了。需要注意的是现在光线参数ttt的范围是全体实数,也就是ttt可以为负数。所以若你把上述代码中球体的圆心移动到z=+1z=+1z=+1处,那么你会得到相同的结果。这不是一个特性(这是一个bug…)!我们接下来会修复这个问题的。

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

首先,为了得到阴影,我们需要得到球体表面的法向量。这个向量与球体表面垂直并且指向外。为了更方便地求得阴影,我们让这些法向量为单位向量。对于一个圆心为CCC的球体来说,球体表面PPP点处的法向量是(P−C)(P-C)(P−C),如下图所示:

我们现在还没有光线,所以让我们通过颜色图的形式把法向量可视化。一个常用的可视化法向量的技巧是把法向量的x/y/z各分量值映射到0和1之间,然后对应地转化为颜色的r/g/b值。为了得到法向量,我们必须知道光线与球体相交的点的坐标,而不是仅仅知道是否相交。让我们来求离眼睛最近的相交点(对应的t最小)。代码如下:

运行代码,我们得到这个照片:

现在让我们尝试实现多个球体。我们设计一个抽象类来描述光线可能与之相交的物体。让我们把它称之为"hitable"类。hitable类会有一个hit函数,以ray作为输入参数。大多数光线追踪器都会为了方便而设置tmintmintmin和tmaxtmaxtmax,只有当相交点的 ttt 满足 tmin<t<tmaxtmin<t<tmaxtmin<t<tmax 的时候我们才计算这次相交。有一个设计问题是当我们的光线与某个物体相交时,是否需要计算该交点的法向量。我们只需要最近的相交物体的法线。让我们把相交处的信息计算出来并存入一个结构体hit_record中。我知道我们在某些时候需要景深效果,所以我之后会增加一个时间的输入变量。这是抽象类hitable的设计:

这是球体类sphere的设计(注意在求解方程时对计算进行了化简):


在计算相交点的时候,我们先计算可能存在的较小的ttt的解 −B−B2−4∗A∗C2∗A\frac{-B-\sqrt{B^2-4*A*C}}{2*A}2∗A−B−B2−4∗A∗C​​(注意代码进行了变量的带入与化简)。若该解存在并满足ttt的范围则我们把这个点的信息记录下来并返回true。否则计算可能存在的另一个较大的解−B+B2−4∗A∗C2∗A\frac{-B+\sqrt{B^2-4*A*C}}{2*A}2∗A−B+B2−4∗A∗C​​,同理若该解存在并满足ttt的范围则把这个点的信息记录下来并返回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章 漫反射材质

现在我们有了物体(球体类)并且每个像素可以发射多条光线了,这样我们就可以编写出看起来很真实的材质。我们先从漫反射材质开始。有一个问题是我们是否应该把物体形状和材质混合并匹配起来,比如把球体设计成一种材质。对于一些几何体和材质互相链接的程序来说这可能是有用的。但正如很多渲染器所做的,我们会把它们分离开开来。但注意这样会有局限性。
漫反射材质的物体是不会发射光线的,它们仅仅反射周围环境的光线而获得颜色。不同颜色的材质会反射不同颜色的光线。当光线从一个漫反射材质表面进行反射时,它的反射光方向是随机的。所以如果我们往两个漫反射材质表面发射三条光线时,它们会有不同的随机的反射行为,如图:

除了反射,这些光线还有可能被吸收。漫反射材质表面越暗,吸收得越多(这就是为什么它们看起来比较暗的原因!)。事实上,任何能把反射光线方向随机化的算法都可以产生看起来不光滑的表面。其中有一个最简单的方法,这恰好是制作理想的漫反射材质表面的正确的方法。
我们在光线与物体表面相交点 ppp 做一个与表面相切的单位球。从这个球中随机选取一个点 sss ,然后从 ppp 点发射一条指向 sss 的光线。显然这个球的球心坐标是p+Np+Np+N,NNN是物体ppp点上的单位法向量。
这样我们就需要一个方法来从上述球中选取一个随机点。从实现的角度来说,我们可以先从球心在坐标原点的单位球中随机选取一个点,然后再通过向量变换到与物体表面相切的球中。我们使用一个几乎是最简单的算法:首先我们随机生成三个处于[−1,1][-1,1][−1,1]的坐标点x,y,zx,y,zx,y,z。然后如果我们生成的这个点位于原点单位球之外,我们就从新生成一个随机点,直至这个随机点刚好位于原点单位球之内。代码如下:

random_in_unit_sphere函数比较简单,循环体中的代码就是把get_random()生成的[0,1)上的随机数变换到[-1,1)上。color函数中求targettargettarget向量坐标的图解如下:

上图中xxx轴从原点垂直指向纸面外。假设从眼睛位置也就是坐标原点处发射处一条光线OPOPOP,与待绘制球体O1O_1O1​相交于PPP点。我们可以得到位于该切点与O1O_1O1​相切的单位球O2O_2O2​。设切点处单位法向量为NNN,那么球O2O_2O2​的球心坐标即为OP+NOP+NOP+N。为了求得O2O_2O2​中的一个随机点,我们先求得了原点单位球中的随机点向量坐标SSS,那么由向量运算的性质我们就知道OP+N+SOP+N+SOP+N+S即为球O2O_2O2​中与SSS对应的随机坐标S‘S^`S‘的坐标。代码中求targettargettarget的坐标的代码就是这个含义。
求得发射光线的目标位置之后,我们就从光线入射点PPP发射一条反射光线指向targettargettarget。我们让点PPP的颜色等于反射光线返回的颜色值的一半,这意味着有一半的光线被吸收了。通过这样递归地求解各像素颜色,就能创造出很真实的画面。这一小节与前面小节最大的不同是这里真正使用了光线追踪(ray tracing)的技术(递归求像素颜色)。而前面小节的内容是一个光线投射(ray casting)的过程。
运行上述代码,速度会稍慢,因为我们递归地计算了像素颜色,运算量比较大。最后得到这个结果:

注意球体下面是有阴影的。这张照片很暗,但我们的球体在光线每次反射的时候只是吸收了50%的能量。如果你看不到阴影也不用担心,我们马上会解决这个问题。实际上这个球应该看起来很亮才对。在实际中应该是一个亮灰色的球。事实上我们在观察所有照片的时候都默认它是已经经过了伽马校正的。关于伽马校正的内容可以参考这里。总之在得到一个颜色值xxx之后我们应该求x1/2.2x^{1/2.2}x1/2.2作为照片中的颜色值。为了方便我们直接利用平方根来求近似,也就是用x\sqrt{x}x​来代替xxx即可。代码如下:

经过伽马校正之后,我们得到了更真实的结果:

然而现在的代码有一个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来查看光线的发射情况。
对于我们已有的漫反射材质来说,它可以始终散射并以反射系数RRR进行衰减,或者它可以不衰减地进行反射但会吸收光线的1−R1-R1−R,或者混合采取这两种策略。我们编写的漫反射材质类如下:

注意我们可以只是以某个概率ppp来散射光线,或者我们只是把衰减率设为albedo/palbedo/palbedo/p,你可以自己选择一种实现。(原书代码似乎是直接把衰减率设为albedoalbedoalbedo,并没有设置一个ppp)。
对于光滑的金属来说,光线不会随机地散射。光线撞击到金属的时候会遵循反射定律。图解如下:

由图可知,V出=V入+2BV_出=V_入+2BV出​=V入​+2B,BBB是入射光线V入V_入V入​在法向量方向的分量取反(所以代码中是减号而不是加号)。求反射光线代码如下:

只反射光线的金属材质会使用这个公式:

scatter函数中,我们先求出反射光线向量坐标,然后以入射点作为起点,反射光线向量作为方向向量构造一条光线,这就是反射光线。然后我们设置衰减率。由于反射光线必定与表面法向量夹角为锐角(为钝角说明光源位于物体表面下面了,这样的情况不需要计算)。所以我们返回出射光线与表面法向量的点积是否大于0的结果(向量点积大于0说明夹角为锐角)。
我们需要修改color函数来使用上述代码:

我们还需要修改sphere类,让它拥有一个指向这个球的材质的material指针,修改后的sphere类如下:


然后修改一下main函数,添加几个金属球:

运行代码,得到以下照片:

我们还可以通过在一个很小的球体中为光线选择一个新的结束点来随机化发射光线。

这个选择随机点的球体越大,那么反射光线的可失真程度就越大。这启示了我们在金属材质类中添加一个反映可失真程度的变量,就是我们选择随机结束点的小球的半径。当小球半径为0时,金属材质的反射没有失真。需要注意如果这个选择随机结束点的球体太大,我们的反射光线有可能反射到物体表面下方去了,这不合理。所以我们会把这个半径限制为小于1的值。修改后金属材质代码如下:

让我们尝试生成两个失真程度分别为0.3和1.0的球体,修改一下main函数:

运行代码得到结果:

第9章 电介质

干净的材质例如水,玻璃和钻石都是电介质。当一条光线击中它们的时候,这条光线会分裂为反射光线和折射光线。为了实现这个,我们会随机选择当光线击中它们时是反射还是折射,并且每次击中只产生一条散射光线。
最难debug的部分就是折射光线了。我经常会这样做:如果存在一条折射光线,那就让所有光线都折射。假如这个项目,我得到以下这张图:

这正确吗?这两个玻璃球看起来很奇怪,很明显这是错误的。真实的世界里,玻璃应该会把周围的景象反过来,并且也不会出现黑色的一块。
光线折射遵循Snell定律:
nsin(theta)=n‘sin(theta‘)nsin(theta)=n^`sin(theta^`)nsin(theta)=n‘sin(theta‘)
这里nnn和n‘n^`n‘是折射系数(比如空气的折射系数为1,玻璃是1.3~1.7,钻石是2.4)。图示如下:

还有一个实践上的问题,就是当光线从较高折射率的介质进入到较低折射率的介质时,如果入射角大于某一临界角θc(光线远离法线)时,折射光线将会消失,snell定律会失效,这个时候没有折射光线。这就是全内反射。这也就是为什么当你在水下的时候,有时候水面和天空的交界处看起来像一块完美的镜子一样。因此折射的代码相比于反射会更复杂:

求折射光线的代码图解如下:

nin_ini​和ntn_tnt​分别是两种介质的折射率。公式推导如下:

由Snell定律知道nint=sinθ2sinθ1\frac{n_i}{n_t}=\frac{sin\theta_2}{sin\theta_1}nt​ni​​=sinθ1​sinθ2​​,所以代码中的ni_over_ntni\_over\_ntni_over_nt就是nint\frac{n_i}{n_t}nt​ni​​。代码中的dtdtdt就是po⃗⋅n⃗\vec {po}·\vec npo​⋅n,也即是−cosθ1-cos\theta_1−cosθ1​。代码中的discriminantdiscriminantdiscriminant就是(cosθ2)2(cos\theta_2)^2(cosθ2​)2,所以只有discriminant>0discriminant>0discriminant>0时才会有折射光线。把推导过程中的①②两式代入(*)式就能得到求折射光线的代码了。注意这个过程默认是入射光线和折射光线均为单位向量,相当于我们求出的是折射光线的方向向量。
所以我们可以编写电介质材质类如下:

代码中的ref_idxref\_idxref_idx就是ntni\frac{n_t}{n_i}ni​nt​​。代码判断了入射光线与法向量之间的夹角是否为锐角。当它们夹角为钝角时就是我们前面的推导过程(从空气入射到介质中)。夹角为锐角的话,就是从介质中折射到空气中,需要相应改一下代码。图解如下(红色的是光线,先从空气折射到介质球中,再从介质球中折射到空气中):

因为玻璃不会吸收光线,所以衰减率是1。让我们修改一下main函数代码来使用这个类:

运行结果如下:

在现实生活中,光线折射时,反射和折射的比重与材质和入射角相关。我们需要计算一个称为菲涅尔反射比的参数来描述这个比重。关于菲涅尔反射比可参考这篇文章。以下方程可以近似计算菲涅尔反射比:
R(θi)≈R(0)+(1−R(0))(1−cosθi)5R(\theta_i)≈R(0)+(1-R(0))(1-cos\theta_i)^5R(θi​)≈R(0)+(1−R(0))(1−cosθi​)5,其中R(0)=(ηi−ηtηi+ηt)2R(0)=(\frac{\eta_i-\eta_t}{\eta_i+\eta_t})^2R(0)=(ηi​+ηt​ηi​−ηt​​)2
其中ηi\eta_iηi​和ηt\eta_tηt​分别是入射前介质和入射后介质的折射率,cosθicos\theta_icosθi​表示折射率较小的一边的光线与法线的夹角的余弦值,原书中给出的代码有误(后来发现作者在github给出的代码已经做了修改):

这里计算的是光线从玻璃球中折射到空气中的情况,这个cosinecosinecosine应该计算的是折射光线向量与球的表面法向量的夹角余弦值。如下图:

也就是cosinecosinecosine应该计算图中的cosθ3cos\theta_3cosθ3​。从代码实现的角度来说,我们可以先计算折射光线向量再来计算这个cosinecosinecosine:

如果要保持原代码的结构,那么需要利用我们前文推导的计算cosθ2cos\theta_2cosθ2​的公式来计算:

为了提高计算效率,我们先计算折射光线向量再计算cosinecosinecosine,完整的电介质材质类如下:

运行代码生成图片:

这个照片看起来和我们不加入菲涅尔反射比的结果差不多…
需要注意的是,编写电介质球有一个有趣而且简单的技巧:如果你创建球体的时候使用的半径是负数,那么几何体会不受影响但是表面法向量是指向球内的。所以这可以用来制作一个玻璃球中有一个气泡的效果,我们修改main代码:

运行代码得到以下结果:

第10章 可移动的相机

相机类和电介质类一样难debug。所以我一般以逐步递进的方式来编写它们。首先让我们设置一个可调节的视野范围fov。我们使用的是垂直方向上的视野范围,单位使用角度制。在前文我们让光线总是从坐标原点出发射向z=−1z=-1z=−1这个平面的。其实我们也可以让光线射到z=−2z=-2z=−2这个平面或者其他平面,只要我们设置一个是光源到视野平面的距离的一定比例的变量hhh即可。如下图所示:

由图可知h=tan⁡(θ2)h=\tan(\frac{\theta}{2})h=tan(2θ​),现在我们的camera类变成了这样:

修改一下main函数代码:

运行代码得到以下结果:

为了得到任意视角的景象,我们首先声明我们关心的点。我们把摄像机的位置称为lookfromlookfromlookfrom,把摄像机看向的点称为lookatlookatlookat。(你也可以定义一个方向来观察而不是一个点去观察。)
我们还需要定义一个表示摄像机绕着lookat−lookfromlookat-lookfromlookat−lookfrom这个轴旋转的量rollrollroll。比如你从一个固定位置看向另一个固定位置,你仍然可以绕着你的鼻子旋转你的头。我们需要为摄像机指定一个上向量,注意我们很容易知道这个上向量应该处在的平面:与视线方向垂直的平面,如下图:

我们使用一个在空间中指向上的向量vupvupvup投影到上述平面来求摄像机的上向量。如下图:www是和lookfrom−lookatlookfrom-lookatlookfrom−lookat向量反向的向量。v,vup,wv,vup,wv,vup,w三个向量应该处于一个平面上。我们之前的相机是朝向−z-z−z的,现在它应该朝向−w-w−w。记住我们可以利用世界空间中的向上的向量(0,1,0)(0,1,0)(0,1,0)来求vupvupvup,但这不是必须的。现在我们的camera类如下:

现在我们可以修改摄像机视图,main函数改一下:

运行代码得到结果:

调整相机观察点可以改变视图:

第11章 散焦模糊

散焦模糊也被摄影师们称为景深。我们的摄像机会产生景深效果的原因是它需要一个大的洞来收集光线而不是我们目前模拟的这样,只从一个点去发出光线。这会使得所有东西都散焦(不理解…)。但是如果我们往这个洞里放一根棍子,那就会存在一个距离使得所有东西都在焦距之内。到使得所有东西都在焦距内的平面的距离是由镜片和投影屏幕/感受器之间的距离决定的。我们定义一个apetureapetureapeture变量来决定镜头的大小。对于真实的相机,如果你想要更多的光线,你会让镜头更大,这样你得到的景深效果更明显。对于我们的虚拟摄像机,我们有一个完美的感受器并且不需要更多的光线,所以当我们需要景深效果时我们只有一个apetureapetureapeture变量即可。
真实的相机镜头很复杂,对于我们的代码,我们可以按顺序模拟感受器,镜片,然后是镜头大小,然后决定我们从哪里发射出光线。当照片生成之后再上下翻转。图形学研究者通常使用一个很薄的近似镜片。
另外我们不需要模拟摄像机的内部。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!(在现实生活中你没有看过很多玻璃球,在阴天的时候它们看起来的确是有点奇怪的并且像是漂浮着。)

Ray Tracing in One Weekend 读书笔记相关推荐

  1. 《Ray Tracing in One Weekend》笔记 - 【Chapter 9】:Dielectrics

    <Ray Tracing in One Weekend> 笔记 - Chapter9:会发生折射的材质 该小节中实现了表面会发生折射现象的材质. 模型假设: 1.文章在实现上做了一些简化, ...

  2. Ray Tracing in One Weekend从零实现一个简单的光线追踪渲染器

    Ray Tracing in One Weekend学习笔记 1.Overview 从零开始实现一个简单的光线追踪渲染器,能够实现漫反射材质.金属材质.透明材质的渲染,此外还实现了摄像机的自由移动和焦 ...

  3. 计算机图形学 读书笔记(八) 光线跟踪加速Ray Tracing Acceleration

    写个读书笔记,一来作为字典以后可以查,二来记录自己的理解. 并没有对每个知识点的详细解释,大部分只有主观的定性的解释. 光线跟踪受到的限制: 1.时间复杂度和空间复杂度都很高. 2.主要时间用在了可见 ...

  4. GAMES101课程学习笔记—Lec 14(2)~16:Ray Tracing(2) BRDF、渲染方程、全局光照、路径追踪

    GAMES101课程学习笔记-Lec 14(2)~16:Ray Tracing(2) BRDF.渲染方程.全局光照.路径追踪 0 引入--辐射度量学概述 1 相关概念 1.1 Radiant Ener ...

  5. 计算机图形学学习笔记——Whitted-Style Ray Tracing(GAMES101作业5讲解)

    计算机图形学学习笔记--Whitted-Style Ray Tracing GAMES101作业5讲解 遍历所有的像素生成光线 光线与平面求交 遍历所有的像素生成光线 关于作业五中如何遍历所有的像素, ...

  6. GAMES101-现代计算机图形学入门-闫令琪 - lecture14 光线追踪2 - 加速结构(Ray Tracing 2 - Acceleration) - 课后笔记

    光线追踪2 - 加速结构(Ray Tracing 2 - Acceleration) 对AABB结构优化来加速光线追踪的速度 均匀网格(Uniform grids) 空间划分(Spatial part ...

  7. GAMES101-现代计算机图形学入门-闫令琪 - lecture13 光线追踪1(Ray Tracing 1 - Whitted-Style Ray Tracing) - 课后笔记

    光线追踪1 (Ray Tracing 1 - Whitted-Style Ray Tracing) 课程一共分为四个大的板块,目前已经学习了光栅化和几何,可以实现图1和2的效果,下面要来学习第三个大的 ...

  8. 《Ray Tracing in One Weekend》阅读笔记 - 5、表面法线和多个物体

    对于表面法向量的设计决策,有两个要考虑的问题: 1.是否要用单位长度 "首先是这些法线是否为单位长度.这对于阴影很方便,所以我会说yes,但我不会在代码中执行它.这可能会出现一些细微的bug ...

  9. 《Real-Time Rendering 4th Edition》读书笔记--简单粗糙翻译 第六章 纹理 Texturing

    写在前面的话:因为英语不好,所以看得慢,所以还不如索性按自己的理解简单粗糙翻译一遍,就当是自己的读书笔记了.不对之处甚多,以后理解深刻了,英语好了再回来修改.相信花在本书上的时间和精力是值得的. -- ...

最新文章

  1. 太厉害了!目前 Redis 可视化工具最全的横向评测
  2. 多线程和Socket——在线聊天室
  3. ubuntu安装openssl命令
  4. 海量数据处理(位图和布隆过滤器)
  5. python基础编程练习题_Python随笔18:Python基础编程练习题1~2
  6. python----关键字参数
  7. MPMoviePlayerViewController 改良版播放器
  8. 设计模式之——单例模式
  9. SAP漏洞:为什么补丁没有发挥作用?
  10. TabLayout实现自定义标题栏目功能
  11. 常用DB9外设接口定义
  12. 产品读书《产品经理的第一本书》
  13. DCMTK相关资料汇总
  14. 计算机职业素养论文1500字,【职业素养论文】职业素养论文范文(共40篇)
  15. 爱普生Epson L301 清零软件+图解教程
  16. 如何批量导出QQ空间相册到电脑中
  17. 鼠标失灵c语言代码,[转载]键盘和鼠标操作失灵代码
  18. C#winform【获取文件路径--遍历文件夹图片】--实战练习六
  19. 使用 libgps 库获取gps数据
  20. 各大工作室都在用的视觉特效软件,开启你的虚拟制作之旅

热门文章

  1. 使用Fiddler抓包工具抓取服务器数据
  2. 中国核电设备产业发展现状分析及投资战略规划报告2022-2027年
  3. 触控笔哪个牌子好?十大电容笔知名品牌
  4. 深圳计算机系考公难吗,深圳市考竞争比1043:1!四大考公里面最难的?
  5. windows7系统无法连接服务器,Win7系统玩LOL提示无法连接服务器的解决办法
  6. 4. Vue入门实战教程之vue-element-admin后端API适配
  7. layui数据表格解析html,layui框架table 数据表格的方法级渲染详解
  8. python库opencv,py-opencv,libopencv的区别
  9. html实践环节制作调查问卷,HTML大学生暑假社会实践调查问卷源代码
  10. 锚定梦想,一切变简单