Asembler - po skrucie mały kursik

Pierwszy program

Tutaj przedstawię najprostszy możliwy program, który tak właściwie nie będzie robić nic, poza zajmowaniem pamięci i miejsca na dysku Uśmiech Nawet taki program trzeba jednak napisać i wcale nie zajmie on 0 bajtów... dzięki niemu poznasz szkielet programu assemblerowego i łatwiej ci będzie zrozumieć kolejne przykłady. Program "nie rób nic" wygląda tak:

.model tiny
.code
 
start:
  mov ah,4ch
  int 21h
 
  .stack 512

end start

Jak widzisz - na początku określany jest model programu. Do wyboru jest kilka, między innymi tiny, small, medium, huge... model tiny charakteryzuje się tym, że cały program wraz z danymi musi zmieścić się w jednym segmencie - czyli (w trybie rzeczywistym) - 64Kb. Nie przejmuj się tym na razie Uśmiech

W drugiej linijce dyrektywa .code rozpoczyna segment kodu. Jest to zarazem segment danych (ponieważ używamy modelu tiny). Możemy więc spokojnie od tego miejsca umieszczać zarówno zmienne jak i kod programu.

Następnie widzimy etykietę start. Pod koniec programu określamy od którego miejsca ma zacząć się wykonanie - to znaczy skąd ma zacząć pobierać rozkazy procesor na początku wykonywania naszego programu - wybrana została właśnie etykieta start.

Następne dwie linijki to rozkazy procesora . Jak widzisz są aż dwa. Pierwszy to mov a drugi - int.

Przygotuj się teraz - bo poznasz pierwsze rozkazy swojego procesora. Rozkaz mov a,b wysyła b do a. Zarówno a jak i b mogą być miejscem w pamięci bądź też rejestrem - z jednym zastrzeżeniem: nie można przesyłać danych wprost z pamięci do pamięci. Drugi z tych rozkazów int x wywołuje przerwanie o numerze x.

Co oznacza więc ten kod? Do rejestru ah wpisywana jest wartość 4ch, a następnie wywoływane jest przerwanie 21h. W zaufaniu powiem ci, że 21h to przerwanie ms-dosu. Służy one do korzystania z funkcji systemu operacyjnego. Numer funkcji podajemy w ah, a funkcja 4ch oznacza zakończenie programu.

Przed zakończeniem programu umieściłem jeszcze dyrektywę .stack. Nie jest ona tu niezbędna. Mówi o tym, ile miejsca zostanie przeznaczone na stos. W tym programie stosu nie używamy, ale w każdym prawdziwym programie stosu używa się bardzo często. 512 bajtów wystarcza dla małych programików, jednak gdy korzystamy np. z rekurencji stos kończy się bardzo szybko - a w assemblerze nie otrzymamy miłego komunikatu w okienku - zostanie po prostu zamazana pamięć - nie wiadomo jakie mogą być tego skutki. Być może zostaną wykonane losowe rozkazy. A czym to grozi chyba nie muszę tłumaczyć Uśmiech

Pozostało jeszcze wytłumaczyć po co są właściwie te dwie instrukcje w programie. Może wystarczyłoby nie umieszczać żadnych instrukcji? Otóż wtedy procesor skoczyłby do etykiety start i zaczął wykonywać to, co by się tam znajdywało... a chyba nikt nie jest w stanie przewidzieć co by się tam mogło znaleźć... W najlepszym przypadku program się po prostu zawiesi.

Jak skompilować ten program? Jeżeli korzystasz z Turbo Assemblera firmy Borland, musisz najpierw skompilować plik źródłowy tasm first.asm a potem to skonsolidować tlink first.obj . W wyniku otrzymasz plik first.exe, który jest gotowy do uruchomienia.


 Wypisywanie tekstu na ekranie

Ten program też jest prosty, ale potrafi już coś zrobić. Wyświetla na ekranie tekst i czeka na naciśnięcie klawisza. Mam nadzieję, że przeanalizowałeś poprzedni artykuł?

.model tiny
.code
 
tekst db 'To jest chyba tekst$'
 
start:
 
mov ax,@data
mov ds,ax
mov ah,09h
mov dx,offset tekst
int 21h
mov ah,01h
int 21h
mov ah,4ch
int 21h
 
