【问题标题】:Sort polygon's points for drawing对多边形的点进行排序以进行绘图
【发布时间】:2015-04-07 23:48:00
【问题描述】:

我有一个矩阵(0 表示什么都没有,1 表示地形)代表我游戏中的一个关卡。该矩阵对应于我的屏幕被分解成的网格,并指示我的地形走向。

我的地形实际上是由网格内每个块的角落中的 4 个点组成的。当您有多个连接的块时,我使用合并单元算法来删除重复点和任何内部点。结果是我最终得到了一个仅代表多边形外边缘的点列表。

要绘制这个多边形,我需要点按某种顺序(顺时针或逆时针),以便每个点后面跟着它的相邻点。显然,第一个点和最后一个点需要是邻居。由于这一切都在一个网格中,我知道相邻点之间的确切距离。

问题是我无法想出一种算法,该算法允许我在将点按顺序排列的同时“行走”在多边形的边缘。我相信应该有一种方法可以利用我拥有代表几何的矩阵这一事实,这意味着只有一种可能的方式来绘制多边形(即使它是凹面的)。

我尝试了几种使用贪心算法的方法,但似乎无法找到一种方法来在每种情况下都知道我想朝哪个方向行驶。鉴于任何特定点最多可以有 3 个邻居(第四个不包括在内,因为它是“起点”,这意味着我已经对其进行了排序)我需要一种知道要移动的方式。

更新

我一直在尝试的另一种方法是按它们的 X 对点进行排序(使用 Y 的决胜局),这给了我最上面/最左边的边缘。它还保证我从外缘开始。但是,我仍在努力寻找一种算法来保证我留在外面而不会越过。

这是一个示例矩阵:

0 0 0 0 0 0 0 0 0 0

0 0 0 1 1 1 0 0 0 0

0 0 0 0 0 1 0 0 0 0

0 0 0 0 0 1 1 1 0 0

与此相对应(黑点代表我的观点):

【问题讨论】:

  • 您尝试基于此矩阵创建网格?
  • @AnthonyRaimondo 有点像。目前我只是在使用 actionscript/flash,所以只需要按正确的顺序排列点即可。但是,如果我想制作网格,我仍然需要按顺序排列它们,所以这并不重要。
  • 是否可以先创建所有三角形,以便您有参考,然后合并所有点?动作脚本/闪存期望点的顺序是什么?三角带?
  • 这是不可能的(据我所知),因为这是一个关卡编辑器,可以按任何顺序创建/销毁块。这意味着你可以从屏幕的一侧开始,然后随机连接块,直到你有一些奇怪的凹形。由于块共享点,因此不可能知道它们是按什么顺序创建的。我认为必须有一种简单的方法来利用我的矩阵来获得订单。对于 flash,我不知道底层绘图代码是什么(他们不提供访问权限或文档)。您只需开始填充,然后按顺序连接点。
  • 我想到的最好的想法是洪水填充算法。 Here。基本上你通过矩阵。一旦你找到一个起点(理想情况下你会从上到下/从左到右来优化洪水填充),然后你从那个点开始洪水。然后,如果它们是周界点并且尚未添加,则插入新点。 +2 偏移量是指元素的插入。请注意,插入索引(不是偏移量)还取决于您从最后一个正方形移动的方式。抱歉,如果这很难解释,只是现在时间紧迫,否则我会写下来。

标签: algorithm sorting math polygon


【解决方案1】:

首先请考虑,对于一般矩阵,输出可以由多个闭环组成;例如矩阵的边界

形成三个不同的循环,其中一个放在另一个里面。

要提取这些循环,第一步是构建所有“墙”的地图:每次一个单元格的内容与同一行上的下一个单元格的内容不同时,您就有一个垂直墙;当内容与下一行中的同一单元格不同时,您将拥有一堵水平墙。

