Процедура рисования линии по алгоритму Брезенхэма

Subliminal Extacy #03

Процедура рисования линии по алгоритму Брезенхэма

Автор: Mikezt
Эта статья посвящается baze/3sc.

Одной из самых базовых графических процедур является рисование линии. Когда вы пробуете написать такую процедуру сами, задача может показаться довольно сложной. Не переживайте — вот один из способов её решения.

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

Отрезок задаётся двумя точками $(x_1, y_1)$ и $(x_2, y_2)$ и может быть описан уравнением:

$$y = k \cdot x + q$$

где $k$ — угловой коэффициент (направление), а $q$ — смещение по оси $Y$.

Коэффициент $k$ определяет наклон прямой и делит координатную сетку на восемь октантов (см. рис. 1). Рассмотрим случай, когда $k$ положителен и меньше 1 ($0 < k < 1$). Предположим, что точка $(x[i], y[i])$ уже нарисована. Для каждого следующего пикселя нам нужно вычислить отклонение от идеального хода отрезка (рис. 2).

Отклонения для двух возможных точек равны:

$$D_1 = y - y[i] = k \cdot (x[i] + 1) + q - y[i]$$
$$D_2 = y[i] + 1 - y = y[i] + 1 - k \cdot (x[i] + 1) - q$$

Разность этих двух величин равна:

$$D = D_1 - D_2 = 2 \cdot k \cdot (x[i] + 1) - 2 \cdot y[i] + 2 \cdot q - 1$$

По знаку этой разности мы и выбираем нужный пиксель. Очевидно, что решающим фактором является именно знак: отрицательное значение выберет точку $(x[i] + 1, y[i] + 1)$, а положительное — $(x[i] + 1, y[i])$. Недостаток заключается в том, что значения остаются вещественными. Мы можем избавиться от этого, умножив выражение разности на $(x_2 - x_1)$, получив так называемую решающую переменную (decision):

$$d[i] = D \cdot (x_2 - x_1) = 2 \cdot (y_2 - y_1) \cdot (x[i] + 1) - 2 \cdot y[i] \cdot (x_2 - x_1) + (2 \cdot q - 1) \cdot (x_2 - x_1)$$

и после небольшого преобразования:

$$d[i] = 2 \cdot (y_2 - y_1) \cdot x[i] - 2 \cdot y[i] \cdot (x_2 - x_1) + 2 \cdot (y_2 - y_1) + (2 \cdot q - 1) \cdot (x_2 - x_1)$$

Выражение $2 \cdot (y_2 - y_1) + (2 \cdot q - 1) \cdot (x_2 - x_1)$ является константой, обозначим её как $c$. Получаем:

$$d[i] = 2 \cdot (y_2 - y_1) \cdot x[i] - 2 \cdot y[i] \cdot (x_2 - x_1) + c$$

Теперь это целочисленное значение, но формула всё ещё довольно громоздкая. Если мы запишем решающее значение для следующей точки, то сможем получить рекуррентное соотношение. Решающее значение для следующего шага:

$$d[i+1] = 2 \cdot (y_2 - y_1) \cdot (x[i] + 1) - 2 \cdot y[i+1] \cdot (x_2 - x_1) + c$$

Вычтя одно из другого, мы выразим значение $d$ в рекурсивном виде:

$$d[i+1] = d[i] + 2 \cdot (y_2 - y_1) - 2 \cdot (x_2 - x_1) \cdot (y[i+1] - y[i])$$

Выражение $(y[i+1] - y[i])$ может принимать только значения 0 или 1, поэтому правила вычисления следующего значения упрощаются:

  • если $d[i] < 0 \implies d[i+1] = d[i] + 2 \cdot (y_2 - y_1)$
  • если $d[i] \ge 0 \implies d[i+1] = d[i] + 2 \cdot (y_2 - y_1) - 2 \cdot (x_2 - x_1)$

Последнее, что осталось узнать — начальное значение на первом шаге:

$$d[1] = 2 \cdot (y_2 - y_1) - (x_2 - x_1)$$

Таким образом, всё, что вам нужно делать в цикле: проверять знак решающей переменной; если он отрицательный — сдвигать координату $Y$, вычислять новое значение ошибки для следующего шага и проверять, не достигли ли вы конечной точки. Если нет — повторять цикл. Для направлений, отличных от $0 < k < 1$, формулы необходимо скорректировать путём аналогичного вывода, либо воспользовавшись иллюстрациями. Я использовал их для определения нужного направления в начальной точке своей процедуры.

Достаточно просто заменить выражение $(x_2 - x_1)$ на первое выражение из последнего сравнения, а $(y_2 - y_1)$ — на второе (думаю, идея понятна :-) ...

Несколько советов по оптимизации процедуры:

  • Проверяйте условие $x_2 > x_1$ (или любое другое, выбирая соответствующую ветку алгоритма): если оно истинно, начальную и конечную точки не нужно менять местами.
  • Уравнения решающей переменной можно разделить на 4 (два сдвига вправо) — тогда ошибку можно хранить в 8-битном регистре без потери точности!

Фух, на этом всё. Моя процедура доступна (надеюсь) на сайте проекта HighTech (http://hightech.da.ru). Надеюсь, суть написанного понятна. Если есть вопросы — пишите.

Пока!

mike/zt/ctl
jurica@sknet.sk

Поделитесь вашим мнением о статье