Элегантный/чистый (специальный случай) Прямолинейный алгоритм обхода сетки?

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

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

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

удалена мертвая ссылка 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]

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

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

(Я пересматриваю алгоритмы Bresenham и Bresenham, из-за неправильного понимания).


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

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

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


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

Ответы

Ответ 1

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

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

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

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

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

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

Обновление 2: еще одна реализация.

Ответ 2

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

Существует также еще один документ здесь, имея дело с чем - то подобным.

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

Ответ 3

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

  • Учитывая две точки A и B, определите точки пересечения линии (A, B) с каждой вертикальной линией вашей сетки, лежащей внутри этого интервала.
  • Вставьте две специальные точки пересечения внутри ячеек, содержащих A и B, в начале/конце списка из точки 1.
  • Интерпретируйте каждые две секвенциальные точки пересечения как минимальные и максимальные векторы прямоугольника, выровненного по оси, и отметьте все ячейки сетки, которые лежат внутри этого прямоугольника (это очень просто (пересечение двух выровненных по прямой оси прямоугольников), особенно учитывая, что прямоугольник имеет ширину 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 t правильно отображаться. Поэтому следует использовать тип данных дробей, который должен иметь возможность корректно обрабатывать пограничные случаи. спасибо brainjam!:)

Ответ 4

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

Это не является общим, так как предполагает, что плитки центрированы в целочисленных координатах, а разделительные линии появляются в каждом целом числе плюс одна половина (0,5, 1,5, 2,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