首发于Milo的编程

面试题:两个匀速移动球体的碰撞

为了不再重复问相同的面试题(怠惰!真是怠惰啊!),我决定公开一条我常用的面试题(游戏客户端/移动端开发职位)。
在三维空间,给定两个球体 A 和 B 的球心初始位置 \mathbf{c}_A\mathbf{c}_B,半径 r_Ar_B,匀速移动速度 \mathbf{v}_A\mathbf{v}_B,如何判断它们两者会否碰撞,如会碰撞求碰撞时间。

错误思路:

  • 求两射线是否相交:在三维中两射线相交几乎是不可能发生的。
  • 求两圆柱体是否相交:即使相交,还不能判断是否碰撞。

如果没有头绪,给第一个提示:

如果不考虑移动,怎样判断两个静态的球体是否相交?

这算是最基本的相交测试,通常都可以答出来。如果两球心的距离小于等于两半径之和,即两者相交:


\left \| \mathbf{c}_A - \mathbf{c}_B \right \| \le r_A+r_B

第二个提示:

那么,可以把两个匀速移动的球心位置表示为时间 t 的函数么?

这是简单的运动学,速度乘以时间等于位移,再加上初始位置:

\begin{align}
\mathbf{x}_A(t) &= \mathbf{c}_A + \mathbf{v}_At\\
\mathbf{x}_B(t) &= \mathbf{c}_B + \mathbf{v}_Bt
\end{align}

到这一步,多数同学能想到可以用这两个函数代入上面的方程。由于只需要考虑两个球刚触碰的时间,不需考虑不等式,只需考虑方程:

\begin{align}
\left \| \mathbf{x}_A(t) - \mathbf{x}_B(t) \right \| &= r_A+r_B\\
\left \| (\mathbf{c}_A + \mathbf{v}_At) - (\mathbf{c}_B + \mathbf{v}_Bt) \right \| &= r_A+r_B\\
\left \| (\mathbf{v}_A - \mathbf{v}_B)t +(\mathbf{c}_A - \mathbf{c}_B)  \right \| &= r_A+r_B\\
\end{align}

为简单起见,设 \mathbf{v} = \mathbf{v}_A - \mathbf{v}_B, \mathbf{c} = \mathbf{c}_A - \mathbf{c}_B, r = r_A + r_B

\left \| \mathbf{v}t + \mathbf{c} \right \| = r

到这一步,有些同学会用 \left \| \mathbf{u} \right \| = \sqrt{u_x^2 + u_y^2 + u_z^2} 去展开,但理想地不需这样以分量去计算,而是以点积表示 \left \| \mathbf{u} \right \|^2 = \mathbf{u} \cdot \mathbf{u},并且知道点积在加法上的分配律:

\begin{align}
\left \| \mathbf{v}t + \mathbf{c} \right \|^2 &= r^2\\
(\mathbf{v}t + \mathbf{c}) \cdot (\mathbf{v}t + \mathbf{c}) &= r^2\\
(\mathbf{v} \cdot \mathbf{v})t^2 + 2(\mathbf{v} \cdot \mathbf{c})t + \mathbf{c} \cdot \mathbf{c} &= r^2\\
(\mathbf{v} \cdot \mathbf{v})t^2 + 2(\mathbf{v} \cdot \mathbf{c})t + \mathbf{c} \cdot \mathbf{c} - r^2 &= 0\\
\end{align}

同学应该看得出,这是关于 t 的一元二次方程 at^2 + bt + c = 0

求解一元二次方程有多少种情况?每种情况表示什么碰撞情况?

前半是初中的数学知识,按判别式 \Delta = b^2 - 4ac

  1. 如果 \Delta > 0,方程有两个根 t_1, t_2,设 t_1 < t_2
  2. 如果 \Delta = 0,方程有重根 t_1
  3. 如果 \Delta < 0,方程无实根。

