Computer Graphics From Scratch - Chapter 5
系列文章目录
内容简介
第1章
第2章
第3章
第4章
Chapter 5
-
系列文章目录
-
延伸光线追踪算法
-
任意相机定位
-
性能优化措施
- 并行化策略
- 不变值缓存方法
- 阴影优化技术
- 空间结构优化
- 抽样方法改进
-
三、Supporting Other Primitives - 覆盖基础元素
-
四、Constructive Solid Geometry - 构造实体几何
-
五、Transparency - 透明度
-
-
5.1. Refraction - 折射
-
六、Supersampling - 超级采样
-
七、Summary - 概括
-
Extending The Raytrace
Extending The Raytrace – 扩展光线跟踪
We will conclude this section with a brief overview of several intriguing subjects we have not yet addressed: positioning the camera at any location within the scene, implementing performance enhancements, utilizing primitives beyond spheres for modeling objects through constructive solid geometry (CSG), incorporating support for transparent materials (not just surfaces), and advancing rendering techniques such as supersampling. While we will not delve into all these innovations in detail here, I strongly encourage you to attempt some of them! The preceding chapters, along with the detailed explanations provided below, offer a solid foundation for exploring and implementing these concepts on your own.
在本书的第一部分中,我们将简要探讨几个尚未涉及的重要主题:相机可在场景中的任意位置布置;我们还将进行性能方面的优化措施;此外还将研究非球形基本元素;通过构造函数来定义物体的几何形状;同时还将支持透明材质表面与高级采样技术。然而我们不打算全部采用这些方法,请您自行尝试;前面章节已经为您提供了必要的基础框架以探索与实现这些改进方案。
一、Arbitrary Camera Positioning - 任意相机定位
当我们开始讨论光线追踪 的最初阶段时,在坐标系中设置了几个基本前提:相机的位置被精确地设定为坐标原点 (0, 0, 0) ,其朝向单位向量沿着正Z轴方向,并且定义其上行方向为正Y轴向量 \vec{Y_+} 。
在本节中, 我们将取消这些约束条件; 以便让相机能够处于场景内的任意位置并朝向任意方向。
让我们从相机的位置开始。
您可能已经意识到,在所有伪代码中变量 O 仅被引用一次:它表示来自相机光线源的起点。
要调整相机的位置,则需将变量 O 设定为不同的值。
从相机到投影平面的矢量
从相机到投影平面的矢量
为了将注意力转向 相机的方向 ,假设您拥有一个 旋转矩阵(rotation matrix) ,它表示 所需的方向(target direction) 。
如果仅对相机进行旋转,则相机的 位置(position) 保持不变;然而它的朝向会发生变换,并与整体相机一起完成相同的旋转变换。
因此,在这种情况下,请考虑存在射线方向 \vec{D} 和 旋转矩阵 R 的情况下,则经过旋转变换后的射线方向即为 {R} \cdot \vec{D}。
总结来说,在示例2-2中重写了main函数。示例5-1展示了更新后的功能模块。
for x in [-Cw/2, Cw/2]
{
for y in [-Ch/2, Ch/2]
{
❶ D = camera.rotation * CanvasToViewport(x, y)
❷ color = TraceRay(camera.position, D, 1, inf)
canvas.PutPixel(x, y, color)
}
}
示例5-1:主循环,更新为支持任意相机位置和方向
我们应用相机的旋转矩阵❶(该矩阵表示了相机在空间中的方向)来计算我们要追踪的光线的方向。然后以相机位置作为射线的起始点❷。
图 5-1 显示了我们的场景从不同的位置和不同的相机方向渲染时的样子。

