j_uk_dev
29.12.09, 01:36
Na życzenie @don_wroc_love skleciłem ten tekst
Reprezentacja koloru na Amigach jak zauważyłeś była zupełnie inna niż na PC.
Obraz reprezentowany był przez tzw. bitplany, przez co pojedynczy bajt
reprezentował po jednym bicie 8 kolorów. W ten sposób potrzebne było kilka
buforów pamięci by reprezentować pojedynczy piksel, a dokładnie tyle buforów (
bitplanów ) ile bitów reprezentowało kolor. Oczywiście reprezentacja samego
koloru to też inna bajka, bo na Amidze paleta była indeksowana i prawdziwy
TrueColor24 był reprezentowany jedynie przez paletę 16mln kolorów, ale użyć
ich maksymalnie ( przy kościach AGA ) ich 256. Skoro jesteśmy przy AGA, to
przy wykorzystaniu 256 kolorów potrzeba było 8 bitplanów. Każda sekwencja
bitów reprezentowała indeks koloru z palety. Teraz wyobraź sobie przykładowe
nie 8 a 4 bitplany dla uproszczenia ( wartości losowe z głowy ):
1 1 1 1 0 1 1 1 - bitplan 3
0 1 1 1 0 1 1 1 - bitplan 2
1 0 1 0 1 0 0 0 - bitplan 1
1 1 0 0 1 1 0 0 - bitplan 0
Otrzymamy więc 8 indeksów kolorów: 11, 13, 12, 3, 13, 12, 12
Na wyliczenie każdego indeksu składa się sięgnięcie po 4 komórki pamięci,
wykonanie maskowania bitu oraz złożenie z tego liczby i przeszukanie palety.
Sporo z tego robił oczywiście hardware, ale wyobraź sobie teraz proste
putpixel() i getpixel() od strony programistycznej, dla postawienia dosłownie
jednego piksela potrzebna była spora gimnastyka ( szczególnie, że stawiając 1
pixel trzeba zabezpieczyć pozostałe 7 wartości bajtu poprzez dodatkowe
operacje logiczne - a to wszystko to cykle, cykle i jeszcze raz cykle ).
Dlaczego w 2D to działało szybko? Odpowiedzią jest Blitter, który świetnie
radził sobie z kopiowaniem obszarów pamięci, jednak kopiowanie to było tym
lepsze im obszary pamięci były większe. Technicznie wielkiego znaczenia nie
miało czy kopiujemy obszar 128x128 czy 16x16. Blitter robił to niemal tak samo
szybko, ale... nie wystarczająco szybko. W grach 2D, gdy były one złożone z
tzw. 'tile', czy bardziej po polsku "bloków grafiki" nie trzeba było rysować
aż tak dużo, blitter doskonale dawał sobie radę z narysowaniem nowych
elementów poziomów i przescrollowaniem świata ( przekopiować bufor w lewo czy
prawo i dorysować nowy rząd tile'ów, zrobić update sprite'ów itp. ). Gdyby nie
blitter, to Amiga by po prostu na tym polu nie dała rady nawet z prostymi
grami 2D, blitter potrafił też szybko wypełniać bufory przez co np.
wypełnianie kolorem w takim Deluxe Paint odbywało się błyskawicznie.
Ale do semi-3D to nie wystarczyło. Dlaczego napisałem 'semi'? Dlatego, że
wówczas wszysct walczyli o to, by stworzyć Wolfenstein3D dla Amigi, który mimo
tytułu 3D wcale nie był. To był "raycaster engine", który nie wymagał
"floating point" i był dosyć lekki jeśli chodzi o przetwarzanie sceny, ale
wymagał szybkiej możliwości stawiania pikseli.
Przejdźmy teraz do PC. Na PC kolor reprezentowany był przez bajt lub ciąg
bajtów, zależnie od formatu. Obecnie jeden kolor jest zwykle reprezentowany
przez 3 ( 24bity, czyli wszystko co nie jest przezroczyste, np. finalny
framebuffer ) lub 4 ( z kanałem alpha, np. tekstury ) bajty. W tamtym czasie
również PC często korzystał z palety indeksowanej i w czasie gdy był już
Wolfenstein3D możliwości były porównywalne z układem AGA. Dlaczego więc
ówczesne PC nie radziły sobie z platformówkami 2D? Bo nie miały blittera. Nie
miały układu, który szybko będzie kopiował całe bufory oraz wykonywał między
nimi często potrzebne operacje logiczne, np. kiedy stawiany był sprite gracza,
hardware nic nie mógł zrobić, wszystko trzeba było napisać samemu. Gry 2D w
dla PC w tamtym czasie stanowiły problem wydajnościowy ( zresztą Wolfenstei3D
również miał swoje wymagania i mimo, że działał też słabym na 286, to jednak
grać dało się po zmniejszeniu viewportu dopiero ).
Przejdźmy teraz do W3D. Wyobraź sobie 320 pionowych kolumn pikseli. Horyzont
postaw sobie pośrodku, a więc w linii 100 ( biorę pod uwagę 320x200 ). Jak
Wolfenstein dał sobie radę z rysowaniem? Wykorzystał swoją największą wadę
dotyczącą 2D i problemów z szybkością kopiowania w połączeniu z operacjami
logicznymi jako zaletę - mógł postawić jednym rozkazem asemblera dowolny
piksel w dowolnym miejscu z efektem natychmiastowym, bez dodatkowych obliczeń.
Owszem, screen tearing i niższy framerate również na słabszych PC było widać w
Wolfenstein3D, ale nikomu to nie przeszkadzało, bo zastosowano sporo tricków
jak np. wycięcie podłóg i sufitów oraz skalowanie tylko połowy ściany. Jeśli
zwrócisz uwagę jak wyglądały ściany w W3D, to jeśli zdejmiesz tekstury i
poprowadzisz prostą linię poziomą przez środek ekranu to okaże się, że to co
jest na górze jest symetryczne z tym co jest na dole. Skoro więc policzyliśmy
który piksel z tekstury zdjąć po jednej stronie, to wystarczy prostym
odejmowaniem ( kolejny jeden rozkaz asemblera ) odszukać piksel tekstury, jaki
musimy postawić w części dolnej. Co dalej? Tekstury przygotowane są od razu do
wklejenia - bierzemy zawartość komórki pamięci i wklejamy ją od razu we
właściwą komórkę framebuffer. Relacja między tekstura - wklejanie piksela w
przypadku Amigi również stanowiła właśnie "bottleneck". Blitter był mocny, ale
nie dawał sobie rady, jeśli musiał kopiować osobno pojedyncze piksele.
Wykonanie 30-40 operacji kopiowania dla gry 2D zapewniało płynny gameplay, ale
wykonać ich np. 1000? Już niestety nie. Oczywiście liczby niekoniecznie
pokrywają się z rzeczywistością, chodzi mi raczej o zaprezentowanie skali
zjawiska.
Wyciągnięcie pojedynczego koloru piksela z tekstury to było kolejne wyzwanie.
Powiedzmy, że na PC piksel textury reprezentował 1 bajt. Jeśli tekstura była w
rozmiarach 64x64 ( takie były w W3D ), to wyciągnięcie piksela o współrzędnych
(x,y) wymagało prostej operacji: offset + ( y * 64 ) + x. Ponieważ szerokość
była potęgą dwójki ( zresztą to też był powód dlaczego karty graficzne
późniejszych generacji wymagały, by tekstury w szerokości zawsze były potęgami
dwójki

), można było mnożenie zastąpić przesuwaniem bitów w lewo co czyniła
znowu jedna instrukcja asemblera czyli: offset + ( y << 6 ) + x. Mamy więc 2
dodawania ( 2 rozkazy ) i jedno przesunięcie ( 1 rozkaz ). Dopieramy się do
piksela w 3 rozkazach po czym kolejnym kopiujemy go na framebuffer.
Teraz Amiga. Robimy tak samo, jeśli teksturę zachowamy w postaci takiej jaka
była w przypadku W3D czyli ciągły bufor z wartościami pikseli jako bajtami.
Tylko, że wkopiowanie tego w jeden piksel ekranu oznaczało przekonwertowanie
wartości na bitplany, a w jaki sposób to można było zrobić? Prosty algorytm (
z głowy co prawda, ale powinien zobrazować sytuację ) ( zakładam rozdzielczość
320x200 ):
value = offset + ( y << 6 ) + x;
screen_x = 100; // przykladowa wspolrzedna docelowa x na ekranie
screen_y = 100; // przykladowa współrzędna docelowa y na ekranie
bytes_per_row = ( 320 >> 3 ); // to akurat będzie stała
byte_in_row = ( screen_x >> 3 ); // sprawdzamy, który bajt w rzędzie zawiera
nasz pixel (
operujemy na liczbach całkowitych, przy screen_x = 100, wyjdzie 12)
bit_in_byte = screen_x - ( byte_in_row << 3 );
// teraz każdy bitplan trzeba zupdatować
byte_bpl_1 = byte_bpl_1 | ((((( tex_color & ( 1 << 0 ) ) >> 0) ) & 1 ) <<
bit_in_byte );
byte_bpl_2 = byte_bpl_2 | ((((( tex_color & ( 1 << 1 ) ) >> 1) ) & 1 ) <<
bit_in_byte );
byte_bpl_3 = byte_bpl_3 | ((((( tex_color & ( 1 << 2 ) ) >> 2) ) & 1 ) <<
bit_in_byte );
byte_bpl_4 = byte_bpl_4 | ((((( tex_color & ( 1 << 3 ) ) >> 3) ) & 1 ) <<
bit_in_byte );
byte_bpl_5 = byte_bpl_5 | ((((( tex_color & ( 1 << 4 ) ) >> 4) ) & 1 ) <<
bit_in_byte );
byte_bpl_6 = byte_bpl_6 | ((((( tex_color & ( 1 << 5 ) ) >> 5) ) & 1 ) <<
bit_in_byte );
byte_bpl_7 = byte_bpl_7 | ((((( tex_color & ( 1 << 6 ) ) >