.stack 512
 
end start

Jak widać początek programu jest podobny - określamy model i zaczynamy segment kodu. Jednak zaraz potem deklarujemy zmienną. Jest to łancuch znaków, czyli bajtów. Definicja zmiennej ma postać: identyfikator typ wartość . Deklarujemy więc zmienną tekst, składającą się z bajtów i przyjmującą wartość 'To jest chyba tekst$'.

Inne typy zmiennych to dw - word (słowo = 2 bajty), dd - double word (podwójne słowo = 4 bajty), jest jeszcze dq - czyli aż 10 bajtów. Jeżeli chcemy aby zmienna przymowała wartości rzeczywiste a nie całkowite (i tym samym aby działać na niej za pomocą koprocesora) - musi to być co najmniej dd.

Pierwsze dwa rozkazy programu wpisują do ds numer segmentu @data. Pamiętasz chyba, że w modelu tiny segment danych jest jednocześnie segmentem kodu. Tak więc po prostu wpisujemy ten jedyny segment do ds, aby procesor miał dostęp do zmiennych znajdujących się w tym segmencie.

Następne trzy rozkazy to wywołanie funkcji 09h ms-dosu. Ta funkcja służy do wysłania na ekran ciągu znaków zakończonego znakiem dolara '$'. Adres ciągu znaków znajduje się w parze rejestrów ds:dx. Do ds wysłaliśmy już odpowiednią wartość - do dx trzeba jeszcze tylko podesłać offset naszej zmiennej. Funkcja 01h ms-dosu czeka na klawisz.

Tak jak w poprzednim programie - musisz myśleć o tym, by procesor się nie pogubił w odmętach pamięci. Jeżeli nie zakończysz łancucha znaków znakiem '$' to ms-dos będzie wysyłał na ekran wszystko aż do napotkania takiego znaku. Tym razem nie grozi ci np. formatowanie twardego dysku Mrugnięcieale za to będziesz miał mnóstwo śmieci na ekranie.

Instrukcje

Poniżej znajduje się opis kilku instrukcji assemblera, które należało by znać, aby stworzyć coś w tym języku.

mov - przesyłanie danych, może odbywać się pomiędzy pamięcią i rejestrem, bądź też między dwoma rejestrami, podawane są dwa argumenty, zawartość drugiego przesyłana jest do pierwszego
mov ax,[_shitka] - zawartość pamięci z miejsca oznaczonego etykietą _shitka zostanie przesłana do rejestru ax

add, sub - dodawanie lub odejmowanie, od argumentu pierwszego odejmowany jest drugi, wynik zapisywany jest w pierwszym
sub ax,5 - odjęcie od wartości w ax pięciu i zapisanie wyniku w ax

inc, dec - zwiększenie lub zmniejszenie o jeden wartości pierwszego (i jedynego) argumentu
inc ecx - zwiększenie o jeden wartości w ecx

push, pop - położenie lub zdjęcie elementu ze stosu, nie można położyć jednego bajtu, można jedynie słowo lub podwójne słowo
push bx - położenie na stosie zawartości rejestru bx

call, ret - dzięki tym rozkazom możesz tworzyć procedury, call oznacza wywołanie procedury, ret - powrót do programu

TekknoSux:
add ax,bx
dec cx
ret
...
call TekknoSux

rozkaz call każe procesorowi skoczyć do etykiety TekknoSux, wykonane zostaną rozkazy add i dec, a potem procesor powróci do programu, czyli wykona instrukcje znajdujące się za call, adres powrotu z procedury przechowywany jest na stosie

cmp - porównanie dwóch argumentów ze sobą i ustawianie na podstawie tego porównania znaczników procesora, tak naprawdę wykonywane jest odejmowanie, od pierwszego argumentu jest odejmowany drugi, wynik nie jest jednak nigdzie zapisywany, najczęściej po instrukcji porównania znajdują się instrukcje skoku warunkowego
cmp al,bl - porównanie wartości al i bl

jmp - skok bezwarunkowy, czyli goto
jmp BlackSabbathRulez - skok do etykiety BlackSabbathRulez

