S01E03:点是否在直线上

573 阅读1分钟

说明

上一文中,求点到直线距离的函数中,我们已经判断过点与垂足是否重合了,重合时即为点在直线上,否则点不在直线上。

static func isPointOnLine(point:simd_float3, line:Line) -> Bool {
    let tarPoint = projectionOnLine(from: point, to: line)
    return distance_squared(tarPoint, point) < 0.0001
}

但还有另一种方法可以用向量点乘直接判断点是否在直线上。虽然算法复杂度几乎一样,但这个方法更容易理解。

几何

这里,我们直接求向量 BA 和 BC 的夹角,如果夹角是零,那么我们就认为点 A 在直线 BC 上。 那么如何求夹角呢?一种方法是根据点乘的定义:

BA ∙BC = ||BA|| * ||BC|| *cosθ
// 那么
θ = arccos((BA ∙BC )/(||BA|| * ||BC|| ))
// 在计算机中计算反三角函数是一个耗时操作,所以一般用 cosθ 的值来代替 arcosθ 来使用,需要注意的是 θ 角越大,cosθ 值越小。而 cosθ 正是两个向量归一化之后的点乘
cosθ = (BA ∙BC )/(||BA|| * ||BC|| )

但是,问题也是存在的,两次归一化,再点乘,导致计算精度相当差。

其实夹角也是可以用叉乘来计算的,叉积的方向垂直与两个向量所在的平面,长度等于两者围成四边行的面积:
叉积|c|=|a×b|=|a||b|sin<a,b>

但是实际开发中,我们很少用叉乘来计算角度,主要原因有以下几个:

  • 三角函数值域问题,如 sin(45) 和 sin(135) 是无法区分的;
  • 得到叉积后,还需要求模长,这需要求平方根,计算较为耗时;

但是在本例中,我们要计算的是,点在直线上,也就是两个向量围成四边形的面积等于 0,不管夹角 0 和 180 是一样的。因此,有时可以不考虑向量长度,只要叉积向量 xyz 值为 0,就说明在直线上。

代码

用点乘求点是否在直线上的代码如下

static func isPointOnLine3(point:simd_float3, line:Line) -> Bool {
    let vector = point - line.position
    if length_squared(vector) < 0.0001  {
        return true
    }
    let normalizedVector = normalize(vector)
    let normalizedDirection = normalize(line.direction)
    let dotValue = dot(normalizedVector, normalizedDirection)
    return abs(dotValue - 1) < 0.0001
}

用叉乘求点是否在直线上的代码如下:

static func isPointOnLine2(point:simd_float3, line:Line) -> Bool {
    let vector = point - line.position
    // 这一步判断可以省略
    if abs(vector.x) < 0.0001, abs(vector.y) < 0.0001, abs(vector.z) < 0.0001  {
        return true
    }
    let crossValue = cross(vector, line.direction)
    return abs(crossValue.x) < 0.0001 && abs(crossValue.y) < 0.0001 && abs(crossValue.z) < 0.0001
}

其实可以看到,叉乘的代码更为简单。从计算量来说,理论上三维向量点乘,需要计算 3 个乘法,2 个加法;而叉乘计算稍多,需要计算 6 个乘法,3 个加法。 但是对于现代 CPU 和 GPU 来说,加法与乘法混合运算往往可以一次完成,而且 SIMD 类型更是提高了向量计算效率,所以可以简单认为,dot()cross()复杂度与计算效率是一样的。另外点乘算法中的归一化函数也需要花费一定时间。也正在因为如此,我们在判断向量是不是 0 向量时,也可以用叉乘和length_squared(vector)来计算向量长度的平方,在 SIMD 的加持下此类运算非常快。

所以,判断点是否在直线上,最后可以优化成三行代码:

static func isPointOnLine3(point:simd_float3, line:Line) -> Bool {
    let vector = point - line.position
    let crossValue = cross(vector, line.direction)
    return length_squared(crossValue) < 0.0001
}

其他

需要说明的是,由于浮点数精度以及三角函数问题,某些情况下,点乘法和叉乘法得到的结果并不相同,一般是否平行用叉乘,是否垂直用点乘。