图 5-1:我们熟悉的场景,用不同的相机位置和方向渲染
该算法的实时实现已在此处提供https://gabrielgambetta.com/computer-graphics-from-scratch/demos/raytracer-06.html
二、Performance Optimizations - 性能优化
Performance Optimizations — 性能优化
本章主要采用了最为直观的方法来解释与实现光线追踪器的各项功能。然而,在其众多优点之外的一个显著缺点是运行速度较慢。以下是一些值得进一步探讨的方向与建议——通过优化算法性能来提升整体运行效率。仅仅出于好奇与兴趣的目的——我们可以设计一些测试案例来进行性能对比分析。你可能会发现一些令人意想不到的结果——甚至会感到震惊!
2.1. Parallelization - 并行化
最为直接的方法即是一次追踪多条光线。由于离开相机的每条光线彼此互不干扰,并且场景数据为只读文件,在每一个CPU核心上单独处理一条光线从而不会造成太多损失或同步复杂性。事实上,在本质上具备高度可并行化的特征正是由于这些算法特性使其变得易于并行化
然而,在为每条射线单独创建一个线程时可能会带来负面影响。
尽管并行处理数百万个射线能显著提升性能,
但其带来的额外计算开销可能导致整体效率降低。
更优的做法是建立一组"任务"集群,
这些在物理内核上运行的任务集群能够高效地对画面进行分区处理。
2.2. Caching Immutable Values - 缓存不可变值
缓存其主要作用是防止反复进行相同的复杂计算。 在进行昂贵且需要频繁调用结果的计算时,请考虑将结果存储在缓存中以备下次调用。 这一策略尤其适合那些数值变化不大或很少的情况。
用于计算 IntersectRaySphere 的值时,光线追踪器往往需要较长的时间
a = dot(D, D)
b = 2 * dot(OC, D)
c = dot(OC, OC) - r * r
不同的值在不同的时间段内是不可变的。
在确定场景并明确球体尺寸之后,在此前提下就可以计算r * r这一数值了。只有当球体尺寸发生变动时,在这种情况下该数值才会发生变化;否则该数值保持不变
至少有一些特定数值在整个帧序列中是固定的不变量。 其中一个这样的数值是dot(OC, OC);当相机或球体发生移动时,在相邻帧之间该数值会发生变化。(需要注意的是,在阴影和平面反射光并非由相机发出的情况下,请避免在这种情况下使用缓存值)。
沿着这条射线的各个部分,在某些情况下这些参数保持恒定。 比如说,在 ClosestIntersection 中找到 dot(D, D) 的值后,并将其传递给 IntersectRaySphere 就行。
除了这些之外,还有一些其他的计算也可以被利用起来。激发你的创造力!不过,并非每一个缓存项都会使得整体性能得到提升,请注意可能产生的额外开销问题。在实施优化之前,请以基准为标准来评估优化措施的效果。
2.3. Shadow Optimizations - 阴影优化
当表面某一点被另一物体遮挡而落入影子时(这被称为 阴影连贯性),其邻近的点很可能也会位于该物体的影子内)。 如图5-2所示即为一个实例。

