身为一个计算机图形学领域的小白,最近由于课程的缘故学习了一些光线追踪方面的知识,并实现了基本的代码,现在分享给大家。希望读者能从一无所知到稍稍理解光线追踪的原理和意义,我的目的就达到了。
光线追踪的效果是生成一幅与真实世界高度相似的图片,先上结果图:
图中包含4个球体、1个正方体和地面。其中,绿球具有光滑表面,可以看到上面反射的其它物体的图案;红球具有光滑表面而且透明,可以透过它看到后面的蓝球;青球、黄色正方体和地面的表面是粗糙的,既没有反射也没有透射。另外,根据阴影的方向可以判断出光源位于屏幕的左上方。
以上就是图中包含的所有信息。接下来,我们将一步步揭开它的神秘面纱。
渲染
我们常常听到“渲染”这个词,却很少人清楚它的真正含义。当我们想要看到一个三维场景时,第一步要建立它的三维模型。这个模型存储在计算机中,它是真正的三维场景,但是我们看不到效果。要想从各个视角来观察这个三维模型,就要把它渲染成二维图片。就好像我们所处的世界是一个巨大的三维场景,无论你看或不看,它就在那里。当我们用眼睛来观察这个世界时,三维世界在视网膜上形成二维的像,这个像就是渲染的结果。
全局光照
对人来说,射入眼睛的光自动拼合成一幅完整的图像,这是自然而然是事情。但是计算机只有三维场景没有光,怎么办?
我们自己创造光。
我们完全仿照真实世界的物理规律,让三维场景中的光源向四面八方发出无数条光线,根据反射、折射、散射等等光学定律追踪每一条光线的轨迹。这样得到的效果称为全局光照,相对于局部光照,全局光照中任何一个物体都受到直接的光源照射和间接的来自其它物体反射光的照射。可以想象的是,这样追踪的结果是无穷无尽的,最初发出的每一条光线都会进一步衍生出无数条光线。在真实世界中,它们可以按照物理规律安静的发生,可是在计算机中我们实在无法处理如此大的计算量。
幸运的是,对于旨在渲染一幅图片的我们来说,大部分光线都是毫不相干的,我们只要追踪那些汇聚入摄像机镜头的光线就可以了。这种方法就是本文所讲的光线追踪。
光线追踪
与上一段提到方法恰好相反,我们让光线从摄像机镜头出发,而且只发出特定角度的光线。这些光线恰好每一条对应最后生成的二维图片中的一个像素,这样的话,生成一幅640x480的位图就只需要307200条光线。
下图展示了生成一个9x8的位图的光线追踪原理。在一个三维笛卡尔坐标系中(图中没有画出坐标轴的位置,可以任意选定),相机(Camera)和图像(Image)的位置一旦确定,图像中所能呈现的三维场景的范围也就随之确定。从相机发出72条光线,穿过图像中每个小方格的中心。对每一条光线,找到场景中与之最近的交点。在本例中,一些光线会与球面相交,如果该球体既能反射又能透射(这些性质作为物体的固有属性事先已在场景中定义),光线在交点处将会生成两条新的反射光线和折射光线。于是,这些新的光线将按照与最初的光线相同的处理办法,一直递归处理下去。但是,递归总要有结束条件的,当光线不与任何物体相交,即射向了无穷远处,递归就可以结束了。然而总有可能出现无论如何总有物体与之相交的情况,那么我们就设置一个最大递归深度,一旦超过了这个深度就强制停止递归。
递归了半天不要忘记我们的初衷,我们是为了在图像上成像而做的光线追踪。图像上每个点代表一个像素,它的RGB分量决定了该点的颜色。而我们做光线追踪的目的就是得到最初穿过这个像素的光线的真实RGB分量。在整个递归过程中,考虑真实世界的物理规律,入射光线的颜色与入射角、物体颜色、反射光线颜色和透射光线颜色有关,它们之间的关系可以用菲涅尔公式来描述,在此不做具体解释。最终,我们递归的结果相当于把所有反射和折射的效果都融合到了最初穿过图像的光线上,因此,把这些光线的颜色存入位图的相应位置就得到了一幅图像。
这就是光线追踪的基本原理。
然而,懂得了这些原理并不代表你就能写出一套光线追踪的程序。因为实际实现过程中存在的问题很多很多,绝不是百十行代码就能搞定的。我也不是做计算机图形学方面研究的,对光线追踪的理解还很是肤浅。因此本文不再详细讲解具体的实现过程,想要更进一步了解的可以访问参考资料中列出的前两篇文章,这两位作者是计算机图形学领域的专家,我从他们那里受益颇多。
另外,需要指出的是,虽然光线追踪已经尽可能地降低了计算量,但目前的计算机硬件并不支持对其加速,因此做不到实时渲染,只能用于离线渲染。
代码实现
无论如何还是应该提一下实现代码,不然对读者来说本文就太过空洞了。
首先,我们用Solid
类来表示三维场景中的物体,这是一个物体的基类,特定的形状如球体、正方体通过继承该类并覆写必要的方法来定义。在本例中,我们定义了球体Sphere
和立方体Cube
两个派生类。它们需要重写Solid
中的intersect
和nhit
方法。intersect
方法用于判断给定光线是否与本物体相交,且给出两个交点(射入和射出)的距离。nhit
方法用于计算给定交点处的法线方向。球体和立方体分别重新实现了这两个方法。
有了三维场景之后,我们就可以开始光线追踪。调用render
函数完成整个光线追踪的过程,并生成一幅图片。可以通过render
函数的参数调整相机和图像所处位置,以达到变换视角的效果。在render
中,对每一条光线递归调用trace
函数,返回值为该光线的颜色。在trace
中遍历场景中的所有物体,找到交点并计算反射和折射光线,进入下一次递归。
我把完整代码上传到了GitHub上,大家可以前往下载。渲染结果是一个ppm格式的文件,为了直接显示该文件,我使用了opencv库。因此,如果你的电脑中没有安装opencv,代码可能会报错。那么你只需要删除showpicture这个文件,并在主函数中去掉showpicture(file)
这一行代码即可。运行结束后在工程目录中找到生成的1.ppm文件,使用Photoshop打开,或到https://www.coolutils.com/online/PPM-to-PNG 上面在线转换成其它图片格式就可以看到效果了。
其它效果展示
如果我们想让画面动起来,有两种方式,让场景中的物体移动或让观察视角移动。像连拍照片一样在移动的同时不断地执行光线跟踪,生成一帧帧图片,再将它们连接起来就形成了一段视频。下面给出两个使用该场景制作出的视频片段。
视频1:视角移动
视频2:光源移动
需要改进的地方
相对于其它成功的光线跟踪作品来说,本文展示的效果有些相形见绌,尚有许多需要改进的地方。
粗糙物体表面受到光照的部分亮度完全一致,区分不开。比如正方体的左表面和上表面颜色完全一样,根本看不到棱的存在。这是因为我们对粗糙表面的处理太简单,只考虑被遮挡和不被遮挡两种情况。然而现实世界中,粗糙表面的颜色和亮度不光与光源有关,还与其他物体反射过来的光线颜色和亮度有关。改进方法是使用一种称为Path Tracing的技术,这会产生比光线追踪更好的效果。感兴趣的读者可以参考《Path Tracing介绍》这篇文章。
给物体表面添加纹理是个常见的话题。这需要做一些额外的工作,在参考资料的第二篇中讲解了如何添加纹理。
最后,我想说计算机图形学是一个非常大的学科,而且非常难。但它又极其重要,现在我们看到的屏幕上各种炫丽的画面都要归功于计算机图形学的贡献。因此感兴趣的同学可以深入研究,这里面大有可以施展才华之处。
参考资料
光线追踪算法综述 Manster
用JavaScript玩转计算机图形学(一)光线追踪入门 Milo Yip
Path Tracing介绍 Manster