如何直观地编写光线跟踪器

2020-07-12 07:59:58

在我的上一篇博客文章中,我展示了我在BBC Micro:Bit上的光线跟踪器。在那篇文章中,我略微谈到了什么是光线跟踪器,但没有讨论如何实际实现光线跟踪器。如果你不知道什么是光线跟踪器,请务必先阅读那篇文章。

这篇文章详细讨论了光线追踪器,逐步告诉你计算过程。它不是最先进的光线追踪器--事实上它就像它们来得那么简单。它在每个像素发射一条光线,不支持反射或折射。但是,一旦你理解了数学,你应该能够根据你的需要来调整它。

我故意没有包含任何代码示例,而是用文字、数学和图片进行解释。这可能看起来很可怕,但请相信我,光线跟踪并不复杂。如果我包含代码示例,您可能只会复制它们,而不会真正理解发生了什么。

在这篇文章结束时,您应该对光线跟踪器的工作原理有了一个直观的理解,您应该能够自己编写代码,更多的高级读者将能够用额外的功能扩展他们的实现。

光线跟踪器模拟光线来渲染3D场景。在开始之前,我们需要定义我们想要渲染的场景。

在我们的简单实现中,场景包含一个照相机和许多三角形。我们在场景中仅使用三角形,因为它们是最简单的渲染形状。我们可以在称为三角剖分的过程中将更复杂的形状转换为三角形。

我们添加了四个金字塔形状的三角形和摄像机,摄像机的位置也是一个3D坐标,它决定了我们在场景中观看的位置。

这款相机很特别,因为它既有位置又有方向。这样我们就可以转动相机,环视场景。相机的旋转方式是(偏航)和(俯仰)。偏航控制左右旋转,上下旋转由俯仰决定。同样,这两种方式可以让相机看向任何方向。

三角形是平面的,是二维的,我们可以把一个三角形想象成无限大平面的一小部分。

我们把平面描述为,满足的任何值都在平面上,反之亦然。

平面的、和分量决定其方向。我们可以通过求平面法线的角度来计算它们。法线是与平面正交(直角)的线。

为了得到法线的角度,我们将使用两个矢量的叉积,叉积是一个代数函数,给定两个矢量,输出一个与两个矢量正交的新矢量。

要找出平面的法线,我们需要在相交的平面上有两个矢量,我们可以选取三角形的任意两条边。

我将使用相交于的两条线。我们可以通过查看从一端到另一端的坐标差来计算这两条线的角度:

现在我们有了两条线,我们可以计算法线,这就是叉积运算符。

我对矢量并不陌生,但我肯定不记得怎么做交叉产品了。对于同一阵营的任何人来说,以下是它的工作原理:

如果你想更深入地解释它是如何工作的,试试这个“数学很有趣”的页面。我不羞于说我在那个网站上花了很长时间。它可能是为孩子们准备的,但它肯定是有帮助的!

使用叉积,我们现在有了法线的角度。的值映射到平面的组件。

要计算最终分量,我们需要返回平面公式:

这里,简单地说是平面上的任意点。为简明起见,最后两行使用的是点积。它的定义为:

现在我们知道了,我们可以通过替换法线的值和平面上的任意点来计算它,选择三角形中你最喜欢的角并使用它。

回到原来的飞机公式,我们可以代入我们的值来简单地得到或者说,就是这样,我们已经计算出了飞机的公式!

现在是开始模拟光线的时候了。线的矢量表示(包括位置和角度)是:

是光线的原点。在我们的示例中,它始终与相机坐标相同。是光线的角度。它指定、和坐标在我们沿线移动时如何变化。该参数指定我们沿线走了多远。

你可能在学校学过类似的二维图形公式,这相当于说:

换句话说,对于每个增加的,增加,你可以沿线移动,使用,很容易看到这是如何扩展到在3D中工作的。

找到光线的原点很容易,所有光线都从相机位置开始,所以我们只使用它。

