Z pogranicza

Programowanie zoptymalizowanej grafiki 13H

  • 0 komentarzy
  • 2260 odsłon
  • Oceń ten tekst jako pierwszy
Programowanie szybkiej grafiki w trybie 13H

0001H. Co to za art ?
0002H. Teoria
0003H. Włączanie i wyłączanie grafiki
0004H. Rysowanie i pobieranie pixela
0005H. Double Buffering (Buforowanie ekranu)
0006H. Paleta kolorów
0007H. Synchronizacja (Retrace)
0008H. Sprajty
0009H. The End

0001H. Co to za art ?
W tym texcie postaram sie przedstawić najważniejsze informacje o programowaniu grafiki
w tzw: trybie 13H. Artykuł ten różni się od innych tym, że przedstawione fragmenty kodu
źródłowego napisane są w assemblerze, dodatkowo są optymalizowane pod kątem prędkości
wyświetlania dlatego też można je wykorzystać przy programowaniu gier.
Kody napisane są pod kątem procesora 8086 ale można je przyśpieszyć stosując instrukcje
procesora 386. Fragmenty testowałem w NASM'ie, ale bez problemu można je przerobić na
dowolną inną składnie...
UWAGA ! Potrzebna będzie przynajmniej podstawowa znajomość Assemblera do zrozumienia tego
tekstu !!!

0002H. Teoria
Dawno dawno temu w czasach gdy królowały pierwsze procesory serii x86 trybiące pod
kontrolą DOS'a najlepsze gry działały w przedziwnym świecie 320x200 pixeli i mogły
maksymalnie wyświetlać 256 kolorow...

Tryb 13H jest standardowym trybem graficznym kart VGA. Dlaczego nazywa się tak a nie
inaczej dowiecie się dalej. W tym trybie mamy do dyspozycji ekran o rozdzielczości
320x200 pixeli i 256 kolorową paletę, co oznacza że w jednym czasie możemy wyświetlić
maksymalnie 256 różnych barw.

Tryb 13H jest jednym z najszybszych trybów graficznych w komputerze !
Jak wiadomo w trybie rzeczywistym procesora (RMode), czyli np: w zwykłym DOS'ie mamy
dostęp do karty graficznej, a dokładniej do tego co wyświetlamy poprzez "okienko" w
pamięci komputera, którym jest segment 0A000H.
Pamięć komputera podzielona jest na 64kB bloki (segmenty). Skoro w trybie 13H
dysponujemy 256-kolorową paletą barw to na jeden pixel przypada dokładnie jeden bajt
w pamięci, a jeżeli rozdzielczość wynosi 320x200 pixeli to łatwo obliczyć, że cały
ekran będzie zajmował 320*200*1=64000 bajtów, czyli prawie cały segment.
I tu tkwi tajemnica szybkości wyświetlanej grafiki w trybie 13H !
Ponieważ cały ekran mieści się w jednym segmencie to nie ma potrzeby korzystania z
przerwań BIOS'u aby ustawić konkretny wycinek ekrany po którym chcemy rysować, co
bardzo spowalnia wyświetlanie grafiki, ale jest niezbędne przy trybach wysokiej
rozdzielczości.

Przy programowaniu aplikacji graficznych w trybie 13H czyli np: gier pod DOS'a
wykorzystuje się dwie metody wyświetlania grafiki:
- zwykłą - wszystko jest odrazu rysowane na ekranie
- double buffering - (podwójne buforowanie) wszystko jest rysowane najpierw w pamięci,
a dopiero później kopiowane na widoczny ekran.
Czym różnią się te dwie metody i jak z nich korzystać dowiecie się później. Najpierw
zajmiemy się standardową i najprostszą metodą wyświetlania, a następnie double
bufferingiem.

0003H. Włączanie i wyłączanie grafiki
Przed rozpoczęciem jakiej kolwiek pracy z grafiką, trzeba powiedzieć komputerowi w jakim
trybie graficznym zamieżamy pracować.
Aby włączyć (zainicjować) tryb 13H musimy do rejestru AL wrzucić wartość 13H i wywołać
funkcję 00H z przerwania 10H. Konkretnie wygląda to tak:

