图形学基础—光线追踪(Whitted-Style)
光线追踪(Ray tracing),是有别于光栅化渲染的另一种渲染方式。
在光栅化渲染中,计算着色点的颜色通常只会考虑着色点本身,因此想要在光栅化渲染中做出一些全局效果(比如全局光照)是非常麻烦的。
同时光栅化渲染会用到很多近似的方法,因此得到的渲染结果也是一个近似的结果。
而光线追踪是一种全局的渲染方式,能够得到一个准确的渲染结果,但代价是这种渲染方式十分耗时。
光线追踪的原理
光线追踪顾名思义,核心就是追踪光线在场景中的弹射。
所以首先,需要对光线做一些定义:
- 光线一定沿着直线传播
- 光线之间无法碰撞
- 光线路径可逆,即从A发出的到B的光线,一定也可以从B发出到A(中途可发生反射和折射)
在现实生活中,我们能看到各种物体,是因为光源发出的光线经过各种反射折射,最终进入我们的眼睛。
通过上面我们对光线的定义,其实也可以理解成我们的眼睛发出“感知光线”,这些”感知光线“在场景中经过反射折射最终连接到光源上。
在早期欧洲科学启蒙阶段,很多学者也是这么认为的
事实上光线追踪也正是这么做的。
Whitted-Style 光线追踪
Whitted-Style 光线追踪是 Turner Whitted 在 1980 年提出的一种光线追踪算法。该算法是光线追踪中最经典的实例之一,本质上是一种递归的算法。
该算法的实现步骤如下:
第一步 Ray Casting
光线追踪的第一步是要向场景中投射所谓的“感知光线”。
从相机位置连接近平面上每一个像素点的中心,向场景投射光线,并计算光线与场景物体的交点。
当然一般情况下,光线会在场景中产生多个交点,考虑到遮挡关系,我们只记录最近的交点。
找到了最近的交点之后,连接该交点和光源,判断是否有其他的物体在这条线段之间。如果有则该点在阴影里。如果没有,则可以利用着色模型(如 phong 模型)计算该点的像素颜色。
通过遍历近平面上所有的像素点就能得到一张完整的图像。如果光线追踪进行到这里就停止的话,那么得到的图像仅仅只包含了局部的光照信息。
第二步 Recursive (Whitted-Style) Ray Tracing
假设在第一步 Ray Casting 中,光线打到了一个玻璃材质的物体上。那么此时这条光线会发生镜面反射现象。
除了镜面反射,光线在玻璃材质中还会发生折射现象。因此,原本的光线在该交点又分裂出两条光线,而这两条光线也可能会在场景中产生交点,进而发生反射或折射现象。
从图中可以发现,根据光路的可逆性,到达眼睛的光不仅仅只来自于当前光线在场景中的第一个交点。光线经过折射或反射后再与场景相交的点也会贡献一部分光。
也就是说,光线与场景每一个交点的颜色贡献都会来自于这几种类型:
- 直接光照
- 反射方向间接光
- 折射方向间接光(如果有折射)
下一步将这些所有交点与光源连接,称这些线为shadow rays(因为可以用来检测阴影),计算这些所有点的局部光照模型的结果,将其按照光线能量权重累加,最终得到近投平面上该像素点的颜色。而这就是一个考虑全局效果的光照模型了,因为不仅仅考虑了直接光源的贡献,还考虑各种折射与反射光线的贡献。
以上就是Whitted-Style光线追踪的整个过程了,还有额外几点要注意的 tips:
- 整体过程是一个递归的过程,因此需要一定的递归终止条件,比如说允许的最大反射或折射次数为10 (也就是最大的递归深度是10)。
- 光线在每次反射和折射之后都有能量损耗的,由系数决定,因此越往后的折射和反射光贡献的能量越小,这也是为什么在上文中提到根据光线能量权重求和。 假设反射系数为0.7,那么第一次反射折损30%,第二次反射折损1-(70%x70%),依次类推。
- 如果反射或折射光线没有碰撞到物体,一般直接返回一个背景色。
光线追踪涉及的技术问题
上面简单介绍了 Whitted-Style 光线追踪的实现原理,但是其中仍然涉及到很多技术上的细节没有说,所以接下来重点讲一下这部分东西。
Ray-Surface Intersection
在一切光线追踪操作之前,首先需要解决的一步是怎么求光线和物体表面的交点。
要做到这一点,我们需要把数学上的光线定义出来。在数学上只需要一个起点和一个方向(通常指单位向量)就能够定义一根光线。
光线上的任意一个点,都能用下面的式子表示:
光线与球的交点
三维空间中的球可以用下面的隐函数表示:
其中, 表示球面上的一个点, 表示球心, 表示半径。
如果光线与球有交点,则球上的一点 可以用光线 表示,也就是:
经过化简可以得到:
其中为了使得交点有意义,所以 需要大于 0, 并且最终计算得到的 需要大于0。
光线与其他隐式表面的交点
通过上面光线与球的交点,可以推广出光线与所有隐式表面的交点。
假设存在隐式表面为:
则只需要求解方程: 中的 即可。
光线与显式表面的交点
在实际的应用中,模型通常使用显式表面去表示,具体来说就是许许多多的三角形。光线与显式表面的求交,事实上就是光线与三角形的求交。
该问题可以分解成两步:
- 计算光线与三角形所在平面的交点
- 判断交点在不在三角形内
因此可以先将三角形所在的平面定义出来,平面的定义与光线类似,只需要一个点和一个方向即可。假设 为平面上的一点, 为平面的法向量,则有:
如果光线和平面有交点,那么就可以写成:
化简可以得到:
此时如果计算得到的 在满足 则光线与平面有交点。
最后,再判断交点是否在三角形内即可,在图形学基础—向量中介绍了利用叉乘计算点是否在三角形内部的方法。
有没有一步就能知道光线和三角形交点的方法呢?答案是有的。
假设光线与三角形有交点,那么利用三角形的重心坐标去表示这个交点,就可以写成:
上面的式子中,包含三个未知量 ,又由于在三维场景中,点都有三个分量,因此事实上,上面的式子可以分解成三个方程组,使用克莱姆法则求解线性方程组可以得到:
其中:
当满足 并且 , , 时,光线与三角形有交点。
加速光线与表面求交
在显式表面中,可能包含很多很多的三角形,在判断光线与该物体求交时,计算量会比较大。如何简化计算是一件必要的事情。
Bounding Volumes
最常见的办法是使用包围盒进行加速计算。也就是先用一个简单的物体将其包围起来,使得当前模型一定在这个包围盒当中。光线先和这个包围盒求交,如果光线并不会与包围盒产生交点,则不再进行模型三角面的求交计算。
在三维中,最常用的包围盒就是一个长方体,而长方体可以理解成三组对面的交集。


