在我的上一篇博客文章中,我展示了我在BBC Micro:Bit上的光线跟踪器。在那篇文章中,我略微谈到了什么是光线跟踪器,但没有讨论如何实际实现光线跟踪器。如果你不知道什么是光线跟踪器,请务必先阅读那篇文章。
这篇文章详细讨论了光线追踪器,逐步告诉你计算过程。它不是最先进的光线追踪器--事实上它就像它们来得那么简单。它在每个像素发射一条光线,不支持反射或折射。但是,一旦你理解了数学,你应该能够根据你的需要来调整它。
我故意没有包含任何代码示例,而是用文字、数学和图片进行解释。这可能看起来很可怕,但请相信我,光线跟踪并不复杂。如果我包含代码示例,您可能只会复制它们,而不会真正理解发生了什么。
在这篇文章结束时,您应该对光线跟踪器的工作原理有了一个直观的理解,您应该能够自己编写代码,更多的高级读者将能够用额外的功能扩展他们的实现。
光线跟踪器模拟光线来渲染3D场景。在开始之前,我们需要定义我们想要渲染的场景。
在我们的简单实现中,场景包含一个照相机和许多三角形。我们在场景中仅使用三角形,因为它们是最简单的渲染形状。我们可以在称为三角剖分的过程中将更复杂的形状转换为三角形。
我们添加了四个金字塔形状的三角形和摄像机,摄像机的位置也是一个3D坐标,它决定了我们在场景中观看的位置。
这款相机很特别,因为它既有位置又有方向。这样我们就可以转动相机,环视场景。相机的旋转方式是(偏航)和(俯仰)。偏航控制左右旋转,上下旋转由俯仰决定。同样,这两种方式可以让相机看向任何方向。
三角形是平面的,是二维的,我们可以把一个三角形想象成无限大平面的一小部分。
我们把平面描述为,满足的任何值都在平面上,反之亦然。
平面的、和分量决定其方向。我们可以通过求平面法线的角度来计算它们。法线是与平面正交(直角)的线。
为了得到法线的角度,我们将使用两个矢量的叉积,叉积是一个代数函数,给定两个矢量,输出一个与两个矢量正交的新矢量。
要找出平面的法线,我们需要在相交的平面上有两个矢量,我们可以选取三角形的任意两条边。
我将使用相交于的两条线。我们可以通过查看从一端到另一端的坐标差来计算这两条线的角度:
现在我们有了两条线,我们可以计算法线,这就是叉积运算符。
我对矢量并不陌生,但我肯定不记得怎么做交叉产品了。对于同一阵营的任何人来说,以下是它的工作原理:
如果你想更深入地解释它是如何工作的,试试这个“数学很有趣”的页面。我不羞于说我在那个网站上花了很长时间。它可能是为孩子们准备的,但它肯定是有帮助的!
使用叉积,我们现在有了法线的角度。的值映射到平面的组件。
要计算最终分量,我们需要返回平面公式:
这里,简单地说是平面上的任意点。为简明起见,最后两行使用的是点积。它的定义为:
现在我们知道了,我们可以通过替换法线的值和平面上的任意点来计算它,选择三角形中你最喜欢的角并使用它。
回到原来的飞机公式,我们可以代入我们的值来简单地得到或者说,就是这样,我们已经计算出了飞机的公式!
现在是开始模拟光线的时候了。线的矢量表示(包括位置和角度)是:
是光线的原点。在我们的示例中,它始终与相机坐标相同。是光线的角度。它指定、和坐标在我们沿线移动时如何变化。该参数指定我们沿线走了多远。
你可能在学校学过类似的二维图形公式,这相当于说:
换句话说,对于每个增加的,增加,你可以沿线移动,使用,很容易看到这是如何扩展到在3D中工作的。
找到光线的原点很容易,所有光线都从相机位置开始,所以我们只使用它。
计算这条线的角度要复杂得多,这是我们沿着这条线移动时每个坐标变化的比率。
如果是这样的话,每增加一次,我们就会看到增加一倍,增加三倍,因为这只是一个比率,我们说还是不说都无关紧要。
我们可以通过观察坐标是如何沿着直线的已知部分变化的。我们将观察从相机到视图平面的直线部分。视图平面是位于相机前面的栅格。栅格中的每个单元格都是我们想要在屏幕上绘制的一个像素。
现在,我们只考虑最简单的情况。相机没有旋转,只是直视前方(方向)。我们正在计算中心光线的角度,它穿过栅格中最中心的像素。
在这种情况下,相机位置和视图平面中心之间的坐标变化很简单。在本例中,我们将视平面为固定宽度。由于屏幕是像素,因此每个像素的宽度和高度都是。
对于中心右侧的每个像素,我们必须加上。这意味着对于作为中心的像素,简单地说就是:
不过,这忽略了摄像机的方向。幸运的是,旋转矢量非常简单,我们可以简单地旋转以匹配摄像机的旋转。
既然我们都知道和,我们就可以计算光线路径上任何点的坐标。
我们终于准备好模拟我们的光线了。我们知道我们的光线从哪里开始,它们要去哪里,以及它们可能击中的一切。剩下要做的就是实际模拟路径,找出每条光线击中的是什么。
更准确地说,我们需要找出每个成对的射线/平面组合的交点,更准确地说,对于每个成对的射线和平面组合,交点是什么?
显然,必须同时在和上。我们可以代入公式以获得:
现在我们知道了,我们可以把它代入到上面的定义中,以计算的坐标。
并不是所有的交点都是有效的,我们只知道它和三角形在同一平面上,但我们只关心那些实际在三角形内的交点。
在执行任何其他检查之前,请注意。如果您的值为负值,您可以提前停止。该平面不会被光线击中,因为交点在相机后面。
然后,我们可以做一个快速的健全性检查,确保它在三角形的边界框内。如果不能在三角形内,因为它太左了。对或、和、或的每个组合重复此操作。
我们首先这样做,因为它比实际检查快得多,而且我们通常可以提前退出。如果在边界框内,我们需要执行完全检查,其工作原理如下:
要检查和是否在线路的同一侧,我们执行以下操作:
这同时使用了叉积和点积,叉积有一个与相同但方向相反的特殊性质。
这更进一步,说方向取决于它的哪一边,如果和在同一边,那么它们之间的角度就是,如果它们在不同的一边,角度就会是,如果和在一边,那么它们之间的角度就是,如果它们在不同的一边,角度就是。
当点积为正时,两个矢量之间的角度为,这意味着它们一定在同一侧。
如果和在同一条边,我们对三角形的三条边中的每一条边重复这一点,任何不在三角形中的交点都可以丢弃。
每条光线只能与一个三角形相交,因此我们需要找到最先命中的三角形。要计算光线行进的距离,我们可以在3D中使用毕达哥拉斯定理:
对于我们的简单实现,我们将每个像素的亮度设置为,这意味着离相机越近的像素越亮。任何不相交的光线都会产生黑色像素。
到此为止!您现在已经了解了所需的所有内容,并且可以编写自己的光线跟踪器。如果您想了解有关3D渲染的更多信息,请确保阅读以下内容:
如果您仍然迫切需要一些示例代码,您可以看看我上一篇文章中的代码,不管是哪种方式,您都应该自己先试一试。
完成基本光线跟踪器后,可以按从简单到困难的顺序添加以下一些附加功能:
如果你真的写了自己的光线追踪器,请在推特上给我看看(特别是如果你实现了引力透镜,你这个十足的疯子)。
我是Scott Logic的一名相当新的开发人员。我写的话题非常广泛--任何我觉得有趣、你也可能感兴趣的话题。在GitHub和推特上找我。