jz, je - skok jeżeli zero, skok jeżeli równe (czyli jedno i to samo), gdy porównywane instrukcją cmp dane są równe, to wynik odejmowania daje 0, co ustawia znacznik ZF, skok zostanie wykonany tylko, gdy ten znacznik jest ustawiony (może go ustawić także instrukcja arytmetyczna)

jb - skok, gdy mniejszy
ja - skok, gdy większy
jl - skok, gdy mniejszy, ale z uwzględnieniem znaku liczby
jg - skok, gdy większy, liczby ze znakiem
js - skok, gdy wynik jest ujemny (sign=znak)
jc - skok przy przeniesieniu (znacznik CF)
jo - skok przy nadmiarze (znacznik OF)

(więcej instrukcji skoków można znaleźć w dodatkach)

out - wysłanie danej do portu, numer portu można podać bezpośrednio, (liczba 8 bitowa), lub skorzystać z rejestru dx
out dx,al - wysłanie bajtu z al to portu dx

in - odebranie danej z portu, numer portu można podać bezpośrednio, (liczba 8 bitowa), lub skorzystać z rejestru dx
in al,dx - odczytanie bajtu z portu dx i umieszczenie go w al

int - wywołanie przerwania, numer przerwania podawany jest jako jedyny argument
int 21h - wywołanie przerwania 21h

IF, WHILE, FOR

W językach wysokiego poziomu często używamy różnych struktur programowych. W assemblerze nie ma niestety instrukcji IF, gotowych do wykorzystania pętli z warunkami czy instrukcji iteracyjnej. Wszystko musimy sobie stworzyć sami.
Zacznijmy od instrukcji warunkowej. W Pascalu wygląda ona tak:

if warunek then instrukcja1 else instrukcja2;


W assemblerze zajmie to trochę więcej, niż jedną linijkę. Musimy najpierw wykonać instrukcję porównania a potem - w zależności od wyniku - wykonać (lub nie) skok. Instrukcją porownania jest cmp . Natomiast instrukcji skoków warunkowych jest mnóstwo. Oto kilka z nich:
jz - skok, jeżeli równe (zero)
jl - skok, jeżeli mniejsze
jg - skok, jeżeli większe
Jeżeli chcemy sprawdzić, czy zawartość rejestru EAX jest większa od 666, musimy stworzyć nastepujący kod:

cmp eax,666
jg wieksze
...
jmp po_zabawie
wieksze:
...
po_zabawie:

proste i logiczne  Uśmiech
Jak stworzyć pętlę z warunkiem na początku lub końcu? Umieścić na początku pętli etykietę, a na końcu skok do tej etykiety. Gdzieś w środku sprawdzić warunek i jeżeli jest prawdziwy - skoczyć za koniec pętli. Trywialne Uśmiech
Zostaje instrukcja iteracyjna. Jak wykonać coś dokładnie n razy? Assembler oferuje nam instrukcję loop która zmniejsza o jeden zawartość rejestru CX i jeżeli po tym zmniejszeniu nie jest on równy zero - skacze do pewnej etykiety. Nasza pętla będzie więc wyglądała tak:

mov cx,LiczbaPowtorzen
tutaj:
...
loop tutaj

Jednak na procesorach Pentium używanie instrukcji loop jest niewskazane. Ciąg instrukcji:

dec cx
jnz etykieta

wykona się szybciej od loop etykieta . Oczywiście "jeżeli nie zero" Uśmiech

Grafika

Jak obsługuje się grafikę w assemblerze. Jeżeli próbowałeś kiedykolwiek pisać coś związanego z grafiką np. w Pascalu przy użyciu BGI - to możesz być za chwilę naprawdę zaskoczony. Rysowanie jest bowiem dziecinnie proste.
Po pierwsze musisz się nauczyć ustawiać odpowiedni tryb graficzny, a także wracać do trybu tekstowego. Do zmiany trybu pracy ekranu służy funkcja 00h przerwania 10h. Numer trybu podaje się w al, a numer funkcji - standardowo w ah.
Następujący kod:

mov ah,00h
mov al,[tryb]
int 10h