init13h:
        MOV AL,13H
        MOV AH,00H
        INT 10H


Ponieważ aby włączyć tryb 13H musimy wywołać funkcję 00H z parametrem 13H, to dlatego
właśnie ten tryb graficzny nazywa się tak a nie inaczej :)

Po zakończeniu pracy w trybie graficznym, przydałoby się powrócić spowrotem do
standardowego trybu tekstowego, czyli musimy poprostu wyłączyć grafike, a dokładniej
wywołać tryb tekstowy.
Standardowy tryb tekstowy wywołuje się wpisując do AL wartość 03H i uruchamiając funkcję
00H przerwania 10H. Do tego celu służy ten kod:

close13h:
        MOV AL,03H
        MOV AH,00H
        INT 10H


W tym miejscu proponuje napisać sobie dowolny program wykorzystujący te dwie wstawki
assemblerowe, który włączy i wyłączy tryb graficzny...

0004H. Rysowanie i pobieranie pixela
Jak wiemy, w trybie 13H każdy pixel jest reprezentowany w pamięci przez dokładnie jeden
bajt ponieważ mamy do dyspozycji dokładnie 256 kolorów (od 0 do 255). Tak więc aby
narysować lub pobrać wartość pojedyńczego pixela musimy obliczyć jego offset od początku
segmentu graficznego 0A000h, czyli po polskiemu która komórka pamięci go reprezentuje.
Aby to zrobić możemy wykorzystać bardzo często spotykany przy grafice wzór:

numer komórki = y * 320 + x

Gdzie:
320 - szerokość ekranu w pixelach
y - pozycja Y pixela
x - pozycja x pixela
W wyniku otrzymamy konkretny numer z przedziału 0 do 63999 - przesunięcie (offset) dla
danego pixela. Teraz wrzucając tam bajt zapalamy pixel o jakiejś barwie, a pobierając
ten bajt odczytujemy numer koloru.

Jak to wykorzystać w praktyce ?? W zwykłym C lub Pascalu moglibyśmy zrobić wskaźnik do
segmentu 0A000H i poprostu obliczać przesunięcie ale mi chodzi o szybkość dlatego
skorzystamy z Assemblera.
Najprostszy sposób wygląda tak:

putpixel:
        MOV AX,[Y]       ; do AX pozycje Y pixela
        MOV BX,320       ; BX = 320
        MUL BX           ; pomnóż AX przez BX
        ADD AX,[X]       ; dodaj pozycje X pixela do AX
        MOV DI,AX        ; rejestr DI = AX
        MOV AX,0A000H    ; do AX adres segmentu graficznego
        MOV ES,AX        ; rejestr ES = AX
        MOV CL,[Color]   ; do CL kolor pixela
        MOV [ES:DI],CL   ; skopiuj numer koloru pixela do danej komórki, czyli zapal pixel
 
getpixel:
        MOV AX,[Y]       ; do AX pozycje Y pixela
        MOV BX,320       ; BX = 320
        MUL BX           ; pomnóż AX przez BX
        ADD AX,[X]       ; dodaj pozycje X pixela do AX
        MOV DI,AX        ; rejestr DI = AX
        MOV AX,0A000H    ; do AX adres segmentu graficznego
        MOV ES,AX        ; rejestr ES = AX
        MOV CL,[ES:DI]   ; skopiuj wartosc komurki w pamieci do CL czyli pobierz numer pixela do rejestru CL
        MOV [Color],CL   ; numer koloru do zmiennej Color


Wstawki powinny działać bez problemów jednak jak ktoś sie nim dogłębnie przyjży i
przetestuje to zauważy kilka wad. Po pierwsze procedurki nie sprawdzają czy pozycje
pixela są w granicach ekranu czyli, czy:

X >= 0 i X < 320
Y >= 0 i Y < 200

