《Fundamentals of Computer Graphics》5th(计算机图形学基础/虎书),中文翻译。
第 4 章 Ray Tracing 光线追踪
计算机图形学的基本任务之一是渲染三维对象:将由许多几何对象排列在三维空间中组成的场景作为输入,计算出一个二维图像,显示从特定视点观察到的对象。这与建筑师和工程师创建图纸以向他人传达设计的方式已经做了几个世纪的相同操作。
从根本上讲,渲染是一个过程,它以一组物体 (a set of objects) 作为输入,并生成一组像素 (an array of pixels) 作为输出。不管怎样,渲染都涉及考虑每个对象如何对每个像素产生影响,可以按两种常规方式组织。在对象顺序渲染 (object-order rendering) 中,依次考虑每个对象,对于每个对象,找到并更新其影响的所有像素。在图像顺序渲染 (image-order rendering) 中,依次考虑每个像素,对于每个像素,找到影响它的所有对象并计算像素值。您可以将区别想象为循环的嵌套方式:在图像顺序渲染中,“对于每个像素”循环在外部,而在对象顺序渲染中,“对于每个对象”循环在外部。
如果输出是矢量图像而不是光栅图像,则渲染过程不必涉及像素,但在本书中我们将假定使用光栅图像。
图像顺序渲染和对象顺序渲染可以计算完全相同的图像,但它们适用于计算不同类型的效果,并具有完全不同的性能特征。在我们讨论了两种方法后,我们将在第 9 章探讨这些方法的比较优势,但总体而言,图像顺序渲染更容易实现,可以产生更灵活的效果,并且通常(但并非总是)需要更多的执行时间才能生成可比较的图像。
在光线追踪器中,计算准确的阴影和反射很容易,在对象顺序框架中则很棘手。
光线追踪是一种用于制作三维场景渲染的图像顺序算法,我们首先考虑它,因为可以在不开发任何用于对象顺序渲染的数学机制的情况下使光线追踪器工作。
光线追踪器通过一次计算一个像素来工作,对于每个像素,基本任务是找到在该像素位置看到的对象。每个像素“看”的方向不同,任何被像素看到的对象都必须与视线 (viewing ray,也有叫 eye ray、camera ray) 相交,所谓视线就是从视点沿着像素所看的方向发射的一条线。我们想要的特定对象是与最靠近相机的视线相交的对象,因为它阻挡了其后面的任何其他对象的视野。一旦找到这个对象,着色计算使用相交点、表面法线和其他信息(取决于所需的渲染类型)来确定像素的颜色。这在图 4.1 中显示,其中光线与两个三角形相交,但只有第一个三角形 被击中并进行了着色。
因此,基本光线追踪器有三个部分:
基本光线追踪程序的结构为:
本章介绍了用于实现简单演示光线追踪器的光线生成、光线相交和着色的基本方法。对于一个真正有用的系统,需要从第 12 章中添加更有效的光线相交技术,并且只有在第 14 章中使用更高级的渲染技术时,光线追踪器的真正潜力才会显现出来。
将三维物体或场景用二维图纸或绘画来表示的问题在计算机出现之前已经被艺术家研究了数百年。照片也用二维图像来表示三维场景。虽然有许多非传统的制作图像的方式,从立体派绘画到鱼眼镜头(图 4.2)到周边相机,但是无论是艺术、摄影还是计算机图形学的标准方法都是线性透视,在此方法中,三维物体以一种使场景中的直线变成图像中的直线的方式投影到一个图像平面上。
最简单的投影类型是平行投影 (parallel projection),其中通过沿着投影方向移动它们,将三维点映射到二维平面上,直到它们碰到图像平面为止(图 4.3-4.4)。产生的视图由投影方向和图像平面的选择确定。如果图像平面垂直于视角方向,则投影称为正交投影 (orthographic);否则,称为斜投影 (oblique)。
一些书籍将“正交投影”保留给平行于坐标轴的投影方向。
平行投影经常用于机械和建筑图纸,因为它们可以让平行线保持平行,并且保留与图像平面平行的平面物体的大小和形状。
平行投影的优点也是它的局限性。在我们日常体验中(甚至在照片中更是如此),随着距离的增加,物体看起来越来越小,因此,远离我们的平行线并不呈现平行状态。这是因为眼睛和相机不会从单一的视角收集光线;它们收集通过特定视点穿过的光线。自文艺复兴时期以来,正如艺术家们所认识到的那样,我们可以使用透视投影 (perspective projection) 产生自然的视图:我们只需沿着通过一个单一点 (视点) 的线投影,而不是沿着平行线投影(图 4.4)。通过这种方式,当对象被投影时,远离视点的对象自然变得更小。透视投影由视点(而不是投影方向)和图像平面选择确定。与平行视图一样,有斜和非斜的透视投影;区别是基于图像中心的投影方向。
您可能在绘画艺术中学习过“三点透视”,这是一个手动构建透视投影的系统(图 4.5)。关于透视的一个令人惊讶的事实是,如果我们遵循透视背后的简单数学规则,即物体直接向眼睛投射,并在眼前的视图平面上绘制它们,所有透视绘画的规则都将自动遵循。
从前面的章节中可以看出,光线生成的基本工具是视点(或者对于平行视图来说是视线方向)和图像平面。有许多方法可以解决摄像机几何形状的细节;在本节中,我们将介绍一种基于标准正交基 (orthonormal bases) 的方法,支持正交和斜视平行投影以及正交投影。
为了生成光线,我们首先需要一个表示光线的数学表示方法。实际上,一条光线只有一个起点和一个传播方向;一条三维参数化直线非常适合这个任务。正如在第 2.7.7 节中讨论的那样,从眼睛 e 穿过图像平面上的点 s(图 4.6)的三维参数化直线是:
这应该被解释为,“我们沿着向量 从 开始前进,到达分数距离 的点 ”。因此,给定 ,我们可以确定一个点 。点 是光线的起点, 是光线的方向。
请注意,,,更一般地说,如果 ,则 比 更接近眼睛。此外,如果 ,则 “在”眼睛后面。当我们搜索由光线碰到的最近的不在眼睛后面的物体时,这些事实将非常有用。
注意:我们正在重载变量 ,它是光线参数和图像顶部边缘的 坐标。
在代码中,光线通常用某种结构或对象来表示,以存储位置和方向。例如,在面向对象的程序中,我们可能会编写:
class Ray Vec3 | Vec3 | Vec3 evaluate(real ) return
我们假设有一个表示三维向量并支持常规算术运算的 Vec3 类。
要计算视线,我们需要知道 (已知)和 。如果我们在正确的坐标系中看问题,找到 可能看起来很困难,但实际上很简单。
所有光线生成方法都从称为相机框架的正交坐标系开始(图 4.7)。我们用 e 表示眼睛点或视点, 表示三个基向量,组织方式是 u 指向右侧(从相机的视角),v 指向上方,w 指向后方,这样 形成一个右手坐标系。构建相机框架的最常见方法是从视点开始,将其变为 ,视线方向取 ,上向量用于构建一个基向量,该基向量在由视线方向和上方向定义的平面内具有 和 ,使用第 2.4.7 节描述的从两个向量构建正交基向量的过程(图 4.8)。
由于 v 和 w 必须垂直,因此上向量和 v 通常不是相同的向量。但是,将上向量设置为指向场景中的正上方将使相机定向为“朝上-右”。
对于正交视图,所有的光线方向都是 。即使平行视图 (parallel view) 本质上没有视点,我们仍然可以使用相机框架的原点定义光线起始的平面,以便处理“对象出现在相机后面”的情况。
观察射线应该从由点 和向量 、 定义的平面上开始;唯一需要的剩余信息是图像应该位于平面的哪个位置。我们将用四个数字来定义图像的尺寸,分别为图像的四个边缘: 和 是从沿着 方向从 测量的图像左侧和右侧的位置; 和 是从沿着 方向从 测量的图像底部和顶部的位置。通常情况下, 且 。(见图 4.9a)
在第 3.2 节中,我们讨论了图像中的像素坐标。为了将一个 像素的图像适配到大小为 的矩形中,水平方向上的像素间距为 ,垂直方向上的像素间距为 ,在图像矩形边缘周围留有半个像素空间,以使像素网格在图像矩形内居中。这意味着栅格图像中位置为 的像素具有位置
许多系统假定 和 ,因此只需要宽度和高度即可。
其中 是像素在图像平面上的位置坐标,相对于原点 和基向量 进行测量。
由于 和 都已经指定,因此存在冗余:将视点向右移动一点并相应地减小 和 不会改变视图(在 v-axis 上同样如此)。
在正交视图中,我们可以简单地使用像素的图像平面位置作为射线的起始点,并且我们已经知道射线的方向是视线方向。生成正交视图光线的过程如下:
制作倾斜的平行视图非常简单:只需允许独立指定图像平面法线 和视线方向 即可。然后,该过程完全相同,但将 替换为 。当然,仍然使用 来构建 和 。
对于透视投影,所有光线都有相同的起点,在视点处;每个像素的区别在于它们的方向不同。图像平面不再位于 处,而是在 前的一定距离 处;这个距离是图像平面距离,通常被称为焦距,因为选择 与在真实相机中选择焦距具有相同的作用。每条光线的方向由视点和图像平面上像素的位置定义。这种情况如图 4.9 所示,生成的过程类似于正射投影:
与平行投影一样,可以通过分别指定图像平面法线和投影方向来实现倾斜透视投影。
一旦我们生成了一个光线 ,接下来需要找到第一个 的任何物体与其相交的位置。在实践中,解决稍微更一般的问题会很有用:找到射线与表面之间的第一个相交点,该相交点处于区间 中的某个 t 值。基本的光线相交是其中 , 的情况。我们将在下一节中针对球和三角形解决这个问题。在下一节中,将讨论多个物体的情况。
给定一个射线 和一个隐式表面 (参见第 2.7.3 节),我们需要知道它们相交的位置。当射线上的点满足隐式方程时,相交点出现在这些点上,因此我们要寻找解方程的 值,该方程为
以中心 和半径 表示的球可以用隐式方程表示为
我们也可以将同一方程写成向量形式:
满足这个方程的任何点 都位于球上。如果我们将射线 中的点插入到这个方程中,我们会得到一个关于 的方程,满足产生球上的点的 值:
重排项得到
在这里,除了参数 t 之外,所有内容都已知,因此这是经典的二次方程,其形式为
解决这个方程的方法在第 2.2 节中讨论。二次方程解中平方根符号下的项 称为判别式 (discriminant),它告诉我们有多少个实数解。如果判别式为负,则其平方根是虚数,线和球不相交。如果判别式为正,则有两个解:一种解是射线进入球体,另一种解是射线离开球体。如果判别式为零,则射线与球体擦边而过,在恰好一个点上接触球体。将球的实际术语插入并消除两个因子,我们得到
在实际实现中,应先检查判别式的值,然后再计算其他项。要正确地在区间 中找到最近的相交点,有三种情况:如果较小的两个解在区间内,则它是第一个撞击;否则,如果较大的解在区间内,则它是第一个撞击;否则,没有命中。
如第 2.7.4 节所述,在点 处的法向量由梯度 给出。单位法向量为 。
有许多算法可用于计算射线 - 三角形相交。我们将介绍使用三角形顶点的重心坐标来表示包含三角形的参数平面的方法,因为它不需要长期存储(除了三角形的顶点)(Snyder&Barr,1987)。
为了使射线与参数曲面相交,我们设置一个方程组,其中笛卡尔坐标全部匹配:
在这里,我们有三个方程和三个未知数(、 和 )。如果表面是参数平面,则参数方程是线性的,并且可以按照第 2.9.2 节中所述的向量形式编写。如果三角形的顶点是 、 和 ,则当以下条件成立时,相交点将出现:
对于某些 , 和 ,解这个方程既可以确定沿着射线定位相交点的 t 值,也可以确定相对于三角形定位相交点的 值。与三角形交点的位置将在 处,如图 4.10 所示。同样根据第 2.9.2 节,我们知道当且仅当 , 且 时,相交点在三角形内部。否则,射线击中了三角形外的平面,因此它未命中三角形。如果没有解决方案,则三角形是退化的或者射线与包含三角形的平面平行。
图 4.10。光线在包含三角形的平面上击中点 p。
要解方程(4.2)中的 、 和 ,我们需要将其从向量形式扩展到三个坐标的三个方程:
这可以重写为标准的线性系统:
解决这个 3×3 线性系统的最快的经典方法是克拉默法则。这给我们的解决方案是
其中矩阵 A 是
表示 的行列式。这些 行列式具有可以在实现中利用的共同子项,以提高效率。查看带有虚拟变量的线性系统
克拉默法则给出
其中
我们可以通过重复使用数字(例如“ei-minus-hf”)来减少操作次数。
对于需要线性解的射线 - 三角形相交的算法可能会有一些提前终止的条件。因此,函数应该类似于:
boolean raytri (Ray , vector3 , vector3 , vector3 , interval ) compute if or then return false compute if or then return false compute if or then return false return true
在射线跟踪程序中,使用一个称为 Surface 的类以及派生类 Triangle、Sphere 等的面向对象设计是个好主意。任何射线可以相交的东西,包括面集或效率结构(第 12.3 节)都应该是 Surface 的子类。然后,射线跟踪程序将对整个模型引用一个 Surface,并且可以透明地添加新类型的对象和效率结构。
Surface 类的关键接口是一种方法来相交射线 (Kirk&Arvo, 1988)。
class Surface HitRecord hit(Ray , real , real )
这里, 是返回击中的区间,HitRecord 是包含有关将需要的表面相交的所有数据的类:
class HitRecord Surface | 被击中的表面 实际值 | 沿着射线的击中点的坐标 Vec3 | 击中点的表面法向量 ...
被命中的表面、 值和表面法向量是最低要求的,但也可以存储其他数据,如纹理坐标或切向量。根据语言,可能无法直接从函数中返回命中记录,而是通过引用传递并进行填充。未命中可以通过具有 的命中来表示。
当然,大多数有趣的场景由多个对象组成,当我们用场景相交射线时,必须找到沿着射线最接近相机的交点。实现这一点的简单方法是将一组对象视为另一种类型的对象。要与组相交射线,只需将射线与组中的对象相交,并返回具有最小 t 值的相交点。以下代码测试在区间 上是否击中:
class Group, Surface 的子类 list-of-Surface surfaces | 组中所有表面的列表 HitRecord hit(Ray ray, real , real ) HitRecord closest-hit() | 初始化以表示未命中 for surf in surfaces do rec = surf.hit(ray, , ) if rec. then closest-hit = rec return closest-hit
请注意,此代码会缩小交点间隔 ,以便调用 surf.hit
仅会击中比迄今为止看到的最近的表面更近的表面。
一旦射线 - 场景相交正常工作,我们就可以像图 4.11 那样进行图像渲染,但更好的结果取决于包括更多的视觉提示,我们将在下面描述。
一旦已知像素的可见表面,就可以通过评估着色模型来计算像素值。如何完成此操作完全取决于应用程序 - 方法从简单的启发式方法到复杂的基于物理模型的方法不等。在光线跟踪或对象排序渲染方法中可以使用完全相同的着色模型。
第 5 章描述了一个适合基本射线追踪器的简单着色模型,并且这是我们在本章中制作渲染所使用的模型。为了更加真实,您可以升级到第 14 章中讨论的更符合真实表面物理特性的模型。在这里,我们将讨论射线跟踪器如何计算输入以进行着色。
为了支持着色,射线跟踪程序始终具有光源列表。对于第 5 章的着色模型,我们需要三种类型的光源:点光源,从空间中的一个点发出光线;定向光,从单个方向照亮场景;以及环境光,提供常量照明以填补阴影。在更高级的系统中,还支持其他类型的光源,例如区域光(基本上是场景几何形状,可以发出光)或环境光(使用图像来代表远处来源(如天空)的光)。
从点光源或定向光源计算着色需要一些几何信息,在射线跟踪器中,确定视图射线击中表面后,我们拥有了确定这四个向量所需的所有内容:
从环境光源中计算的着色要简单得多:没有 ,因为光线来自任何地方;着色不依赖于 ;并且对于第 5 章的简单模型,它甚至不依赖于 或 。
在包含多个光源的场景中计算着色只是将各个光源的贡献相加。在基本的射线跟踪器中,您可以简单地循环遍历所有光源,计算每个光源的着色,并将结果累积到像素颜色中。
射线跟踪程序通常包含代表光源和材质的对象。光源可以是 Light 类的子类的实例,并且它们必须包含足够的信息来完全描述光源。由于着色还需要描述表面材料的参数,因此另一个有用的类是 Material,它封装了评估着色模型所需的所有内容。
不同的系统对于如何将着色计算分解为光源和材料采取不同的方法。与本章的演示相一致的方法是使光源负责整体照明计算,而材料负责计算 值。在这种设置下,这些类的接口可能如下所示:
每个曲面都将存储其材质的引用,通过以下方式可以实现点光源照明:
这些计算假定 Color 类具有颜色的 RGB 成分,并支持逐分量乘法。这种安排也适合将环境光作为一种光源来处理,方法是将环境系数作为材料的属性:
完整的光线着色计算,包括交点和处理多个光源,可以像这样:
这种设置使材料和光源保持相对独立,允许您以透明的方式稍后添加新类型的材料和光源。纹理会给光线跟踪器的架构增加一些复杂性;请参见第 11.2.5 节。
仅使用着色可以使 3D 对象的图像更加真实和易于理解,但它并没有展示它们与其他对象的交互。例如,图 4.12 中的球体似乎漂浮在它们所靠的地板上方。
一旦您的光线跟踪器具有基本着色功能,就可以非常容易地为点光源和定向光源添加阴影。如果我们想象自己处于被着色的曲面上的一个点 ,当我们“朝向”光源并看到我们和光源之间有一个物体时,该点会在阴影中。如果之间没有物体,则光线不会被遮挡。
图 4.13 展示了这一点,其中光线 不会击中任何物体,因此点 x 不在阴影中。另一方面,由于光线 碰到了物体,因此点 处于阴影中。确定阴影的光线与视线相区别,称为阴影光线 (shadow rays)。
为了得到着色算法,我们在添加来自光源的着色代码之前添加一个 if 语句以首先确定光源是否被阴影覆盖。在一个简单的实现中,阴影光线将检查 ,但由于数值上的不精确性,这可能导致光线与点 所在的曲面相交。因此,为了避免这个问题,通常的调整方法是测试 ,其中是一些小的正常数(图 4.14)。
可以通过跟踪阴影光线并添加条件向 PointLight.illuminate 方法中添加阴影测试,如上所示:
定向光源的阴影测试类似,但使用 而非 。请注意,每个光源的照明计算需要一个单独的阴影光线,并且在计算环境着色时没有阴影测试。
阴影在显示附近物体之间的关系方面起着重要的视觉作用,如图 4.15 所示。
为光线跟踪程序添加理想的镜面反射非常简单。关键观察结果如图 4.16 所示,从方向 看到在方向 处与表面相交的物体。向量 是向量 关于曲面法向量 的反射,可以使用将 投影到曲面法向量方向上来计算:
在现实世界中,当光线从曲面反射时会有一些能量损失,并且这种损失对不同颜色可能是不同的。例如,金色比蓝色更有效地反射黄色,因此它会改变反射对象的颜色。可以通过在 shade-ray 中添加递归调用来实现这一点,在所有光源考虑完后再添加一个贡献:
其中 (代表“镜面反射”)是规定的 RGB 镜面反射颜色。我们需要确保将 传递给反射光线,原因与之前处理阴影光线时相同;我们不希望反射光线击中生成它的物体。
上述递归调用的问题是它可能永远不会终止。例如,如果一条光线从一个房间内开始,则会永远弹跳。可以通过添加最大递归深度来解决这个问题。如果 为零,则只有在生成反射光线时代码才更有效。
使用恒定的镜面反射系数 给出了简单的光线跟踪器特有的外观(图 4.17);在现实世界中,这个系数取决于入射角度而有很大变化。有关更好的模型,请参见第 14 章。
光线跟踪是计算机图形学早期开发的技术(Appel,1968),但直到有足够的计算能力可用时才被广泛使用(Kay&Greenberg,1979;Whitted,1980)。
与基本的对象排序渲染相比,光线跟踪具有更低的渐进时间复杂度(Snyder&Barr,1987;Muuss,1995;Parker 等,1999;Wald,Slusallek,Benthin 和 Wagner,2001)。虽然传统上认为它是一种离线方法,但实时光线跟踪实现变得越来越普遍。
在 z 缓冲中的透视矩阵存在的原因是我们可以将透视投影转换为平行投影。这在光线跟踪中不需要,因为可以通过从眼睛处扇形地发射光线来隐式执行透视投影。
对于足够小的模型和图像,任何现代 PC 都足够强大,使得光线跟踪可以交互。实际上,全屏实现需要多个具有共享帧缓冲区的 CPU。计算机性能增长的速度比屏幕分辨率要快得多,很快传统 PC 就可以在屏幕分辨率下光线跟踪复杂场景。
光线跟踪经常用于拾取。当用户在 3D 图形程序中点击鼠标上的像素时,程序需要确定该像素内可见的物体。光线跟踪是确定该物体的理想方法。
本文作者:青波
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!