Алгоритм растеризации отрезков Брезенхема на языке Ассемблера

Ассемблер. Небессмысленный, но беспощадный.

Вступление

Итак, что же такое алгоритм Брезенхема и растеризация. Как только машины начали использоваться для работы с графикой, встала проблема рисования изображений: на растровом устройстве вывода, коим является монитор, прямую нарисовать нельзя в виду его дискретности. С тех пор появилось достаточное количество соответствующих алгоритмов, из них самый интуитивный — ЦДА(DDA), цифровой дифференциальный анализатор. Но он использует в работе операции с плавающей запятой, что резко ухудшает производительность. Чуть погодя сотрудником IBM Джеком Е. Брезенхемом был предложен целочисленный алгоритм, выполняющий эту же задачу. Алгоритм решает, какой пиксель находится ближе к прямой, и какой пиксель надо засветить, корректируя себя по ошибке. Как отмечено ранее, алгоритм использует только целочисленные операции, а значит, обеспечивает выигрыш по производительности.

За более подробной информацией на этот счёт есть Википедия(Брезенхема алгоритм), ещё раз Википедия(ЦДА) и книга Д. Роджерса «Алгоритмические основы машинной графики».

Алгоритм в схемах

Схема алгоритма достаточно проста и представлена ниже:

Блок-схема:

Блок-схема алгоритма

По блок-схеме строить ассемблерный код трудновато, посему есть смысл нарисовать линейную схему:

Линейная схема

Последняя схема даёт уже лучшее представление о том, какой код будет на выходе. По крайней мере, она выявляет положения важных опорных точек.

В обеих схемах защита от дурака некорректных действий персонала не показана, но подразумевается, что она стоит перед блоком OP0/INIT.

Реализация

Позволю себе небольшое предисловие перед тем, как будет код.

Расчитывал я это реализовать за 3 часа, но реализация растянулась на 5. В начале работы я долго бился с глупой ошибкой: не зря переменные названы dfx и dfy именно с буквой f(differential) — когда переменная приращения x звалась dx, я долго не мог понять, почему компилятору не нравится её объявление, и только спустя минут 20 вспомнил, что DX — двухбайтный кусок регистра EDX. Не считая дальнейших описок sgx/dfx[sgy/dfy], сильно тормозящих работу ошибок не было, но были трудности с арифметикой. В операциях OP4, OP5, OP7, OP8 мне пришлось вбивать костыли в код, и вручную увеличивать/уменьшать значения зависимо от определённого вручную знака sgx/sgy. В итоге так и не стало понятно, почему выходят сбои при сложении чисел, одно из которых отрицательное. Вероятно, есть более элегантный способ обойти это, нежели использованный. Но долго думать не хотелось.

Начнём с того, что C–шный прототип функции выглядит так:

extern "C" {
    void rasterize_bresenham( long pt1x, long pt1y, long pt2x, long pt2y, void* bufptr, long bufwidth, long bufheight );
};

Здесь:

  • pt1x, pt1y, pt2x, pt2y — абсциссы и ординаты точек начала и конца соответственно;
  • bufptr — указатель на буфер байтов;
  • bufwidth, bufheight — размеры буфера в длину и ширину.

Функция при работе своей записывает в буфер байты 0x1h в те места, где должны быть точки, полагая, что все остальные обнулены.

Итак, вот он — код. Дальнейшие комментарии, я думаю, излишни, в виду того, что ими тут дополнена почти каждая строка.

.model flat, c
.386

.data
    SGN     EQU 80000000h
    NEGSGN  EQU 0FFFFFFFEh
    x       dd 0
    y       dd 0
    xch     db 0
    err     dd 0
    dfx     dd 0
    dfy     dd 0
    sgx     dd 0
    sgy     dd 0

.code
rasterize_bresenham PROC USES EAX EBX ECX EDX, pt1x:dword, pt1y:dword, pt2x:dword, pt2y:dword, buf_ptr:dword, buf_wid:dword, buf_hei:dword

__CHECK:
    ; === проверка координат на выход за пределы буфера
    ; == x1
__CHECK_x1:
    mov     edx, pt1x               ; достаём pt1x в EDX
    cmp     edx, 0                  ; проверяем координату на знак
    jge     __CHECK_x1_1            ; если >= 0, то следующая проверка
    ret
