说明

最近 发现一个功能 空间中有多条线 A B C … 要找出 A距离其他哪条线 最接近。并连接起来,查询资料后,发现原理不难,但是没有 可以直接使用的代码,在下作为一个九漏鱼程序员,推导起来还是比较麻烦的,所幸还是基本完成了此功能,在此记录记录一下分析只是在下的心路历程,没有太多技术上的内容,如有需要,可直接取代码整理部分,代码整理都是静态方法,按需取用即可

分析

曲线是由很多线段构成的,那么应当由最小的单元 线段 开始
已知、空间中的两段 线段 间的关系 有其下三种:
1.相交(延长线相交)
相交线段
↑相交了,交点最近 距离为0
延长线相交
↑虽然没相交,但是延长线是相交的 最近的端点 与线算最近距离即可

2.平行(双线段投影 有重合/没有重合)

有重合部分
↑部分重合,重合部分做垂线即最近的位置
没有重合部分
↑不重合,最近的端点连线为最近
3.异面
异面
↑怎么形容要如何计算呢,计算 懂点P在线A上 滑动 与线B的最近的位置?

AI提供的解决方法

遇事不决问AI
虽然AI不靠谱 经常骗我
话不多说,直接上计算方法
别问为什么这么算,我也不懂

    /// <summary>
    /// 计算两条线段之间的最短距离
    /// </summary>
    /// <param name="l1s">线段1起点</param>
    /// <param name="l1e">线段1终点</param>
    /// <param name="l2s">线段2起点</param>
    /// <param name="l2e">线段2终点</param>
    /// <returns>[0]是线1上的最小位置,[1]线2上的到线1的最小</returns>
    public static List<Vector3> GetShortestDistanceBetweenSegments(Vector3 l1s, Vector3 l1e, Vector3 l2s, Vector3 l2e)
    {
        Vector3 d1 = l1e - l1s; // 线段1的方向向量
        Vector3 d2 = l2e - l2s; // 线段2的方向向量
        Vector3 r = l1s - l2s;

        float a = Vector3.Dot(d1, d1); // 线段1长度的平方
        float b = Vector3.Dot(d1, d2);
        float c = Vector3.Dot(d2, d2); // 线段2长度的平方
        float d = Vector3.Dot(d1, r);
        float e = Vector3.Dot(d2, r);

        float f = (a * c - b * b); // 分母

        float s, t;

        if (f <= 0.00001f)
        {
            // 线段平行或接近平行,直接计算端点距离
            s = 0.0f;
            t = Mathf.Clamp(e / c, 0.0f, 1.0f);
        }
        else
        {
            s = Mathf.Clamp((b * e - c * d) / f, 0.0f, 1.0f);
            t = Mathf.Clamp((a * e - b * d) / f, 0.0f, 1.0f);
        }

        // 计算最近点
        Vector3 p1 = l1s + d1 * s;
        Vector3 p2 = l2s + d2 * t;
        return new(){p1,p2};
   }

异面的问题解决了
↑居然还真给解决了?

该方法会产生的BUG

好吧,其实该方法 在实际使用上 有个 BUG,就是当线段共面 的时候 计算 会因为上面平行的判断直接连接端点,或者 是延长线,导致计算共面的问题时错误百出

错误

解决思路(计算 点 到 线段的距离)

由于我也是问AI的代码,搞不明白这个错误要怎么改 经过多次 测试后,发现 其上 是一根线的端点是找对了的 但是 在另一根线上的 距离最近的点找错了,于是在其基础上,多一个 计算 点到线段的最近距离,同时 计算 线L1和 L2的最近点的距离 与 线L2 和 L1的最近点的距离,那么 最短的那个就是正确的,舍去错误值即可。

