O grafice wykład chyba drugi
Za ten temat biorę się już od dłuższego czasu – będzie z pół roku. Wprawdzie kilka wykładów wstecz pisałem o stosowaniu różnych technik do generowania grafiki, jednak przyszło potem kilka listów, – że po łebkach, że bez szczegółów, że nie wiadomo o co tak naprawdę chodzi. Jeden czytelnik zapałał nawet świętym oburzeniem, że nie wydrukowałem ani kawałka kodu, na podstawie którego mógłby napisać własnego jeśli nie „Quake”, to przynajmniej „Dooma”… Tak nie będzie – o ile dobrze pamiętam wydrukowaliśmy w TS kawałek programu dokładnie jeden raz i w najbliższym czasie nie należy się spodziewać zmiany tego wyniku. Piszemy o grach, fragmenty kodu będą, jak zaczniemy się zajmować procesem ich tworzenia. Mam tylko wątpliwości, czy będzie to jeszcze ciągle to samo pismo.
Ale dość o tym – przechodzimy do samego mięsa. Załóżmy, że okazje po temu są trzy – po pierwsze, pojawił się „Quake”, po drugie, dostaliśmy do testów kartę graficzną opartą o kość ViRGE. Po trzecie wreszcie, po okrągłym roku walki udało mi się poprawnie zainstalować Windows 95 wraz z DirectX i Di-rect3D (nie będę pisał co myślę o „tfurcach” Windows 95, ale kilka rzeczy im nie wyszło; Windowsy jak ten głupek – czego nie wiedzą, to zmyślą). Wszystkie te trzy fakty mają jeden wspólny mianownik – sposób tworzenia grafiki stosowany w najbardziej zaawansowanych technicznie grach. Dlaczego – będę wyjaśniać powoli, w trakcie wykładu.
Naszym zadaniem będzie dzisiaj zrozumienie, w jaki sposób tworzony jest obraz taki, jak w „Quake” (albo „DOOMie”, albo „Duke Nukem 3D”, wbrew pozorom różnice są stosunkowo niewielkie). Zacznijmy od mojego ulubionego eksperymentu myślowego (można go zresztą przeprowadzić w rzeczywistości). Stajemy przed oknem, albo ustawiamy sobie przed nosem szybę, zamykamy jedno oko, bierzemy do ręki mamy szminkę i zaczynamy na szybie rysować krawędzie widzianych przez nią przedmiotów. Najlepiej patrzeć na coś stosunkowo prostego – pudełko od zapałek, albo klocowaty budynek. Po chwili na szybie znajdzie się mniej lub bardziej udane odwzorowanie rzeczywistości – wprawdzie składające się z samych krawędzi, ale kształt pudełka (albo budynku) powinien być zachowany. Właściwie zadanie można sobie jeszcze bardziej uprościć – w przypadku pudełka wystarczy zaznaczyć położenie jego rogów, a potem połączyć je prostymi odcinkami przy linijce. Mamy odwzorowane to, co widzimy? Jakoś tam mamy. Jeżli będziemy wykonywać takie odwzorowanie co chwila, w zależności od położenia bohatera gry, a zamiast szyby użyjemy ekranu monitora – wykonaliśmy pierwszy krok na drodze do „Quake”.
To, co zrobiliśmy, nazywa się rzutowaniem i wymaga wykonania serii obliczeń – na tyle prostych, że powinien się z nimi uporać przeciętny maturzysta. Sposób postępowania powinien być następujący: najpierw tworzymy sobie jakiś świat, składający się ze ścian (zupełnie jak pudełko zapałek). Potem wymyślamy sposób przechowywania informacji o tym świecie (sprawy nie ruszam, bo napisano na ten temat kilka opasłych książek; do prostych zadań jak ktoś potrzebuje sam sobie coś na poczekaniu wymyśli. Potęga „DOOMa” i gier mu podobnych polega właśnie na odpowiednich strukturach danych, w których przechowywane są informacje o świecie gry). Teraz rzutujemy położenie wszystkich wierzchołków i odpowiednie rzuty łączymy ze sobą liniami prostymi (dociekliwych informuję, że do wykonania rzutowania wystarczy znajomość twierdzenia Talesa).
Jak na razie mamy świat, składający się z samych krawędzi (wireframe, jak pisują anglosasi). Trzeba go jeszcze po pierwsze -wypełnić mięsem, po drugie – po-zasłaniać te kawałki, których ma nie być widać. Zacznijmy od tego drugiego. O ile wszystkie wieloboki, z których składają się ściany obiektów w naszym świecie spełniają kilka warunków (wystarczy, że są wypukłe i nigdy się nie przecinają), sprawa jest mało skomplikowana – trzeba je posortować tak, żeby zawsze te, które przesłaniają inne były przed nimi. Z listy wyrzucamy wszystkie te wieloboki, których widoczne powierzchnie nie są skierowane w stronę ekranu. Teraz rysujemy je w kolejności od najdalszych do najbliższych – i sprawa załatwiona, to co miało być zasłonięte jest zasłonięte! Wypełnianie mięsem jest stosunkowo proste – mamy położenia wierzchołków, jeżeli nie zapomnieliśmy w naszych danych zapisać informacji o tym, które wierzchołki tworzą które wieoboki, wystarczy te wieloboki wypełnić. Wprawdzie świat będzie strasznie płaski (podobny do videoklipu „Money for nothing” Dire Straits), ale da się poznać co jest co. W grach odpowiednikiem tej techniki będzie oczywiście „Retaliator”, albo – z nowszych tytułów – amigowy hit z ostatnich miesięcy „Desert Wolf”.
Weźmy się za „uplastycznianie” naszego świata. Najprostszy sposób polega na zastąpieniu gładkich, jednobarwnych wieloboków, wielobokami cieniowanymi. Wystarczy, że jedna krawędź wieloboku jest ciemniejsza, druga jaśniejsza, a barwa powierzchni między obydwoma krawędziami zmienia się liniowo, żeby obrazek nabrał głębi, a technika zyskała nazwę cieniowania Gourauda. Tak, tak – cieniowanie Gourauda polega właśnie na tym, że kolor zmienia się w sposób liniowy. Wyznaczenie barw krawędzi jest już bardziej skomplikowane – można albo na rympał przyjąć, że jedna krawędź jest jaśniejsza, a druga ciemniejsza (pamiętając tylko, że krawędź należy równocześnie do DWÓCH wieloboków), albo spróbować opracować jakiś algorytm, który określi barwę krawędzi na podstawie barwy wieloboku i jego ustawienia w stosunku do ekranu i jakiegoś źródła światła. Gwoli ścisłości dodam, że metoda na rympał sprawdziła się już w kilku całkiem niezłych tytułach. Najlepsze efekty uzyskuje się jednak w nieco inny sposób, polegający na nakładaniu na wieloboki tekstur. Czego? No, tych, tekstur, przecież napisałem. Zaraz będzie dokładniej.
Tym razem do eksperymentu myślowego posłuży nam łopata bojowa herbu młot. Jej uproszczona wersja, przygotowana na potrzeby kolejnej wersji „Toshindena”, powinna się składać z bardzo długiego prostopadłościanu (stylisko) i bardzo płaskiego prostopadłościanu (sama łopata). Herbowy młotek znajduje się na płaskim prostopadłościanie. Namalujmy go na prostokątnym kawałku gumy, po czym znanym nam już sposobem narysujmy łopatę szminką na szybie. Miejsce, w którym powinien znajdować się młotek nie ma już wiele wspólnego z prostokątem – o ile nie zrobiliśmy żadnego błędu, mamy do czynienia z jakimś rombem. Weźmy teraz nasz kawałek gumy z młotkiem i naciągnijmy go tak, żeby krawędzie gumy pokrywały się z liniami zrobionymi szminką na szkle. Wygląda nieźle, nie sądzicie?
A teraz o tym, jak to się robi w praktyce, bo nikt w komputerze nie będzie się zajmował naciąganiem gumy na łopatkę. Wyobraźcie sobie narysowany na ekranie romb (czyli łopatę). Każdy punkt na jego krawędzi odpowiada jakiemuś punktowi na krawędzi naszej tekstury z młotkiem, policzenie który punkt odpowiada któremu znowu nie jest trudniejsze, niż zadanie maturalne z matematyki dla klasy humanistycznej (bardzo humanistycznej). Teraz robimy coś takiego – nakładamy teksturę na ekran poziomymi paskami (albo pionowymi). Robimy tak dlatego, że w ten sposób najłatwiej jest adresować pamięć ekranu, a co za tym idzie algorytm będzie bardzo szybki. Ponieważ już przed chwilą ustaliliśmy, że potrafimy odpowiednio poprzeliczać punkty na krawędziach, robimy właśnie taką operację. Bierzemy poziomy (albo pionowy) odcinek na ekranie, obliczamy, które punkty na krawędziach naszej tekstury odpowiadają końcom odcinka na ekranie i zaczynamy kolejne piksele (będą się od tego momentu nazywać tekselami, od TEXture ELement) odcinka łączącego punkty na krawędziach tekstury przenosić na ekran, na którym staną się kolejnymi pikselami poziomego (lub pionowego) odcinka. Uff, powyższe zdanie to straszny potworek, ale chyba nie jest trudniejsze do zrozumienia niż algorytm, który opisuje.
W sumie sprawa sprowadza się do wycięcia z tekstury odpowiedniego odcinka łączącego dwa punkty na jej krawędzi i przeniesienia go na ekran. Najszybszym znanym algorytmem wyznaczania kolejnych punktów odcinka jest algorytm Bresenhama i to on jest tu stosowany – jak już kiedyś wspominałem (choć nie wprost), bez algorytmu Bresenhama nie dałoby się chyba napisać żadnej sensownej gry (sensownej zarówno graficznie, jak i dźwiękowo). Oczywiście, ponieważ długość odcinków pobieranego z tekstury i rysowanego na ekranie nie jest zwykle taka sama, trzeba się liczyć z kłopotami – niektóre piksele tekstury będą niewykorzystane, inne wykorzystywane kilkakrotnie (tak powstaje sławetna „pikseloza” w „DOOMie”).
Oczywiście tak przenoszone tekstury nie wyglądają najlepiej, w zależności od odległości giną w nich piksele, albo niektóre rosną aż do bólu, jednak w szybkiej, dynamicznej grze nie ma to znaczenia – mózg i oko ludzkie łatwo dają się oszukać, wypełniając dziury treścią, której na ekranie nie było. Sytuacja nie jest zresztą tak tragiczna jak mogłoby się zdawać -popatrzcie na obrazki. O ile mały młotek wygląda tak sobie po wykonaniu na nim różnych operacji, ten prawdziwszy w trakcie obrotów wygląda całkiem nieźle (oryginalna tekstura którą obracałem miała rozmiary 100 na 100 pikseli, mały młotek miał 32 na 32).
Zastosowanie tesktur jest bardzo szerokie – w ten sposób można nałożyć na ścianę nie tylko cegły, ale i drzwi, okno, a nawet zawartość widzianego przez to okno pokoju (choć w tym ostatnim wypadku efekty będą nieszczególne).
Teraz mamy już z głowy praktycznie wszystkie elementy, niezbędne do pisania „DOOMa” (poza strukturami danych oczywiście). Żeby stworzyć „Quake”, trzeba dołożyć coś jeszcze. Tym czymś będzie jakaś technika poprawiająca wygląd tekstur w sytuacjach ekstremalnych – kiedy są oglądane z bardzo bliska albo bardzo daleka. Napisałem „jakaś” celowo, bo nie mam zamiaru wnikać za bardzo w szczegóły – stosuje się tych technik kilka, z czego najwygodniejsza do wykorzystania (bo mało obciążająca procesor, choć dość pamęciożerna) polega na użyciu kilku tekstur do pokrywania tego samego obiektu (tzw. MIP mapping). Im obiekt jest dalej, tym mniejszej tekstury się używa. Można stosować również techniki polegające na aproksymowaniu barw pikseli na ekranie na podstawie barw sąsiadujących ze sobą pikseli tekstury, co jest jednak bardzo obliczeniochłonne.
Są jeszcze dwie techniki, o których warto wiedzieć. Pierwsza dotyczy tworzenia obrazów, w których tworzące je wieloboki mogą się przecinać, mogą być wklęsłe, a nawet wcale nie muszą być wielobokami i to w dodatku płaskimi. Ten cud techniki nazywa się Z-buffer i polega na zapamiętywaniu dla każdego rysowanego na ekranie piksela odległości odpowiadającego mu elementu od rzutni (czyli ekranu). Oczywiście piksel zaznacza się tylko wtedy, gdy jest bliżej obserwatora niż już narysowany – w ten sposób (niestety, pamięcio- i procesorożerny) można uzyskać znacznie lepsze efekty niż przy sortowaniu wieloboków.
Druga rzecz nie jest już takiego kalibru, ale też warto ją wspomnieć – chodzi o mgłę, czyli efekt atmosferyczny. Sprawa jest w sumie banalna – im dalej od obserwatora znajduje się rysowany piskel, tym bardziej jego barwę miesza się z jakimś dowolnie wybranym kolorem tła. Wprawdzie znowu wymaga to dodatkowych obliczeń, jednak efekt bywa tego wart.
Dlaczego wspomniałem na początku „Quake” jest chyba dla wszystkich zrozumiałe, ale co mają do tematu ViRGE i Direct3D? Oj mają, i to bardzo dużo. ViRGE potrafi wszystkie opisywane tu operacje (poza rzutowaniem) wykonywać sprzętowo, odciążając procesor. A Direct3D to biblioteka, zawierająca procedury wykonujące wszystkie opisywane tu czynności. Zadaniem piszącego grę pod Windows 95 będzie więc przygotowanie tylko tej części programu, która odpowiada za przekształcenia geometryczne i rzutowanie – samo rysowanie i teksturowanie można zwalić na Di-rect3D. Co chyba najistotniejsze -jeżeli w komputerze znajdować się będzie karta graficzna z kością ViRGE albo dowolną inną, potrafiącą sprzętowo wykonać operacje teksturowania, Direct3D umyje ręce (?) i przekaże zadanie dalej sprzętowi, odciążając tym samym programistę od pisania driverów do wszystkich możliwych kości akceleratorów. Dlatego niestety gry pod Windows 95 mogą zwyciężyć, choć – jak już wspominałem na początku – kilka rzeczy w tych Windowsach nie wyszło…
Naczelny
PS. Kilka razy pisałem w tekście (z właściwą sobie swobodą omijając sprawę jako oczywistą) o „obliczaniu barwy”. W trybach 256 kolorowych nie jest to tak proste i wymaga bardzo starannego doboru palety, jednak można to sobie bardzo prosto wyobrazić w trybie True Color – każda składowa (Red, Green, Blue) przyjmuje wartości z przedziału 0..255 (całkiem ciemno, zupełnie jasno), tym samym przejście od koloru niebieskiego do czerwonego to nic innego, jak start z punktu (0, 0, 255) i dojście do punktu (255, 0, 0). Jak nie trudno zgadnąć, po drodze natkniemy się między innymi na punkt (128, 0, 128), w którym będzie nam całkiem fioletowo.
<- Uniwersytet Gracza 53 |