说明
两个平面,如果不平行,那必然是相交的。平面相交就会形成一条直线。
几何
相交形成的这条直线,方向很好确定:就是同时垂直于两个平面的法线。也就是说,这条相交直线,我们已经求出一半了。剩下的就是在平面上,再找一个点就可以了。
如下图,平面法线 AB,另一个平面的法线 CD,叉乘后即可得到直线的方向 EF。
那么这个直线上的任意一点,应该怎么求呢?我们可以利用坐标原点(0,0,0) 来求。一个简单的事实就是,我们将原点沿法线 AB 移动 x 倍距离,再沿法线 CD 移动 y 倍距离,就能得到相交处的投影点 G。这里 x、y 是两个未知数,如果为负数,代表向法线的相反方向移动。那么 G 的坐标就是
(0,0,0) + x*AB + y*CD。
那么,如何求 x 和 y 的值呢?一种方法是解方程组,很显然:GA 垂直于 AB,而 GC 垂直于 CD。这样就得到了关于 x、y 的二元一次方程组,代入并求解就能得到 x、y 的值,也就得到了点 G 的真实坐标。
但是这种方法虽然很好想到,但方程组不好求解(当然也不是不可能,后面我们会讲到用 LAPACK 解线性方程组)。有没有更简单一点的方法呢?当然是有的。仔细观察可以想到:过原点再构造一个新平面,它的法线就用前面得到的相交线的方向。那么,点 G 的坐标,实际就是三个平面的交点。
那求三个平面的交点不是更复杂了么?不要紧,1990 年数学家 Goldman 得到了公式:
其中 P0 是平面 0 上的任一点,n0 则是平面 0 的法线,其余 P1、n1、P2、n2 同理。
注意:在三维空间中,三个平面并不一定相交于一点,如下图。它们有多种可能性:
要让三个平面相交于一点,需要满足 n0 ∙(n1 X n2) != 0 。但在本文情况下,第三个平面是我们用前两个平面叉乘得到,同时垂直于前两个平面,那么他们必然相交于同一点。而前两个平面如果平行(不相交),就不会有相交直线。
利用这个公式,我们可以得到相交点的坐标,进而得到相交直线。这个公式的另一个好处是:无需对法线进行归一化处理,也就是法线长度只要不是 0 就可以了,这样也减少了归一化带来的误差。
代码
实际代码中,先判断给出的两个平面是否平行,然后照搬公式得到交点坐标。
static func intersectionLine(plane1:Plane, plane2:Plane) -> Line? {
let crossValue = cross(plane1.normal, plane2.normal)
if length_squared(crossValue) < 0.0001 {
// 平行
return nil
}
// Goldman(1990), 法线无需归一化
let p0 = simd_float3.zero, n0 = crossValue
let p1 = plane1.position, n1 = plane1.normal
let p2 = plane2.position, n2 = plane2.normal
let cross20 = cross(n2, n0), cross01 = cross(n0, n1), cross12 = crossValue
let dot0 = dot(p0, n0), dot1 = dot(p1, n1), dot2 = dot(p2, n2)
let dotCross012 = dot0 * cross12, dotCross120 = dot1 * cross20, dotCross210 = dot2 * cross01
let position = (dotCross012 + dotCross120 + dotCross210) / dot(n0, cross12)
return Line(position: position, direction: n0)
}