计算线段到点 的方法就简单啦,如下:

    /// <summary>
    /// 计算点到线段的最短距离
    /// </summary>
    /// <param name="point">点坐标</param>
    /// <param name="lineStart">线段起点</param>
    /// <param name="lineEnd">线段终点</param>
    /// <returns>最短距离</returns>
    public static Vector3 PointToLineSegmentDistance(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
    {
        Vector3 lineVec = lineEnd - lineStart;
        Vector3 pointVec = point - lineStart;

        float lineLengthSqr = lineVec.sqrMagnitude;
        float dotProduct = Vector3.Dot(pointVec, lineVec);

        // 计算投影比例
        float projectionRatio = dotProduct / lineLengthSqr;

        // 检查投影是否在线段上
        if (projectionRatio < 0.0f)
        {
            return lineStart;
        }
        else if (projectionRatio > 1.0f)
        {
            return lineEnd;
        }

        // 计算投影点
        Vector3 projection = lineStart + projectionRatio * lineVec;

        return projection;
    }

让我们 写一个验证的方法

/// <summary>
/// 校验 其最近点位 与线是否有更近的连接方式
/// </summary>
/// <param name="s1">线1起始点</param>
/// <param name="e1">线1结束点</param>
/// <param name="s2">线2起始点</param>
/// <param name="e2">线2结束点</param>
/// <param name="p1">线1上的离线2最近的点</param>
/// <param name="p2">线2上的离线1最近的点</param>
/// <returns>最近的两个点([0]是线1上的点 [1]是线2上的点)</returns>
public static List<Vector3> VerificationPoint(Vector3 s1,Vector3 e1,Vector3 s2,Vector3 e2,Vector3 p1,Vector3 p2)
{
    // s1 e1 与 p2 求最近距离  s2 e2 与 p1 求最近 最后 哪两个 最近 返回哪两个
    var vs1 = PointToLineSegmentDistance(p2, s1, e1);
    var vs2= PointToLineSegmentDistance(p1, s2, e2);

    if(Vector3.Distance(vs1,p2)> Vector3.Distance(vs2, p1))
    {
        return new() { p1, vs2 };
    }
    else
    {
        return new() { vs1, p2 };
    }
}

最后结果

这样 在最后 返回时,返回验证后的值即可,修改后的 代码如下:

    /// <summary>
    /// 计算两条线段之间的最短距离
    /// </summary>
    /// <param name="l1s">线段1起点</param>
    /// <param name="l1e">线段1终点</param>
    /// <param name="l2s">线段2起点</param>
    /// <param name="l2e">线段2终点</param>
    /// <returns>[0]是线1上的最小位置,[1]线2上的到线1的最小</returns>
    public static List<Vector3> GetShortestDistanceBetweenSegments(Vector3 l1s, Vector3 l1e, Vector3 l2s, Vector3 l2e)
    {
        Vector3 d1 = l1e - l1s; // 线段1的方向向量
        Vector3 d2 = l2e - l2s; // 线段2的方向向量
        Vector3 r = l1s - l2s;

        float a = Vector3.Dot(d1, d1); // 线段1长度的平方
        float b = Vector3.Dot(d1, d2);
        float c = Vector3.Dot(d2, d2); // 线段2长度的平方
        float d = Vector3.Dot(d1, r);
        float e = Vector3.Dot(d2, r);

        float f = (a * c - b * b); // 分母

        float s, t;

        if (f <= 0.00001f)
        {
            // 线段平行或接近平行,直接计算端点距离
            s = 0.0f;
            t = Mathf.Clamp(e / c, 0.0f, 1.0f);
        }
        else
        {
            s = Mathf.Clamp((b * e - c * d) / f, 0.0f, 1.0f);
            t = Mathf.Clamp((a * e - b * d) / f, 0.0f, 1.0f);
        }

        // 计算最近点
        Vector3 p1 = l1s + d1 * s;
        Vector3 p2 = l2s + d2 * t;

        //return new() { p1, p2 };
        //验证
       return VerificationPoint(l1s, l1e, l2s, l2e, p1, p2);
    }

和预期所想了

曲线

其实曲线的话,通常是由一条条 直线拼接而成的,暂时没有 想到什么好的方法,只能力大砖飞的 全部循环遍历一遍了,
项目中 常用的 LineRenderer 为例,则 需要

先获取其 点位信息

没有查到 LineRenderer 可以直接获取 某个点位到线条其实点或终点的长度的方法,姑且只能自己算了:

    /// <summary>
    /// 记录曲线中线的信息
    /// </summary>
    /// <param name="line"></param>
    /// <returns></returns>
    public static List<LineData> LineRendererToLineDatas(LineRenderer line)
    {
        Vector3[] positions = new Vector3[line.positionCount];
        int count = line.GetPositions(positions);//获取 LineRenderer的点位
        float lineLen = 0;
        List<LineData> ll = new();
        for (int i = 0; i < count-1; i++)
        {
            LineData l = new();
            l.startPos = positions[i];
            l.endPos = positions[i+1];
            float s = lineLen;
            l.length = s;
            lineLen += Vector3.Distance(l.startPos, l.endPos);
            ll.Add(l);
        }
        return ll;
    }

    /// <summary>
    /// 用于 计算 弧线的每段线的参数
    /// </summary>
    public class LineData
    {
        /// <summary>
        /// 起始点
        /// </summary>
        public Vector3 startPos;
        /// <summary>
        /// 结束点
        /// </summary>
        public Vector3 endPos;
        /// <summary>
        /// 该段起点到线段起点的长度
        /// </summary>
        public float length;

    }

然后 循环遍历 看 线1中的每一段 和线2中的每一段 计算 最小距离 返回最小的一组点位信息

    /// <summary>
    /// 获取 两条线最近的点的位置(没段线取1)
    /// </summary>
    /// <param name="line1">线1</param>
    /// <param name="line2">线2</param>
    /// <returns> 线1和线2上 最近的点, 偶数0、2、4、6、...为Line1上的点 奇数 1、3、5、7、..为Line2上的点 理论上 结果 一一对应,</returns>
    public static List<float> GetLineLength(LineRenderer line1, LineRenderer line2)
    {

        //记录两条线中的所有直线信息(直线 是只有一条直线 ListCount=1的数据 理论上来说也可以使用)
        var l1 = LineRendererToLineDatas(line1);
        var l2 = LineRendererToLineDatas(line2);
        //记录返回结果 偶数0、2、4、6、...为Line1上的点 奇数 1、3、5、7、..为Line2上的点 理论上 结果 一一对应
        List<float> result = new List<float>();
        float disMin = 99999;//记录最小的距离(由于 距离不能为- 最小 就是 相交的情况)
        for (int i1 = 0; i1 < l1.Count ; i1++)
        {
            var d1 = l1[i1];
            Vector3 l1s = d1.startPos;
            Vector3 l1e = d1.endPos;

            for (int i2 = 0; i2 < l2.Count; i2++)//将线2上的所有段全部取出 与线1当前段做计算
            {
                var d2 = l2[i2];
                Vector3 l2s = d2.startPos;
                Vector3 l2e = d2.endPos;
                //计算 两线之间的最小距离
                //取点
                var ps = CalculationVector3.GetShortestDistanceBetweenSegments(l1s, l1e, l2s, l2e);
                //取值
                var disN = Vector3.Distance(ps[0], ps[1]);
                if (disMin > disN)//如果 当前距离小于 之前记录的距离 则 更新记录的数据
                {
                    result.Clear();
                    float f1 = Vector3.Distance(ps[0], l1s) + d1.length;
                    float f2 = Vector3.Distance(ps[1], l2s) + d2.length;
                    result.Add(f1);
                    result.Add(f2);

                    disMin = disN;

                }
                else if (disMin == disN)//如果 当前距离 等于记录的距离 则将 该段也添加进数据
                {
                    float f1 = Vector3.Distance(ps[0], l1s) + d1.length;
                    float f2 = Vector3.Distance(ps[1], l2s) + d2.length;
                    result.Add(f1);
                    result.Add(f2);
                }

            }

        }
        return result;//返回  结果

    }

结果测试:
多个点距离一致
空间中距离最近

代码整理

计算曲线的距离

 /// <summary>
 /// 获取 两条线最近的点的位置(没段线取1)
 /// </summary>
 /// <param name="line1">线1</param>
 /// <param name="line2">线2</param>
 /// <returns> 线1和线2上 最近的点, 偶数0、2、4、6、...为Line1上的点 奇数 1、3、5、7、..为Line2上的点 理论上 结果 一一对应,</returns>
 public static List<float> GetLineLength(LineRenderer line1, LineRenderer line2)
 {

     //记录两条线中的所有直线信息(直线 是只有一条直线 ListCount=1的数据 理论上来说也可以使用)
     var l1 = LineRendererToLineDatas(line1);
     var l2 = LineRendererToLineDatas(line2);
     //记录返回结果 偶数0、2、4、6、...为Line1上的点 奇数 1、3、5、7、..为Line2上的点 理论上 结果 一一对应
     List<float> result = new List<float>();
     float disMin = 99999;//记录最小的距离(由于 距离不能为- 最小 就是 相交的情况)
     for (int i1 = 0; i1 < l1.Count ; i1++)
     {
         var d1 = l1[i1];
         Vector3 l1s = d1.startPos;
         Vector3 l1e = d1.endPos;

         for (int i2 = 0; i2 < l2.Count; i2++)//将线2上的所有段全部取出 与线1当前段做计算
         {
             var d2 = l2[i2];
             Vector3 l2s = d2.startPos;
             Vector3 l2e = d2.endPos;
             //计算 两线之间的最小距离
             //取点
             var ps = CalculationVector3.GetShortestDistanceBetweenSegments(l1s, l1e, l2s, l2e);
             //取值
             var disN = Vector3.Distance(ps[0], ps[1]);
             if (disMin > disN)//如果 当前距离小于 之前记录的距离 则 更新记录的数据
             {
                 result.Clear();
                 float f1 = Vector3.Distance(ps[0], l1s) + d1.length;
                 float f2 = Vector3.Distance(ps[1], l2s) + d2.length;
                 result.Add(f1);
                 result.Add(f2);

                 disMin = disN;

             }
             else if (disMin == disN)//如果 当前距离 等于记录的距离 则将 该段也添加进数据
             {
                 float f1 = Vector3.Distance(ps[0], l1s) + d1.length;
                 float f2 = Vector3.Distance(ps[1], l2s) + d2.length;
                 result.Add(f1);
                 result.Add(f2);
             }

         }

     }
     return result;//返回  结果

 }
 /// <summary>
 /// 记录曲线中线的信息
 /// </summary>
 /// <param name="line"></param>
 /// <returns></returns>
 public static List<LineData> LineRendererToLineDatas(LineRenderer line)
 {
     Vector3[] positions = new Vector3[line.positionCount];
     int count = line.GetPositions(positions);//获取 LineRenderer的点位
     float lineLen = 0;
     List<LineData> ll = new();
     for (int i = 0; i < count-1; i++)
     {
         LineData l = new();
         l.startPos = positions[i];
         l.endPos = positions[i+1];
         float s = lineLen;
         l.length = s;
         lineLen += Vector3.Distance(l.startPos, l.endPos);
         ll.Add(l);
     }
     return ll;
 }

 /// <summary>
 /// 用于 计算 弧线的每段线的参数
 /// </summary>
 public class LineData
 {
     /// <summary>
     /// 起始点
     /// </summary>
     public Vector3 startPos;
     /// <summary>
     /// 结束点
     /// </summary>
     public Vector3 endPos;
     /// <summary>
     /// 该段起点到线段起点的长度
     /// </summary>
     public float length;

 }

计算线段到线段、线段到点的最小距离

    /// <summary>
    /// 计算两条线段之间的最短距离
    /// </summary>
    /// <param name="l1s">线段1起点</param>
    /// <param name="l1e">线段1终点</param>
    /// <param name="l2s">线段2起点</param>
    /// <param name="l2e">线段2终点</param>
    /// <returns>[0]是线1上的最小位置,[1]线2上的到线1的最小</returns>
    public static List<Vector3> GetShortestDistanceBetweenSegments(Vector3 l1s, Vector3 l1e, Vector3 l2s, Vector3 l2e)
    {
        Vector3 d1 = l1e - l1s; // 线段1的方向向量
        Vector3 d2 = l2e - l2s; // 线段2的方向向量
        Vector3 r = l1s - l2s;

        float a = Vector3.Dot(d1, d1); // 线段1长度的平方
        float b = Vector3.Dot(d1, d2);
        float c = Vector3.Dot(d2, d2); // 线段2长度的平方
        float d = Vector3.Dot(d1, r);
        float e = Vector3.Dot(d2, r);

        float f = (a * c - b * b); // 分母

        float s, t;

        if (f <= 0.00001f)
        {
            // 线段平行或接近平行,直接计算端点距离
            s = 0.0f;
            t = Mathf.Clamp(e / c, 0.0f, 1.0f);
        }
        else
        {
            s = Mathf.Clamp((b * e - c * d) / f, 0.0f, 1.0f);
            t = Mathf.Clamp((a * e - b * d) / f, 0.0f, 1.0f);
        }

        // 计算最近点
        Vector3 p1 = l1s + d1 * s;
        Vector3 p2 = l2s + d2 * t;

        //return new() { p1, p2 };
        //验证
       return VerificationPoint(l1s, l1e, l2s, l2e, p1, p2);
    }

    /// <summary>
    /// 计算点到线段的最短距离
    /// </summary>
    /// <param name="point">点坐标</param>
    /// <param name="lineStart">线段起点</param>
    /// <param name="lineEnd">线段终点</param>
    /// <returns>最短距离</returns>
    public static Vector3 PointToLineSegmentDistance(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
    {
        Vector3 lineVec = lineEnd - lineStart;
        Vector3 pointVec = point - lineStart;

        float lineLengthSqr = lineVec.sqrMagnitude;
        float dotProduct = Vector3.Dot(pointVec, lineVec);

        // 计算投影比例
        float projectionRatio = dotProduct / lineLengthSqr;

        // 检查投影是否在线段上
        if (projectionRatio < 0.0f)
        {
            return lineStart;
        }
        else if (projectionRatio > 1.0f)
        {
            return lineEnd;
        }

        // 计算投影点
        Vector3 projection = lineStart + projectionRatio * lineVec;

        return projection;
    }

    /// <summary>
    /// 校验 其最近点位 与线是否有更近的连接方式
    /// </summary>
    /// <param name="s1">线1起始点</param>
    /// <param name="e1">线1结束点</param>
    /// <param name="s2">线2起始点</param>
    /// <param name="e2">线2结束点</param>
    /// <param name="p1">线1上的离线2最近的点</param>
    /// <param name="p2">线2上的离线1最近的点</param>
    /// <returns>最近的两个点([0]是线1上的点 [1]是线2上的点)</returns>
    public static List<Vector3> VerificationPoint(Vector3 s1,Vector3 e1,Vector3 s2,Vector3 e2,Vector3 p1,Vector3 p2)
    {
        // s1 e1 与 p2 求最近距离  s2 e2 与 p1 求最近 最后 哪两个 最近 返回哪两个
        var vs1 = PointToLineSegmentDistance(p2, s1, e1);
        var vs2= PointToLineSegmentDistance(p1, s2, e2);

        if(Vector3.Distance(vs1,p2)> Vector3.Distance(vs2, p1))
        {
            return new() { p1, vs2 };
        }
        else
        {
            return new() { vs1, p2 };
        }
    }

结束语

由于水平一般,实力有限,文章中有谬误的地方也 欢迎各位大佬指正,如果 有不清楚的地方,也欢迎提问,我会尽力补充说明。
当然,如果有更好的解决方法,也可以推荐分享一下,让大家多一点解决问题的思路,在此先谢过各位大佬了。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