图 5-2:靠近的点很可能在同一个物体的阴影中。
当处理处于搜索范围内的物体时
同样地,在确定一个点是否位于阴影中时,并非必须寻找到最近的那个射线与物体的交点;只要存在至少一个交叉路口就足够了!这是因为这足以阻止光线到达该特定位置!因此建议实现一个新的函数 ClosestIntersection ,一旦找到任何一个射线与物体的交点即可立即返回结果;而无需计算并返回最小的那个参数t值;相反地,则只需返回一个布尔值指示即可。
2.4. Spatial Structures - 空间结构
处理光线与场景中每个球体的交点效率不高。多种数据结构允许您一次性丢弃一组对象而不必逐一处理它们。
假设有多个彼此相邻的三维几何体。我们需要确定能够包容所有这些几何体的空间最小包围球,并计算出其中心坐标及其半径值。当一条射线未与该包围空间发生接触时,则可以确定该射线也不会与其内部任何原始几何体会产生交集;然而,在这种情况下仍需验证该射线是否与内部所有原始几何体会存在交点。
您可以进一步深化发展拥有多个分层的球体组(即球体集合),构建为一个分层架构。仅在某个实际的球体很可能达到特定条件时才需持续遍历至底层以完成与光线的交互作用。
然而这些技术的精妙之处超出了此书的内容框架 您可以在名称边界卷层次结构中获取更多资料
2.5. Subsampling - 二次抽样
这是使您的光线追踪器速度提高 N 倍的简单方法:计算的像素数减少 N 倍!
在画布上的每一个像素点上,在视线方向追踪一条射线以获取来自该区域的光线颜色值。
当我们的样本数量不足时,在这一区域执行二次采样的过程。
那么我们该如何实现这一目标的同时确保渲染过程依然准确无误?
当您追踪这两个特定像素(10, 100)和(12, 100)的光线时,并观察到它们恰好击中了同一目标后,则可以合理推断中间位置(11, 100)处的光线也会与此目标相交。从而无需进行与场景所有物体相交计算即可直接确定该点的颜色。
如果在水平和垂直方向上每两个像素跳转一次,则可最大限度地减少关键光线相交计算——其实现效率可达4倍以上!
当然,在大多数情况下(尤其是在涉及极薄物体的情况下),您可能会忽略它们; 这是一种"不纯"的优化方法;从某种程度上讲(尤其是在技术细节层面),它与之前讨论的内容有所不同;经过这种优化后的图像与未经优化版本极为接近;然而,并非完全一致; 在某种程度上而言(尤其是在性能优先考虑的情况下),这是通过以不正当手段来作弊实现效率提升的方式;诀窍在于能够在不影响最终视觉效果的前提下(即保持令人满意的结果)识别可裁剪的部分; 在计算机图形学诸多领域中,则更加注重结果的人为质量。
三、Supporting Other Primitives - 支持其他原语
Supporting Other Primitives — 支持其他原语
在前面的章节中, 我们基于球体作为基元进行建模, 其原因在于其数学特性使其易于处理。 换句话说, 相对于复杂的几何形状而言, 寻找射线与球体的交点过程较为容易。 不过, 如果具备基础光线追踪能力的系统想要扩展支持更多几何元素, 那么扩展这部分功能所需的工作量并不会太大。
请特别注意,在实现基于光线追踪的方法时,请确保程序仅需计算光线与任何给定物体之间的两个问题:确定光线与物体的最近交点及其处的法线。其余相关的内容均不涉及这些对象。
三角形是一个卓越的基础单元。
由于其极简主义的设计原则,三角形是最简单的多边形,并且您可以利用它来构造更为复杂的多边形状。
在数学领域中具有高度可操作性的同时,在几何建模中这些形状也是高效技术中表示复杂表面近似值的理想选择。
为了使光线追踪器能够支持三角形渲染,请更改函数 TraceRay。首先,请计算由射线(定义为从指定原点沿特定方向延伸)与包含三角形的平面(基于给定法向量及距离参数)之间的交集。
因为平面无限延伸,在三维空间中光线通常会与任意给定的平面相交(除非这些光线方向与该平面严格平行)。因此,在进行下一步之前,必须确定射线与三角形的交点是否确实存在于三角形内部。多种方法可用于实现这一目标,例如,可采用重心坐标计算法或利用向量叉积来验证该交点相对于三角形三条边的位置关系。
一旦确认此点位于三角形内部,则交点处的法向量即为平面的法向量。 令 TraceRay 输出相应的数值,并无须进行任何额外修改!
四、Constructive Solid Geometry - 构造实体几何
Constructive Solid Geometry — 构造实体几何
为了渲染比球体或曲面更加复杂的物体, 这些复杂物体难以仅靠三角形精确建模. 两个经典的案例是透镜(类似于放大镜中的透镜)以及双子星(它并不是普通的天然卫星…)
我们可以比较容易地以简单明了的语言阐述这些对象。放大镜呈现为两片连在一起的球体;死星呈现出一个球体,并从其上截去了一个较小的球体。
我们可以用更加规范的语言将其表示出来,并以更为严谨的方式执行集合运算(包括并集、交集和差集等)。进一步说明的是,在前面的例子中,一个透镜可以用两个球体的交点来描述;而死星则是一个巨大的球体,在其内部减去一个小球体即可得到其结构(见图5-3)。

