11

I have a matrix (0 means nothing, 1 means terrain) that represents a level in my game. The matrix corresponds to a grid that my screen is broken up into, and indicates where my terrain goes.

My terrain is actually composed of 4 points in the corners of each block within the grid. When you have multiple blocks that are connected, I use a merge-cell algorithm that removes the duplicate points and any interior points. The result is that I end up with a list of points representing only the outer edges of the polygon.

To draw this polygon, I need the points to be in some sort of order (either clockwise or counter-clockwise) such that each point is followed by it's neighboring point. Obviously the first and last points need to be neighbors. Since this is all in a grid, I know the exact distance between neighboring points.

The problem is that I am having trouble coming up with an algorithm that allows me to "walk" around the edge of the polygon while putting the points in order. I believe there should be a way to utilize the fact that I have the matrix representing the geometry, meaning there is only 1 possible way to draw the polygon (even if it is concave).

I have tried several approaches using greedy-type algorithms, but can't seem to find a way to know, in every case, which direction I want to travel in. Given that any particular point can have up to 3 neighbors (the fourth isn't included because it is the "starting" point, meaning that I have already sorted it) I need a way of knowing which way to move.

Update

Another approach that I have been trying is to sort the points by their X (with tiebreaker of Y) which gives me the topmost/leftmost edge. It also guarantees that I am starting on an outer edge. However, I'm still struggling to find an algorithm that guarantees that I stay on the outside without crossing over.

Here is an example matrix:

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

Which corresponds to this (black dots represent my points): Polygon

Community
  • 1
  • 1
Kinru
  • 389
  • 1
  • 6
  • 22
  • your trying to create a mesh based on this matrix? – Anthony Raimondo Feb 08 '15 at 01:24
  • @AnthonyRaimondo Sort of. For the time being I am simply using actionscript/flash so all it takes is having the points in the correct order. However, if I wanted to make a mesh I would still need them in order so it doesn't really matter. – Kinru Feb 08 '15 at 01:28
  • is it possible to first create all the triangles so you have a reference and then merge all the points? What order is action-script/flash expecting the points in? Triangle strips? – Anthony Raimondo Feb 08 '15 at 01:38
  • It's not possible (as far as I can tell) because this is a level editor and blocks can be created/destroyed in any order. Meaning you could start on one side of the screen and then randomly connect blocks until you have some weird concave shape. Since blocks share points, it's impossible to know what order they were created in. I think there must be a simple way to utilize my matrix to get an order though. For flash, I don't know what the underlying drawing code is (they don't give access or documentation for it). You just begin a fill and then connect the points in order. – Kinru Feb 08 '15 at 01:44
  • The best idea I had involved a flood-fill algorithm. [Here](http://i.imgur.com/277ri8q.png). Basically you go through the matrix. Once you find a start point (ideally you'll go top-down/left-right to optimize the flood fill a bit), then you flood from that point. You then insert the new points iff they are perimeter points and have not already been added. The `+2` offset refers to the insertion of elements. Note that the insertion index (not the offset) also depends on the way you moved from the last square. Sorry if this is hard to explain, just strapped for time right now or I'd write it up. – Obicere Feb 08 '15 at 01:50
  • @Obicere I actually use flood fill for merging the cells, but I don't really follow how that can be used to sort the points? Not sure I'm understanding your diagram. – Kinru Feb 08 '15 at 01:52
  • @Kinru you have an initial array with the first detected block. Say its `[p0, p1, p2, p3]`. Then, if you detect a square to the right, you'd add the new points `[n0, n1]` representing the new block (2 points already added) like: `[p0, p1, n0, n1, p2, p3]`. However, if the block was to the left, it would be like: `[n0, n1, p0, p1, p2, p3]`, etc. The placement of `n0` and `n1` depend on the movement direction for the flood fill. – Obicere Feb 08 '15 at 01:55
  • @Obicere I understand that, but I don't really see how that allows you to know which direction you are going in. What stops you from going down vs. to the right? If you have a 5x5 block of terrain, the flood fill might take some random route through the middle which would mess up the order. – Kinru Feb 08 '15 at 01:57
  • @Kinru you would iterate over every possible direction in a defined order. It would be a recursive definition. So say you always go `[left, up, right, down]`, then the path can easily be calculated as well as the place to insert the new points if necessary. – Obicere Feb 08 '15 at 01:59
  • @Obicere Would you be able to write this up as an answer with a full example/pseudo-code of what you mean? – Kinru Feb 08 '15 at 22:49
  • @Kinru look here http://stackoverflow.com/a/21884021/2521214 you can ignore the map computation because you already have it as your level matrix so just use the outline polygon computation from it – Spektre Feb 10 '15 at 08:27
  • Hi I think that is duplicate http://stackoverflow.com/questions/4956919/convert-simplified-discrete-area-to-borders-polygon – dfens Feb 10 '15 at 15:46
  • @dfens I don't think that algorithm will handle polygons with holes in them? – Kinru Feb 10 '15 at 17:44
  • Why don't you try to write the algorithm with TDD and very small steps? – inf3rno Feb 16 '15 at 09:09

4 Answers4

3

First of all please consider that for a general matrix the output can be composed of more than one closed loop; for example boundaries of the matrix

multi-boundary example

form three distinct loops, one of them placed inside another.

To extract these loops the first step is to build a map of all "walls": you have a vertical wall each time the content of one cell is different from the next cell on the same row; you have instead an horizontal wall when the content is different from the same cell in the next row.

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)]