ustawi tryb graficzny, którego numer znajduje się w zmiennej tryb. Zmienna ta oczywiście jest typu db (jeden bajt). Zwróć uwagę, że w przeciwieństwie do biblioteki graficznej firmy Borland - tutaj nie musisz mieć żadnych sterowników i pisać jakichś wymyślnych konstrukcji. Po prostu prosisz BIOS aby ustawił ci tryb graficzny - a on to robi.
Musisz jeszcze wiedzieć, że tryb 320x200x256, czyli standardowy 256-kolorowy tryb karty VGA (notabene niedostępny przez bgi:) ma numer 13h. Natomiast standardowy tryb tekstowy (80x25) ma numer 03h.
Aby wypełnić ekran musisz wiedzieć jak zorganizowana jest pamięć video. W trybie 13h jednemu pixelowi odpowiada po prostu jeden bajt. Cała pamięć ekranu znajduje się w segmencie 0a000h. W pierwszym bajcie tego segmentu znajduje się lewy, górny pixel ekranu. W kolejnym - jego sąsiad z prawej strony. Dalej - reszta sąsiadów z pierwszego wiersza... Następnie znajduje się drugi wiersz, trzeci... i tak aż do końca.
Do zapalenia pixela o wspólrzędnych (x,y) musisz wyliczyć jego adres w pamięci. Wzór jest bardzo prosty Uśmiech320 * y + x . I to wszystko. Wystarczy wysłać tam wartość koloru, aby ekran w tym miejscu zmienił barwę... Spróbuj to zrobić teraz:

mov ax,0a000h
mov es,ax
mov di,320*10+10
mov al,10
mov es:[di],al

(oczywiście to tylko fragment programu - przedtem musisz zainicjować tryb graficzny a potem wrócić do trybu tekstowego i zakończyć program)
Jeżeli cię to interesuje - to wiedz, że tryb tekstowy również zorganizowany jest dosyć prosto. Pamięć ekranu znajduje się wtedy w segmencie 0b800h. Każdy znak na ekranie opisywany jest przez dwa bajty. W pierwszym podaje się numer ascii znaku, a w drugim - jego atrybuty (kolor, tło, miganie).

Paleta kolorów

Jak bawić się kolorami wykorzystując do tego naszą VGA. Co prawda pewnie nikt z was nie ma już prawdziwej VGA, ale chyba każda z tych dzisiejszych super-mega-rakiet potrafi VGA emulować (np. tryb 13h).

Na początek trochę teorii. Obraz na ekranie jest rysowany tak (w dużym uproszczeniu oczywiście), że karta bierze sobie zawartość kolejnej komórki pamięci z segmentu 0a000h i znajduje odpowiedni dla tej zawartości kolor z palety. Jeżeli w komórce pierwszej będzie np. 36, to na monitorze zobaczymy w tym miejscu kropkę w kolorze jaki jest zdefiniowany na 36 miejscu w palecie.

Kolor definiujemy przez określenie nasycenia 3 barwami - czerwona, zielona i niebieska. W trybie 13h możemy ustalić wartości RGB na 0-63. Przy ustawieniu 0,0,0 mamy kolor idealnie czarny, przy 63,63,63 - idealna biel. Kolor najbardziej czerwony to 63,0,0. Kolor żółty to mieszanka zielonego i czerwonego - czyli np. 50,50,0 - im mniejsze będą wartości tym bardziej będzie on przypominał brązowy.

A jak definiuje się kolor w palecie za pomocą assemblera? Bardzo prosto UśmiechMusimy tylko wysłać do portu 03c8h numer koloru, który chcemy zdefiniować, a następnie do 03c9h po kolei trzy wartości - R, G i B.

Aby wysłać coś do portu używamy instrukcji:

out dx,al

gdzie dx to numer portu a al wartość tam wysyłana

Aby ustawić kolor tła na niebieski trzeba wykonać następujący kod:

mov dx,03c8h
xor al,al
out dx,al
inc dx
out dx,al
out dx,al
mov al,60
out dx,al

Jest jeszcze jedna rzecz którą powinieneś wiedzieć. Po ustawieniu koloru n - karta jest gotowa do przyjmowania ustawień koloru n+1. Nie musimy wiec ciągle wpisywać numeru koloru do 03c8h. Wystarczy ciągle pisać do 03c9h.

Koprocesor - podstawy

