如何防止点多边形滑动碰撞检测中的浮点误差

How to prevent floating point error in point-polygon sliding collision detection

提问人:ARutherford 提问时间:5/11/2019 最后编辑:ARutherford 更新时间:5/16/2019 访问量:709

问:

我正在尝试编写一个函数来处理我正在编程的游戏中的移动。我所拥有的几乎有效,但有几种情况会崩溃。

我编写了一个最小的演示示例,如下所示。在此示例中,我尝试计算由点和移动矢量表示的对象的行程。根据多边形集合检查此对象的移动路径,多边形被分解为线段进行测试。当这个对象与线段碰撞时,我希望它沿着该线段滑动(而不是停止或反弹)。

为此,我沿着我的预期路径检查碰撞,如果我找到一个交叉点,我会沿着我碰撞的线段的路径从该交叉点开始进行新的测试,并测量剩余运动的大小。

enter image description here

当我们沿着线段滑入“口袋”时,问题就出现了。通常,碰撞检查会通过形成型腔的两个线段,并且物体会滑过。因为我平行于其中一条线段行进,并且我在端点处与两条线段相交,所以我认为这个问题是由浮点误差引起的。无论它是否溜走、被抓住,或者被抓住一次然后在第二次检查中溜走似乎完全是随机的。

我正在使用我在这里找到的简单算法计算交集:https://stackoverflow.com/a/20679579/4208739,但我也尝试了许多其他算法。都表现出相同的问题。

(Vector2 是 Unity 库提供的类,它只是将 x 和 y 坐标保存为浮点数。Vector2.Dot 函数仅计算点积)。

//returns the final destination of the intended movement, given the starting position, intended direction of movement, and provided collection of line segments
//slideMax provides a hard cap on number of slides allowed before we give up
Vector2 Move(Vector2 pos, Vector2[] lineStarts, Vector2[] lineEnds, Vector2 moveDir, int slideMax)
{
    int slideCount = 0;
    while (moveDir != Vector2.zero && slideCount < slideMax)
    {
        pos = DynamicMove(pos, lineStarts, lineEnds, moveDir, out moveDir);
        slideCount++;
    }
    return pos;
}

//returns what portion of the intended movement can be performed before collision, and the vector of "slide" that the object should follow, if there is a collision
Vector2 DynamicMove(Vector2 pos, Vector2[] lineStarts, Vector2[] lineEnds, Vector2 moveDir, out Vector2 slideDir)
{
    slideDir = Vector2.zero;
    float moveRemainder = 1f;
    for (int i = 0; i < lineStarts.Length; i++)
    {
        Vector2 tSlide;
        float rem = LineProj(pos, moveDir, lineStarts[i], lineEnds[i], out tSlide);
        if (rem < moveRemainder)
        {
            moveRemainder = rem;
            slideDir = tSlide;
        }
    }
    return pos + moveDir * moveRemainder;
}

//Calculates point of collision between the intended movement and the passed in line segment, also calculate vector of slide, if applicable
float LineProj(Vector2 pos, Vector2 moveDir, Vector2 lineStart, Vector2 lineEnd, out Vector2 slideDir)
{
    slideDir = new Vector2(0, 0);
    float start = (lineStart.x - pos.x) * moveDir.y - (lineStart.y - pos.y) * moveDir.x;
    float end = (lineEnd.x - pos.x) * moveDir.y - (lineEnd.y - pos.y) * moveDir.x;
    if (start < 0 || end > 0)
        return 1;
    //https://stackoverflow.com/a/20679579/4208739
    //Uses Cramer's Rule
    float L1A = -moveDir.y;
    float L1B = moveDir.x;
    float L1C = -(pos.x *(moveDir.y + pos.y) - (moveDir.x + pos.x)*pos.y);
    float L2A = lineStart.y - lineEnd.y;
    float L2B = lineEnd.x - lineStart.x;
    float L2C = -(lineStart.x * lineEnd.y - lineEnd.x * lineStart.y);
    float D = L1A * L2B - L1B * L2A;
    float Dx = L1C * L2B - L1B * L2C;
    float Dy = L1A * L2C - L1C * L2A;
    if (D == 0)
        return 1;
    Vector2 inter = new Vector2(Dx / D, Dy / D);
    if (Vector2.Dot(inter - pos, moveDir) < 0)
        return 1;
    float t = (inter - pos).magnitude / moveDir.magnitude;
    if (t > 1)
        return 1;
    slideDir = (1 - t) * Vector2.Dot((lineEnd - lineStart).normalized, moveDir.normalized) * (lineEnd - lineStart).normalized;
    return t;
}

