S02E06:线段与线段最近点坐标

228 阅读5分钟

说明

线段与线段最近点坐标,当然也可以仿照射线那样,分情况讨论处理,只不过线段的情况比射线复杂了一倍而已。从理论上讲写出来也不算太难,但是这里,我们可以试试其它方法,比如万能的矩阵:构造一个局部坐标系,将两根线段转换到一个平面上进行处理,求出最近点。

几何

如下图,线段 AB 和 CD 在空间内的位置。我们要利用它们构建一个坐标系,以点 A 为原点,AB 为 x 轴。

构造局部坐标系

那么接下来,用叉乘来构造 z 轴,即同时垂直于 AB 和 CD 的轴。再用 x 轴和 z 轴叉乘来得到 y 轴,这样一来就可以将两根线段的位置关系,从三维空间降级到 xy 平面上进行处理了。如下图,向量 AF 就是我们构造出来的 z 轴,它同时垂直于 AB 和 CD。 不过这里有个小问题:如果 AB 和 CD 是平行的,那叉乘就没有意义了。这里应该怎么办?这时,我们可以任选一个向量来当临时向量,比如选以前的原始 x 轴,即(1, 0, 0)方向,利用叉乘得到垂直于 AB 和(1, 0, 0)的向量,把它当做局部坐标系的 z 轴,就可以继续后面的处理。

不过这里又有一个问题:如果 AB 平行于 CD,并且平行于原始的 x 轴,又该怎么办?这样其实更简单,我们直接选取原始的 z 轴(0, 0, 1)作为局部坐标系的 z 轴就可以了。这里显然,z 轴是垂直于 x 轴的,也垂直于 AB 和 CD。

总之,不管什么情况,我们的目标是构造出同时垂直于 AB 和 CD 的局部 z 轴,这样就可以暂时丢掉 z 坐标,专心在 xy 平面处理两个线段的位置关系,这样将三维问题化简为二维平面问题。而且其中线段 AB 还在局部坐标系的 x 轴上,A 点是局部坐标系的原点,这样大大降低了处理的难度。

如下图,我们最后得到的局部坐标系如下:以点 A 为坐标原点,以 AB 为 x 轴,以 AG 为 y 轴,以 AF 为 z 轴。 然后,我们可以将点 B、C、D 的坐标转换到这个局部坐标系中,并暂时丢弃 z 轴坐标。得到一个平面的线段关系图。

平面关系处理

那么接下来,我们在二维平面上,对线段 AB 和 CD 的关系进行讨论并处理。 首先,我们要考虑,AB 和 CD 平行的问题,当平行时,判断 x 轴坐标是否有重合部分,如果有,就有无穷多个最近点;如果没有重合部分,那端点就是最近点。

不平行时,我们先考虑,CD 延长线与 AB (即 x 轴)相交的情况,按与 AB 所在直线相交的位置不同,可以分为三种情况:

  • 交点在点 A 左侧
  • 交点在点 B 右侧
  • 交点在 AB 中间

而上述三种情况下,交点在左右两侧时,又可以根据 CD 的位置分为三种情况(我们以左侧为例,右侧同理):

  • 线段 CD 有部分落在左侧,则最近点是点 A,及 A 到 CD 的最近点;
  • 线段 CD 全都在点 A 右侧,其中一点离点 C 近,则最近点是点 C,及点 C 到线段 AB 的最近点;
  • 线段 CD 全都在点 A 右侧,其中一点离点 D 近,则最近点是点 D,及点 D 到线段 AB 的最近点;

那么当交点在线段 AB 中间时,则 CD 的情况也是分三种处理:

  • 交点在 CD 中间,则最近点可按两条直线最近点处理,但在平面上,交点坐标可以直接求出来;
  • 交点在 CD 延长线,点 C 一侧,故最近点分别是点 C,点 C 到线段 AB 的最近点;
  • 交点在 CD 延长线,点 D 一侧,故最近点分别是点 D,点 D 到线段 AB 的最近点;

情况总结

所以总共有 3x3 种情况,而交点 F 的计算也并不复杂,因为 F 可以用点 C 和点 D 来表示,而 F 的 y 坐标等于 0。由此不仅能计算出 F 点坐标,还能知道 F 点在 CD 的位置情况:在 CD 中间,在 C 的外侧,在 D 的外侧。

最后,如果是在局部坐标系得到的交点,还需要再转换到世界坐标系中。

代码