后半的问题难倒了一些同学,主要是忘记了方程本来是表达什么。这个方程的意义,是求出某个时间点,当时两个球体刚好接触。那么三种情况对应的是:

  1. 两个球体在时间 t_1 触碰,然后互相进入穿过(物理上不可能数学上可以),在时间 t_2 分离;
  2. 两个球体在时间 t_1 擦身而过;
  3. 两个球体不碰撞。

但两者是否碰撞还有一个约束 t \ge 0,不应考虑逆向时间碰撞。所以我们还是需要求根,才能判断它们是否碰撞。

怎样解一元二次方程?写成程序有什么地方要注意?

不就是背公式么?


t = \frac{-b\pm\sqrt{\Delta}}{2a}

有同学看到除数都不敏感,除数是会出现 division by zero 错误的啊!

a 在什么时候会为零?怎样处理?
a=0 的时候,一元二次方程就退化成一元一次方程 bt + c = 0 \Leftrightarrow t=-c/b。不过在这个问题中,当 a=\mathbf{v}\cdot\mathbf{v}=0 的时候,也表示 \mathbf{v} = 0,所以 b =  2(\mathbf{v} \cdot \mathbf{c})=0。最后原来的一元二次方退化成:
c=0
这是什么意思?若 c = 0 代表什么?若 c \ne 0 又代表什么?
从数学上说,若 c = 0 代表方程有无穷解,t 为任意值都满足方程;若 c \ne 0,代表方程无解,无论 t 为任何值都不能满足方程。

对于本问题来说,a=0 \Leftrightarrow \mathbf{v} = 0 \Leftrightarrow \mathbf{v}_A = \mathbf{v}_B 即两个球体速度相同,那么它们是平行地往同一个方向匀速移动。而 c=0 \Leftrightarrow \mathbf{c}\cdot\mathbf{c} - r^2 = 0\Leftrightarrow \left \| \mathbf{c}_A - \mathbf{c}_B \right \| =r_A + r_B 即两球球心初始位置的距离等于两球半径之和。换句话说,如果两球的速度相同,我们需要判断它们初始位置是否刚好接触,若是,它们永远碰撞不分离,若否,它们永远不碰撞。

前天,有一位同学想到,利用閔可夫斯基差的概念,这个问题可以转变为射线与球体相交问题(A 球体缩小至一点,B球体膨胀成半径 r_A + r_B;按相对速度,当 B 变为静上,A 的速度变为 \mathbf{v}_A - \mathbf{v}_B)。其实这也会得到相同的一元二次方程。不过按这个思路,也可以用上 RTR 中提到射线和球体相交测试的一些优化方法。

这个面试题的好处是有多个考查点,评估同学的数学基础能力,并可按同学的情况提供协助。缺点是时间比较长,前天面 12 位同学真是累死(还有问其他题目)。我还要想一些新题目,如想到有类似这种面试题,求私信提供。