图 5-3:正在运行的构造立体几何。 A ∩ B 给了我们一个镜头。 C - D 给了我们死星。
您可能会以为计算实体对象之间的布尔运算是一个十分困难的几何学难题。 你说得确实没错!不过别担心——实际上发现了一种方法:通过构建立体模型来呈现这些集合操作的结果!
为了实现光线追踪器的功能,请说明具体的步骤或方法。针对每个物体而言,在其表面确定光线的入射点和出射点是必要的步骤。例如,在球体的情况下,在时间参数t_1和t_2中较小的那个值对应着入射时间点,在较大的那个值对应着出射时间点。当计算两个球体的相交区域时,在它们的时间参数范围内重叠的部分即为它们共同存在的区域。在差集操作中(即A-B),只有当一个物体完全位于另一个物体内部时才不考虑与之相关的遮挡关系。对于求并集的操作(A∪B),只要其中一个物体包含另一个物体中的任何一个点即可认为该区域属于整体。
更普遍地说,在求取光线与对象 A⊙B 的几何关系时(其中 ⊙ 表示任意集合操作),我们通常会先分别计算光线与单独的 A 和 B 集合之间的相交情况。这一步骤能够分别获得每个集合对应的参数区间 R_A 和 R_B(即它们各自的内部区域)。随后通过计算 R_A ⊙ R_B 这一操作的结果(即对应于 A 和 B 集合操作后的内部参数区间),我们能够确定出整体集合 A⊙B 内部区域的有效 t 值范围。一旦获得了这一结果范围之后,在判断该射线何时首次进入或离开整体区域 A⊙B 时,则只需比较该 t 值是否同时落在物体的有效范围内以及在 t_min 和 t_max 之间即可。图 5-4 显示了两个球体并集、交集以及差集运算后所形成的复合区域的具体形状。

图 5-4:两个球体的并集、交集和减法.
在相交处形成的法线要么与该物体相关联并指向其表面的那个法线之一重合要么与其相对的方向相反这取决于您是从物体外部还是内部的角度来观察这个情况
显然地讲,“A 和 B 无需为基元;它们本身就是集合操作的结果!当这一目标得以达成时(即无需关心它们的具体内容),您即可利用这些交点与法线进行进一步的计算。例如,在涉及三个球体的情况下(如(A ∪ B) ∩ C),这样的方法同样适用。”
五、Transparency - 透明度
Transparency ---- 透明度
到目前为止为止的所有对象都已经被完整地进行不透明化处理;然而实际情况并非如此;我们也可以选择对部分透明物体进行渲染处理,并非仅限于单一材质的应用场景
该目标的达成方式与处理镜面反射时的方式极为相似。 当光线照射到部分透明的表面时,类似于在处理镜面反射时的做法,您首先计算局部颜色和反射颜色,但除此之外,您还会额外计算一种来自光源的颜色——通过一次对 TraceRay 的调用即可获得这种光源特性的影响。 接着,根据物体表面透光率的不同程度,将这种光源特性影响的颜色值与之前的局部和反射颜色进行融合组合,这一过程与其在渲染物体反光特性时所采用的方式具有高度的一致性。
5.1. Refraction - 折射
在现实中,在透明物体中观察到光路会发生偏折现象(这就是说,在水中漂浮一根稻草时你可能会觉得它看起来断裂了一般)。更具体地说,在光从一种介质(如空气)进入另一种介质(如水)的过程中会发生折射现象
方向变化的规律受到每种材料属性的影响,并被称为折射率。由下述公式所表示的定律即被称为斯涅尔定律:
\dfrac{sin(α_1)}{sin(α_2)} = \dfrac{n_2}{n_1}
此处,在光束进入介质前后的界面处形成的角度分别为α₁和α₂,并表示光线与该界面法线所形成的入射角和出射角;同时,在物体内部与外部材料交界处的折射率分别为n_1和n_2
举个例子来说,在空气中n_{air}大约是1.0,在水中n_{water}大约是1.33。由此可知,在光线以60°角进入水中时:
\dfrac{\sin(60°)}{\sin(\alpha_2)} = \dfrac{n_water}{n_air} = \dfrac{1.33}{1.0}
进一步计算可得:
\sin(\alpha_2) = \dfrac{\sin(60°)}{n_water} = \dfrac{\sin(60°)}{1.33}
α_2 = arcsin( \dfrac{sin(60)}{1.33} ) = 40.628°
本示例如图 5-5 所示:

图 5-5:一束光线在离开空气并进入水中时被折射(改变方向)。
在实现层面上,在每条光线传播路径上都需要记录下其当前所处介质的折射率信息。当光线与一个部分透明的物体表面发生交集时,则需通过分析当前介质与新介质之间的折射率差异来确定光线在该界面处发生折射后的新传播方向,并按照此规律持续更新光线传播路径信息。
请思考一下:如果你具备了足够的几何构造能力与光学透彻度,并能实现基于交叠区域的三维模型渲染技术,则可以通过构建一个光学模型(即交叠区域)来模拟这种现象;这个模型的行为将严格遵循物理规律并呈现真实的放大部分效果!
六、Supersampling - 超级采样
Supersampling — 超级采样
在程度上或少或多地与子采样相反,在这种情况下您正在追求准确性而非性能。假设两个相邻像素的射线分别撞击不同的物体 您将绘制相应颜色到每个像素的位置
请注意从我们的起点出发进行类比:每一条光线都负责指定我们在观察中使用到的那个'单元'颜色表示方式。对于每一个像素而言,在被动状态下被指定为穿过其所在'网格结构'中心线的那一束光的颜色来表示该区域的信息状态变化情况。然而这可能并不准确
该方法旨在通过追踪更多的光线来解决问题,在每一个像素中选择4, 9, 16等数量的光线作为代表,并将这些光线的结果进行平均处理以确定每个像素的颜色值
然而使用这种技术会使您的光线追踪器性能分别降低4倍、9倍或16倍的原因与二次采样的效果提升N倍的根本原因一致。值得庆幸的是,在此问题上存在一个折衷方案。为了简化问题我们可以假设物体表面的颜色变化是平缓连续的。基于此建议您应从每个像素单独发射一条射线,并将相邻射线的结果进行比较。若该射线击中不同物体表面或其颜色差异超出预设阈值则需对该区域进行进一步细分。
七、Summary - 概括
在本章中概述了几种您可以自行探索的想法。这些基本光线追踪器通过新颖且有趣的途径进行了改进,并提升了效率。不仅能够表示更为复杂的物体,并且能够更加贴近真实的物理世界来模拟光线的行为。
这本书的第一部分将旨在展示光线追踪器这台引人注目的软件,并将通过简洁且易于理解的方法以及基础的数学运算来生成令人惊叹的画面。
令人惋惜的是,在追求这种纯粹性的同时,
代价高昂:性能。
如前所述,
尽管存在多种方法以优化和并行化光线追踪器,
但就实时性能而言,
它们的计算成本依然过高;
硬件的速度逐年提升,
然而某些应用程序对速度的要求却达到了原先至少100倍,
同时保证质量不受影响。
在所有这类应用中,
游戏行业最为严苛:
我们希望每一秒都能生成至少60帧完美画面。
光线追踪器在这方面也没有丝毫妥协。
那么说从 90 年代初期开始电子游戏是如何实现其发展的呢?答案就藏在我们即将在本书第二部分深入探讨的那一系列全新的算法架构中。