jeżeli któryś z tych warunków się nie zgadza to po grzyba rysować pixel :P

Kolejną wadą powyższego rozwiązania jest użycie instrukcji MUL co w Assemblerowym
żargonie oznacza "pomnóż". Konkretnie MUL jest bardzo wolną operacją ale prostą w użyciu,
zamiast niej możemy pomnożyć Y * 320 uzywając przesunięć bitowych (kto nie wie co to jest
niech uda się do kursu Assemblera).
Poprawione wstawki wyglądają tak:

putpixel:
        MOV AX,[Y]         ; do AX pozycje Y pixela
        CMP AX,0           ; jeśli AX mniejsze od 0
        JL putpixel_end    ; to skocz do etykiety nie_rysuj
        CMP AX,200         ; jeśli AX większe lub równe 200
        JAE putpixel_end   ; to skocz do etykiety nie_rysuj
        MOV BX,[X]         ; do BX pozycje X pixela
        CMP BX,0           ; jeśli BX mniejsze od 0
        JL putpixel_end    ; to skocz do etykiety nie_rysuj
        CMP BX,320         ; jeśli BX większe lub równe 320
        JAE putpixel_end   ; to skocz do etykiety nie_rysuj
        PUSH BX            ; odłóż BX na stos (pozycje X)
        MOV BX,AX          ; BX = AX, czyli do BX pozycja Y
        SAL AX,6           ; ten fragment mnoży Y przez 320
        SAL BX,8           ; używając przesunięć bitowych
        ADD AX,BX          ; co jest znacznie szybsze :)
        POP BX             ; zdejmij BX ze stosu (pozycje X)
        ADD AX,BX          ; dodaj do AX pozycje X
        MOV DI,AX          ; rejestr DI = AX
        MOV AX,0A000H      ; do AX adres segmentu graficznego
        MOV ES,AX          ; rejestr ES = AX
        MOV CL,[Color]     ; do CL numer koloru pixela
        MOV [ES:DI],CL     ; skopiuj wartośc CL (kolor pixela) do danej komurki - zapal pixel
putpixel_end:              ; koncowa etykieta
 
getpixel:
        MOV AX,[Y]         ; do AX pozycje Y pixela
        CMP AX,0           ; jeśli AX mniejsze od 0
        JL getpixel_end    ; to skocz do etykiety nie_rysuj
        CMP AX,200         ; jeśli AX większe lub równe 200
        JAE getpixel_end   ; to skocz do etykiety nie_rysuj
        MOV BX,[X]         ; do BX pozycje X pixela
        CMP BX,0           ; jeśli BX mniejsze od 0
        JL getpixel_end    ; to skocz do etykiety nie_rysuj
        CMP BX,320         ; jeśli BX większe lub równe 320
        JAE getpixel_end   ; to skocz do etykiety nie_rysuj
        PUSH BX            ; odłóż BX na stos (pozycje X)
        MOV BX,AX          ; BX = AX, czyli do BX pozycja Y
        SAL AX,6           ; ten fragment mnoży Y przez 320
        SAL BX,8           ; używając przesunięć bitowych
        ADD AX,BX          ; co jest znacznie szybsze :)
        POP BX             ; zdejmij BX ze stosu (pozycje X)
        ADD AX,BX          ; dodaj do AX pozycje X
        MOV DI,AX          ; rejestr DI = AX
        MOV AX,0A000H      ; do AX adres segmentu graficznego
        MOV ES,AX          ; rejestr ES = AX
        MOV CL,[ES:DI]     ; kolor pixela do CL
        MOV [Color],CL     ; numer koloru do zmiennej Color
getpixel_end:              ; koncowa etykieta


Powyższe wstawki powinny chodzić bez większych problemów. Ich zaletą jest o wiele
większa prędkość oraz sprawdzanie czy dany pixel mieści się w obrębie ekranu czy nie.

Mając te procedurki możemy śmiało napisać dowolne inne rysujące np: linie, prostokąt,
ale to pozostawiam już waszej inwencji twórczej :) Dodam jeszcze tylko prosty kod, który
czyści ekran na dany kolor:

clear_screen:
        MOV AX,0A000H         ; do AX adres segmentu graficznego
        MOV ES,AX             ; ES = AX
        MOV DI,0              ; DI = 0 - zacznij od pierwszego pixela (0,0)
        MOV AL,[Color]        ; do AL kolor
        MOV AH,[Color]        ; do AH kolor
        MOV CX,32000          ; zapisujemy po dwa bajty (64000 / 2) bo tak jest szybciej
        REP STOSW             ; powtarzaj instrukcję STOSW 32000 razy


Jedyne co się tutaj rzuca w oczy to to że zamiast liczby 64000 użyta została liczba
32000. Przed pisaniem kodu (zwłaszcza do grafiki) warto się upewnić jak napisać
najszybszy kod, trzeba także posprawdzać jakie operacje wykonują sie najszybciej i
ile danych można przesłać naraz. Liczba 32000 jest dlatego, że zamiast lecieć pixel
po pixelu w pętli czyli ustawiać bajt po bajcie, możemy za każdym razem ustawić 2 bajty
- slowo. W przypadku procesorów 386 i nowszych można to zrobić jeszcze szybciej
wykorzystując 32 bitowe rejestry, ale w tym tekście będe sie trzymał kodu dla procesora
intel x86.

0005H. Double Buffering (Buforowanie ekranu)
Kto bawił się z częstym zamalowywaniem ekranu i stawianiem na nim pixela, mógł zauważyć
bardzo niepokojącą rzecz - migotanie ekranu. Dzieje się tak dlatego że najpierw czyścimy
ekran jednym kolorem (zamalowujemy wszystko) a następnie stawiamy gdzie pixel o innej
barwie. W przypadku jednego pixela taki efekt można w sumie olać, ale kiedy w gre wchodzi
wyświetlanie skomplikowanej grafiki i sprajtów nie można na to pozwolić.

Otóż wymyślono pewnien bardzo prosty sposób na obejście tego efektu. Sposóbe ten nazywa
się Podwójnym Buforowaniem (Double Bufering). Ogólnie chodzi poprostu oto aby narysować
wszystko gdzieś w innym segmencie a następnie skopiować cały "virtualny ekran" lub jak
kto woli bufor ekranu do segmentu graficznego, gdzie dalej będzie widoczny na ekranie.
Buforem ekranu nazywamy pewien obszar pamięci gdzie można zmieścić wszystkie bajty
które reprezentują pixele. Oczywiście w trybie 13H taki bufor będzie miał rozmiar 64000
bajtów, czyli prawie cały segment.
W programach pisanych w językach wysokiego poziomu np: C/C++ czy Pascal zaleca się
tworzenie bufora poprzez zarezerwowanie pamięci. W niewielkich programach EXE lub
programach typu COM, na utwożenie bufora istnieje nieco prostsza metoda. Konkretnie jako
bufor można wykorzystać ostatni segment z pamięci konwencjonalnej dostępnej dla programu
w środowisku DOS. Segment ten zaczyna się od adresu 09000H.

Czym się różni rysowanie w buforze od zwykłego trybu gdzie odrazu rysujemy wszystko
na ekranie ??

Nie różni się właściwie niczym. Jedyna różnica to to że aby zobaczyć to co narysowaliśmy
w buforze musimy go w całości skopiować na ekran.

Oto przerobione procedury do rysowania/pobierania pixela i czyszczenia ekranu, które
działają w buforze (konkretnie buforem jest segment 09000H, ale można to przerobić tak
aby buforem była przydzielona do tego celu pamięć):