__CHECK_x1_1:
    cmp     edx, buf_wid            ; сравниваем с широной буфера
    jl      __CHECK_x2              ; если меньше, то проверяем дальше
    ret
    ; = x2
__CHECK_x2:
    mov     edx, pt2x               ; достаём pt2x в EDX
    cmp     edx, 0                  ; проверяем координату на знак
    jge     __CHECK_x2_1            ; если >= 0, то следующая проверка
    ret
__CHECK_x2_1:
    cmp     edx, buf_wid            ; сравниваем с широной буфера
    jl      __CHECK_y1              ; если меньше, то проверяем дальше
    ret
    ; == y1
__CHECK_y1:
    mov     edx, pt1y               ; достаём pt1y в EDX
    cmp     edx, 0                  ; проверяем координату на знак
    jge     __CHECK_y1_1            ; если >= 0, то следующая проверка
    ret
__CHECK_y1_1:
    cmp     edx, buf_hei            ; сравниваем с широной буфера
    jl      __CHECK_y2              ; если меньше, то проверяем дальше
    ret
    ; == y2
__CHECK_y2:
    mov     edx, pt2x               ; достаём pt2y в EDX
    cmp     edx, 0                  ; проверяем координату на знак
    jge     __CHECK_y2_1            ; если >= 0, то следующая проверка
    ret
__CHECK_y2_1:
    cmp     edx, buf_hei            ; сравниваем с широной буфера
    jl      __INIT                  ; если меньше, то переходим к инициализации
    ret

__INIT:
    ; === сбрасываем значения переменных
    mov     sgx, 0
    mov     sgy, 0
    mov     xch, 0
    ; === достаём значения из аргументов в переменные
    mov     edx, pt1x               ; достаём pt1x в EDX
    mov     x, edx                  ; отправляем EDX в x
    mov     edx, pt1y               ; достаём pt1y в EDX
    mov     y, edx                  ; отправляем EDX в y
    ; === считаем x2 - x1
    mov     edx, pt2x               ; достаём pt2x в EDX
    sub     edx, x                  ; вычитаем x из EDX
    mov     dfx, edx                ; кладём результат в dfx
    ; === вычисляем dfx и sgx
    cmp     edx, 0                  ; проверяем, что за число в EDX
    jg      __l_sgx_g               ; если > 0, то переходим
    jz      __l_sgx_cont            ; если 0, то действие не требуется
        neg     edx                 ; меняем знак
        mov     dfx, edx            ; возвращаем полученное в dfx
        mov     sgx, NEGSGN         ; отправляем -1 в sgx
        jmp     __l_sgx_cont        ; уходим на продолжение
__l_sgx_g:
        mov     sgx, 1              ; устанавливаем sgx в 1
__l_sgx_cont:
    ; === считаем y2 - y1
    mov     edx, pt2y               ; достаём pt2y в EDX
    sub     edx, y                  ; вычитаем x из EDX
    mov     dfy, edx                ; кладём результат в dfx
    ; === вычисляем dfy и sgy
    cmp     edx, 0                  ; проверяем, что за число в EDX
    jg      __l_sgy_g               ; если > 0, то переходим
    jz      __l_sgy_cont            ; если 0, то действие не требуется
        neg     edx                 ; меняем знак
        mov     dfy, edx            ; возвращаем полученное в dfy
        mov     sgy, NEGSGN         ; отправляем -1 в sgy
        jmp     __l_sgy_cont        ; уходим на продолжение
__l_sgy_g:
        mov     sgy, 1              ; устанавливаем sgx в 1
__l_sgy_cont:
__IF0:
    cmp     edx, dfx                ; сравниваем dfx и EDX(dfy)
    jle     __OP2                   ; если dfy <= dfx, то операции не требуются
        xchg    dfx, edx            ; обмениваем значениями EDX(dfy) и dfx
        mov     dfy, edx            ; восстанавливаем в dfy значение из EDX(dfx)
        mov     xch, 1              ; запоминаем, что был перенос
