【Unity】 计算三维空间中曲线的距离 和对应的点
本文介绍了在Unity中计算三维空间曲线间最短距离的方法。通过分析线段间的三种关系(相交、平行、异面)提出解决方案,使用向量计算最近点坐标。针对原方法在共面情况下可能出现的错误,提出了验证机制:分别计算线1到线2和线2到线1的最近点,取距离更短的结果。提供了两个关键方法:计算线段间最短距离和验证最近点。该方法适用于多条空间曲线的最近距离计算需求
Unity中 计算三维空间中曲线的距离和对应的点
说明
最近 发现一个功能 空间中有多条线 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 };
}
}
结束语
由于水平一般,实力有限,文章中有谬误的地方也 欢迎各位大佬指正,如果 有不清楚的地方,也欢迎提问,我会尽力补充说明。
当然,如果有更好的解决方法,也可以推荐分享一下,让大家多一点解决问题的思路,在此先谢过各位大佬了。

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