平面与球体的解析射线求交 —— 光线追踪背后的数学

计算机图形学
平面与球体的解析射线求交 —— 光线追踪背后的数学
Shot by Cátia Matos

从零推导射线与平面、射线与球体的交点公式,配合交互可视化、逐步数学推导,以及平行射线和负 t 值等边界情况的实用处理技巧。

什么是射线?

在现实生活中,我们通常将射线描述为从一个点出发,沿某个方向无限延伸的路径。

我们可以用数学方式来表达这个定义。
射线从一个点出发,我们记为 voriginv_{origin},并沿着某个特定方向 vdirectionv_{direction} 延伸。
由于计算机无法表示“无限”,我们引入一个参数 tt,表示射线上的长度。

R=vorigin+tvdirectionR = v_{\text{origin}} + t * v_{\text{direction}}
3
长度
0.00
方向

射线与平面求交

平面是讨论求交问题的一个很好的起点,一个常见的问题是:我们如何判断一条射线是否与一个平面相交?

基本思路是,如果射线与平面相交,那么交点 pip_{i} 必须位于该平面上。

根据射线的定义,我们可以表示交点 pip_{i}

pi=vorigin+tvdirectionp_{i} = v_{\text{origin}} + t * v_{\text{direction}}

另一方面,我们知道,如果一个点在平面上,那么该点与平面法向量 nn 的点积应为零:

pin=0p_{i} \cdot n = 0

因此,我们可以将 pip_{i} 的表达式代入点积公式中:

(vorigin+tvdirection)n=0(v_{\text{origin}} + t * v_{\text{direction}}) \cdot n = 0

这个判断射线是否与平面相交的问题,就转化为求解未知数 tt

如果这个方程有解,那么就说明射线与平面存在交点。

那平面的法向量 nn 是怎么来的呢?

我们可以假设平面固定在原点,并朝上方朝向。当需要变换时,可以使用矩阵对平面进行变换。当前情况下,平面的法向量可以取为 (0,1,0)(0, 1, 0)

接下来,我们将用代数方法求解这个方程。

逐步求解 tt

我们从射线与平面相交的条件出发:

(vori+tvdir)n=0(v_{\text{ori}} + t * v_{\text{dir}}) \cdot n = 0

展开点积为两个部分:

(vorin)+(tvdir)n=0(v_{\text{ori}} \cdot n) + (t * v_{\text{dir}}) \cdot n = 0

利用数量乘法的结合律,将 tt 提出点积之外:

(vorin)+t(vdirn)=0(v_{\text{ori}} \cdot n) + t * (v_{\text{dir}} \cdot n) = 0

接着,我们从两边同时减去 vorinv_{\text{ori}} \cdot n,以便将含 tt 的项单独列出:

t(vdirn)=(vorin)t * (v_{\text{dir}} \cdot n) = - (v_{\text{ori}} \cdot n)

最后,将两边同时除以 vdirnv_{\text{dir}} \cdot n,得到 tt 的解:

t=vorinvdirnt = - \frac{v_{\text{ori}} \cdot n}{v_{\text{dir}} \cdot n}

这个标量 tt 表示沿射线方向前进多远会与平面相交。

边界情况:负 tt 值与平行射线

在这个求交方程中,有两种情况是无效的:

如果 tt 为负值,说明平面在射线的后方。
可以想象我们就是射线,朝着 vdirectionv_{\text{direction}} 的方向前进,此时一个负的 tt 表示平面在我们身后,在相交的语义下这是没有意义的,因此这种情况被视为无效。

-4
x
0.00
方向

另一种无效情况是分母为零。
这意味着射线的方向与平面平行,此时 vdirn=0v_{\text{dir}} \cdot n = 0,会导致除以零的错误,不仅在数学上无解,在程序中也可能引发编译错误或运行时异常。
因此,这种情况也必须在实现中加以处理。

0.00
dir

总结一下:我们只在 t[0,)t \in [0, \infty) 的范围内,才认为这个交点是有效的。

射线与球体求交

众所周知,如果一个点位于球面上,那么该点 pp 到球心 (x0,y0,z0)(x_0, y_0, z_0) 的距离的平方必须等于球半径的平方 r2r^2。也就是说:

(pxx0)2+(pyy0)2+(pzz0)2=r2(p_x - x_0)^2 + (p_y - y_0)^2 + (p_z - z_0)^2 = r^2

