多边形剪裁-有点详尽吗?

时间:2018-07-13 00:10:59

标签: c++ geometry

我已经阅读了很多有关sutherland hodgman多边形裁剪算法的文章,并了解了总体思路。但是,当我看到它的实际实现时(如下面的例子),我对诸如intersectioninside方法中的坐标比较感到困惑。因此,我想知道是否有人可以详细说明什么以及原因?我看到了大量的视频和文章解释了这些通用概念,但是我确实很难找到有关实施细节的一些解释。

  bool inside(b2Vec2 cp1, b2Vec2 cp2, b2Vec2 p) {
      return (cp2.x-cp1.x)*(p.y-cp1.y) > (cp2.y-cp1.y)*(p.x-cp1.x);
  }

  b2Vec2 intersection(b2Vec2 cp1, b2Vec2 cp2, b2Vec2 s, b2Vec2 e) {
      b2Vec2 dc( cp1.x - cp2.x, cp1.y - cp2.y );
      b2Vec2 dp( s.x - e.x, s.y - e.y );
      float n1 = cp1.x * cp2.y - cp1.y * cp2.x;
      float n2 = s.x * e.y - s.y * e.x;
      float n3 = 1.0 / (dc.x * dp.y - dc.y * dp.x);
      return b2Vec2( (n1*dp.x - n2*dc.x) * n3, (n1*dp.y - n2*dc.y) * n3);
  }

  //http://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#JavaScript
  //Note that this only works when fB is a convex polygon, but we know all 
  //fixtures in Box2D are convex, so that will not be a problem
  bool findIntersectionOfFixtures(b2Fixture* fA, b2Fixture* fB, vector<b2Vec2>& outputVertices)
  {
      //currently this only handles polygon vs polygon
      if ( fA->GetShape()->GetType() != b2Shape::e_polygon ||
           fB->GetShape()->GetType() != b2Shape::e_polygon )
          return false;

      b2PolygonShape* polyA = (b2PolygonShape*)fA->GetShape();
      b2PolygonShape* polyB = (b2PolygonShape*)fB->GetShape();

      //fill subject polygon from fixtureA polygon
      for (int i = 0; i < polyA->GetVertexCount(); i++)
          outputVertices.push_back( fA->GetBody()->GetWorldPoint( polyA->GetVertex(i) ) );

      //fill clip polygon from fixtureB polygon
      vector<b2Vec2> clipPolygon;
      for (int i = 0; i < polyB->GetVertexCount(); i++)
          clipPolygon.push_back( fB->GetBody()->GetWorldPoint( polyB->GetVertex(i) ) );

      b2Vec2 cp1 = clipPolygon[clipPolygon.size()-1];
      for (int j = 0; j < clipPolygon.size(); j++) {
          b2Vec2 cp2 = clipPolygon[j];
          if ( outputVertices.empty() )
              return false;
          vector<b2Vec2> inputList = outputVertices;
          outputVertices.clear();
          b2Vec2 s = inputList[inputList.size() - 1]; //last on the input list
          for (int i = 0; i < inputList.size(); i++) {
              b2Vec2 e = inputList[i];
              if (inside(cp1, cp2, e)) {
                  if (!inside(cp1, cp2, s)) {
                      outputVertices.push_back( intersection(cp1, cp2, s, e) );
                  }
                  outputVertices.push_back(e);
              }
              else if (inside(cp1, cp2, s)) {
                  outputVertices.push_back( intersection(cp1, cp2, s, e) );
              }
              s = e;
          }
          cp1 = cp2;
      }

      return !outputVertices.empty();
  }

(从iforce2d盗取的代码:))

1 个答案:

答案 0 :(得分:1)

您说您了解一般的想法,大概是通过阅读Sutherland Hodgman Algorithm之类的东西。这就从高层次上确切地解释了insideintersection的作用。

关于它们如何实现目标的细节,这全都只是教科书的线性代数。

inside正在测试(cp2 - cp2)(p - cp1)的正负号,如果正负号严格大于零,则返回true。您可以将return语句重写为:

return (cp2.x-cp1.x)*(p.y-cp1.y) - (cp2.y-cp1.y)*(p.x-cp1.x) > 0;

将第二项移到>的左侧,这样您就可以在左侧获得正叉积。

请注意,叉积通常是vec3vec3的运算,需要计算所有三个项。但是,我们正在2d中执行此操作,这意味着vec3的格式为(x, y, 0)。因此,我们只需要计算z的输出项,因为十字必须垂直于xy平面,因此其形式为(0, 0, value)