__OP2:
    ; === вычисляем err [ err = 2*dfy - dfx ]
    mov     err, 0                  ; сбрасываем err в 0
    mov     edx, dfy                ; отправляем в EDX dfy
    shl     edx, 1                  ; сдвигаем EDX влево на 1 бит(умножение на 2)
    add     err, edx                ; прибавляем EDX(dfy*2) к err
    mov     edx, dfx                ; отправляем в EDX dfx
    sub     err, edx                ; вычитаем EDX(dfx) из err    
    ; === подготовка к главному циклу
    mov     ecx, dfx                ; устанавливаем счётчик на dfx
__IF1:
    ; === начало основного цикла
    cmp     ecx, 0                  ; сравниваем ECX и 0
    jle     __OP11                  ; если ECX < 0, то на выход
__MAINLOOP:
__OP3:
        ; == вычисляем требуемое положение в буфере и кладём точку
        mov     ebx, buf_ptr        ; кладём в базовый регистр ссылку на буфер
        mov     eax, buf_wid        ; кладём длину строки буфера в EAX
        mul     y                   ; множим на ординату
        add     ebx, eax            ; складываем значение EBX с EAX
        add     ebx, x              ; прибавляем абсциссу
        mov     byte ptr [ebx], 1   ; кладём 1 в буфер по адресу точки
__IF2:
        ; == циклически вычисляем значение ошибки
        cmp     err, 0              ; сравниваем err с 0
        jl      __IF4               ; если err < 0, то переходим дальше
__IF3:
            cmp     xch, 0          ; проверяем наличие обмена
            jg      __OP5           ; если был обмен, то на __OP5
__OP4:
                cmp     sgy, 0      ; сравниваем знак y с 0
                jg      __OP4_g     ; если > 0, то переход
                jz      __OP6       ; если 0, то ничего не делать
                dec     y           ; иначе (если < 0), уменьшаем на 1
                jmp     __OP6
__OP4_g:
                inc     y           ; увеличиваем на 1
                jmp     __OP6
__OP5:
                cmp     sgx, 0      ; сравниваем знак x с 0
                jg      __OP5_g     ; если > 0, то переход
                jz      __OP6       ; если 0, то ничего не делать
                dec     x           ; иначе (если < 0), уменьшаем на 1
                jmp     __OP6
__OP5_g:
                inc     x           ; увеличиваем на 1
__OP6:
            ; == вычисляем приращение ошибки [ err -= 2 * dfx ]
            mov     edx, dfx        ; забираем dfx в EDX
            shl     edx, 1          ; сдвигаем EDX влево на 1 бит(умножение на 2)
            sub     err, edx        ; вычитаем из err EDX(2*dfx)
            jmp     __IF2           ; возвращаемся обратно
        ; == конец цикла вычисления ошибки
__IF4:
        cmp     xch, 0              ; проверяем наличие обмена
        jnz     __OP8               ; если был обмен, то на __OP8
__OP7:
            cmp     sgx, 0          ; сравниваем знак x с 0
            jg      __OP7_g         ; если > 0, то переход
            jz      __OP9           ; если 0, то ничего не делать
            dec     x               ; иначе (если < 0), уменьшаем на 1
            jmp     __OP9
__OP7_g:
            inc     x               ; увеличиваем на 1
            jmp     __OP9
__OP8:
            cmp     sgy, 0          ; сравниваем знак y с 0
            jg      __OP8_g         ; если > 0, то переход
            jz      __OP9           ; если 0, то ничего не делать
            dec     y               ; иначе (если < 0), уменьшаем на 1
            jmp     __OP9
__OP8_g:
            inc     y               ; увеличиваем на 1
__OP9:
        ; == вычисляем приращение ошибки [ err += 2 * dfy ]
        mov     edx, dfy            ; забираем dfy в EDX
        shl     edx, 1              ; сдвигаем EDX влево на 1 бит(умножение на 2)
        add     err, edx            ; прибавляем к err EDX(2*dfy)
__OP10:
        dec     ecx                 ; ECX--
        jmp     __IF1               ; зацикливаемся
    ; === конец основного цикла
__OP11:
    ret                             ; возврат

rasterize_bresenham ENDP
end

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

Оговорюсь по поводу умножения: сдвиг влево на 1 бит равносилен умножению на 2.

И, наконец, результат работы этого кода:

Результат работы кода

Код написан на MASM и расчитан на подключение к программам, основной язык которых C.

Василий «DarkWolf» Юмашов, 5 X 2011