如何有效检查点是否在旋转的矩形内?


11

我一方面要出于优化的目的,另一方面出于学习的目的,我敢问:如何使用C#或C ++ 最有效地检查2D点P是否在2D旋转的矩形内XYZW

当前,我正在做的是使用《实时碰撞检测》一书中的“三角形中的点”算法,并运行两次(对于组成矩形的两个三角形,例如XYZ和XZW):

bool PointInTriangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P)
{
 // Compute vectors        
 Vector2 v0 = C - A;
 Vector2 v1 = B - A;
 Vector2 v2 = P - A;

 // Compute dot products
 float dot00 = Vector2.Dot(v0, v0);
 float dot01 = Vector2.Dot(v0, v1);
 float dot02 = Vector2.Dot(v0, v2);
 float dot11 = Vector2.Dot(v1, v1);
 float dot12 = Vector2.Dot(v1, v2);

 // Compute barycentric coordinates
 float invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
 float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
 float v = (dot00 * dot12 - dot01 * dot02) * invDenom;

 // Check if point is in triangle
 if(u >= 0 && v >= 0 && (u + v) < 1)
    { return true; } else { return false; }
}


bool PointInRectangle(Vector2 X, Vector2 Y, Vector2 Z, Vector2 W, Vector2 P)
{
 if(PointInTriangle(X,Y,Z,P)) return true;
 if(PointInTriangle(X,Z,W,P)) return true;
 return false;
}

但是,我觉得可能会有一种更清洁,更快的方法。具体来说,减少数学运算的次数。


您有很多点,还是有很多矩形?这是您在尝试优化如此小的任务之前应该问自己的第一个问题。
sam hocevar,2015年

好点子。我将拥有非常多的点,但还要检查更多的矩形。
路易

有关找到点到旋转矩形的距离的相关问题。这是一种简并的情况(仅在距离为0时检查)。当然,此处将存在不存在的优化。
安口

您是否考虑过将点旋转到矩形的参考框架中?
理查德·廷格

@RichardTingle实际上我不是一开始的。后来我做了,因为我认为这与下面给出的答案之一有关。但只是要澄清一下:在您的建议中,将点旋转到矩形的参考框架后,应该仅通过max.x,min.x等之间的逻辑比较来检查是否包含?
Louis15,2015年

Answers:


2

一个简单而直接的优化将是更改最终条件PointInTriangle

bool PointInRectangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P) {
  ...
  if(u >= 0 && v >= 0 && u <= 1 && v <= 1)
      { return true; } else { return false; }
  }
}

该代码已经非常多PointInRectangle了,条件(u + v) < 1是在那里检查它是否不在矩形的“第二”三角形中。

另外,您也可以进行isLeft四次测试(第一个代码示例,在页面上,也有很多说明),并检查它们是否均返回相同的符号(该符号取决于点是按顺时针还是逆时针顺序给出)返回的结果要点在里面。这也适用于任何其他凸多边形。

float isLeft( Point P0, Point P1, Point P2 )
{
    return ( (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y) );
}
bool PointInRectangle(Vector2 X, Vector2 Y, Vector2 Z, Vector2 W, Vector2 P)
{
    return (isLeft(X, Y, P) > 0 && isLeft(Y, Z, P) > 0 && isLeft(Z, W, P) > 0 && isLeft(W, X, p) > 0);
}

高超。我不知道我是否更喜欢您的建议,它真的比我的建议更快,更优雅,或者我是否更喜欢您已经注意到我的PointInTri代码很容易成为PointInRec!谢谢
Louis15 2015年

+1为isLeft方法。它不需要触发函数(也不需要Vector2.Dot),这可以大大加快处理速度。
Anko

顺便说一句,不能通过直接将isLeft包含在主函数中,并用“ ||”代替“ &&”运算符来对代码进行调整(无法测试;在此计算机中没有操作方法)。通过逆逻辑? public static bool PointInRectangle(Vector2 P, Vector2 X, Vector2 Y, Vector2 Z, Vector2 W) { return !(( (Y.x - X.x) * (P.y - X.y) - (P.x - X.x) * (Y.y - X.y) ) < 0 || ( (Z.x - Y.x) * (P.y - Y.y) - (P.x - Y.x) * (Z.y - Y.y) ) < 0 || ( (W.x - Z.x) * (P.y - Z.y) - (P.x - Z.x) * (W.y - Z.y) ) < 0 || ( (X.x - W.x) * (P.y - W.y) - (P.x - W.x) * (X.y - W.y) ) < 0 ); }
路易