计算这条线的角度要复杂得多,这是我们沿着这条线移动时每个坐标变化的比率。

如果是这样的话,每增加一次,我们就会看到增加一倍,增加三倍,因为这只是一个比率,我们说还是不说都无关紧要。

我们可以通过观察坐标是如何沿着直线的已知部分变化的。我们将观察从相机到视图平面的直线部分。视图平面是位于相机前面的栅格。栅格中的每个单元格都是我们想要在屏幕上绘制的一个像素。

现在,我们只考虑最简单的情况。相机没有旋转,只是直视前方(方向)。我们正在计算中心光线的角度,它穿过栅格中最中心的像素。

在这种情况下,相机位置和视图平面中心之间的坐标变化很简单。在本例中,我们将视平面为固定宽度。由于屏幕是像素,因此每个像素的宽度和高度都是。

对于中心右侧的每个像素,我们必须加上。这意味着对于作为中心的像素,简单地说就是:

不过,这忽略了摄像机的方向。幸运的是,旋转矢量非常简单,我们可以简单地旋转以匹配摄像机的旋转。

既然我们都知道和,我们就可以计算光线路径上任何点的坐标。

我们终于准备好模拟我们的光线了。我们知道我们的光线从哪里开始,它们要去哪里,以及它们可能击中的一切。剩下要做的就是实际模拟路径,找出每条光线击中的是什么。

更准确地说,我们需要找出每个成对的射线/平面组合的交点,更准确地说,对于每个成对的射线和平面组合,交点是什么?

显然,必须同时在和上。我们可以代入公式以获得:

现在我们知道了,我们可以把它代入到上面的定义中,以计算的坐标。

并不是所有的交点都是有效的,我们只知道它和三角形在同一平面上,但我们只关心那些实际在三角形内的交点。

在执行任何其他检查之前,请注意。如果您的值为负值,您可以提前停止。该平面不会被光线击中,因为交点在相机后面。

然后,我们可以做一个快速的健全性检查,确保它在三角形的边界框内。如果不能在三角形内,因为它太左了。对或、和、或的每个组合重复此操作。

我们首先这样做,因为它比实际检查快得多,而且我们通常可以提前退出。如果在边界框内,我们需要执行完全检查,其工作原理如下:

要检查和是否在线路的同一侧,我们执行以下操作:

这同时使用了叉积和点积,叉积有一个与相同但方向相反的特殊性质。

这更进一步,说方向取决于它的哪一边,如果和在同一边,那么它们之间的角度就是,如果它们在不同的一边,角度就会是,如果和在一边,那么它们之间的角度就是,如果它们在不同的一边,角度就是。

当点积为正时,两个矢量之间的角度为,这意味着它们一定在同一侧。

如果和在同一条边,我们对三角形的三条边中的每一条边重复这一点,任何不在三角形中的交点都可以丢弃。

每条光线只能与一个三角形相交,因此我们需要找到最先命中的三角形。要计算光线行进的距离,我们可以在3D中使用毕达哥拉斯定理:

对于我们的简单实现,我们将每个像素的亮度设置为,这意味着离相机越近的像素越亮。任何不相交的光线都会产生黑色像素。

到此为止!您现在已经了解了所需的所有内容,并且可以编写自己的光线跟踪器。如果您想了解有关3D渲染的更多信息,请确保阅读以下内容:

如果您仍然迫切需要一些示例代码,您可以看看我上一篇文章中的代码,不管是哪种方式,您都应该自己先试一试。

完成基本光线跟踪器后,可以按从简单到困难的顺序添加以下一些附加功能:

如果你真的写了自己的光线追踪器,请在推特上给我看看(特别是如果你实现了引力透镜,你这个十足的疯子)。

我是Scott Logic的一名相当新的开发人员。我写的话题非常广泛--任何我觉得有趣、你也可能感兴趣的话题。在GitHub和推特上找我。