Элегантный / Чистый (особый случай) Алгоритм прямого обхода сетки? - PullRequest
21 голосов
/ 13 июля 2010

Я отряхиваю свой старый проект. Одна из вещей, которую он должен был сделать, - с учетом декартовой системы координат и двух квадратов на сетке найти список всех квадратов, через которые пройдет линия, соединяющая центр этих двух квадратов.

Особый случай здесь заключается в том, что все начальные и конечные точки ограничены точным центром квадратов / клеток.

Вот несколько примеров - с парами начальных и конечных точек выборки. Затененные квадраты - это те, которые должны быть возвращены соответствующим вызовом функции

удалена мертвая ссылка ImageShack - пример

Начальная и конечная точки обозначаются квадратами, в которых они находятся. На изображении выше, предполагая, что нижний левый угол равен [1,1], линия в правом нижнем углу будет обозначена как [6,2] - [9,5] .

То есть от (центра) квадрата в шестом столбце слева, во втором ряду снизу до (центра) квадрата на девятый столбец слева, в пятом ряду снизу

Что на самом деле не кажется таким сложным. Однако я каким-то образом нашел в сети какой-то сложный алгоритм и реализовал его.

Я помню, что это было очень, очень быстро. Как, например, оптимизированный для сотен или тысяч раз за кадры.

По сути, он прыгнул от границы к границе квадратов вдоль линии (точек, где линия пересекает линии сетки). Она знала, где находится следующая точка пересечения, видя, какая точка пересечения была ближе - горизонтальная или вертикальная, - и перемещалась к этой следующей.

Что-то вроде концепции, но фактическая реализация оказалась довольно не очень привлекательной, и я боюсь, что уровень оптимизации может быть слишком высоким для того, что мне практически нужно (я вызов этого алгоритма обхода может быть пять или шесть раз в минуту).

Существует ли простой, понятный, прозрачный алгоритм обхода сетки по прямой линии?

В программном отношении:

def traverse(start_point,end_point)
  # returns a list of all squares that this line would pass through
end

, где данные координаты идентифицируют квадратов сами.

Некоторые примеры:

traverse([0,0],[0,4])
# => [0,0], [0,1], [0,2], [0,3], [0,4]
traverse([0,0],[3,2])
# => [0,0], [0,1], [1,1], [2,1], [2,2], [3,2]
traverse([0,0],[3,3])
# => [0,0], [1,1], [2,2], [3,3]

Обратите внимание, что линии, проходящие непосредственно через углы, не должны содержать квадратов на "крыле" линии.

(Здесь могут работать добрые старины Брезенхэма, но это немного отстает от того, что я хочу. Насколько я знаю, для того, чтобы использовать его, мне в основном пришлось бы применить его к линии, а затем сканировать каждый квадрат на сетке на наличие истинного или ложного. Невозможно - или, по крайней мере, не элегантно - для больших сеток)

(я пересматриваю алгоритмы Брезенхэма и Брезенхема на основе моего неправильного понимания)


Для пояснения, одно из возможных применений этого было бы, если бы я хранил все свои объекты в игре внутри зон (сетка), и у меня есть луч, и я хочу увидеть, к каким объектам этот луч касается. Используя этот алгоритм, я мог проверить луч только для объектов, которые находятся в пределах указанных зон, а не для каждого объекта на карте.

Фактическое использование в моем приложении состоит в том, что каждая плитка имеет связанный с ней эффект, и объект проходит через данный отрезок линии каждый ход. На каждом шагу необходимо проверять, через какие квадраты прошел объект и, следовательно, какие эффекты применить к объекту.

Обратите внимание, что на данный момент текущая реализация, которую я имею, работает. Этот вопрос в основном для любопытства. Должен быть более простой способ ... как-то ... для такой простой проблемы.


Что я ищу именно? Что-то концептуально / аккуратно и чисто. Кроме того, я понял, что из-за того, что я точно указываю, все начальные и конечные точки всегда будут в центре квадратов / ячеек; так что, возможно, что-то, что использует в своих интересах, было бы также опрятно.

Ответы [ 4 ]

17 голосов
/ 13 июля 2010

То, что вы хотите, является частным случаем суперобложки, в которой все пиксели пересекаются геометрическим объектом.Объект может быть линией или треугольником, и есть обобщения для более высоких измерений.

В любом случае, здесь есть одна реализация для отрезков линии .На этой странице также сравнивается суперобложка с результатом алгоритма Брезенхема - они разные. alt text http://eugen.dedu.free.fr/projects/bresenham/diff.png

Я не знаю, считаете ли вы алгоритм там элегантным / чистым, но он, безусловно, кажется достаточно простым для адаптации кода и перехода к другим частям вашего проекта.

Кстати, ваш вопрос подразумевает, что алгоритм Брезенхэма неэффективен для больших сеток.Это не правда - он генерирует только пиксели в строке.Вам не нужно делать тест на истинность / ложность для каждого пикселя сетки.

Обновление 1: Я заметил, что на рисунке есть два «лишних» синих квадрата, которые ясчитаю, что линия не проходит.Один из них примыкает к «h» в «Этот алгоритм».Я не знаю, отражает ли это ошибку в алгоритме или диаграмме (но см. Комментарий @ kikito ниже).

В общем, «тяжелые» случаи - это когда линия проходит точно через точку сетки.,Я предполагаю, что если вы используете плавающую точку, ошибка с плавающей точкой может привести к путанице в этих случаях.Другими словами, алгоритмы, вероятно, должны придерживаться целочисленной арифметики.

Обновление 2: Другая реализация .

5 голосов
/ 13 июля 2010

Бумагу на эту тему можно найти здесь . Это касается трассировки лучей, но в любом случае это имеет отношение к тому, что вам нужно, и я предполагаю, что вы сможете немного поработать с ним.