1
@ Louis15我认为您不需要-&&和|| 如果找到一个否定/肯定(或还有其他原因?)将停止执行进一步的语句。声明isLeft为内联编译器将为您做类似的事情(可能更好,您可能会做得更好,因为编写编译器的工程师最了解CPU,选择最快的选项)使您的代码更具可读性,并且效果相同或更好。
温德拉2015年

8

编辑: OP的评论者对建议的负圆边界检查的效率表示怀疑,以改善算法,以便检查是否有任意2D点位于旋转和/或移动的矩形内。摆弄我的2D游戏引擎(OpenGL / C ++),我提供了我的算法相对于OP当前的矩形检查算法(和变体)的性能基准,从而补充了我的答案。

我最初建议保留该算法(因为它几乎是最佳的),但仅通过游戏逻辑即可简化:(1)使用围绕原始矩形的预处理圆;(2)进行距离检查,以及该点是否在给定的圆内;(3)使用OP或其他任何简单算法(我建议使用其他答案中提供的isLeft算法)。我的建议背后的逻辑是,检查点是否在圆内要比旋转矩形或任何其他多边形的边界检查有效得多。

我进行基准测试的最初方案是在受约束的空间中运行大量出现和消失的点(每个游戏循环中其位置都发生变化),该空间将填充约20个旋转/移动方块。我发布了一个视频(youtube链接)用于说明。请注意以下参数:随机出现的点数,数字或矩形。我将使用以下参数进行基准测试:

OFF:OP提供的直接算法,不进行圆弧边界否定检查

ON:使用经过处理的(边界)圆围绕矩形进行的第一次排除检查

ON + Stack:在运行时在堆栈上的循环内创建圆边界

ON +平方距离:使用平方距离作为进一步的优化以避免采用更昂贵的平方根算法(Pieter Geerkens)。

这是通过显示迭代循环所花费的时间来总结不同算法的各种性能。

在此处输入图片说明

x轴通过添加更多点(从而减慢了循环速度)显示出更高的复杂性。(例如,在20个矩形的密闭空间中的1000个随机出现的点检查处,循环迭代并调用算法20000次。)y轴显示使用高分辨率完成整个循环所需的时间(ms)。性能计时器。超过20毫秒对于一个体面的游戏会造成问题,因为它不会利用高fps来插值平滑的动画,并且游戏有时会显得“粗糙”。

结果1:与常规算法(不进行检查的原始循环时间的5%)相比,在循环内进行快速否定检查的预处理循环边界算法将性能提高了1900%。结果大约与循环内的迭代次数成正比,因此我们检查10个或10000个随机出现的点都没有关系。因此,在该图示中,可以安全地将对象的数量增加到10k,而不会感觉到性能损失。

结果2:先前的评论建议该算法可能更快,但占用大量内存。但是,请注意,为预处理的圆圈大小存储浮点数仅需4个字节。除非OP计划同时运行100000+个对象,否则这不会造成任何实际问题。一种有效的内存替代方法是在循环内计算堆栈上的最大循环大小,并使其在每次迭代时都超出范围,因此对于某些未知的速度代价,实际上没有内存使用情况。确实,结果表明该方法的确比使用预处理的圆圈大小慢,但仍显示出大约1150%(即原始处理时间的8%)的显着性能改进。

结果3:我通过使用平方距离而不是实际距离来进一步改进结果1算法,从而采用了计算量大的平方根运算。这只能提高性能(2400%)。(注意:我也尝试对预处理数组的哈希表求平方根近似值,但结果类似,但略差一些)

结果4:我进一步检查了移动/碰撞矩形。但是,这不会改变基本结果(如预期的那样),因为逻辑检查基本上保持不变。

结果5:我改变了矩形的数量,发现填充的空间越少,算法变得越有效(演示中未显示)。由于在圆与对象边界之间的微小空间内出现点的概率降低,因此也可以预期结果。另一方面,我试图在相同的狭窄空间内将矩形的数量增加到100个以上,并在运行时在循环内(sin(iterator))动态改变它们的大小。仍然可以将性能提高570%(或原始循环时间的15%),性能非常好。

结果6:我测试了此处建议的替代算法,发现性能差异很小(但不明显)(2%)。有趣且更简单的IsLeft算法性能很好,性能提高了17%(原始计算时间的85%),但远不及快速否定检查算法的效率。

