图形学基础—光线追踪(Whitted-Style)

光线追踪(Ray tracing),是有别于光栅化渲染的另一种渲染方式。

在光栅化渲染中,计算着色点的颜色通常只会考虑着色点本身,因此想要在光栅化渲染中做出一些全局效果(比如全局光照)是非常麻烦的。

同时光栅化渲染会用到很多近似的方法,因此得到的渲染结果也是一个近似的结果。

而光线追踪是一种全局的渲染方式,能够得到一个准确的渲染结果,但代价是这种渲染方式十分耗时。

光线追踪的原理

光线追踪顾名思义,核心就是追踪光线在场景中的弹射。

所以首先,需要对光线做一些定义:

  • 光线一定沿着直线传播
  • 光线之间无法碰撞
  • 光线路径可逆,即从A发出的到B的光线,一定也可以从B发出到A(中途可发生反射和折射)

在现实生活中,我们能看到各种物体,是因为光源发出的光线经过各种反射折射,最终进入我们的眼睛。

通过上面我们对光线的定义,其实也可以理解成我们的眼睛发出“感知光线”,这些”感知光线“在场景中经过反射折射最终连接到光源上。

在早期欧洲科学启蒙阶段,很多学者也是这么认为的

感知光线

事实上光线追踪也正是这么做的。

Whitted-Style 光线追踪

Whitted-Style 光线追踪是 Turner Whitted 在 1980 年提出的一种光线追踪算法。该算法是光线追踪中最经典的实例之一,本质上是一种递归的算法。

Whitted-Style

该算法的实现步骤如下:

第一步 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

在一切光线追踪操作之前,首先需要解决的一步是怎么求光线和物体表面的交点。

要做到这一点,我们需要把数学上的光线定义出来。在数学上只需要一个起点和一个方向(通常指单位向量)就能够定义一根光线。

定义光线

光线上的任意一个点,都能用下面的式子表示:

r(t)=o+tdr(t) = o + t\vec{d}

0t<0 \leq t < \infty

光线与球的交点

三维空间中的球可以用下面的隐函数表示:

p:(pc)2R2=0p:{(p-c)}^2 - R^2 = 0

其中,pp 表示球面上的一个点,cc 表示球心,RR 表示半径。

如果光线与球有交点,则球上的一点 pp 可以用光线 o+tdo + td 表示,也就是:

(o+tdc)2R2=0{(o + t\vec{d}-c)}^2 - R^2 = 0

经过化简可以得到:

A=ddA=\vec{d} \cdot \vec{d}

B=2(oc)dB=2(o-c) \cdot \vec{d}

C=(oc)(oc)R2C=(o - c) \cdot (o - c) - R^2

t=b±b24ac2at=\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

其中为了使得交点有意义,所以 b24acb^2 - 4ac 需要大于 0, 并且最终计算得到的 tt 需要大于0。

光线与球的交点

光线与其他隐式表面的交点

通过上面光线与球的交点,可以推广出光线与所有隐式表面的交点。

假设存在隐式表面为: p:f(p)=0p:f(p) = 0

则只需要求解方程: p:f(o+td)=0p:f(o + t\vec{d}) = 0 中的 tt 即可。

隐式表面

光线与显式表面的交点

在实际的应用中,模型通常使用显式表面去表示,具体来说就是许许多多的三角形。光线与显式表面的求交,事实上就是光线与三角形的求交。

该问题可以分解成两步:

  • 计算光线与三角形所在平面的交点
  • 判断交点在不在三角形内

因此可以先将三角形所在的平面定义出来,平面的定义与光线类似,只需要一个点和一个方向即可。假设 p,p^, 为平面上的一点, nn 为平面的法向量,则有:

p:(pp,)n=0p:(p - p^,) \cdot \vec{n} = 0

如果光线和平面有交点,那么就可以写成:

p:(o+tdp,)n=0p:(o + t\vec{d} - p^,) \cdot \vec{n} = 0

化简可以得到:

t=(p,o)ndnt = \frac{(p^, - o) \cdot \vec{n}}{\vec{d} \cdot \vec{n}}

此时如果计算得到的 tt 在满足 0t<0 \leq t < \infty 则光线与平面有交点。

光线与平面的交点

最后,再判断交点是否在三角形内即可,在图形学基础—向量中介绍了利用叉乘计算点是否在三角形内部的方法。

有没有一步就能知道光线和三角形交点的方法呢?答案是有的。

假设光线与三角形有交点,那么利用三角形的重心坐标去表示这个交点,就可以写成:

O+tD=(1b1b2)(P0+b1P1+b2P2)\vec{O} + t\vec{D} = (1 - b_1 - b_2)(\vec{P}_0 + b_1\vec{P}_1 + b_2\vec{P}_2)

上面的式子中,包含三个未知量 tt b1b_1 b2b_2,又由于在三维场景中,点都有三个分量,因此事实上,上面的式子可以分解成三个方程组,使用克莱姆法则求解线性方程组可以得到:

[tb1b2]=1S1E1[S2E2S1SS2D]\begin{bmatrix} t \\ b_1 \\ b_2 \end{bmatrix} = \frac{1}{\vec{S}_1 \cdot \vec{E}_1} \begin{bmatrix} \vec{S}_2 \cdot \vec{E}_2 \\ \vec{S}_1 \cdot \vec{S} \\ \vec{S}_2 \cdot \vec{D} \end{bmatrix}

其中:

E1=P1P0\vec{E}_1 = \vec{P}_1 - \vec{P}_0

E2=P2P0\vec{E}_2 = \vec{P}_2 - \vec{P}_0

S=OP0\vec{S} = \vec{O} - \vec{P}_0

S1=D×E2\vec{S}_1 = \vec{D} \times \vec{E}_2

S2=S×E1\vec{S}_2 = \vec{S} \times \vec{E}_1

当满足 0t<0 \leq t < \infty 并且 0<b1<10 < b_1 < 1 , 0<b2<10 < b_2 < 1 , 0<(1b1b2)<10 < (1 - b_1 - b_2) < 1 时,光线与三角形有交点。

加速光线与表面求交

在显式表面中,可能包含很多很多的三角形,在判断光线与该物体求交时,计算量会比较大。如何简化计算是一件必要的事情。

Bounding Volumes

最常见的办法是使用包围盒进行加速计算。也就是先用一个简单的物体将其包围起来,使得当前模型一定在这个包围盒当中。光线先和这个包围盒求交,如果光线并不会与包围盒产生交点,则不再进行模型三角面的求交计算。

在三维中,最常用的包围盒就是一个长方体,而长方体可以理解成三组对面的交集。

长方体
长方体对面

并且在实际应用中,包围盒通常选用轴对齐包围盒,即AABB形式的包围盒,这种包围盒的六个平面都与坐标轴垂直或平行。

在分析三维空间中光线与包围盒求交之前,可以先在二维平面下进行分析。

二维情况下分析光线与包围盒求交

我们可以对X方向上的两个对面求交点,得到最早进入的时间 tmint_{min} 和最晚出去的时间 tmaxt_{max}

再对Y方向上的两个对面求交点,得到最早进入的时间 tmint_{min} 和最晚出去的时间 tmaxt_{max}

最后取两组时间的并集,就可以得到光线进入二维包围盒的最早时间和最晚时间。

同样拓展到三维空间中,就是三组对面,同样分别计算出光线进入三组对面的时间 tmint_{min} 和出去的时间 tmaxt_{max}

在三维场景中,只有当光线全部进入了三个对面,才认为光线进入了包围盒,并且光线只要离开了任意一个对面,那么就认为光线离开了包围盒

因此在三维场景中需要求得 tenter=max(tmin)t_{enter} = max(t_{min}) , texit=min(tmax)t_{exit} = min(t_{max}) 。此时:

  • 如果 texit<0t_{exit} < 0 则包围盒在光线背后,不会与光线产生交点。

  • 如果 texit0t_{exit} \geq 0 并且 tenter<0t_{enter} < 0 则光线在包围盒内部向外照射。会存在交点

  • 如果 texitt_{exit}tentert_{enter} 都大于零,并且$t_{exit} > tentert_{enter} 则光线与包围盒有两个交点。

所以光线与包围盒有交点的条件如下:

tenter<texit&&texit0t_{enter} < t_{exit} \&\& t_{exit} \geq 0

光线追踪示例

下面是我实现的一个光线追踪的示例,采用的是纯webgl实现。

默认光线最大允许弹射5次(可通过控件修改,每次修改会重新编译着色器,编译会造成短时间卡顿)。

渲染每一帧都会参考上一帧的画面进行混合,所以如果场景发生变动,那么将不再参考上一帧画面(否则会出现重影),这时画面会变得模糊,当静止下来后画面才会逐渐清晰。


图形学基础—光线追踪(Whitted-Style)
https://www.liaomz.top/2022/06/03/tu-xing-xue-ji-chu-guang-xian-zhui-zong-whitted-style/
作者
发布于
2022年6月3日
许可协议