Twój komputer prawie na pewno wyposażony jest w bardzo fajną zabawkę, jaką jest koprocesor. Coś takiego stosowano już nawet do pierwszych PC'tów, jednak dopiero od pentium zaczął on być szybki, a nawet szybszy w obliczeniach od samego procesora! Okazuje się, że komputer szybciej policzy na koprocesorze ile jest 5.1243123 razy 3.123123 niż na procesorze ile jest 5 razy 3 ! To pierwsze zajmie 3 takty (a przy dobrym kodowaniu - jeden), to drugie natomiast aż 9. Z dzieleniem nie ma już wielkiej różnicy.

Koprocesor potrafi komunikować się z pamięcią, z własnym stosem (czyli właściwie też pamięcią), natomiast nie potrafi czytać rejestrów procesora. Oznacza to, że aby wysłać zawartość rejestru EAX na stos koprocesora, musimy najpierw wysłać go do jakiejś zmiennej, a potem dopiero wczytać koprocesorem.

Wszystkie dane na stosie koprocesora przechowywane są jako liczby rzeczywiste. Jeżeli wczytamy zmienną całkowitą - jest ona konwertowana na rzeczywistą, co zabiera odpowiednio dużo czasu. Analogicznie jest przy wysyłaniu do pamięci zmiennych całkowitych. Dlatego przy korzystaniu z koprocesora należy unikać liczb całkowitych, najlepiej korzystać z nich dopiero pod koniec - gdy są naprawdę potrzebne. W moim engine-3D na floatach było praktycznie wszystko i integery pojawiły się dopiero przy współrzędnych ekranowych.

Podstawowe instrukcje:

FLD - wczytaj z pamięci na stos
FST - zapisz ze stosu do pamięci, nie usuwając ze stosu
FSTP - jak FST, ale z usunięciem danej ze stosu
FADD - dodawanie - dodaj dwie dane z wierzchołka stosu, usuń je i zapisz wynik na wierzchołku
FSUB - odejmowanie
FMUL - mnożenie
FDIV - dzielenie
FILD - wczytaj daną całkowitą, przekonwertuj ją i zapisz na stosie
FIST - przekonwertuj daną ze stosu na całkowitą i wyślij do pamięci
FISTP - jak FIST ale z usunięciem ze stosu
FSQRT - pierwiastek kwadratowy z liczby na wierzchołku stosu
FSIN - sinus
FCOS - cosinus
FPATAN - arctangens (przyjmuje dwa argumenty, ten drugi najlepiej ustawić jako 1.0)

Neleży jeszcze dodać, jak tworzyć w Turbo Assemblerze zmienne typu rzeczywistego. Musimy na taką zmienną poświęcić co najmniej 4 bajty, czyli musi być zadeklarowana jako DD . Jeżeli chcemy przypisać jej na początku jakąś wartość, trzeba użyć w niej kropki (przecinka numerycznego), nawet, jeżeli część ułamkowa równa jest zero:

promyczek dd 1000.0
skala dd 1.5
prędkość dd ?

Jedną z najciekawszych cech koprocesora jest to, że może on pracować wtedy, gdy pracuje tez procesor. Można to wykorzystać np. do wykonania dwóch dzieleń na raz:

fild [_zmienna1]
fild [_zmienna2]
mov eax,[_zmienna3]
mov ebx,[_zmienna4]
fdiv
idiv ebx

...dzielenia wykonają się praktycznie w tym samym czasie, czyli dwa razy szybciej, niż gdyby wykonywały się oddzielnie.

Nie jest jednak tak fajnie, aby pracę można było zawsze podzielić pomiędzy procesor a koprocesor. W trakcie fdiv procesor nie może mnożyć. Nie przeszkadza to jednak mu wykonywać wielu innych instrukcji. Jest to wykorzystywane przy korekcji perspektywy, gdy koprocesor oblicza iloraz, podczas gdy procesor rysuje na ekranie teksturkę.

Perspektywa - podstawy świata 3D

Jeżeli chcesz wyświetlać na płaskim ekranie trójwymiarowy świat, musisz dowiedzieć się jak wykonać jego rzut perspektywiczny. Masz do wyboru dwie możliwości: pierwsza to próba wyprowadzenia razem ze mną wzorów na perspektywę, druga - przepisanie ich i zapamiętanie.