intersection使用此处列出的精确算法Line Intersection from Two Points查找两个向量相交的点。特别是,我们会在紧随“行列式可以写为:”之后紧随其后的公式。

在该公式中,n1(x1 y2 - y1 x2)n2(x3 y4 - y3 x4),而n31 / ((x1 - x2) (y3 - y4) - (y1 - y2) (x3 - x4))

-编辑-

要覆盖评论中提出的问题,这里是我能给出的完整解释,以解释为什么inside()的返回值是对叉积符号的检验。

我要稍微切线,显示我的年龄,并注意,叉乘积公式具有非常简单的记忆辅助功能。您只需要记住伍兹和克劳瑟的文字冒险游戏Colossal Cave中的第一个魔语。 xyzzy

如果您在三个维度上有两个向量:(x1, y1, z1)(x2, y2, z2),则其叉积(xc, yc, zc)的计算方式如下:

xc = y1 * z2 - z1 * y2;
yc = z1 * x2 - x1 * z2;
zc = x1 * y2 - y1 * x2;

现在,请看第一行,从术语,所有空格和运算符中删除c12后缀,然后查看其余字母。这是神奇的词。然后,您垂直向下走,在逐行移动时,将{{1}替换为x,将y替换为y,将z替换为z

回到业务上,xxc扩展名右边的两个术语都包含ycz1。但是我们知道这两个都是零,因为我们的输入向量在z2平面中,因此具有零的xy分量。这就是为什么我们可以完全省去计算这两个项的原因,因为我们知道它们将为零。

这与叉积的定义一致100%,结果矢量始终垂直于两个输入矢量。因此,如果两个输入向量都在z平面中,我们知道输出向量必须垂直于xy平面,因此其形式为xy

那么,(0, 0, z)这个词有什么用?

z

在这种情况下,向量zc = x1 * y2 - y1 * x2; 1,向量cp2-cp12。因此,将其插入上面,我们得到:

p-cp1

但是如上所述,我们并不关心它的价值,而只是它的标志。我们想知道那是否大于零。因此:

zc = (cp2.x-cp1.x)*(p.y-cp1.y) - (cp2.y-cp1.y)*(p.x-cp1.x);

然后将其重写为:

return (cp2.x-cp1.x)*(p.y-cp1.y) - (cp2.y-cp1.y)*(p.x-cp1.x) > 0;

最后,该术语的符号与点p在剪切多边形的内部还是外部有什么关系?您完全正确,所有剪辑都发生在2d return (cp2.x-cp1.x)*(p.y-cp1.y) > (cp2.y-cp1.y)*(p.x-cp1.x); 平面中,那么为什么我们要完全涉及3d操作呢?

要意识到的重要一点是3d中的叉积公式不是可交换的。就两个向量操作数之间的角度而言,它们的顺序很重要。维基百科页面上Cross Product的第一张图片完美地展示了它。在该图中,如果从上方向下看,则在评估xy交叉a时,从ba的最短角度方向是逆时针方向。在这种情况下,假设正b在页面上移动,将导致交叉积具有z正值。但是,如果评估z十字b,则从ab的最短角距离是顺时针,并且十字积具有负a值。 / p>

回顾算法本身的Wikipedia页面,您已经获得了蓝色的“ clipping”线,该线沿裁剪多边形逆时针运行。如果您认为该矢量在逆时针方向上始终具有正值,则对于裁剪多边形中的任意一对相邻顶点,它始终为z

请牢记这一点,想象一下如果您站在cp2 - cp1时鼻子直指cp1会看到什么。裁剪多边形的内部将在您的左侧,外部在右侧。现在考虑两个点cp2p1。我们将说p2在剪切多边形内,而p1在剪切多边形内。这意味着将鼻子指向p2的最快方法是逆时针旋转,而指向p1的最快方法是顺时针旋转。

因此,通过研究叉积的符号,我们实际上是在问“我们要从当前边沿顺时针还是逆时针旋转以查看该点”,这等同于询问该点是否在剪切多边形内或外面。

我将添加最后一条建议。如果您对此类事物,3D渲染或涉及对真实世界的数学表示进行建模的程序完全感兴趣,可以参加线性代数的坚实课程,涵盖交叉乘积,点乘积,向量,矩阵以及它们之间的相互作用将是您可以做的最好的事情之一。它将为大量使用计算机完成的工作提供非常的坚实基础。