putpixel:
        MOV AX,[Y]         ; do AX pozycje Y pixela
        CMP AX,0           ; jeśli AX mniejsze od 0
        JL putpixel_end    ; to skocz do etykiety nie_rysuj
        CMP AX,200         ; jeśli AX większe lub równe 200
        JAE putpixel_end   ; to skocz do etykiety nie_rysuj
        MOV BX,[X]         ; do BX pozycje X pixela
        CMP BX,0           ; jeśli BX mniejsze od 0
        JL putpixel_end    ; to skocz do etykiety nie_rysuj
        CMP BX,320         ; jeśli BX większe lub równe 320
        JAE putpixel_end   ; to skocz do etykiety nie_rysuj
        PUSH BX            ; odłóż BX na stos (pozycje X)
        MOV BX,AX          ; BX = AX, czyli do BX pozycja Y
        SAL AX,6           ; ten fragment mnoży Y przez 320
        SAL BX,8           ; używając przesunięć bitowych
        ADD AX,BX          ; co jest znacznie szybsze :)
        POP BX             ; zdejmij BX ze stosu (pozycje X)
        ADD AX,BX          ; dodaj do AX pozycje X
        MOV DI,AX          ; rejestr DI = AX
        MOV AX,09000H      ; do AX adres ostatniego wolnego segmentu pamięci który jest dla nas buforem
        MOV ES,AX          ; rejestr ES = AX
        MOV CL,[Color]     ; do CL numer koloru pixela
        MOV [ES:DI],CL     ; skopiuj wartośc CL (kolor pixela) do danej komurki - zapal pixel
putpixel_end:              ; koncowa etykieta
 
getpixel:
        MOV AX,[Y]         ; do AX pozycje Y pixela
        CMP AX,0           ; jeśli AX mniejsze od 0
        JL getpixel_end    ; to skocz do etykiety nie_rysuj
        CMP AX,200         ; jeśli AX większe lub równe 200
        JAE getpixel_end   ; to skocz do etykiety nie_rysuj
        MOV BX,[X]         ; do BX pozycje X pixela
        CMP BX,0           ; jeśli BX mniejsze od 0
        JL getpixel_end    ; to skocz do etykiety nie_rysuj
        CMP BX,320         ; jeśli BX większe lub równe 320
        JAE getpixel_end   ; to skocz do etykiety nie_rysuj
        PUSH BX            ; odłóż BX na stos (pozycje X)
        MOV BX,AX          ; BX = AX, czyli do BX pozycja Y
        SAL AX,6           ; ten fragment mnoży Y przez 320
        SAL BX,8           ; używając przesunięć bitowych
        ADD AX,BX          ; co jest znacznie szybsze :)
        POP BX             ; zdejmij BX ze stosu (pozycje X)
        ADD AX,BX          ; dodaj do AX pozycje X
        MOV DI,AX          ; rejestr DI = AX
        MOV AX,09000H      ; do AX adres bufora
        MOV ES,AX          ; rejestr ES = AX
        MOV CL,[ES:DI]     ; kolor pixela do CL
        MOV [Color],CL     ; numer koloru do zmiennej Color
getpixel_end:              ; koncowa etykieta
 
clear_screen:
        MOV AX,09000H         ; do AX adres ostatniego segmentu (bufora ekranu)
        MOV ES,AX             ; ES = AX
        MOV DI,0              ; DI = 0 - zacznij od pierwszego pixela (0,0)
        MOV AL,[Color]        ; do AL kolor
        MOV AH,[Color]        ; do AH kolor
        MOV CX,32000          ; zapisujemy po dwa bajty (64000 / 2) bo tak jest szybciej
        REP STOSW             ; powtarzaj instrukcje STOSW 32000 razy


Jak widać jedyna różnica to to że zamiast 0A000H jest 09000H :P
Ostatnią funkcją która jest NIEZBĘDNA do rysowania a właśćiwie wyświetlania tego co jest
w buforze jest funkcja która która skopiuje wszystkie bajty bufora do pamięci graficznej.
W tego typu procedurach najważniejsza jest prędkość, oto przykładowy bardzo szybki kod:

draw_screen:
        MOV AX,09000H   ; do AX adres segmentu bufora
        MOV DS,AX       ; DS = AX
        MOV SI,0        ; SI = 0 - wskazuje na pierwszy pixel bufora
        MOV AX,0A000H   ; do AX adres segmentu graficznego
        MOV ES,AX       ; ES = AX
        MOV DI,0        ; DI = 0 - wskazuje na pierwszy pixel ekranu
        MOV CX,32000    ; ilosc powtórzeń (pętla)
        REP MOVSW       ; powtarzaj 32000x instrukcje MOVSW, która kopiuje słowo po słowie
                        ; z DS:SI do ES:DI


Ogólny schemat działania programu opartego o podwójne buforowanie wygląda tak:
1) wyczyść bufor
2) rysuj wszystko w buforze
3) wywal bufor na ekran :)                

0006H. Paleta kolorów
Barwy pixeli na monitorze odtwarzane są na podstawie danych z karty graficznej (VGA).
Wysyła ona do niego natężenia barw składowych z jakich składa się światło. Każdą barwę
można zbudować z trzech kolorów - Czerownego, Zielonego i Niebieskiego. Wartość minimalna
każdej z tych barw składowych tworzy kolor czarny, natomiast wartość maksymalna wszystkich
kolorów tworzy kolor biały.
Przyjdzie w końcu taki moment w którym ilość dostepnych standardowo barw w trybie 13H
przestanie nam wystarczać. Co wtedy zrobić ?? Otóż nie pozostaje nic innego jak zmienić
paletę aktualnie używanych kolorów. Z pomocą przychodzą nam porty kart VGA :)

UWAGA ! Ponieważ tryb 13H jest trybem 8 bitowym (256 kolorów) to w przeciwieństwie do
trybów wysokiej głębi kolorów, natężenia barw składowych RGB (Red Green Blue) podajemy
nie w przedziale od 0 do 255 tylko od 0 do 63 !

Aby ustawić kolor na palecie kolorów karty VGA musimy wysłać do portu 03C8H numer koloru,
a następnie wysyłać po kolei dane RGB (Red Green Blue) bajt po bajcie do portu 03C9H.
Aby odczytać natężenia składowe danego koloru musimy wysłać do portu 03C7H numer koloru,
a nastepnie odczytywać dane RGB bajt po bajcie z portu 03C9H.

W Assemblerze robimy to tak:

set_color:
        MOV DX,03C8H     ; do DX numer portu VGA (zapis koloru)
        MOV AL,[Color]   ; do AL numer koloru
        OUT DX,AL        ; wyslij numer koloru do portu 03C8H
        INC DX           ; ustaw DX na port 03C9H
        MOV AL,[R]       ; do AL natężenie barwy czerwonej
        OUT DX,AL        ; wyślij
        MOV AL,[G]       ; do AL natężenie barwy zielonej
        OUT DX,AL        ; wyślij
        MOV AL,[B]       ; do AL natężenie barwy niebieskiej
        OUT DX,AL        ; wyślij
 
get_color:
        MOV DX,03C7H     ; do DX numer portu VGA (odczyt koloru)
        MOV AL,[Color]   ; do AL numer koloru
        OUT DX,AL        ; wyślij numer koloru do portu 03C8H
        INC DX           ; ustaw DX
        INC DX           ; na port 03C9H
        IN AL,DX         ; odczytaj wartość barwy czerwonej
        MOV [R],AL       ; zmienna R = AL (barwa czerwona)
        IN AL,DX         ; odczytaj wartość barwy zielonej
        MOV [G],AL       ; zmienna G = AL (barwa zielona)
        IN AL,DX         ; odczytaj wartość barwy niebieskiej
        MOV [B],AL       ; zmienna B = AL (barwa niebieska)


To by było w sumie na tyle. Programiści często stosują zamiast ustawiania pojedyńczego koloru
ustawianie odrazu całej palety. W zasadzie jest to pętla w której ustawia się kolor po
kolorze :)

0007H. Synchronizacja (Retrace)
Przy grafice niskopoziomowej dochodzi jeden mały problemik. Otóż monitor potrzebuje także
troche czasu na wyświetlenie wszystkiego za pomocą tzw: plamki. Kiedy plamka skonczy rysować
powraca do punktu wyjścia. Ten moment jest sygnalizowany ustawieniem odpowiednich rejestrów w
karcie graficznej. Ale poco nam to wiedzieć ?? Czasem może sie zdarzyć, że np: przy
wykonywaniu operacji na palecie kolorów, zauważalna będzie zmiana konkretnych barw własnie
dlatego, że plamka nie skończyła odświerzać ekranu. Jak temu zapobiec ?? Wystarczy, że przed
ustawieniem koloru a także przed narysowaniem bufora ekranu (przy podwójnym buforowaniu)
wykonamy ten kod (synchronizacja pionowa - tak sie to nazywa):

vsync:
        MOV DX,03DAH
vsync_1:
        IN AL,DX
        TEST AL,0
        JNE vsync_1


0008H. Sprajty
Sprajty to poprostu obrazki wyświetlane na monitorze. Sposobów na ich wyświetlenie jest wiele
jednak my postaramy się wykorzystać do tego celu możliwości jakie daje nam Assembler.
Dla programisty sprajt to prostokątne pole pixeli (obrazek). Sprajty możemy wyświetlać w
całości, w częściach (np: gdy jeden obrazek to kilka ruchów postaci), a także możemy
uwzględnić to aby nie było wyświetlane tło sprajtu. Pole pixeli można reprezentować jako
dwuwymiarową tablicę lub poprostu ciąg bajtów (tablica jedno wymiarowa) o długości
równej iloczynowi szerokości i wysokości obrazka (w grafice 13H jeden pixel to jeden bajt).
Przy wyświetlaniu sprajtów najważniejsza jest szybkość wyświetlania i ilość potrzebnych
operacji do wykonania - im jest ich mniej lub sprajt jest mniejszy tym szybciej będzie
wyświetlony. Prędkość jest najważniejsza z tego względu, że w grach 90% grafiki to właśnie
sprajty.
Oto przykładowy, bardzo szybki kod wyświetlający cały sprajt bez uwzględniania koloru
przezroczystego:

draw_sprite:
        PUSH AX              ; odkłada rejestry na stos
        PUSH BX
        PUSH CX
        PUSH DX
        PUSH ES
        PUSH DI
        PUSH DS
        PUSH SI
        PUSH CS              ; po tych dwóch instrukcjach
        POP DS               ; DS = CS (MOVSB tego wymaga)
        MOV SI,BX            ; do SI adres tablicy zaladowanej do BX
        MOV AX,09000H        ; do AX adres segmentu bufora
        MOV ES,AX
        MOV AX,[SY]          ; te instrukcje obliczają offset dla pierwszego pixela
        MOV BX,[SY]          ;
        SAL AX,6             ;
        SAL BX,8             ;
        ADD AX,BX            ;
        ADD AX,[SX]          ;
        MOV DI,AX
        MOV DX,[SW]          ; do DX szerokość obrazka (przyda się puźniej)
        MOV CX,[SH]
draw_sprite_1:
        PUSH CX
        MOV CX,DX            ; do CX szerokość obrazka z DX aby wykonać pętle
        REP MOVSB            ; kopiuje bajty
        ADD DI,320           ; przesówa się do następnej lini
        SUB DI,DX            ; odejmuje szerokosć obrazka aby grafika się nie rozjechała
        POP CX
        LOOP draw_sprite_1   ;
        POP SI               ; zdejmuje rejestry ze stosu
        POP DS
        POP DI
        POP ES
        POP DX
        POP CX
        POP BX
        POP AX


Aby załadować obrazek do tego kodu wystarczy przed wywołaniem procedury załadować adres
tablicy bajtów (pixeli) do rejestru BX. Robi to ten kod:

MOV BX,Image


gdzie Image to zwykła tablica bajtów.

W powyrzszym kodzie zmienne mają takie funkcje:
SX - pozycja X gdzie ma być wyświetlony obrazek;
SY - pozycja Y gdzie ma być wyświetlony obrazek;
SW - szerokość obrazka w pixelach (width);
SH - wysokość obrazka w pixelach (height);

Kod działa następująco. Najpierw obliczana jest pozycja pierwszego pixela do namalowania,
następnie program wchodzi w pierwszą pętle, która jest odpowiedzialna za rysowanie kolejnych
"lini" obrazka, czyli "schodzenie" w dół ekranu. Następnie program wchodzi w drugą pętle a
dokładniej wykona instrukcję MOVSB tyle razy ile pixeli przypada na szerokość obrazka.
Po wykonaniu tej pętli, program powraca do pierwszej pętli gdzie dodaje przesunięcie offsetu
o 320 czyli schodzi do następnej lini i jednocześnie odejmuje od rejestru DI szerokość
sprajtu - w przeciwnym wypadku wszystko by się rozjechało. Troche to zagmatwałem, ale po
przetestowaniu kodu sami zrozumiecie jak działa. Najważniejsze jest to, że jest on naprawde
szybki :)

Teraz do rozwiązania pozostał jeszcze problem przezroczystośći. Co zrobić gdy chcemy aby
nie było wyświetlane tło obrazka o jakimś kolorze ?? Wystarczy, że przed skopiowaniem bajtu
sprawdzimy czy jego wartość nie oznacza koloru tła (przezroczystości).
Oto szybki, ale nieco wolniejszy niz ten wyżej kod który wyświetli cały sprajt bez pixeli,
o danym numerze koloru (w przykładzie kolor przezroczysty to 0):

draw_transparent_sprite:
        PUSH AX
        PUSH BX
        PUSH CX
        PUSH DX
        PUSH ES
        PUSH DI
        PUSH DS
        PUSH SI
        PUSH CS
        POP DS
        MOV SI,BX
        MOV AX,09000H
        MOV ES,AX
        MOV AX,[SY]
        MOV BX,[SY]
        SAL AX,6
        SAL BX,8
        ADD AX,BX
        ADD AX,[SX]
        MOV DI,AX
        MOV DX,[SW]
        MOV CX,[SH]
draw_transparent_sprite_1:
        PUSH CX
        MOV CX,DX
draw_transparent_sprite_2:
        CMP BYTE [DS:SI],0                  ; sprawdz czy dany bajt nie ma wartość 0 czyli nie jest kolorem przezroczystym
        JE draw_transparent_sprite_2_next   ; jak tak to nie kopiuj bajtow tylko skocz do etykiety gdzie przesunie się na następny pixel
        MOVSB                               ; jak wszystko ok to kopiuj bajt
        JMP draw_transparent_sprite_2_end   ; skacz na koniec pętli
draw_transparent_sprite_2_next:
        INC DI
        INC SI
draw_transparent_sprite_2_end:
        LOOP draw_transparent_sprite_2
        ADD DI,320
        SUB DI,DX
        POP CX
        LOOP draw_transparent_sprite_1
        POP SI
        POP DS
        POP DI
        POP ES
        POP DX
        POP CX
        POP BX
        POP AX


Procedura działa dokładnie tak samo jak ta wyżej, z tą różnicą że przed skopiowaniem
sprawdzany jest dany bajt czy nie jest kolorem transparentnym (przezroczystym).

Hmm to będzie na tyle teori o bardzo szybkich sprajtach. Powyższy kod testowałem z
powodzeniem na 486 i jego prędkość była nieporównywalna z procedurami napisanymi w Pascalu
lub C/C++. Jeżeli komuś potrzebny jest kod kopiujący tylko fragment sprajtu to po zapoznaniu
się z powyrzyszymi dwoma prockami powinien umieć samemu napisać odpowiednie :)

0009H. The End

C0pYr16hT 3y C3p@ 2004

2004-07-24

www.cepa.end.pl
[email protected]

Jeżeli chciałbyś zamieścić ten tekst na swojej stronie, lub wykorzystać zamieszczone w nim
kody źródłowe powinienneś mnie o tym powiadomić ;)

OldSchool RULEZZZ ];->