Musimy ustalić jak definiujemy układ współrzędnych.

Jak widzisz na rysunku - oś X biegnie od lewej do prawej, oś Y od dołu do góry, natomiast oś Z leci wgłąb ekranu. Musisz przyzwyczaić się do takiego układu - bo będę go używał cały czas Uśmiech

Spójrz teraz na ekran. Wyobraź sobie, że gdzieś tam - w głębi za szklaną szybką - znajduje się jakiś obiekt. Musimy go z rzutować na ekran. Ty znajdujesz się kilkadziesiąt centymetrów przed ekranem. Obiekt znajduje się za nim. Jak obliczyć gdzie go narysować?

Ten rysunek przedstawia całą sytuację jakby "z góry". Obiekt znajduje się w punkcie a. Obserwator w punkcie o. Ekran jest narysowany poziomo. Ten zielony odcinek to odległość pomiędzy obserwatorem a ekranem. Musimy znaleźć miejsce w którym ekran przecina linia prosta pomiędzy obserwatorem a obiektem. Jest to punkt a'. Ponieważ patrzymy na wszystko z góry - szukamy jego współrzędnej X. (jeżeli nie rozumiesz dlaczego, stań NAD monitorem - widzisz wtedy, gdzie jest prawo a gdzie lewo, ale nie widzisz gdzie jest dół a gdzie góra UśmiechPamiętaj tez o tym, że współrzędna Z biegnie wgłąb ekranu.

Z twierdzenia Talesa mamy:

ab / bo = a'b' / b'o

czyli:

a'b' = ab * b'o / bo

Ponieważ punkt b' znajduje się w środku ekranu - ma on współrzędna X równą 0. Długość odcinka a'b' jest więc równa poszukiwanej współrzędnej X punktu a'! Długość ab to po prostu współrzędna X punktu a - czyli obiektu. Długości b'o i bo to odpowiednio: odległość obserwatora od ekranu, oraz różnica współrzędnych Z obiektu i obserwatora.

Nasz wzór możemy więc zapisać trochę czytelniej:

xe = x * z / ( z + d )

gdzie:
xe - współrzędna pozioma na płaskim ekranie
x - współrzędna X obiektu
z - współrzędna Z obiektu
d - odległość obserwatora od ekranu

Jeżeli zamiast z góry popatrzymy na cały obrazek z boku - będziemy mogli w podobny sposób wyliczyć współrzędną pionową. Gotowy wzór wygląda tak:

ye = y * z / ( z + d )

Jak widzisz w wyliczaniu współrzędnych ekranowych korzystamy z dzielenia. Jest to najwolniejsze z czterech podstawowych działań. Ponieważ dzielimy przez tą samą liczbę - dobrze będzie podzielić tylko RAZ a potem pomnożyć przez odwrotność. Wyliczamy najpierw:

pomoc = z / ( z + d )

A potem:
xe = x * pomoc
ye = y * pomoc

Czyli zamiast dwóch mnożeń, dwóch dzieleń i dwóch dodawań - mamy dwa mnożenia, ale tylko jedno dzielenie i jedno dodawanie! Zawsze gdy jakiś wzór będzie często używany (ten będzie praktycznie cały czas), to staraj się obniżyć ilość dzieleń. Dobrze też uprościć pozostałe działania.
Masz już gotowe wzory. Spróbuj teraz narysować na ekranie kilka punktów w przestrzeni. Spróbuj nimi poruszać... uważaj, żeby ci nie wychodziły poza ekran .

Trójkąty - wypełnianie jednokolorowe

Aby stworzyć 3D-engine musimy się nauczyć rysować na ekranie trójwymiarowe bryły. Każda taka bryła, niezależnie od kształtu składać się będzie z płaskich ścian. Każda ściana jest wielokątem. Każdy wielokąt można podzielić na trójkąty. Dlatego bardzo ważne jest, aby mieć gotową procedurę potrafiącą wypełnić trójkąt. W tym artykule dowiesz się jedynie jak wypełnić go jednym kolorem - ale jego zrozumienie jest potrzebne do zrozumienia dalszych lekcji na temat cieniowania, teksturowania, etc...
Jak zapewne wiecie, każdy trójkąt ma dokładnie trzy wierzchołki:

Do naszych celów wierzchołki powinne być posortowane. Ten najwyższy powinien być pierwszy, a najniższy - trzeci. Lepiej od razu podam jak posortować wierzchołki, żeby ktoś do tego quicksorta nie zaciągał Mrugnięciea także dlatego, że już w kilku miejscach widziałem sortowanie nieoptymalne (np. w pliku fatmap.txt).

jeżeli y1>y2 to
  Zamień(1,2)
jeżeli y2>y3 to
  Zamień(2,3)
jeżeli y1>y2 to
  Zamień(1,2)


(zwróć uwagę na wcięcia, ostatni warunek sprawdzamy tylko wtedy, gdy warunek drugi jest prawdziwy)

Po posortowaniu wierzchołków sytuacja wygląda tak:

Trójkąt rysować będziemy liniami poziomymi. Zaczynamy od góry i posuwamy się w dół. Rysowanie składa się z dwóch części - najpierw linie od wierzchołka 1 do 2 (zielone pole), potem linie od wierzchołka 2 do 3 (żółte pole).

Narysowanie każdej linii polega na pokolorowaniu pixeli od pewnego punktu położonego na lewej krawędzi, do punktu położonego na prawej krawędzi. Dla każdego Y będziemy mieli wyliczone XL (lewy X) i XR (prawy X). Niektórzy wyliczają je na początku, jeszcze przed rysowaniem, nie ma to jednak sensu. Ja ustalam wartości XL i XR na początku i zmieniam je w każdej linii po narysowaniu poprzedniej.

Linię więc narysujemy tak:

dla każdego x od XL do XR

    pokoloruj_pixel(x,y)


W typowym trójkącie (nie w każdym! jest jeden wyjątek o którym trzeba pamiętać) pierwsza linia ma długość zero, ponieważ zarówno XL jak i XR są równe współrzędnej poziomej najwyższego wierzchołka - czyli X1. Potem w każdej linii lewy i prawy X zmieniają się o pewną wartość. Nazwijmy te wartości - delta_xl dla lewego X i delta_xr dla prawego X.

delta_xl=(x2-x1)/(y2-y1)
delta_xr=(x3-x1)/(y3-y1)

Te wzory należy zrozumieć. Delta_lx mówi nam o ile zmienia się lewy x w każdej linii. Oblicza się więc ją dzieląc różnicę X na obu końcach lewej krawędzi przez ilość linii do narysowania.

Zwróć uwagę, że powyższe wzory prawdziwe są tylko wtedy, gdy drugi wierzchołek trójkąta znajduje się po lewej stronie. Trzeba to sprawdzić i jeżeli okaże się, że jest po prawej - delty zamieniają się miejscami.

Po narysowaniu y2-y1 linii (czyli pierwszej części trójkąta), jedna z delt się zmieni. W tym przypadku będzie to lewa delta.

delta_lx=(x3-x2)/(y3-y2)

Jeżeli wierzchołek numer 2 był po prawej stronie - zmieni się prawa delta.

Po narysowaniu drugiej części nie zostaje już nic - bo cały trójkąt jest narysowany UśmiechCały algorytm wygląda więc w przybliżeniu tak:

posortuj_wierzchołki_według_y

xl=x1
xr=x1
oblicz_deltę (lewą,1,2)
oblicz_deltę (prawą,1,3)
narysuj_linie_od_y1_do_y2
oblicz_deltę (lewą,2,3)
narysuj_linie_od_y2_do_y3

Napisanie dobrze całego algorytmu nie jest takie proste. Trzeba bowiem pamiętać o wielu przypadkach, które trzeba uwzględnić. Co będzie np. gdy nie będzie dolnej lub górnej części. Wtedy np. y2-y1 będzie równe 0. I jeżeli tego nie sprawdzisz - dostaniesz dzielenie przez 0.

Najlepszy sposób na sprawdzenie czy algorytm działa w każdym przypadku, to napisanie obracającego się trójkącika. Bierzesz trzy punkty na ekranie i co klatkę obracasz je wokół środka ekranu, a następnie rysujesz trójkąt, którego są wierzchołkami.

 

Projekt © 2009 - INFOPROG 67905 odwiedzin