data = [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
        [ 0, 1, 1, 1, 1, 0, 0, 0, 0, 0 ],
        [ 0, 1, 0, 0, 1, 0, 1, 1, 0, 0 ],
        [ 0, 1, 0, 0, 1, 0, 1, 1, 1, 0 ],
        [ 0, 1, 1, 1, 1, 0, 0, 1, 1, 0 ],
        [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]]

rows = len(data)
cols = len(data[0])

walls = [[2*(data[r][c] != data[r][c+1]) + (data[r][c] != data[r+1][c])
          for c in range(cols-1)]
         for r in range(rows-1)]

在上面的示例中,我使用了两个位:0x01 标记水平墙壁,0x02 标记垂直墙壁。对于给定的(r, c) 单元格,墙壁是单元格的右侧和底部。

为简单起见,我还假设有趣的区域没有触及矩阵的限制;这可以通过添加额外的行和零列或通过将矩阵访问包装在一个函数中来解决,该函数为矩阵外的虚拟元素返回 0。

要构建边界列表,您只需从墙上的任意点开始,然后移动后面的墙,在处理这些墙时将其从地图中移除。当你不能再移动时,一个循环已经完成(你可以保证完成循环,因为在以这种方式从内部/外部标志矩阵构建的图中,度数保证在所有顶点中都是均匀的)。

使用奇偶填充规则同时填充所有这些循环也可以保证重现原始矩阵。

在下面的代码中,我使用rc 作为行/列索引,并使用ij 来表示边界上的点...例如对于单元格(r=3, c=2) 架构是:

红墙对应位0x02,绿墙对应位0x01walls 矩阵比原始数据矩阵少了一行和一列,因为它假定最后一行或最后一列不能出现墙。

result = []
for r in range(rows-1):
    for c in range(cols-1):
        if walls[r][c] & 1:
            i, j = r+1, c
            cycle = [(i, j)]
            while True:
                if i < rows-1 and walls[i][j-1] & 2:
                    ii, jj = i+1, j
                    walls[i][j-1] -= 2
                elif i > 0 and walls[i-1][j-1] & 2:
                    ii, jj = i-1, j
                    walls[i-1][j-1] -= 2
                elif j < cols-1 and walls[i-1][j] & 1:
                    ii, jj = i, j+1
                    walls[i-1][j] -= 1
                elif j > 0 and walls[i-1][j-1] & 1:
                    ii, jj = i, j-1
                    walls[i-1][j-1] -= 1
                else:
                    break
                i, j = ii, jj
                cycle.append((ii, jj))
            result.append(cycle)

基本上,代码从边界上的一个点开始,并检查它是否可以在墙上向上、向下、向左或向右移动。当它不能再移动时,一个循环已经完成,可以添加到最终结果中。

该算法的复杂度为 O(rows*cols),即它与输入大小成正比,并且是最优的(在大 O 意义上),因为至少不读取输入就无法计算结果。这很容易看出,因为 while 主体的输入次数不能超过地图中墙的总数(每次迭代都会移除一堵墙)。

编辑

可以修改算法以仅生成简单循环(即每个顶点仅访问一次的路径)作为输出。

result = []
index = [[-1] * cols for x in range(rows)]
for r in range(rows-1):
    for c in range(cols-1):
        if walls[r][c] & 1:
            i, j = r+1, c
            cycle = [(i, j)]
            index[i][j] = 0
            while True:
                if i > 0 and walls[i-1][j-1] & 2:
                    ii, jj = i-1, j
                    walls[i-1][j-1] -= 2
                elif j > 0 and walls[i-1][j-1] & 1:
                    ii, jj = i, j-1
                    walls[i-1][j-1] -= 1
                elif i < rows-1 and walls[i][j-1] & 2:
                    ii, jj = i+1, j
                    walls[i][j-1] -= 2
                elif j < cols-1 and walls[i-1][j] & 1:
                    ii, jj = i, j+1
                    walls[i-1][j] -= 1
                else:
                    break
                i, j = ii, jj
                cycle.append((ii, jj))
                ix = index[i][j]
                if ix >= 0:
                    # closed a loop
                    result.append(cycle[ix:])
                    for i_, j_ in cycle[ix:]:
                        index[i_][j_] = -1
                    cycle = cycle[:ix+1]
                index[i][j] = len(cycle)-1

这是通过在处理中遇到两次相同的顶点时向输出添加一个单独的循环来实现的(index 表存储给定的i,j 指向正在构建的当前循环中基于 0 的索引) .

【讨论】:

  • 我在这段代码中看到的唯一问题是它将对角线块视为同一个整体块的一部分。这意味着如果矩阵中只有两个对角线的“1”,您的算法将返回一个包含两者的循环。在这种情况下,我需要它是两个不同的周期。有没有可以做的快速改变来做到这一点?
  • @Kinru:见编辑。此修改版本的输出由非自相交循环组成。但是请注意,您仍然可以将一个循环包含在另一个循环中......(孔)
  • @Kinru 为什么需要两个不同的周期?这不是原始问题定义的一部分。
  • @MarkRansom 那是我最初的问题中的一个错误,我应该更清楚地说明这一点。
  • @Kinru 我仍然想知道为什么存在约束,因为它使问题复杂化。
【解决方案2】:

这似乎对我有用:

对于每个填充的正方形,检查它的哪些邻居是填充的。对于那些不是的,将适当的边缘添加到边缘列表中。根据您的喜好顺时针或逆时针生成这些边。

要构建完整路径,首先从集合中拉出任何边并将其添加到路径中。它有一个顺序,所以看看第二个顶点。在集合中找到第一个顶点等于第二个顶点的边。从集合中拉出该边缘并将其添加到路径中。继续,直到路径关闭。

重复生成路径列表。一个简单的多边形最终应该是一条路径。一个复杂的多边形——在这个例子中是一个中间有洞的多边形——将是多个。

【讨论】:

    【解决方案3】:

    我想有不同的方法可以做到这一点,我想当对角线连接的单元格计为不同的轮廓时,我想有一个非常简单的方法:

    你只需要保持单元格和角落的方向。例如,您从某个地球单元格的右上角开始(假设上部单元格或右侧单元格,或者如果它是边界,则两者都不是)并且想要顺时针方向移动。

    如果右侧的单元格是地球,则将当前单元格更改为它并将角更改为左上角(它是同一点)。然后进行下一次迭代。

    在其他情况下,如果您从某个地球单元的右上角开始并想顺时针方向移动。如果右侧的单元格不是地球,则不要更改当前单元格并将角更改为右下角,(这是下一个点)

    所以你对其他三个可能的角也有对称情况,你可以继续下一次迭代,直到回到起点。

    所以这是我写的伪代码,它使用与图片使用相同的索引,并假设沿边界的所有单元格都是空闲的,否则您需要检查索引 id 是否超出范围。

    我还需要与矩阵几乎相同尺寸的附加数组来标记处理过的轮廓,它需要比矩阵宽 1 个单元格,因为我要标记垂直线,并且每条垂直线的坐标应该是它右侧的单元格。注意,上面8个中只有2个案例需要标注竖线。

        int mark[,] = new int[height,width+1]       
        start_i = i = 0;
        start_j = j = 0;
        direction = start_direction = top_left;
        index = 0;
    
        //outer cycle through different contours
        while(true)
        {
          ++index;
          //scanning for contours through all the matrix
          //continue from the same place, we stopped last time
          for(/*i = i*/; i < n; i++)
          {
            for(/*j = j*/; j < n; j++)
            {
               //if we found earth
               if(m[i,j] == 1)
               {
                   //check if previous cell is nothing
                   //check if line between this and previous contour doesn't already added
                   if(m[i,j - 1] == 0 && mark[i,j] == 0)
                   {
                       direction = bottom_left;
                       break;
                   }
    
                   //the same for next cell
                   if(m[i,j + 1] == 0 && mark[i,j+1] == 0)
                   {
                       direction = top_right;
                       break;
                   }
               }
            }
            //break if we found contour
            if(i != start_i || j != start_j)
              break;
          }
    
          //stop if we didn't find any contour
          if(i == start_i && j == start_j)
          {
            break;
          }
    
          polygon = new polygon;
          start_i = i;
          start_j = j;
          start_direction = direction;
    
          //now the main part of algorithm described above
          do
          {
            if(direction == top_left)
            {
               if(n(i-1,j) == 1)
               {
                  direction = bottom_left;
                  position = (i-1,j)
               }
               else
               {
                  direction = top_right;
                  polygon.Add(i,j+1);       
               }
            }
            if(direction == top_right;)
            {
               if(n[i,j + 1] == 1)
               {
                  direction = top_left;
                  position = (i,j + 1)
               }
               else
               {
                  direction = bottom_right;
                  mark[i, j + 1] = index;//don't forget to mark edges!
                  polygon.Add(i+1,j+1);       
               }
            } 
            if(direction == bottom_right;
            {
               if(n[i+1,j] == 1)
               {
                  direction = top_right;
                  position = (i+1,j)
               }
               else
               {
                  direction = bottom_left;
                  polygon.Add(i+1,j);       
               }
            } 
            if(direction == bottom_left)
            {
               if(n[i,j - 1] == 1)
               {
                  direction = bottom_right;
                  position = [i,j - 1]
               }
               else
               {
                  direction = top_left;
                  mark[i, j] = index;//don't forget to mark edges!
                  polygon.Add(i,j);       
               }
            } 
          //and we can stop as we reached the starting state
          }while(i != start_i || j != start_j || direction != start_direction);
    
          //stop if it was last cell
          if(i == n-1 && j == n- 1)
          {
            break;
          }
        }
    

    此外,您可能需要知道哪个轮廓在哪个内部,并且您需要一个堆栈来保存您在扫描时所处的轮廓,因此每次越过现有轮廓时,您都需要将其添加到堆栈中或者如果它已经在堆栈的顶部,则将其删除。 它将导致代码的下一个更改:

      ...
      //continue from the same place, we stopped last time
      for(/*i = i*/; i < n; i++)
      {
        for(/*j = j*/; j < n; j++)
        {
           if(mark[i,j] != 0)
           {
               if(stack.top() == mark [i,j])
               {
                    stack.pop();
               }
               else
               {
                    stack.push(mark [i,j]);
               }
           }
           //if we found earth
           if(m[i,j] == 1)
           {
               ...
    

    【讨论】:

      【解决方案4】:

      如果您的矩阵可以包含随机模式,那么答案远比看起来复杂。

      一方面,它们可能是任意数量的不同多边形,并且每个多边形都可能是空心的。

      此外,找到一个区域的轮廓(即使没有孔)对于绘制表面几乎没有帮助。您的 GPU 最终将需要三角形,这意味着您需要将多边形分解为矩形。

      找到一组空心正方形(即覆盖所有正方形的最小矩形集)的最佳分解是一个经过充分研究的 NP 完全问题,没有已知的解决方案。

      存在一些算法可以找到这种无孔形状的最佳分解,但它们非常复杂。

      贪心算法更容易实现,并且通常会产生可接受的结果。

      所以我会对你的矩阵进行贪婪搜索,收集矩形直到所有“1”值都被访问过。将这些矩形转换为坐标应该很容易,因为您确切地知道左上角和右下角的位置。

      贪婪扫描将如下所示:

      while your matrix is not empty
          move to first "1" value. This is the rectangle top left corner
          from this corner, extend the rectangle in x and y to maximize its surface
          store the rectangle in a list and clear all corresponding "1" values
      

      【讨论】:

        猜你喜欢
        • 2011-11-14
        • 2015-10-04
        • 2021-11-15
        • 2012-12-05
        • 2012-07-18
        • 1970-01-01
        • 2014-05-23
        • 2012-12-17
        • 2011-04-14
        相关资源
        最近更新 更多