这个式子实际上就是点 pp 到球心之间的欧几里得距离的平方。我们可以用向量的方式更简洁地表示为:

pc2=r2\|p - c\|^2 = r^2

其中 c=(x0,y0,z0)c = (x_0, y_0, z_0) 表示球心的位置。由于距离是非负的,两边同时开平方根得到:

pc=r\|p - c\| = r

为了简化分析,我们假设球心位于原点,即 c=(0,0,0)c = (0, 0, 0)。此时公式进一步简化为:

p=r\|p\| = r

根据定义,向量 pp 的长度为:

p=pp\|p\| = \sqrt{p \cdot p}

因此,我们的关系式可以写成:

pp=r\sqrt{p \cdot p} = r

为了简化运算,我们对等式两边平方,消掉平方根:

pp=r2p \cdot p = r^2

接下来,和之前一样的套路:把 pp 替换成射线的定义 R=vorigin+tvdirectionR = v_{\text{origin}} + t * v_{\text{direction}},于是得到:

(vo+tvd)(vo+tvd)=r2(v_{o} + t * v_{d}) \cdot (v_{o} + t * v_{d}) = r^2

你可能已经猜到了,点积具有分配律:

(vovo)+(votvd)+(tvdvo)+((tvd)(tvd))=r2(v_{o} \cdot v_{o}) + (v_{o} \cdot t * v_{d}) + (t * v_{d} \cdot v_{o}) + ((t * v_{d}) \cdot (t * v_{d})) = r^2

实际上这正是一个完全平方公式:

(vovo)+2t(vovd)+t2(vdvd)=r2(v_{o} \cdot v_{o}) + 2t * (v_{o} \cdot v_{d}) + t^2 * (v_{d} \cdot v_{d}) = r^2

你可能觉得我们提取 tt 的步骤有点快,但别担心,tt 是一个标量,之前我们也做过类似处理,这样操作是完全合法的。 我们已经知道射线的全部信息,即 vov_{o} 是射线的起点,vdv_{d} 是射线的方向,同时也知道球体的半径 rr

因此,这个公式就变成了关于未知数 tt 的一个二次方程。
为了更清晰地看出它的结构,我们可以按下面这种顺序写出它:

t2(vdvd)+t2(vovd)+(vovo)=r2t2(vdvd)+t2(vovd)+(vovor2)=0\begin{align*} t^2 * (v_{d} \cdot v_{d}) + t * 2 * (v_{o} \cdot v_{d}) + (v_{o} \cdot v_{o}) &= r^2 \\[12pt] t^2 * (v_{d} \cdot v_{d}) + t * 2 * (v_{o} \cdot v_{d}) + (v_{o} \cdot v_{o} - r^2) &= 0 \end{align*}

我们可以使用二次方程公式来求解:

x=b±b24ac2ax = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

在这个问题中,我们的各项对应如下:

  • a=vdvda = v_{d} \cdot v_{d}
  • b=2(vovd)b = 2 * (v_{o} \cdot v_{d})
  • c=vovor2c = v_{o} \cdot v_{o} - r^2

老实说,这个公式我们不一定要把它完全展开。
但如果你真的感兴趣,下面就是展开后的表达式:

x=2(vovd)±[2(vovd)]24(vdvd)(vovor2)2(vdvd)x = \frac{ -2(\vec{v_o} \cdot \vec{v_d}) \pm \sqrt{ [2(\vec{v_o} \cdot \vec{v_d})]^2 - 4 (\vec{v_d} \cdot \vec{v_d}) (\vec{v_o} \cdot \vec{v_o} - r^2) } }{ 2 (\vec{v_d} \cdot \vec{v_d}) }

希望你没被吓到——其实我很想用颜色来标注关键项,可惜 KaTeX 不支持 :( 不过不妨碍我们继续。

两个解,一个交点 —— 为什么用 min()

首先要明确:如果我们选择解析解法,那么每一帧都需要重新计算这个方程。

因此,第一个检查点就是根号里的内容(判别式)不能小于零,因为我们这里不涉及复数解。

然后,没错——我们实际上会得到两个解。
想象一下:射线第一次击中球体的地方是进入点,而之后还会有一个离开点。
所以在实现时,我们只关心最靠近的一次交点。
这就是为什么在代码中通常会使用 min() 函数来取最小的那个解。

0.00
旋转

今天就到这里啦——希望你觉得内容有趣又实用!
下次见!