(题图 Photo by Sebastian Pichler

更新1(2016/9/26)

评论中很多人谈到相对速度、变换参考系等,我这里统一回应一下。

我们可以不考虑两球分别的速度,而仅考虑它们的相对速度。例如,假设球 B 是观察者,那么球 A 相对于球 B 的速度为:

\mathbf{v}_{A|B} = \mathbf{v}_A - \mathbf{v}_B

我们也知道,两球的绝对位置是不重要的,我们只需要考虑它们的相对位移。以球 B 为原点的话,球 A 相对于球 B 的初始位置为:

\mathbf{c}_{A|B}=\mathbf{c}_A - \mathbf{c}_B

然后,我们可以发现,这样和之前列出的方程是一模一样的:

\left \| \mathbf{c}_{A|B} + \mathbf{v}_{A|B}t \right\| = r^2

此外,有人提出

在一个球静止的情况下,可用直线与原点的距离是否小于半径之和,来判定两者是否碰撞。

我们来试一下。首先,设直线为参数式 \mathbf{r}(t) = \mathbf{c} + \mathbf{v}t。设直线上最近原点的点为 \mathbf{r}(t_0),原点过 \mathbf{r}(t_0) 的直线与 \mathbf{v} 垂直,因此它们的点积为零:


\begin{align}
(\mathbf{c} + \mathbf{v}t_0)\cdot\mathbf{v} &= 0\\
\mathbf{c}\cdot \mathbf{v} + \mathbf{v} \cdot \mathbf{v} t_0 &= 0\\
t_0 &= -\frac{\mathbf{c}\cdot \mathbf{v}}{\mathbf{v} \cdot \mathbf{v}}
\end{align}

得出 t_0 后,\mathbf{r}(t_0) 与原点的距离就是它的模,我们计算其模的平方:

\begin{align}
\left\|\mathbf{r}(t_0)\right\|^2 &= \mathbf{r}(t_0) \cdot \mathbf{r}(t_0)\\
&= (\mathbf{c}+\mathbf{v}t_0) \cdot (\mathbf{c}+\mathbf{v}t_0)\\
&= \mathbf{v}\cdot\mathbf{v}t_0^2+2\mathbf{c}\cdot\mathbf{v}t_0+\mathbf{c}\cdot\mathbf{c}\\
&= \mathbf{v}\cdot\mathbf{v} \left(-\frac{\mathbf{c}\cdot\mathbf{v}}{\mathbf{v}\cdot\mathbf{v}} \right )^2+2\mathbf{c}\cdot\mathbf{v}\left(-\frac{\mathbf{c}\cdot\mathbf{v}}{\mathbf{v}\cdot\mathbf{v}} \right )+\mathbf{c}\cdot\mathbf{c}\\
&= \frac{(\mathbf{c}\cdot\mathbf{v})^2}{\mathbf{v}\cdot\mathbf{v}} - 2  \frac{(\mathbf{c}\cdot\mathbf{v})^2}{\mathbf{v}\cdot\mathbf{v}} + \mathbf{c} \cdot \mathbf{c}\\
&= \mathbf{c}\cdot\mathbf{c}- \frac{(\mathbf{c}\cdot\mathbf{v})^2}{\mathbf{v}\cdot\mathbf{v}}
\end{align}

然后我们检查它是否小于半径和的平方:

\begin{align}
\mathbf{c}\cdot\mathbf{c}-\frac{(\mathbf{c}\cdot\mathbf{v})^2}{\mathbf{v}\cdot\mathbf{v}} &\le r^2\\
-\frac{(\mathbf{c}\cdot\mathbf{v})^2}{\mathbf{v}\cdot\mathbf{v}} &\le r^2 -\mathbf{c}\cdot\mathbf{c} \\
\frac{(\mathbf{c}\cdot\mathbf{v})^2}{\mathbf{v}\cdot\mathbf{v}} &\ge \mathbf{c}\cdot\mathbf{c} - r^2 \\
(\mathbf{c}\cdot\mathbf{v})^2 &\ge (\mathbf{v}\cdot\mathbf{v})(\mathbf{c}\cdot\mathbf{c} - r^2) \\
(\mathbf{c}\cdot\mathbf{v})^2 - (\mathbf{v}\cdot\mathbf{v})(\mathbf{c}\cdot\mathbf{c} - r^2) &\ge 0 \\
\end{align}

这个不等式是不是有点眼熟?我们之前没展开判别式\Delta

\begin{align}
\Delta &= b^2 - 4ac\\
&= (2 \mathbf{v}\cdot\mathbf{c})^2 - 4 (\mathbf{v}\cdot\mathbf{v})(\mathbf{c}\cdot\mathbf{c} - r^2)\\
&= 4 (\mathbf{v}\cdot\mathbf{c})^2 - 4 (\mathbf{v}\cdot\mathbf{v})(\mathbf{c}\cdot\mathbf{c} - r^2)
\end{align}

我们发现,刚才计算直线\mathbf{r}(t)与原点的距离是否小于半径和的不等式,等价于检测 \Delta \ge 0 。这不是巧合,事实上这两个问题是等价的。如果有同学想到直线距离的判断,也是正确的,不过之后也是要求直线和原点距离刚好等于半径和时候的 t,结果也要做相同的计算。

编辑于 2016-09-26

文章被以下专栏收录