Здесь есть еще одна статья , посвященная чему-то похожему.

Обе эти статьи связаны в часть 4 из Jakko Bikker превосходные учебники по трассировке лучей (которые также включают его исходный код, так что вы можете просмотреть / изучить его реализации).

4 голосов
/ 13 июля 2010

Существует очень простой алгоритм для вашей задачи, который работает за линейное время:

  1. Учитывая две точки A и B, определите точки пересечения линии (A, B) с каждой вертикальной линией вашей сетки, которая находится в этом интервале.
  2. Вставьте две специальные точки пересечения внутри ячеек, содержащих A и B, в начале / конце списка от точки 1.
  3. Интерпретировать каждые две последовательные точки пересечения как минимальный и максимальный векторы прямоугольника, выровненного по оси, и отметить все ячейки сетки, которые находятся внутри этого прямоугольника (это очень легко (пересечение канав, выровненных по двум осям), особенно учитывая, что этот прямоугольник имеет ширину 1 и поэтому занимает только 1 столбец Вашей сетки)
Пример:
+------+------+------+------+
|      |      |      |      |
|      |      | B    *      |
|      |      |/     |      |
+------+------*------+------+
|      |     /|      |      |
|      |    / |      |      |
|      |   /  |      |      |
+------+--/---+------+------+
|      | /    |      |      |
|      |/     |      |      |
|      *      |      |      |
+-----/+------+------+------+
|    / |      |      |      |
*   A  |      |      |      |
|      |      |      |      |
+------+------+------+------+ 

«A» и «B» - это точки, оканчивающиеся на линию, обозначенную «/». «*» отмечает точки пересечения линии с сеткой. Две специальные точки пересечения необходимы для маркировки ячеек, содержащих A и B, и для обработки особых случаев, таких как A.x == B.x

Оптимизированной реализации требуется & Theta; (| B.x - A.x | + | B.y - A.y |) время для строки (A, B). Кроме того, можно написать этот алгоритм для определения точек пересечения с горизонтальными линиями сетки, если это проще для разработчика.

Обновление: пограничные случаи

Как ясно указывает в своем ответе брейнджем, тяжелые случаи - это те, когда линия проходит точно через точку сетки. Предположим, что такой случай имеет место, и арифметические операции с плавающей точкой правильно возвращают точку пересечения с интегральными координатами. В этом случае предлагаемый алгоритм помечает только правильные ячейки (как указано на изображении, предоставленном OP).

Однако ошибки с плавающей запятой рано или поздно возникнут и приведут к неверным результатам. На мой взгляд, даже использования double недостаточно, и нужно переключиться на тип номера Decimal. Оптимизированная реализация будет выполнять & Theta; (| max.x - min.x |) дополнения для этого типа данных, каждый из которых занимает & Theta; (log max.y) время. Это означает, что в худшем случае (строка ((0, 0), (N, N)) с огромным N (> 10 6 ) алгоритм ухудшается до времени выполнения O (N log N) в худшем случае Даже переключение между обнаружением пересечения вертикальной / горизонтальной линии сетки в зависимости от наклона линии (A, B) не помогает в этом наихудшем случае, но, безусловно, помогает в среднем случае - я бы рассмотрел только реализацию такого переключения если профилировщик возвращает операции Decimal в качестве горлышка бутылки.

Наконец, я могу представить, что некоторые умные люди могли бы предложить решение O (N), которое правильно обрабатывает эти пограничные случаи. Все ваши предложения приветствуются!

Исправление

brainjam указал, что десятичный тип данных не удовлетворяет, даже если он может представлять числа с плавающей запятой произвольной точности, поскольку, например, 1 / 3 не может быть представлены правильно. Поэтому следует использовать тип данных дроби, который должен уметь правильно обрабатывать граничные случаи. Thx Brainjam! :)

0 голосов
/ 09 июля 2019

Вот простая реализация на Python, использующая numpy.Однако здесь используются только двухмерные векторы и компонентные операции с компонентами, что довольно часто.Результат выглядит довольно элегантно для меня (~ 20 loc без комментариев).

Это не является общим, так как предполагается, что плитки центрированы по целочисленным координатам, в то время как разделительные линии появляются в каждом целом числе плюс одна половина (0,5, 1,52,5 и т. Д.).Это позволяет использовать округление для получения целочисленных координат тайла из мировой координаты (которая на самом деле не нужна в вашем особом случае) и дает магическое число 0.5, чтобы определить, когда мы достигли последней плитки.

Наконец,обратите внимание, что этот алгоритм не имеет дело с точкой, точно пересекающей сетку на пересечении.

import numpy as np

def raytrace(v0, v1):
    # The equation of the ray is v = v0 + t*d
    d = v1 - v0
    inc = np.sign(d)  # Gives the quadrant in which the ray progress

    # Rounding coordinates give us the current tile
    tile = np.array(np.round(v0), dtype=int)
    tiles = [tile]
    v = v0
    endtile = np.array(np.round(v1), dtype=int)

    # Continue as long as we are not in the last tile
    while np.max(np.abs(endtile - v)) > 0.5:
        # L = (Lx, Ly) where Lx is the x coordinate of the next vertical
        # line and Ly the y coordinate of the next horizontal line
        L = tile + 0.5*inc

        # Solve the equation v + d*t == L for t, simultaneously for the next
        # horizontal line and vertical line
        t = (L - v)/d

        if t[0] < t[1]:  # The vertical line is intersected first
            tile[0] += inc[0]
            v += t[0]*d
        else:  # The horizontal line is intersected first
            tile[1] += inc[1]
            v += t[1]*d

        tiles.append(tile)

    return tiles
...