这里介绍的是“基元”之间的碰撞检测,所谓“基元”就是线段、三角形、矩形、平面、圆、椭圆等各种常见的、能用一两个数学公式表示的图形。“基元碰撞检测”是游戏开发中常用的手段,用数学公式求解碰撞结果,能让我们系统性的理解其中的原理。大家也不用担心,里面用到的数学公式,充其量高中、大一都学过,都属于“空间解析几何”范畴。
----------------------------------------------- 华丽的分割线 -----------------------------------------------
前面讲了2D线段”与“2D线段”之间的碰撞检测,这次是“2D线段”与“2D圆”的碰撞检测。
先说结果:
1. 两者如果有正常的“相交”,那么结果是1~2个交点,我们只取“离线段起始点最近的”那个点。
2. 如果两者相切,就是两个交点在同一个位置,随便取一个即可。
3. 在以下情况下认为没有交点:
圆心到线段的距离 > 圆的半径
线段很短、完全在圆内
线段与圆相交在延长线上
如果线段的两个端点重合,即线段是一个点,也认为没有交点。
开始推导:
假设在2D坐标系中,有一个圆,如下图所示:
圆心为C,坐标为(Xc, Yc),半径为R
那么,对于圆周上任意一点P,到C的距离为R,两点之间的距离可以用如下公式计算:
d = sqrt( (Xp - Xc)^2 +(Yp - Yc)^2) ) = R
用向量可以表示为:
d = | P - C | = R
对于线段P1 -> Q1,前面讲过,对于该线段上任意一个点P,可以用如下“参数方程”表示:
P = P1 + t * V1 t ∈ [0, 1]
----------------------------------------------- 华丽的分割线 -----------------------------------------------
假设两根线段相交于点P,那么联立方程组:
| P - C | = R
P = P1 + t * V1 t ∈ [0, 1]
把P代进圆的方程:
| P1 + t * V1 - C | = R
| t * V1 + (P1 - C) | = R
| t * V1 + CP1 | = R
注意这里左边的“V1”、“CP1”都是向量,右边的R是标量。
两边平方去掉绝对值符号:
(t * V1 + CP1)^2 = R^2
V1^2 * t^2 + (2 * CP1 * V1) * t + CP1^2 = R^2
V1^2 * t^2 + (2 * CP1 * V1) * t + (CP1^2 - R^2) = 0
这里,向量的平方就是对自己点积,向量的乘法就是点积,我们做进一步计算:
V1^2 = V1 * V1 = dot_product(V1, V1) = a2 * CP1 * V1 = 2 * dot_product(CP1, V1) = b
CP1^2 - R^2 = CP1 * CP1 - R^2 = dot_product(CP1, CP1) - R^2 = c
我们可以把方程简化为:
a * t^2 + b * t + c = 0
这个方程,大家有没有很熟悉,初中里都学过,一元二次方程。
它有一个专门的求根公式:
其中,,
当 Δ >= 0的时候,可以开根号,能求得两个“实数解”,线段与圆有1~2个交点。
当 Δ < 0的时候,这种情况下,其实是“圆心到线段的距离”大于圆的半径,故不会相交。
求根公式图片是从baidu上找的,我们把 x 替换成 t,就能得到两个解:
t1 = ( -b - sqrt( b^2 - 4ac ) ) / 2a
t2 = ( -b + sqrt( b^2 - 4ac ) ) / 2a
可以看到t1 <= t2。
至此,我们根据t1、t2的值就可以判断,线段与圆是否有效相交:
t1、t2都在[0, 1]范围内时,即 0 <= t1 <= t2 <= 1,这两个都是有效解,相交于两个点。
如果只有一个在[0, 1]范围内,则那个是有效解,即只相交一个点。
如果t1 = t2,则线段与圆相切,线段所在直线是圆的切线,切点就是相交点,正好在圆周上。
----------------------------------------------- 华丽的分割线 -----------------------------------------------
原理讲完了,求解部分的伪代码如下:
if (a == 0) return No Intersection; // 线段两个端点重合
Δ = b^2 - 4ac
if (Δ < 0 ) return No Intersection;
t1 = ( -b - sqrt(Δ) ) / 2a
if ( t1 >= 0 && t1 <= 1 )
P = P1 + t1 * V1
return P;
t2 = ( -b + sqrt(Δ) ) / 2a
if ( t2 >= 0 && t2 <= 1 )
P = P1 + t2 * V1
return P;
return No Intersection;
特别需要注意的是,如果V1的长度为0,线段两个端点重合,即a == 0,要特殊处理。
各种相交情况可以看以下这些图: