www.math.uni.lodz.pl/ radmat
Cel wykładu Celem wykładu jest prezentacja koncepcji podwójnego buforowania w grach 2D oraz animacja oparta na sprite ach.
Gry 2D Może wydawać się to dziwne, ale gry 2D są wciąż bardzo popularne, szczególnie jeśli wziąć pod uwagę upowszechnienie urządzeń mobilnych. Swoją popularność zawdzięczają także faktowi, iż do ich stworzenia potrzebny jest o wiele mniejszy budżet oraz zespół (analitycy, graficy, programiści, itp.) niż w przypadku pisania gier trójwymiarowych.
Renderowanie gier 2D Aby w pełni zrozumieć renderowanie 2D, należy najpierw zapoznać się z ograniczeniami urządzeń, na których te gry są wyświetlane. Zauważmy, że metody renderowania opracowane dawniej, są nadal wykorzystywane na wyświetlaczach LCD oraz plazmowych.
Monitory CRT Przez wiele lat dominowały monitory CRT (CRT - cathode ray tube). Najważniejszy element takiego monitora stanowi kineskop, w którym znajdują się trzy działa elektronowe. Wystrzeliwują one na powierzchnię ekranu strumienie elektronów dla trzech kolorów podstawowych: czerwonego, zielonego i niebieskiego. Kiedy wiązka elektronów trafia na powierzchnię ekranu, wtedy specjalna warstwa światłoczuła zwana luminoforem generuje czerwony, zielony lub niebieski punkt. Takie trzy punkty tworzą piksel. Rozdzielczość wyświetlacza determinuje całkowitą liczbę pikseli. Na przykład wyświetlacz o rozdzielczości 300 200 pikseli składa się z 200 wierszy. Każdy taki wiersz zawiera 300 pikseli. Piksel o współrzędnych (0, 0) zazwyczaj znajduje się w lewym górnym rogu ekranu.
Rysunek: Tworzenie obrazu na ekranie CRT
Gdy narysowana zostanie cała ramka, tj. gdy wiązka elektronów zostanie wystrzelona w prawy dolny róg ekranu, kolejna ramka będzie budowana oczywiście ponownie od lewego górnego rogu ekranu. Czas pomiędzy przesunięciem działa elektronowego wystrzeliwującego elektrony z prawego dolnego rogu ekranu do lewego górnego nazywany jest VBLANK - vertical blank interval.
Zalety technologii CRT Do zalet wyświetlacza kineskopowego można zaliczyć: szybkość generowania obrazu; kontrastowy obraz o wiernym odwzorowaniu barw wygląda bardzo dobrze z niemal każdego kąta; 32-bitową głębię koloru.
Wady technologii CRT Załóżmy, że działo elektronowe znajduje się w połowie ekranu. W tym czasie główna pętla gry zaczyna generować wyjście, wobec czego zaczyna zapisywać informacje o pikselach dla następnej ramki, podczas gdy CRT ciągle rysuje poprzednią ramkę. Wówczas powstaje efekt rozdarcia obrazu, gdy jednocześnie prezentowane są dwie różne ramki.
Rysunek: Efekt rozdarcia obrazu CRT
Podwójne buforowanie Istnieje metoda rozwiązania problemu związanego z powstawaniem efektu rozdartego obrazu, zwana podwójnym buforowaniem. W tej metodzie renderowania istnieją dwa bufory przechowujące kolory. W jednej ramce główna pętla gry może zapisywać informacje do bufora A, podczas gdy CRT pokazuje dane zapisane w buforze B. Dla następnej ramki CRT będzie wykorzystywać bufor A, podczas gdy główna pętla gry będzie zapisywała informacje do bufora B. Tak długo, jak zarówno główna pętla gry, jak też CRT nie będą miały dostępu do tego samego bufora w tym samym czasie, tak długo nie będzie istniało ryzyko, że CRT narysuje niekompletną ramkę.
Aby całkowicie zapobiec efektowi rozdarcia obrazu, niezbędna jest wymiana buforów podczas operacji VBLANK. W ustawieniach grafiki dla gier komputerowych przez VBLANK rozumie się opcję VSYNC. Ponieważ wymiana buforów jest szybką operacją, to podczas gry istnieje dostatecznie dużo czasu, aby cała klatka została poprawnie zrenderowana (idealnie byłoby, gdyby czas potrzebny na wymianę między buforami był mniejszy od czasu potrzebnego do narysowania ramki przez CRT). Dopóki będzie następowała wymiana między buforami, to VBLANK zapobiegnie efektowi rozdartego obrazu.
Definicja (Wikipedia) Podwójne buforowanie (ang. double buffering) jest to technika programistyczna lub funkcja sprzętowa polegająca na tworzeniu w pamięci dwóch buforów o tym samym rozmiarze do przechowywania obrazu, który ma być wyświetlony na monitorze komputera. Obraz z jednego z buforów jest przenoszony za pomocą instrukcji przenoszenia w całości i bezpośrednio do pamięci karty graficznej, a w tym czasie na drugim buforze jest rysowana kolejna klatka animacji. Podwójne buforowanie podwaja liczbę pamięci potrzebnej przy określonej rozdzielczości, daje zatem płynniejszą animację, pozwala na uniknięcie rysowania kolejnych klatek bezpośrednio na ekranie i eliminuje lub redukuje ilość widocznych na ekranie artefaktów.
Przykład Rysunek: Zasada działania podwójnego buforowania
Przykład Rysunek: Przykład podwójnego buforowania
Niektóre gry umożliwiają wymianę między buforami, gdy tylko skończy się renderowanie, co oznacza, że może nastąpić efekt rozerwanego obrazu. Ta sytuacja może mieć miejsce, gdy gracz uruchomi grę z większą wartością liczby ramek od szybkości odświeżania obrazu. Jeśli monitor ma odświeżanie 60 Hz, to synchronizowanie wymiany między buforami do VBLANK zachowałoby wartość ramki na poziomie 60 klatek na sekundę.
Sprite Spritem w grach komputerowych nazywamy dwuwymiarowy obrazek lub animację, które mogą zostać scalone w większą scenę. Najczęściej sprite y są wykorzystywane do reprezentowania dynamiki obiektów. W prostych grach 2D, sprite y mogą być również wykorzystywane jako tło. Oryginalnie sprite y były metodami scalającymi niezwiązane ze sobą bitmapy. Wówczas stwarzały wrażenie normalnych bitmap na ekranie. Takie sprite y mogą zostać utworzone albo za pomocą obwodów elektrycznych, albo oprogramowania.
W pierwszym podejściu taki sprite jest konstrukcją sprzętową, która wykorzystuje kanały DMA do scalenia elementów wizualnych z główym ekranem, nakładającym dwa źródła obrazu. W tym podejściu CPU poleca zewnętrznym chipom pobieranie obrazów i zintegrowanie ich z głównym ekranem przy wykorzystaniu bezpośrednigo dostępu do kanałów pamięci. Wykorzystywanie zewnętrznego sprzętu zamiast samego procesora poprawia jakość przetwarzanej grafiki. Ponieważ procesor nie jest obarczony wykonywaniem prostych operacji przekazywania danych, oprogramowanie może działać szybciej.
Format sprite ów Pierwszą decyzją jaką należy podjąć chcąc wykorzystać sprite y jest ustalenie formatu grafiki, który zależy od użytego sprzętu, jak też ograniczeń pamięci. Na przykład pliki PNG zajmują mniej pamięci, ale typowy sprzęt nie jest w stanie obsłużyć tego formatu, stąd podczas ładowania gry pliki będące w formacie PNG muszą zostać przkonwertowane do innego formatu. Z drugiej strony - pliki TGA mogą zostać narysowane bezpośrednio, ale rozmiar tych plików jest za duży. W systemie ios typowym formatem plików wykorzystywanym do zapisu sprite ów jest PVR, ponieważ jest on skompresowany i może być natywnie narysowany przez sprzęt.
Rysowanie sprite ów Załóżmy, że w grze 2D mamy scenę zawierającą tło i postać na środku tejże sceny. Najprostszy sposób narysowania takiej sceny polega na narysowaniu tła, a dopiero potem samej postaci. W ten sposób malarz namalowałby na płótnie taką scenę. Stąd takie podejście nosi nazwę algorytmu malarskiego. W przypadku tego algorytmu wszystkie sprite y są sortowane od tła do frontu i w taki sposób rysowane na scenie.
Przykład Rysunek: Tworzenie sceny za pomocą algorytmu malarskiego
Rysunek: Tworzenie sceny za pomocą algorytmu malarskiego
Rysunek: Tworzenie sceny za pomocą algorytmu malarskiego
Animowane sprite y Kineograf jest rodzajem animacji, najczęściej wykonywanym na rogu książki, gdzie wraz z szybkim jej przekartkowywaniem oczom oglądającego ukazuje się krótki film rysunkowy. Wrażenie animacji powstaje na podstawie szybko zmieniających się, statycznych rysunków. W wielu grach 2D animacja bazuje właśnie na zasadach tradycyjnego kineografu.
W celu stworzenia płynnej animacji powinno się używać minimum 24 klatek na sekundę, czyli tyle, ile jest wykorzystywanych w filmach. To oczywiście oznacza, że w każdej sekundzie animacji trzeba wyświetlić 24 pojedyncze obrazki. Niektóre gry dwuwymiarowe wymagają nawet 60 klatek na sekundę, co drastycznie zwiększa liczbę potrzebnych grafik.
Typowe podejście polega na stworzeniu tablicy, która zawiera wszystkie możliwe sekwencje ruchów naszego obiektu. W powyższym przykładzie chcąc przedstawić postać płynącą na basenie musielibyśmy stworzyć 35 obrazków, a następnie odtwarzać je sekwencyjnie.
Przykładowa struktura do przechowywania animacji opartej na sprite ach struct AnimFrameData { int startframe; int numframes; } struct AnimData { ImageFile images[]; AnimFrameData frameinfo[]; }
class Sprite { ImageFile image; int drawimage; int x, y; function Draw(); } class AnimatedSprite inherits Sprite { AnimData animdata; int animnum; int framenum; float frametime; float animfps = 24.0f; function Initialize(AnimData mydata, int startinganimnum) function UpdateAnim(float deltatime); function ChangeAnim(int num); }
function AnimatedSprite.Initialize(AnimData mydata, int startinganimnum) { animdata = mydata; ChangeAnim(startingAnimNum); } function AnimatedSprite.ChangeAnim(int num) { animnum = num; framenum = 0; animtime = 0.0f; int imagenum = animdata.frameinfo[animnum].startframe; image = animdata.images[imagenum]; }
function AnimatedSprite.UpdateAnim(float deltatime) { frametime += deltatime; if(frametime > 1 / animfps) { framenum += frametime * animfps; if(framenum >= animdata.frameinfo[animnum].numframes) { framenum = framenum % animdata.frameinfo[animnum].numframes; } int imagenum = animdata.frameinfo[animnum].startframe + framenum; image = animdata.images[imagenum]; frametime = fmod(frametime, 1 / animfps); } }
Arkusze sprite ów Żeby stworzyć poprawną animację opartą na sprite ach, wszystkie sprite y powinny mieć ten sam rozmiar. W przeszłości wiele bibliotek wymagało, aby wymiary wszystkich obrazków były potęgą dwójki. W nowoczesnych bibliotekach graficznych to obostrzenie nie jest konieczne. Każda ramka animowanego sprite a może być oddzielnym obrazkiem. W zdecydowanej większości przypadków sprite y nie są prostokątami (którymi muszą być obrazki). Takie podejście powoduje duże straty pamięci. Weźmy obrazek o wielkości 100 kb. Jeśli jednak grafika wypełnia 85% przestrzeni, to oznacza, że tracimy 15 kb. Jeśli mamy wiele ramek, powiedzmy 100, i jedynie pięciosekundową animację, to już w tym przypadku tracić będziemy 1.5 MB.
Rozwiązanie powyższego problemu może polegać na użyciu jednego obrazka, który będzie zawierał wszystkie sprite y. Ten jeden obrazek nosi nazwę arkusza sprite ów. Wykorzystując arkusz możemy ulokować wszystkie sprite y blisko siebie, aby zredukować wolną przestrzeń i zaoszczędzić pamięć. Popularnym narzędziem do tworzenia sprite ów jest TexturePacker - https://www.codeandweb.com/texturepacker, który jest natywnie obsługiwany przez wiele bibliotek 2D. Rysunek: Pojedyncze sprite y
Rysunek: Arkusz sprite ów
Wiele procesorów graficznych musi wczytać teksturę do swojej własnej pamięci w takiej kolejności, w jakiej będą rysowane. Jeśli następowałoby przełączanie pomiędzy wieloma pojedynczymi teksturami podczas renderowania ramki, byłby zauważalny spadek wydajności, szczególnie w przypadku dużych sprite ów. Trzymając wszystkie sprite y w jednej, olbrzymiej teksturze jesteśmy w stanie wyeliminować koszt związany z przełączaniem. Jednak w zależności od liczby wykorzystywanych sprite ów, a także od ich rozmiaru, nie zawsze będziemy w stanie stworzyć jeden arkusz sprite ów. Wiele urządzeń ma zdefiniowaną maksymalną wielkość tekstury, którą jest w stanie obsłużyć, np. sterownik używany w ios nie obsłuży tekstury o rozmiarze większym od 2048 2048 pikseli.
Przewijanie W relatywnie prostych grach jak Pac-Man czy Tetris wszystko odbywa się na jednym ekranie. W bardziej złożonych grach 2D świat gry jest większy od pojedynczego ekranu. W tego typu grach niezbędne jest przewijanie ekranu.
Przewijanie względem jednej współrzędnej W tego typu przewijaniu ekran gry można przewijać w jednym kierunku: albo względem współrzędnej x, albo względem współrzędnej y. W przypadku gier, w których można za jednym razem wczytać wszystkie grafiki do pamięci, można podzielić tło na segmenty odpowiadające wielkości ekranu. Wczytywanie takich segmentów i umieszczanie ich na odpowiedniej pozycji jest prostą operacją.
Przewijanie nieskończone Nieskończone przewijanie występuje w grach, w których gra jest kontynuowana dopóki gracz nie przegra. Oczywiście nie może być nieskończonej liczby segmentów, wobec czego w pewnym punkcie gry jakiś obrazek musi zostać powtórzony.
Przewijanie zmienne Przewijanie zmienne jest techniką wykorzystywaną w grach komputerowych, gdzie obrazki będące tłem przybliżają się do kamery wolniej, niż obrazki będące bliżej pierwszego planu, co stwarza w scenie 2D wrażenie głębi. W przewijaniu zmiennym tło jest podzielone na wiele warstw o różnej głębi. Każda taka warstwa przewija się z różną prędkością - najdalsza najwolniej, a najbliższa warstwa najszybciej.
Rysunek: Prędkość przewijania x
Rysunek: Prędkość przewijania 0.5x
Rysunek: Prędkość przewijania 0.25x
Rysunek: Warstwy przesuwające się z różną prędkością
Przewijanie czterostronne W przewijaniu czterostronnym - które występuje w większości gier typu Super Mario - świat gry jest przewijany poziomo i pionowo. Ponieważ przewijanie występuje w obu kierunkach, to na ekranie jednocześnie mogą być widoczne cztery segmenty.