In the example above I'm using two bits: 0x01 to mark horizontal walls and 0x02 to mark vertical walls. For a given (r, c) cell the walls are the right and bottom wall of the cell.

For simplicity I'm also assuming that the interesting areas are not touching the limits of the matrix; this can be solved by either adding extra rows and cols of zeros or by wrapping matrix access in a function that returns 0 for out-of-matrix virtual elements.

To build the list of boundaries you need to simply start from any point on a wall and move following walls, removing the walls from the map as you process them. When you cannot move any more a cycle has been completed (you're guaranteed to complete cycles because in a graph built in this way from a matrix of inside/outside flags the degree is guaranteed to be even in all vertices).

Filling all those cycles simultaneously using odd-even filling rules is also guaranteed to reproduce the original matrix.

In the code following I'm using r and c as row/col index and i and j instead to represent points on the boundary... for example for cell (r=3, c=2) the schema is:

coordinates

where the red wall corresponds to bit 0x02 and the green wall to bit 0x01. The walls matrix has one row and one column less than the original data matrix because it's assumed that no walls can be present on last row or column.

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)

Basically the code starts from a point on a boundary and the checks if it can move on a wall going up, down, left or right. When it cannot move any more a cycle has been completed and can be added to the final result.

The complexity of the algorithm is O(rows*cols), i.e. it's proportional to the input size and it's optimal (in big-O sense) because you cannot compute the result without at least reading the input. This is easy to see because the body of the while cannot be entered more times than the total number of walls in the map (at each iteration a wall is removed).

Edit

The algorithm can be modified to generate as output only simple cycles (i.e. paths in which each vertex is visited only once).

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

This is implemented by adding to the output a separate cycle once the same vertex is met twice in the processing (the index table stores for a given i,j point the 0-based index in the current cycle being built).