我的观点是首先考虑精益设计和游戏逻辑,尤其是在处理边界和碰撞事件时。OPs当前的算法已经相当高效,并且进一步的优化并不像优化基础概念本身一样重要。而且,最好交流游戏的范围和目的,因为算法的效率主要取决于它们。

我建议在游戏设计阶段始终尝试对任何复杂算法进行基准测试,因为仅查看普通代码可能无法揭示有关实际运行时性能的真相。如果例如仅希望测试鼠标光标是否位于矩形内,或者当大多数对象已经在触摸时,建议的算法甚至在这里就没有必要。如果大多数点检查都在矩形内,则该算法将效率较低。(但是,有可能建立一个“内圆”边界作为次要负检查。)圆/球边界检查对于大量物体之间进行自然碰撞检测的体面碰撞检测非常有用。 。

Rec Points  Iter    OFF     ON     ON_Stack     ON_SqrDist  Ileft Algorithm (Wondra)
            (ms)    (ms)    (ms)    (ms)        (ms)        (ms)
20  10      200     0.29    0.02    0.04        0.02        0.17
20  100     2000    2.23    0.10    0.20        0.09        1.69
20  1000    20000   24.48   1.25    1.99        1.05        16.95
20  10000   200000  243.85  12.54   19.61       10.85       160.58

尽管我喜欢这种非同寻常的方法并且喜欢达芬奇的参考文献,但我认为处理圆圈(更不用说半径)会这么有效。同样,只有所有矩形都已固定且事先已知的情况,该解决方案才是合理的
Louis15

矩形的位置不需要固定。使用相对坐标。想起来也是这样。无论旋转如何,该半径均保持不变。
Majte

这是一个很好的答案。更好,因为我没有想到这一点。您可能需要注意,使用平方距离代替实际距离就足够了,从而节省了计算平方根的需要。
Pieter Geerkens

快速正/负测试的有趣算法!问题可能是额外的内存来保存预处理的边界圆(和宽度),它可能是很好的启发式方法,但也要注意它的用途有限-主要用于内存无关紧要的情况(较大对象上的静态大小矩形= Sprite游戏对象)并有时间进行预处理。
温德拉

编辑并添加了基准测试。
Majte

2

定义一个具有4个点的矩形可以制作一个梯形。但是,如果通过x,y,宽度,高度和围绕其中间的旋转来定义它,则可以通过矩形的反向旋转(围绕相同的原点)旋转要检查的点,然后检查它是否为在原始矩形中。


嗯,谢谢你的建议,但是旋转并获得反向旋转似乎效率不高。实际上,它几乎不像我的解决方案那么有效-更不用说windra的了
Louis15年

您可能会注意到,旋转3D点和矩阵是6个乘法和3个加法,以及一个函数调用。@wondra的解决方案充其量是等效的,但是意图更不清晰。并且更容易因违反DRY而导致维护错误
Pieter Geerkens 2015年

@Pieter Geerkens令人不安的说法是,我的任何解决方案都违反了DRY(并且DRY是关键编程原理之一吗?直到现在为止都没有听说过)?最重要的是,这些解决方案有哪些错误?随时准备学习。
温德拉

@wondra:干=不要重复自己。您的代码段建议在功能出现在代码中的任何地方通过向量乘法对矩阵的细节进行编码,而不是调用标准的“矩阵应用程序到向量”方法。
Pieter Geerkens

@PieterGeerkens当然只建议部分内容-1)您没有明确的矩阵(为每个查询分配新矩阵会严重影响性能)2)我仅使用乘法的特定情况,针对这种情况进行了优化,这降低了泛型的膨胀一。它是低级操作,应保持封装状态以防止意外行为。
温德拉

1

我没有时间对此进行基准测试,但是我的建议是存储将矩阵转换为从0到1的x和y范围内的矩形轴对齐正方形的转换矩阵。换句话说,存储矩阵将矩形的一个角转换为(0,0),将另一角转换为(1,1)。

如果移动矩形并很少检查碰撞,这当然会更加昂贵,但是如果检查的次数多于对矩形的更新,则至少比对两个三角形进行测试的原始方法要快,因为六个点积将被一个矩阵乘法代替。

但是,与往常一样,该算法的速度在很大程度上取决于您希望执行的检查类型。如果大多数点甚至都没有靠近矩形,则执行简单的距离检查(例如(point.x-firstCorner.x)> aLargeDistance)可能会导致较大的加速,而如果几乎所有的点都可能使速度变慢这些点在矩形内。

编辑:这是我的矩形类的样子:

class Rectangle
{
public:
    Matrix3x3 _transform;

