S02E03:射线与射线最近点的坐标

520 阅读4分钟

说明

前面我们已经说过了,如何求:点到射线的最近点坐标。接下来,我们更进一步,来探讨下,如何求射线与射线最近点的坐标。在此过程中,我们需要用到点到射线最近点,直线与直线最近点等知识。

几何

为了简化理解,我们先介绍二维情况下,射线与射线的位置关系,再推广到三维。先来看看给定一条射线,那么到射线上距离相等的点,都在哪里。 可以看到,到射线 AB 距离相等的点,在二维空间中构成了一个圆角矩形(三维空间中类似胶囊的形状)。

第一种情况

现在我们来考虑第一种情况:什么时候,另一个射线的起点就是离射线 AB 的最近点?也就是说:另一个射线的方向,离射线 AB 越来越远。

如下图,我们先假设,另一条射线起点是点 C,它到射线 AB 的最近点是 T。那么,当从点 C 出发的射线,与向量 TC(从 T 出发,指向 C)方向一致时,就是远离。也就是向量点乘为正时,即远离。

再假设,如果另一条射线起点是点 L,它到射线 AB 的最近点是 A,也就是射线 AB 的起点。那判断标准也是一样,当从点 L 出发的射线,与向量 AL 点乘结果为正时,就远离。

以上情况,在二维平面上,表现为一个半圆,在三维空间中则是一个半球,即当另一条射线方向落在外半球时,即为远离射线 AB。

第二种情况与第三种

这两种情况,分别是:另一条射线指向射线 AB 的负方向,也就是,最近点是点 A 到另一条射线上的点;另一条射线与射线 AB 有交叉,三维空间中可能不一定交叉,但可简化为:直线到直线的最近点来求解。

我们先来考虑起点是点 C 时的情况,这两种情况如何区分呢?从下图看出,分界线就是点 A 与点 C 的连线,那么如何判断它们的位置关系呢,尤其是在三维空间中,更为困难

为了解决这个问题,我们必须回到三维空间中,我们可以构造一个平面,过 AC,并垂直于 ABC 所在平面,利用平面的法线,来判断射线 CH、CI 的方向。

如下图,我们先用 AB、AC 的叉乘,得到 AR,再用 AR、AC 叉乘,就可以得到 AS,这就是我们要的法线。射线 CG、CH、CI,分别与向量 AS 点乘,如果为正,说明指向了射线 AB 的负方向(情况二);如果为负,则说明靠近射线 AB 的正方向(情况三):

那么当射线起点是点 L 时,上面方法还适用么?当然是适用的。我们先利用 AB、AL 叉乘,得到 AU,再用 AU、AL 叉乘,得到向量 AV,这就是我们要的法线方向。最后我们判断向量 LP、LO、LN 与 AV 的点乘,结果为正时,说明另一条射线朝向 AB 的负方向;为负时,朝向正方向,可以按两条直线的最近点处理。

三种情况总结

  • 情况一:射线 CX 远离射线 AB,故最近点分别是:点 C,点 C 到射线 AB 的最近点;
  • 情况二:射线 CX 朝向接近射线 AB,但指向 A 点外侧,故最近点分别是:点 A 到射线 CX 的最近点,点 A;
  • 情况三:射线 CX 朝向接近射线 AB,并指向 A 点内侧,故最近点分别是:直线 CX 与直线 AB 的最近点;

代码

static func nearestPoints(ray1:Ray, ray2:Ray) -> (simd_float3, simd_float3)? {
    let nearestPointOnRay1 = nearestPointOnRay(from: ray2.position, to: ray1)
    let nearestPointToRay2Vector = ray2.position - nearestPointOnRay1
    
    if dot(nearestPointToRay2Vector, ray2.direction) >= 0 {
        // ray2 远离 ray1(情况一)
        return (nearestPointOnRay1, ray2.position)
    }
    
    let ray1ToRay2 = ray2.position - ray1.position
    let crossValueRay1 = cross(ray1.direction, ray1ToRay2)
    let faceFar = cross(crossValueRay1, ray1ToRay2)
    
    if dot(faceFar, ray2.direction) >= 0 {
        // ray2 指向了 ray1 原点的负方向(情况二)
        let nearestPointOnRay2 = nearestPointOnRay(from: ray1.position, to: ray2)
        return (ray1.position, nearestPointOnRay2)
    }
    
    // 可按直线最近点处理(情况三)
    let parallelResult = ray2.direction.almostParallelRelative(to: ray1.direction)
    let crossValue = parallelResult.crossValue
    if parallelResult.isParallel {
        // 平行
        return nil
    }
    let distanceVector = normalize(crossValue)
    
    let vector = ray1.position - ray2.position
    let dis = dot(distanceVector, vector)
    
    let point2OnPlane = ray2.position + dis * distanceVector
    
    let projectionOnLine1 = nearestPointOnRay(from: point2OnPlane, to: ray1)
    let result = projectionOnLine1.almostSamePoint(to: point2OnPlane)
    if result.isSame {
        // 垂足是 line2.position
        return (point2OnPlane,ray2.position)
    }
    let projectionVector = projectionOnLine1 - point2OnPlane
    let squared = result.distanceSquared
    
    let x1:Float = squared / dot(ray2.direction, projectionVector)
    let footPoint2 = point2OnPlane + x1 * ray2.direction
    let footPoint1 = footPoint2 - dis * distanceVector
    
    return (footPoint1, footPoint2)
}

总结

射线与射线最近点的判断,要比直线复杂很多,即使在二维平面上,也要分情况讨论。三维空间中,更是复杂,主要是想象三维位置关系非常困难,不过好在情况和二维是类似的,所以可以先在二维平面上理顺思路再考虑三维情况。