Сортировка точек полигона для рисования
У меня есть матрица (0 ничего не значит, 1 означает местность), которая представляет собой уровень в моей игре. Матрица соответствует сетке, в которой мой экран разбит, и указывает, где идет моя местность.
Мой ландшафт фактически состоит из 4 точек в углах каждого блока в сетке. Когда у вас есть несколько блоков, которые связаны, я использую алгоритм слияния, который удаляет дубликаты точек и любые внутренние точки. В результате я получаю список точек, представляющих только внешние ребра многоугольника.
Чтобы нарисовать этот многоугольник, мне нужно, чтобы точки находились в некотором порядке (по часовой или против часовой стрелки), так что за каждой точкой следует соседняя точка. Очевидно, что первый и последний пункты должны быть соседями. Поскольку это все в сетке, я знаю точное расстояние между соседними точками.
Проблема заключается в том, что у меня возникают проблемы с алгоритмом, который позволяет мне "ходить" по краю многоугольника, вставляя точки в порядок. Я считаю, что должен быть способ использовать тот факт, что у меня есть матрица, представляющая геометрию, то есть есть только один возможный способ рисования многоугольника (даже если он вогнутый).
Я пробовал несколько подходов, используя алгоритмы жадного типа, но не могу найти способ узнать, во всяком случае, в каком направлении я хочу путешествовать. Учитывая, что в любой конкретной точке может быть до трех соседей ( четвертый не включен, потому что это "начальная" точка, а это значит, что я уже ее отсортировал). Мне нужен способ узнать, какой путь двигаться.
Update
Другой подход, который я пытался, - это отсортировать точки по их 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
Что соответствует этому (черные точки представляют мои точки):
![Polygon]()
Ответы
Ответ 1
Прежде всего, рассмотрим, что для общей матрицы вывод может состоять из более чем одного замкнутого цикла; например, границы матрицы
![multi-boundary example]()
образуют три различные петли, один из которых помещен внутри другого.
Чтобы извлечь эти петли, первым шагом будет построение карты всех "стен": у вас есть вертикальная стена каждый раз, когда содержимое одной ячейки отличается от следующей ячейки в той же строке; у вас вместо этого горизонтальная стена, когда содержимое отличается от той же ячейки в следующей строке.
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 для виртуальных элементов вне матрицы.
Чтобы создать список границ, вам нужно просто начать с любой точки на стене и перемещать стены, удаляя стены с карты при их обработке. Когда вы больше не можете двигаться, цикл завершен (вы гарантированно завершаете циклы, потому что в графе, построенном таким образом, из матрицы внутренних/внешних флагов степень гарантирована даже во всех вершинах).
Заполнение всех этих циклов одновременно с использованием правил нечетного заполнения также гарантированно воспроизводит исходную матрицу.
В следующем коде я использую r
и c
как индекс row/col и i
и j
вместо этого, чтобы представлять точки на границе... например, для ячейки (r=3, c=2)
схема является:
![coordinates]()
где красная стена соответствует бит 0x02
, а зеленая стена - бит 0x01
. Матрица walls
имеет одну строку и один столбец меньше исходной матрицы данных, поскольку она предполагала, что никакие стены не могут присутствовать в последней строке или столбце.
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 (строки * cols), т.е. пропорциональна размеру входа и оптимальна (в смысле большого О), потому что вы не можете вычислить результат без по крайней мере чтения ввода. Это легко увидеть, потому что тело в то время не может быть введено больше времени, чем общее количество стен на карте (при каждой итерации стена удаляется).
Изменить
Алгоритм может быть изменен, чтобы генерировать в качестве выходных только простые циклы (т.е. пути, в которых каждая вершина посещается только один раз).
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 в текущем цикле).
Ответ 2
Я предполагаю, что есть разные способы сделать это, я полагаю, что существует довольно простой случай, когда диагональные связанные ячейки считаются различными контурами:
Вам просто нужно держать ячейку и угловое направление. Например, вы начали с верхнего правого угла какой-либо земной ячейки (он предположил, что либо верхняя, либо правая ячейка, или оба они ничего, если это растение) и хотите идти по часовой стрелке.
Если ячейка справа - это земля, то вы меняете текущую ячейку на нее и меняете угол в верхнем левом углу (это одна и та же точка). Затем вы переходите к следующей итерации.
![moving right if we have earth to the right]()
В другом случае, если вы начали с верхнего правого угла какой-либо земной ячейки и хотите идти по часовой стрелке. Если ячейка справа не является землей, чем вы не меняете текущую ячейку и меняете угол внизу справа, (это следующая точка)
![rotate clockwise if there is no earth to the right]()
Таким образом, у вас также есть симметричная ситуация для других трех возможных углов, и вы можете перейти к следующей итерации, пока не вернетесь к начальной точке.
![behavior for other 6 starting conditions]()
Итак, вот псевдокод, который я написал, он использует ту же самую индексацию, что и использование изображений, и предполагает, что все ячейки вдоль границ свободны, иначе вам нужно будет проверить, не является ли индекс индекса вне диапазона.
Мне также понадобится дополнительный массив с почти теми же размерами, что и матрица для маркировки обработанных контуров, он должен быть на 1 ячейку шире, чем у матрицы, потому что я собираюсь отметить вертикальные линии, и каждая вертикальная линия должна иметь координаты ячейку справа от нее. Обратите внимание, что есть только 2 случая midst 8, описанных выше, когда вам нужно отметить вертикальную линию.
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)
{
...
Ответ 3
Кажется, это сработало бы со мной:
Для каждого заполненного квадрата проверьте, какие из его соседей заполнены. Для тех, которые не являются, добавьте соответствующие ребра в список ребер. Создайте эти ребра по направлению, по желанию, по часовой стрелке или против часовой стрелки.
Чтобы построить полный путь, начните с вытягивания любого ребра из набора и добавьте его в путь. Он имеет порядок, чтобы посмотреть на вторую вершину. Найдите ребро в множестве с первой вершиной, равной этой второй вершине. Вытяните это ребро из набора и добавьте его в путь. Продолжайте, пока путь не будет закрыт.
Повторите, чтобы создать список путей. Простой полигон должен быть одним из путей. В этом случае сложный многоугольник - один с отверстиями в середине - будет несколько.
Ответ 4
Если ваша матрица может содержать случайные шаблоны, ответ намного сложнее, чем кажется.
С одной стороны, они могут быть произвольным числом различных полигонов, и каждый из них может быть полым.
Кроме того, поиск контура области (даже без отверстий) мало помогает при рисовании поверхности. Ваш графический процессор в конечном итоге будет нуждаться в треугольниках, что означает, что вам нужно разложить ваши полигоны на прямоугольники.
Нахождение оптимального разложения полого пучка квадратов (т.е. наименьшего множества прямоугольников, которые будут покрывать их все) является хорошо изученной 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