    Rectangle()
    {}

    void setCorners(Vector2 p_a, Vector2 p_b, Vector2 p_c)
    {
        // create a matrix from the two edges of the rectangle
        Vector2 edgeX = p_b - p_a;
        Vector2 edgeY = p_c - p_a;

        // and then create the inverse of that matrix because we want to 
        // transform points from world coordinates into "rectangle coordinates".
        float scaling = 1/(edgeX._x*edgeY._y - edgeY._x*edgeX._y);

        _transform._columns[0]._x = scaling * edgeY._y;
        _transform._columns[0]._y = - scaling * edgeX._y;
        _transform._columns[1]._x = - scaling * edgeY._x;
        _transform._columns[1]._y = scaling * edgeX._x;

        // the third column is the translation, which also has to be transformed into "rectangle space"
        _transform._columns[2]._x = -p_a._x * _transform._columns[0]._x - p_a._y * _transform._columns[1]._x;
        _transform._columns[2]._y = -p_a._x * _transform._columns[0]._y - p_a._y * _transform._columns[1]._y;
    }

    bool isInside(Vector2 p_point)
    {
        Vector2 test = _transform.transform(p_point);
        return  (test._x>=0)
                && (test._x<=1)
                && (test._y>=0)
                && (test._y<=1);
    }
};

这是我的基准测试的完整清单:

#include <cstdlib>
#include <math.h>
#include <iostream>

#include <sys/time.h>

using namespace std;

class Vector2
{
public:
    float _x;
    float _y;

    Vector2()
    :_x(0)
    ,_y(0)
    {}

    Vector2(float p_x, float p_y)
        : _x (p_x)
        , _y (p_y)
        {}

    Vector2 operator-(const Vector2& p_other) const
    {
        return Vector2(_x-p_other._x, _y-p_other._y);
    }

    Vector2 operator+(const Vector2& p_other) const
    {
        return Vector2(_x+p_other._x, _y+p_other._y);
    }

    Vector2 operator*(float p_factor) const
    {
        return Vector2(_x*p_factor, _y*p_factor);
    }

    static float Dot(Vector2 p_a, Vector2 p_b)
    {
        return (p_a._x*p_b._x + p_a._y*p_b._y);
    }
};

bool PointInTriangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P)
{
 // Compute vectors        
 Vector2 v0 = C - A;
 Vector2 v1 = B - A;
 Vector2 v2 = P - A;

 // Compute dot products
 float dot00 = Vector2::Dot(v0, v0);
 float dot01 = Vector2::Dot(v0, v1);
 float dot02 = Vector2::Dot(v0, v2);
 float dot11 = Vector2::Dot(v1, v1);
 float dot12 = Vector2::Dot(v1, v2);

 // Compute barycentric coordinates
 float invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
 float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
 float v = (dot00 * dot12 - dot01 * dot02) * invDenom;

 // Check if point is in triangle
 if(u >= 0 && v >= 0 && (u + v) < 1)
    { return true; } else { return false; }
}


bool PointInRectangle(Vector2 X, Vector2 Y, Vector2 Z, Vector2 W, Vector2 P)
{
 if(PointInTriangle(X,Y,Z,P)) return true;
 if(PointInTriangle(X,Z,W,P)) return true;
 return false;
}

class Matrix3x3
{
public:
    Vector2 _columns[3];

    Vector2 transform(Vector2 p_in)
    {
        return _columns[0] * p_in._x + _columns[1] * p_in._y + _columns[2];
    }
};

class Rectangle
{
public:
    Matrix3x3 _transform;

    Rectangle()
    {}

    void setCorners(Vector2 p_a, Vector2 p_b, Vector2 p_c)
    {
        // create a matrix from the two edges of the rectangle
        Vector2 edgeX = p_b - p_a;
        Vector2 edgeY = p_c - p_a;

        // and then create the inverse of that matrix because we want to 
        // transform points from world coordinates into "rectangle coordinates".
        float scaling = 1/(edgeX._x*edgeY._y - edgeY._x*edgeX._y);

        _transform._columns[0]._x = scaling * edgeY._y;
        _transform._columns[0]._y = - scaling * edgeX._y;
        _transform._columns[1]._x = - scaling * edgeY._x;
        _transform._columns[1]._y = scaling * edgeX._x;

        // the third column is the translation, which also has to be transformed into "rectangle space"
        _transform._columns[2]._x = -p_a._x * _transform._columns[0]._x - p_a._y * _transform._columns[1]._x;
        _transform._columns[2]._y = -p_a._x * _transform._columns[0]._y - p_a._y * _transform._columns[1]._y;
    }