///线段与线段最近点坐标
static func nearestPoints(segment1:Segment, segment2:Segment) -> (simd_float3, simd_float3)? {
    //构造局部坐标系,segment1 为 x 轴,point1 为坐标原点;z 轴垂直于线段构成的平面;
    let direction1 = segment1.point2 - segment1.point1
    let direction2 = segment2.point2 - segment2.point1
    let p = direction1.almostParallelRelative(to: direction2)
    
    var zValue = p.crossValue
    if p.isParallel {
        // 平行时,构造特殊的矩阵
        let xDirection = simd_float3(1, 0, 0)
        let px = direction1.almostParallelRelative(to: xDirection)
        if px.isParallel {
            zValue = simd_float3(0, 0, 1)
        } else {
            zValue = cross(xDirection,direction1)
        }
    }
    let xAxis = normalize(direction1)
    let zAxis = normalize(zValue)
    let yAxis = normalize(cross(zAxis, xAxis))
    let localMatrix = matrix_float4x4(
        simd_float4(xAxis, 0),
        simd_float4(yAxis, 0),
        simd_float4(zAxis, 0),
        simd_float4(segment1.point1, 1)
    )
    
    //计算线段各个点,在局部坐标系中的位置
//        let s1p1 = localMatrix.inverse * simd_float4(segment1.point1, 1)
    let s1p2 = localMatrix.inverse * simd_float4(segment1.point2, 1)
    let s2p1 = localMatrix.inverse * simd_float4(segment2.point1, 1)
    let s2p2 = localMatrix.inverse * simd_float4(segment2.point2, 1)
    
    if p.isParallel {
        // 平行时,可能有重合部分,无数个最近点;也可能无重合部分,端点最近
        if s2p1.x < 0 && s2p2.x < 0 {
            let p2 = s2p1.x > s2p2.x ? segment2.point1 : segment2.point2
            return (segment1.point1, p2)
        } else if s2p1.x > s1p2.x && s2p2.x > s1p2.x {
            let p2 = s2p1.x < s2p2.x ? segment2.point1 : segment2.point2
            return (segment1.point2, p2)
        }
        return nil
    }
    
    let factor = s2p2.y / (s2p2.y - s2p1.y)//前面已判断,不可能平行,故除数不为0
    // segment2 或延长线必定与 x 轴相交,交点为 crossP2
    let crossP2 = factor * s2p1 + (1-factor) * s2p2
    
    if crossP2.x < 0 {
        // 相交在 segment1 范围外,左侧
        if s2p1.x < 0 || s2p2.x < 0 {
            // segment2 有部分在 segment1 左侧
            let p2 = nearestPointOnSegment(from: segment1.point1, to: segment2)
            return (segment1.point1, p2)
        } else if factor > 1 {
            // s2p1 离交点近
            let p1 = nearestPointOnSegment(from: segment2.point1, to: segment1)
            return (p1, segment2.point1)
        } else {
            // s2p2 离交点近
            let p1 = nearestPointOnSegment(from: segment2.point2, to: segment1)
            return (p1, segment2.point2)
        }
    } else if crossP2.x > s1p2.x {
        // 相交在 segment1 范围外,右侧
        if s2p1.x > s1p2.x || s2p2.x > s1p2.x {
            // segment2 有部分在 segment1 右侧
            let p2 = nearestPointOnSegment(from: segment1.point2, to: segment2)
            return (segment1.point2, p2)
        } else if factor > 1 {
            // s2p1 离交点近
            let p1 = nearestPointOnSegment(from: segment2.point1, to: segment1)
            return (p1, segment2.point1)
        } else {
            // s2p2 离交点近
            let p1 = nearestPointOnSegment(from: segment2.point2, to: segment1)
            return (p1, segment2.point2)
        }
    } else {
        // 相交在 segment1 范围内
        if factor > 1 {
            // s2p1 离交点近
            let p1 = nearestPointOnSegment(from: segment2.point1, to: segment1)
            return (p1, segment2.point1)
        } else if factor < 0 {
            // s2p2 离交点近
            let p1 = nearestPointOnSegment(from: segment2.point2, to: segment1)
            return (p1, segment2.point2)
        }
        
        let crossP1x = crossP2.x
        
        let p2 = localMatrix * crossP2
        let p1 = localMatrix * simd_float4(crossP1x, 0, 0, 1)
        return (simd_float3(p1.x, p1.y, p1.z), simd_float3(p2.x, p2.y, p2.z))
    }
}