S01E02:点到直线的距离

784 阅读2分钟

说明

上一文中,我们已经得到了点到直线的垂足。所谓距离,其实就是点到这个垂足的向量长度(模长)。我们直接相减,然后求 xyz 的平方和,再开方不就行了?更简单的是 simd 框架提供了计算两个坐标距离的函数distance(),可以直接调用。

static func distanceBetween(point:simd_float3, line:Line) -> Float {
    let position = projectionOnLine(from: point, to: line) //上文的求投影点函数
    return distance(position, point)
}

但是在计算机中,求平方根是个很耗时的操作,尤其是在早期的老式 CPU 上。而在 3D 和 AR 中,每一帧可能需要处理几百甚至几十万个点,每秒 60 帧。如果运算中有大量求平方根操作,会给 CPU/GPU 造成很大压力。C数学函数库中的sqrt具有理想的精度,但对于 3D 游戏来说速度太慢。所以 3D 游戏代码,我们经常看到利用向量或矩阵,尽可能代替求平方根操作来求距离的算法。

几何

接上文求出点到直线的最近点(垂足)后,我们就得到了 DA 向量,这时只要我们再将 BA 向量投影一次,利用点乘分解到 DA 方向上,就直接得到了距离。

需要注意的是,如果 D 点与 A 点重合,那 DA 向量就是零向量,无法向其投影。所以需要单独判断。

代码

在计算机中,当一个向量过小时,是无法归一化的,或者返回 0 向量。而当一个向量过大时,归一化时也会丢失精度。两次归一化,两次点乘,会让这个算法精度较差,使用时应当特别注意。

truct Line {
    var position = simd_float3.zero
    var direction = simd_float3.zero
}

static func distanceBetween2(point:simd_float3, line:Line) -> Float {
//计算垂足坐标,因为还需要用到vector的值,所以不直接调用`projectionOnLine(from point:simd_float3, to line:Line)`函数,而是选择再写一遍
    let vector = point - line.position
    let normalizedDirection = normalize(line.direction)
    let dotValue = dot(vector, normalizedDirection)
    let tarPoint = line.position + dotValue * normalizedDirection

    // 判断点是否已经在直线上
    let disVector = point - tarPoint
    if length_squared(disVector) < 0.001  {//用平方来代替长度计算
        return 0
    }
    // 利用点乘,再次投影求距离
    return dot(vector, normalize(disVector))
}

现代计算机硬件及 simd 框架,都对点乘、向量归一化函数做了优化,相比较耗时的求平方根来说,这里用点乘和向量归一化,就解决了求距离问题,节省了不少。

可能大家会感到疑惑:向量归一化,也是用到模长了,也就是除以平方根才能得到归一化值,为什么还要用向量方式来代替开平方根呢?

  • 遗留的习惯:早期 CPU 求平方根效率低下,C数学函数库中的sqrt提供了理想的精度,但对于 3D 游戏来说速度太慢,所以导致了奇葩思路和优化代码;
  • 3D 框架的加速:一般 3D 中对精度要求不高,所以使用 Float 类型(单精度),有时甚至是 half 类型(半精度),那么算法也可以舍弃部分精度以加快速度。比如归一化函数得到的向量长度往往并不是 1,只是非常接近 1。因此有很大优化空间,比如快速平方根倒数算法,可以快速得到平方根的倒数(误差稍大),甚至有些 CPU 已经支持了类似的指令;又比如查表+插值得到近似值,等等;

所以,这就造成了很多 3D/AR 代码中,出现的大量宁可归一化,不求平方根代码。

其他

当我们在 3D/AR 中求距离时,也应尽量避免使用平方根的方法。如果不想费力思考向量解法,还可以用距离的平方函数distance_squared(),来代替距离函数distance()来尽量减少求平方根的操作,比如在比较距离大小时,距离的平方比较大,显然距离也大。

所以,很多时候,当不得不开平方以计算距离时,会同时提供一个距离平方的方法,用来在某些场景,用平方来代替距离本身。

static func distanceSquaredBetween(point:simd_float3, line:Line) -> Float {
    let position = projectionOnLine(from: point, to: line)
    return distance_squared(position, point)
}