并且在实际应用中,包围盒通常选用轴对齐包围盒,即AABB形式的包围盒,这种包围盒的六个平面都与坐标轴垂直或平行。
在分析三维空间中光线与包围盒求交之前,可以先在二维平面下进行分析。
我们可以对X方向上的两个对面求交点,得到最早进入的时间 和最晚出去的时间 。
再对Y方向上的两个对面求交点,得到最早进入的时间 和最晚出去的时间 。
最后取两组时间的并集,就可以得到光线进入二维包围盒的最早时间和最晚时间。
同样拓展到三维空间中,就是三组对面,同样分别计算出光线进入三组对面的时间 和出去的时间 。
在三维场景中,只有当光线全部进入了三个对面,才认为光线进入了包围盒,并且光线只要离开了任意一个对面,那么就认为光线离开了包围盒。
因此在三维场景中需要求得 , 。此时:
-
如果 则包围盒在光线背后,不会与光线产生交点。
-
如果 并且 则光线在包围盒内部向外照射。会存在交点
-
如果 和 都大于零,并且$t_{exit} > 则光线与包围盒有两个交点。
所以光线与包围盒有交点的条件如下:
光线追踪示例
下面是我实现的一个光线追踪的示例,采用的是纯webgl实现。
默认光线最大允许弹射5次(可通过控件修改,每次修改会重新编译着色器,编译会造成短时间卡顿)。
渲染每一帧都会参考上一帧的画面进行混合,所以如果场景发生变动,那么将不再参考上一帧画面(否则会出现重影),这时画面会变得模糊,当静止下来后画面才会逐渐清晰。