    bool isInside(Vector2 p_point)
    {
        Vector2 test = _transform.transform(p_point);
        return  (test._x>=0)
                && (test._x<=1)
                && (test._y>=0)
                && (test._y<=1);
    }
};

void runTest(float& outA, float& outB)
{
    Rectangle r;
    r.setCorners(Vector2(0,0.5), Vector2(0.5,1), Vector2(0.5,0));

    int numTests = 10000;

    Vector2 points[numTests];

    Vector2 cornerA[numTests];
    Vector2 cornerB[numTests];
    Vector2 cornerC[numTests];
    Vector2 cornerD[numTests];

    bool results[numTests];
    bool resultsB[numTests];

    for (int i=0; i<numTests; ++i)
    {
        points[i]._x = rand() / ((float)RAND_MAX);
        points[i]._y = rand() / ((float)RAND_MAX);

        cornerA[i]._x = rand() / ((float)RAND_MAX);
        cornerA[i]._y = rand() / ((float)RAND_MAX);

        Vector2 edgeA;
        edgeA._x = rand() / ((float)RAND_MAX);
        edgeA._y = rand() / ((float)RAND_MAX);

        Vector2 edgeB;
        edgeB._x = rand() / ((float)RAND_MAX);
        edgeB._y = rand() / ((float)RAND_MAX);

        cornerB[i] = cornerA[i] + edgeA;
        cornerC[i] = cornerA[i] + edgeB;
        cornerD[i] = cornerA[i] + edgeA + edgeB;
    }

    struct timeval start, end;

    gettimeofday(&start, NULL);
    for (int i=0; i<numTests; ++i)
    {
        r.setCorners(cornerA[i], cornerB[i], cornerC[i]);
        results[i] = r.isInside(points[i]);
    }
    gettimeofday(&end, NULL);
    float elapsed = (end.tv_sec - start.tv_sec)*1000;
    elapsed += (end.tv_usec - start.tv_usec)*0.001;
    outA += elapsed;

    gettimeofday(&start, NULL);
    for (int i=0; i<numTests; ++i)
    {
        resultsB[i] = PointInRectangle(cornerA[i], cornerB[i], cornerC[i], cornerD[i], points[i]);
    }
    gettimeofday(&end, NULL);
    elapsed = (end.tv_sec - start.tv_sec)*1000;
    elapsed += (end.tv_usec - start.tv_usec)*0.001;
    outB += elapsed;
}

/*
 * 
 */
int main(int argc, char** argv) 
{
    float a = 0;
    float b = 0;

    for (int i=0; i<5000; i++)
    {
        runTest(a, b);
    }

    std::cout << "Result: " << a << " / " << b << std::endl;

    return 0;
}

该代码当然不是很漂亮,但是我没有立即看到任何主要的错误。使用该代码,我得到的结果表明,如果在每次检查之间移动矩形,我的解决方案的速度大约是原来的两倍。如果它不动,那么我的代码似乎快了五倍以上。

如果您知道代码的使用方式,则可以通过将转换和检查分为两个维度来甚至进一步提高代码速度。例如,在赛车游戏中,首先检查指向行驶方向的坐标可能会更快,因为许多障碍物会在汽车的前面或后面,但几乎没有障碍物会在它的右边或左边。


有趣,但不要忘记,您还需要对点进行矩阵旋转。我的游戏引擎中有一个矩阵腐烂运算,以后可以对您的算法进行基准测试。关于您的最后评论。然后,您也可以定义一个“内圆”,并进行双重否定检查,以确认圆点是否位于内圆之外和外圆内,如上所述。
Majte

是的,如果您希望大多数点都接近三角形的中间,那将有所帮助。我正在想象一种类似矩形赛道的情况,例如,您使用角色必须留在其中的外部矩形和必须避开的较小内部矩形来定义矩形路径。在那种情况下,每个检查都将接近矩形的边界,而那些圆形检查可能只会使性能变差。当然,这是一个结构化的示例,但是我想说这是可能会发生的事情。
拉尔斯·科莫莫尔

这样的事情可能发生,是的。我想知道反对该算法的最佳点是什么。最后,它归结为您的目的。如果有时间,您可以使用OP发布代码,我可以对您的算法进行基准测试吗?让我们看看您的直觉是否正确。我对您对IsLeft算法的想法的执行感到好奇。
Majte 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.