有没有某种方法可以计算不易受此类问题影响的碰撞?我想我不能完全消除浮点误差,但是有没有办法检查至少可以保证我与口袋中的两个线段之一发生冲突?还是以这种方式做事有什么更根本的错误?

如果有什么不清楚的地方,我可以画图表或写例子。

编辑:在对这个问题进行了更多思考之后,并针对埃里克的回答,我想知道将我的数学从浮点转换为定点是否可以解决这个问题?在实践中,我真的只是将我的值(可以在 -100 到 100 的范围内轻松适应)转换为 int,然后在这些约束下执行数学运算?我还没有把所有的问题拼凑在一起,但我可以试一试。如果有人有任何关于这样的事情的信息,我将不胜感激。

C# 碰撞检测 游戏物理 浮动精度

评论

1赞 meowgoesthedog 5/13/2019
碰撞线是否始终是闭合多边形的一部分?如果是这样,假设玩家总是在外面会有所帮助。
0赞 ARutherford 5/14/2019
通常,有一个大型边界多边形,我们希望留在其中,其中还包含许多我们希望保留在外部的“障碍物”多边形。如果能够仅使用简单的线段进行碰撞也很好,但如有必要,可以用非常窄的多边形进行近似。值得一提的是,边界多边形逆时针缠绕,障碍多边形顺时针缠绕。这就是为什么我们只需要检查线段的起点是否在运动矢量的左侧,终点是否在右侧,反之亦然。

答:

2赞 Eric Postpischil 5/12/2019 #1

理想情况下,您有一条线正好瞄准一个点,即线段的端点。这意味着计算中的任何错误,无论多么小,都可能说这条线没有达到重点。我看到了三种潜在的解决方案:

  • 分析算术并对其进行设计,以确保它没有错误,也许通过使用扩展精度技术。
  • 分析算术并对其进行设计,以确保它以有利于碰撞的轻微误差完成,也许通过增加对碰撞的轻微偏差。
  • 稍微延长线段。

似乎第三种是最简单的——形成一个口袋的两条线段可以稍微延长一点,所以它们会交叉。那么滑动路径就不会瞄准一个点;它将针对一个段的内部,并且会有出错的余地。

评论

0赞 ARutherford 5/12/2019
我很想使用方法一或二解决这个问题,尽管我不确定如何去做。方法三并不能解决问题,因为下一次迭代有时会使对象沿着该线滑动,并错过它滑动的第一行,由于浮点错误而被放置在“超出”它的位置。下面是一个视觉示例:i.imgur.com/zv39l58.png
0赞 ARutherford 5/16/2019
如果我的目标是减少误差,或者计算有利于碰撞,转换为定点会有所帮助吗?我不太关心最终结果的精确度,而是关心潜在的遗漏碰撞。我可以通过它进行数学计算,还是更进一步,将这些线段转换为网格点是否有意义?以前只尝试过一次这样的事情,不确定我可能不认为会遇到什么样的问题。
1赞 Eric Postpischil 5/16/2019
@ARutherford:定点也有错误。例如,如果一条线的斜率为 4/3,则只能用 1.3333 之类的值来近似。该软件仍然必须设计为处理错误。在你上面的评论中,我看不出这个例子在展示什么。迭代 2 与问题中显示的预期结果匹配 - 您击中对象并沿着它滑动到角落。在迭代 3 中,您希望检测到自己在拐角处。那么为什么不呢?你已经击中了另一条线,你知道这一点,因为插图显示了方向因此而改变。
0赞 ARutherford 5/16/2019
它之所以没有被夹在角落里,是因为没有我们沿着那段滑行的“记忆”。我们只是将我们的位置与线段进行比较,我们看到我们的新运动矢量(几乎没有)与它发生碰撞。我可以尝试一些更复杂的东西,我们为我们已经与之交互过的线段做一个特例。然而,我不知道这是否会产生奇怪的边缘情况。