6502
  • 112,025
  • 15
  • 165
  • 265
  • The only problem I see with this code is that it regards diagonal blocks as being part of the same overall block. Meaning if there were only two "1s" in the matrix that were diagonal, your algorithm returns a cycle that envelops both. I need it to be two different cycles in that case. Is there a quick alteration that could be made to do that? – Kinru Feb 15 '15 at 18:47
  • @Kinru: See edit. The output of this modified version is composed of non self-intersecting cycles. Note however that still you can have a cycle contained in another cycle... (holes) – 6502 Feb 15 '15 at 22:43
  • @Kinru why does it need to be two different cycles? That wasn't part of the original problem definition. – Mark Ransom Feb 16 '15 at 04:25
  • @MarkRansom That was a mistake in my original problem, I should have made that more clear. – Kinru Feb 16 '15 at 16:24
  • @Kinru I'd still like to know why the constraint exists, since it complicates the problem. – Mark Ransom Feb 16 '15 at 16:29
  • @MarkRansom Due to the type of "terrain" that these points actually make up, they can only ever have two neighbors in my current implementation. This is because there is a spring-like force acting between them and if you allow a point that is a corner to intersect with what I am calling multiple blocks, then it greatly complicates my physics. It doesn't really have anything to do with drawing the polygon. – Kinru Feb 16 '15 at 16:34
0

I guess there are different ways to do this, I suppose there is quite simple one for case when diagonal connected cells counted as different contours:

You just need too keep cell and corner direction. For example you started from upper right corner of some earth cell (it supposed that either upper or right cell, or both are nothing if it is bourder) and want to go clockwise.

If cell to the right is earth, than you change current cell to it and change corner to upper left (it is the same point). Then you go to next iteration.

moving right if we have earth to the right

In other case, if you started from upper right corner of some earth cell and want to go clockwise. If cell to the right is NOT earth than you don't change current cell and change corner to bottom right, (it's next point)

rotate clockwise if there is no earth to the right

So you also have symmetrical situation for other three possible corners, and you can go to next iteration until returning to start point.

behavior for other 6 starting conditions

So here is pseudo-code I wrote, it uses the same indexing as picture uses, and supposes that all cells along borders are free, otherwise you will need to check if index id not out of range.

I will also need additional array with almost the same dimensions as matrix to mark processed contours, it need to be 1 cell wider than matrix cause I'm going to mark vertical lines, and each vertical line is supposed to have coordinates of cell to the right of it. Note that there are only 2 cases midst 8 dwscribed above when you need to mark vertical line.

    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;
      }
    }

Also you may need to know which contour is inside which, and you mat need a stack to keep what contours you are inside while you are scanning, so every time you are crossing the existing contour you need to add it to the stack or remove if it is already at the top of the stack. It will cause the next changes in code:

  ...
  //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)
       {
           ...
Lurr
  • 861
  • 4
  • 11
0

This seems like it would work to me:

For every filled square, check which of its neighbours are filled. For those that aren't, add the appropriate edges to a list of edges. Generate those edges as directed, either clockwise or anticlockwise as you prefer.

To construct a full path, start by pulling any edge from the set and add it to the path. It has an order so look at the second vertex. Find the edge in the set with the first vertex that is equal to that second vertex. Pull that edge from the set and add it to the path. Continue until the path is closed.

Repeat to generate a list of paths. A simple polygon should end up as one path. A complex polygon — one with holes in the middle in this case — will be several.

Tommy
  • 99,986
  • 12
  • 185
  • 204
0

If your matrix can contain random patterns, the answer is far more complicated than it seems.

For one thing they may be an arbitrary number of distinct polygons, and each of them might be hollow.

Besides, finding the contour of a region (even with no holes) is of little help for drawing the surface. Your GPU will eventually need triangles, which means you will need to decompose your polygons into rectangles.

Finding an optimal decomposition of a hollow bunch of squares (i.e. the smallest set of rectangles that will cover them all) is a well studied NP-complete problem with no known solution.

There exist algorithms to find an optimal decomposition of such shapes with no holes, but they are very complex.

A greedy algorithm is much easier to implement and usually yields acceptable results.

So I would do a greedy search on your matrix, collecting rectangles until all "1" values have been visited. Turning these rectangles into coordinates should be easy enough, since you know exactly where the top left and bottom right corners are.

The greedy scan will look like:

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
kuroi neko
  • 8,479
  • 1
  • 19
  • 43