Praca (część teoretyczna)
Transkrypt
Praca (część teoretyczna)
WYDZIAŁ ELEKTROTECHNIKI, AUTOMATYKI, INFORMATYKI I INŻYNIERII BIOMEDYCZNEJ KATEDRA INFORMATYKI STOSOWANEJ Praca dyplomowa magisterska Renderowanie szczegółowego terenu z użyciem teselacji Detailed terrain rendering using tessellation Autor: Piotr Martynowicz Kierunek studiów: Informatyka Opiekun pracy: dr inż. Grzegorz Rogus Kraków, 2014 Oświadczam, świadomy odpowiedzialności karnej za poświadczenie nieprawdy, że niniejszą pracę dyplomową wykonałem) osobiście i samodzielnie i że nie korzystałem ze źródeł innych niż wymienione w pracy. …………………………………………………………………… Pracę dedykuję moim Rodzicom – jako podziękowanie za trud włożony w zapewnienie mi odpowiedniej edukacji. 2.1. Przedmowa 5 1 SPIS TREŚCI 1 SPIS TREŚCI ....................................................................................................................................... 5 2 WSTĘP .............................................................................................................................................. 7 2.1 2.2 3 PRZEDMOWA ..................................................................................................................................... 7 CEL PRACY ......................................................................................................................................... 8 WYMAGANIA I TECHNOLOGIE ........................................................................................................ 11 3.1 DECYZJE .......................................................................................................................................... 11 3.2 C++ ............................................................................................................................................... 11 3.3 DIRECTX 11.0 .................................................................................................................................. 12 3.4 VISUAL STUDIO 2012 ........................................................................................................................ 12 3.5 ASSIMP ........................................................................................................................................... 16 3.6 INNE BIBLIOTEKI ................................................................................................................................ 16 3.6.1 stb_image ................................................................................................................................ 16 3.6.2 SimpleJSON .............................................................................................................................. 16 3.6.3 WinAPI ..................................................................................................................................... 17 3.6.4 DirectXMath............................................................................................................................. 17 3.6.5 Boost ........................................................................................................................................ 17 3.7 AUTORSKIE ROZWIĄZANIA ................................................................................................................... 17 4 ZAGADNIENIA TEORETYCZNE .......................................................................................................... 19 4.1 TESELACJA ....................................................................................................................................... 19 4.1.1 Zasada działania ...................................................................................................................... 19 4.1.2 Etapy potoku renderującego.................................................................................................... 20 4.1.3 Zalety i wady ............................................................................................................................ 24 4.2 TESELACJA PHONGA .......................................................................................................................... 27 4.3 HEIGHTMAPA ................................................................................................................................... 29 4.4 MAPA OBIEKTÓW TERENU .................................................................................................................. 31 4.5 DISPLACEMENT MAPPING ................................................................................................................... 34 4.6 OŚWIETLENIE ................................................................................................................................... 36 4.6.1 Podstawowe informacje .......................................................................................................... 36 4.6.2 Użyte wzory ............................................................................................................................. 37 4.6.3 Typy świateł ............................................................................................................................. 39 4.6.4 Metody cieniowania pikseli ..................................................................................................... 39 4.7 NORMAL MAPPING............................................................................................................................ 40 4.8 FRUSTUM CULLING ............................................................................................................................ 41 4.8.1 Bounding volume ..................................................................................................................... 41 4.8.2 Frustum .................................................................................................................................... 42 4.8.3 Zasada działania ...................................................................................................................... 43 4.9 INSTANCING ..................................................................................................................................... 44 4.10 SKYBOX ........................................................................................................................................... 44 5 IMPLEMENTACJA ............................................................................................................................ 47 5.1 OBSŁUGA APLIKACJI ........................................................................................................................... 47 Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 6 2.1. Przedmowa 5.2 ŁADOWANIE MODELI I TWORZENIE MATERIAŁÓW ..................................................................................... 48 5.2.1 Elastyczny system ładowania modeli ....................................................................................... 48 5.2.2 Pliki materiałów ....................................................................................................................... 49 5.2.3 Ładowanie modeli .................................................................................................................... 54 5.3 TEREN ............................................................................................................................................. 54 5.3.1 Wykorzystanie teselacji do renderowania terenu .................................................................... 54 5.3.2 Automatyczne teksturowanie terenu ....................................................................................... 56 5.4 WODA ............................................................................................................................................ 58 5.5 OBIEKTY .......................................................................................................................................... 60 5.6 NIEBO ............................................................................................................................................. 65 5.7 UŻYTE MODELE ................................................................................................................................. 66 5.8 WYDAJNOŚĆ .................................................................................................................................... 66 6 PODSUMOWANIE ........................................................................................................................... 69 6.1 6.2 REALIZACJA CELÓW ............................................................................................................................ 69 MOŻLIWE USPRAWNIENIA I PERSPEKTYWY ROZWOJU ................................................................................ 69 7 PRZYPISY ........................................................................................................................................ 71 8 BIBLIOGRAFIA ................................................................................................................................. 73 Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 2.1. Przedmowa 7 2 WSTĘP 2.1 PRZEDMOWA Podstawą współczesnej trójwymiarowej grafiki czasu rzeczywistego jest siatka (ang. mesh) złożona z prymitywów (ang. primitives), najczęściej – trójkątów, na którą „nałożony” jest materiał definiujący kolor powierzchni. Liczba trójkątów bezpośrednio przekłada się na stopień szczegółowości modelu – im więcej jest trójkątów i im są one mniejsze, tym wierniej oddają modelowany kształt, tym gładsza/bardziej zaokrąglona jest powierzchnia i tym lepiej model wygląda. Niestety liczba trójkątów jest również wprost proporcjonalna [1] do ilości pracy, jaką karta graficzna musi wykonać, i odwrotnie proporcjonalna do wydajności aplikacji (liczba FPS). Dlatego od początku grafiki czasu rzeczywistego programiści skupiali się na szukaniu sposobów „oszukiwania”, przybliżania i upraszczania efektów graficznych, jakie chcieli uzyskać. Przykładowo bardzo prosta (ang. low poly – mało wielokątów) siatka może wyglądać bardzo ładnie i udawać znacznie dokładniejszy model, jeśli oświetlenie będziemy obliczać osobno dla każdego piksela, jaki model zajmuje na ekranie (ang. per pixel lighting), zamiast dla każdego wierzchołka siatki (ang. per vertex lighting). Jednak nawet takie metody mają swoje ograniczenia, których nie da się przeskoczyć (np. na brzegach modelu zawsze będzie widać kanciaste kształty przy mało dokładnej siatce modelu). Dlatego równolegle z technikami i sztuczkami wymyślanymi przez programistów twórcy sprzętu bezustannie pracują nad zwiększeniem mocy obliczeniowej kart graficznych. Czasami poprzez bezpośrednie zwiększanie prędkości przetwarzania danych, a czasami poprzez wymyślanie nowych sposobów wykorzystania istniejących rozwiązań i omijanie tzw. wąskich gardeł (ang. bottleneck). Jednym z takich rozwiązań jest sprzętowa teselacja (ang. tessellation). Głównym wąskim gardłem współczesnych kart graficznych nie jest prędkość przetwarzania dużej liczby wierzchołków/trójkątów, ale samo przesyłanie danych modelu z procesora komputera do karty graficznej (ang. bandwidth). Pomysł jest taki, że żeby zamiast grafik 3D tworzył bardzo szczegółową siatkę modelu (której przesłanie z CPU do GPU zajęłoby dużo czasu), stworzył on uproszczoną wersję modelu (o znacznie niższej liczbie trójkątów) oraz np. displacement mapę (pojęcie to zostanie szczegółowo wyjaśnione w dalszej części pracy). Uproszczona siatka modelu może zostać znacznie szybciej przesłana do karty graficznej, która to, na podstawie displacement mapy lub innych wytycznych, sama wygeneruje dodatkowe wierzchołki siatki, omijając tym samym wąskie gardło. Okazuje się bowiem, że, paradoksalnie, przesyłanie uproszczonego modelu i generowanie dodatkowych wierzchołków co każdą klatkę animacji jest znacznie szybsze niż przesyłanie do karty graficznej gotowego modelu. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 8 2.2. Cel pracy Technika ta ma wiele zalet, które zostaną opisane w dalszej części pracy, jednak ma też kilka wymagań, które trzeba spełnić, aby móc jej używać. Po pierwsze nie każdy sprzęt (w tym konsole) ją obsługuje, po drugie wymaga innego, specjalnego przygotowania modeli, więc użycie jej to poważna decyzja dla twórców aplikacji, a po trzecie wreszcie nieumiejętne zastosowanie teselacji może znacząco pogorszyć wydajność aplikacji nie poprawiając (znacząco) jej wyglądu. Z tych powodów, pomimo istnienia technologii na rynku już od paru lat, niewiele gier ją wykorzystuje (teselacja, jak i cała grafika czasu rzeczywistego, może być oczywiście wykorzystywana w innych celach, np. do różnorakich symulacji, ale odsetek innych zastosowań jest znacznie mniejszy niż w przypadku gier komputerowych i autor nie posiada danych dot. jej zastosowania). Jednak wraz z pojawieniem się na rynku konsol obsługujących teselację (PlayStation 4 i Xbox One) i zwiększaniem się udziału w rynku kart graficznych dla komputerów domowych obsługujących tę technologię można mieć nadzieję, że coraz więcej gier będzie używać teselacji, umożliwiając graczom oglądanie pięknych szczegółowych scenerii i modeli. 2.2 CEL PRACY Celem niniejszej pracy jest implementacja i prezentacja efektów zastosowania teselacji do renderowania szczegółowego terenu. Aplikacja renderuje duży otwarty teren (wyspę tropikalną, cały teren ma powierzchnię 1 km2) i umożliwia użytkownikowi swobodne poruszanie się po całym jego obszarze. Wyspa jest pokryta roślinnością i otoczona wodą. Jednak wszystkie te elementy stanowią jedynie pretekst do pokazania możliwości teselacji i jej zastosowania w kilku różnych aspektach renderowania terenu. Przez „teren” autor rozumie zarówno sam ląd (wyspę), jak i różne obiekty znajdujące się na niej (rośliny, trawę, kamienie) czy wodę otaczającą wyspę. Wszystkie te elementy bardzo mocno wykorzystują teselację, dając zarówno ładny wizualnie efekt, jak i dobrą wydajność. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 2.2. Cel pracy 9 Rysunek 1. Widok z kamery po uruchomieniu aplikacji. Jedynym wyjątkiem jest trawa, która nie jest teselowana (dokładniej – ma zawsze współczynnik teselacji równy 1). Wynika to z tego prostego powodu, że pokrycie tak dużego obszaru modelami kępek trawy wymaga wyrenderowania dziesiątek albo nawet setek tysięcy kopii (sic!) tego modelu w każdej klatce. Dlatego model powinien być bardzo prosty i mieć minimalną liczbę geometrii. Jednak gdyby pokryć trawą mniejszy obszar, można by użyć teselacji również na niej, umożliwiając np. ładną animację pochylania się kłosów trawy na wietrze. W następnym rozdziale pracy zostaną omówione użyte technologie i biblioteki. Rozdział czwarty przedstawia i opisuje zagadnienia teoretyczne związane z pracą. Przede wszystkim wyjaśnia, czym jest i jak działa teselacja, oraz przedstawia jej zalety i wady, a także wyjaśnia znaczenie innych pojęć i technik użytych w pracy. Rozdział piąty opisuje, jak te zagadnienia zostały zaimplementowane w pracy oraz jakie rozwiązania autor zastosował w aplikacji. W ostatnim rozdziale dokonano podsumowania, a także wskazano możliwości poprawy i rozwoju projektu. Ponadto – w branży gier komputerowych wiele pojęć, które zostały wymyślone przez anglojęzycznych twórców, zostało przyjętych do języka polskiego w oryginalnej wersji i jest powszechnie używanych i rozumianych. Autor pracy nie uznał za celowe podejmowania prób ich tłumaczenia i tworzenia polskich odpowiedników, które i tak nie byłyby zrozumiałe w środowisku. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 10 Renderowanie szczegółowego terenu z użyciem teselacji 2.2. Cel pracy Piotr Martynowicz 3.1. Decyzje 11 3 WYMAGANIA I TECHNOLOGIE 3.1 DECYZJE Przed rozpoczęciem pracy bardzo ważne jest określenie zakresu pracy i wymagań oraz wybranie odpowiednich technologii i narzędzi. Dobre przygotowanie pozwoli oszczędzić zbędnej pracy, skupić się na istocie zagadnienia i wykonać pracę efektywnie. Cel pracy został przedstawiony w poprzednim rozdziale. Aplikacja ma renderować przykładowy otwarty teren z modelami rozmieszczonymi na nim. Praca nie zakładała korzystania z żadnego gotowego silnika graficznego. Zamiast tego, autor stworzył własny silnik graficzny od zera, który potrafi wczytać modele 3D zapisane w popularnych formatach, wraz z teksturami, załadować przygotowane specjalnie shadery, wczytać heightmapę i wygenerować teren wyspy, i wreszcie całość wyrenderować. Autor uznał, że celowanie w platformy Windows i komputery PC oraz laptopy będzie wystarczające na potrzeby tej pracy. Autor zdecydował się na język C++, środowisko programistyczne Visual Studio 2012 i bibliotekę DirectX 11.0. Do wczytywania modeli 3D użyto biblioteki Assimp, a do wczytywania tekstur –stb_image. Program używa także plików w formacie JSON, które są wczytywane za pomocą biblioteki SimpleJSON. Do operacji na macierzach i wektorach wykorzystano DirectXMath. Aplikacja posiada również własny system logujący i wyświetlający błędy i informacje diagnostyczne. 3.2 C++ Podstawowym wyborem jest kwestia języka programistycznego. Na rynku dostępnych jest wiele opcji, jednak nie wszystkie nadają się do implementacji silnika graficznego. Podstawowym kryterium wyboru jest szybkość działania i kontrola nad operacjami, jakie program faktycznie wykonuje podczas działania. Tutaj numerem jeden jest język C++, którego kod jest kompilowany do kodu maszynowego (brak maszyny wirtualnej), w przeciwieństwie do paru innych popularnych języków, takich jak Java czy C#. To po prostu daje szybciej wykonujący się program oraz większy determinizm działania (np. brak włączającego się w losowych momentach Garbage Collectora). Choć C++ jest językiem wysokopoziomowym, daje on bezpośredni dostęp do pamięci (wskaźniki) i kontrolę nad jej użyciem (brak Garbage Collectora). W tym sensie jest on „mniej wysokopoziomowy” niż wspomniane C# i Java, ale przy programowaniu grafiki jest to pożądana cecha. Innym ważnym czynnikiem jest dostępność bibliotek graficznych dla danego języka programistycznego (bo bez nich nie da się stworzyć silnika graficznego). Wybrany DirectX został stworzony dla języków C/C++, więc to kryterium jest spełnione. Warto Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 12 3.3. DirectX 11.0 dodać, że istnieje open-source’owa wersja biblioteki – SharpDX – dla platformy .NET. W przypadku Javy nie jest już tak prosto. Poza tym DirectX jest dostępny jedynie na platformy Microsoftu, więc zamiast używać Javy, można równie dobrze użyć C#. Jeszcze jednym ważnym argumentem jest popularność języka i dostępność dokumentacji, przykładów, tutoriali i dodatkowych bibliotek dla danego języka – czasem sam brak odpowiedniego wsparcia technicznego może uniemożliwić rozwijanie projektu. Język C++ jest jednym z najczęściej używanych, więc wybranie go pod tym względem nie może być złą decyzją. 3.3 DIRECTX 11.0 Jak wspomniano wcześniej – nie da się stworzyć własnej aplikacji graficznej bez użycia jakiejś biblioteki graficznej. [2] Dla grafiki 3D na rynku istnieją 2 dominujące rozwiązania: DirectX i OpenGL (technicznie rzecz biorąc DirectX to kilka bibliotek do różnych zadań, część odpowiedzialna za grafikę nazywa się Direct3D, jednak nazwa DirectX jest często używana na określenie samej części graficznej). Ta pierwsza to produkt firmy Microsoft i jest przeznaczona wyłącznie na platformy tej firmy (systemy Windows oraz konsole Xbox), jej kod jest zamknięty. Ta druga jest biblioteką opensource rozwijaną pod kierunkiem Khronos Group i jest dostępna na bardzo wiele platform (PC – wszystkie systemy operacyjne, urządzenia przenośne, konsole). Obu natomiast można używać za darmo, obie oferują analogiczne rozwiązania, mają bardzo duże możliwość i podobną wydajność. Dlatego wybór tutaj, o ile nie jest ograniczony jakimiś odgórnymi wymaganiami (np. konkretna platforma, polityka firmy), jest dowolny – obie opcje są dobre. Dlatego autor, w celu nauczenia się nowej technologii, zdecydował się na DirectX. Natomiast ważna tutaj jest wersja biblioteki. Teselacja bezpośrednio narzuca wersję 11.0 lub wyższą, gdyż dopiero od tej wersji DirectX wspiera tę technikę. Ta wersja bezpośrednio pociąga za sobą wersję systemu Windows (7 lub wyższa), co wynika z polityki Microsoftu. To również daje konkretne ograniczenia, jeśli chodzi o karty graficzne. DirectX 11.0 jest obsługiwany przez karty Ati od serii HD 5xxx oraz przez karty Nvidia od serii GTX 4xx. Obecnie większość (jeśli nie wszystkie) nowych komputerów sprzedawanych w sklepach posiada sprzęt i system (o ile jest to Windows) obsługujących teselację, jednak bardzo wielu ludzi posiada komputery kupione wcześniej, które nie spełniają tych wymagań. To oznacza, że nie każdy może skorzystać z teselacji, jednak nowe technologie mają to do siebie, że wymagają nowego sprzętu. 3.4 VISUAL STUDIO 2012 Wybór środowiska programistycznego (IDE) ma mniejsze znaczenie, aczkolwiek dobre środowisko może znacznie ułatwić i przyspieszyć proces tworzenia. Visual Studio Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 3.4. Visual Studio 2012 13 jest również produktem firmy Microsoft i jest jednym z najbardziej popularnych IDE. Jest to bardzo rozbudowane narzędzie, które posiada swój własny kompilator języka C++ (o wysokim stopniu zgodności ze standardem), bogaty i konfigurowalny edytor z podświetlaniem składni i wieloma narzędziami ułatwiającymi tworzenie dużych projektów programistycznych, wiele wtyczek rozszerzających jeszcze jego możliwości oraz zaawansowany debugger. Co ważne, od wersji 2012 VS posiada również wbudowany graficzny debugger do debugowania aplikacji używających DirectX. Pozwala on na debugowanie kodu shaderów (rysunek 2), podgląd listy wywołań API wysłanych do karty graficznej, sprawdzanie stanu i zawartości buforów (rysunki 3 i 4), tekstur i zasobów używanych przez aplikację itd… Na chwilę obecną nie działa on idealnie (autor podczas tworzenia pracy znalazł przypadki, w których debugger np. nie potrafił debugować shaderów albo wyświetlał nieprawidłowe wartości), ale i tak okazał się pomocny. Jeśli w przyszłości narzędzie zostanie poprawione i zawsze będzie działać poprawnie, to będzie nieocenione przy tworzeniu aplikacji graficznych – różnica między pisaniem shaderów w ciemno i zgadywaniem, co faktycznie w danym momencie robi karta graficzna, a możliwością faktycznego sprawdzenia stanu buforów i zmiennych to niebo i ziemia. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 14 3.4. Visual Studio 2012 Rysunek 2. Debugowanie shaderów w Visual Studio 2012. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 3.4. Visual Studio 2012 15 Rysunek 3. Sprawdzanie zawartości buforów w Visual Studio 2012. Rysunek 4. Graficzny podgląd buforów w Visual Studio 2012. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 16 3.5. Assimp 3.5 ASSIMP Na rynku dostępnych jest bardzo wiele różnych formatów zapisu modeli i animacji 3D i większość jest bardzo rozbudowana, skomplikowana i – niestety – bardzo słabo udokumentowana. To sprawia, że własnoręczne zaimplementowanie obsługi choćby jednego z tych formatów jest pracochłonne i trudne. Na szczęście istnieją gotowe biblioteki umożliwiające wczytywanie modeli 3D w popularnych formatach. Jedną z nich jest Assimp (Asset Import Library). Jest to open-source’owa biblioteka rozwijana przez kilku programistów jako zajęcie w wolnym czasie. Niemniej jednak jest to dobra biblioteka, która obsługuje bardzo wiele formatów i oszczędza mnóstwo pracy każdemu, kto chce móc używać tych formatów w swoim projekcie. Co znaczy, że Assimp umożliwia „wczytywanie” modeli zapisanych w różnych formatach? Otóż sposób przechowywania danych o modelu, ich format w pamięci, jest bezpośrednio zależny od konkretnej aplikacji 3D – nie ma jednego ustalonego standardu. Dlatego przyjęte jest tutaj następujące rozwiązanie: Assimp wczytuje dane z każdego wspieranego formatu do jednego, zaprojektowanego przez twórców biblioteki formatu (opisanego na oficjalnej stronie), a następnie aplikacja używająca Assimpa konwertuje te dane do swojego własnego formatu. Teoretycznie mogłaby ona używać po prostu formatu Assimpa, ale zwykle z wielu powodów byłoby to niepraktyczne i niewygodne. Oznacza to oczywiście dodatkowy czas pracy procesora, jaki trzeba poświęcić na dodatkową konwersję danych w pamięci, ale jest to bardzo niska cena za możliwość łatwego skorzystania z popularnych formatów 3D. 3.6 INNE BIBLIOTEKI 3.6.1 STB_IMAGE Wczytywanie obrazów 2D stanowi podobne wyzwanie, co w przypadku modeli 3D – mnogość formatów i ich wariantów przysparza niemałych problemów przy samodzielnej implementacji. Dlatego aplikacja korzysta z gotowej biblioteki: stb_image. Została ona stworzona przez Seana Barretta i udostępniona w formie pojedynczego pliku źródłowego. Obsługuje podstawowe formaty graficzne i jest bardzo prosta w użyciu, co było wymaganiem autora pracy przy wyborze biblioteki. 3.6.2 SIMPLEJSON Opis modelu 3D to nie tylko dane jego siatki – to również informacja o materiałach używanych przy jego renderowaniu: shadery, tekstury, sposób próbkowania itd… Informacje te można zawrzeć w kodzie programu, ale jest to bardzo nieelastyczne i mało praktyczne rozwiązanie. Dlatego autor zdecydował się na inne rozwiązanie – każdemu Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 3.7. Autorskie rozwiązania 17 plikowi z modelem 3D towarzyszy dodatkowy plik z danymi o materiałach. Pliki te przechowują dane w formacie stworzonym przez autora pracy i używają notacji JSON. Do ich wczytywania używana jest open-source’owa biblioteka SimpleJSON autorstwa Mike’a Anchora. 3.6.3 WINAPI Do stworzenia okna, w którym ma działać aplikacja, można użyć czystego WinAPI, jego opakowanej w klasy wersji – MFC lub którejś z bibliotek do tworzenia interface’ów – Qt, GTK(+), wxWidgets… Z racji braku interface’u graficznego (aplikacja wymaga jedynie pustego okna, w którym renderuje obraz) wybrano WinAPI, gdyż daje bezpośrednią kontrolę nad działaniem aplikacji, gwarantuje szybkość działania (inne biblioteki mają własne narzuty) oraz nie wymaga zaprzęgania do pracy dodatkowej biblioteki (w szczególności wielkiej – jak Qt). Zresztą każda biblioteka na platformie Windows gdzieś w swojej implementacji i tak musi użyć WinAPI. 3.6.4 DIRECTXMATH Nie da się programować grafiki 3D bez macierzy i wektorów. Są one podstawą każdej aplikacji 3D i dlatego powstał szereg bibliotek do obsługi operacji na nich. Autor zdecydował się na DirectXMath autorstwa Microsoftu. Biblioteka ta stawia na szybkość i wykorzystanie operacji SIMD. To ostatnie wymaga specjalnego sposobu korzystania z biblioteki (osobne typy do przechowywania danych i do wykonywania operacji matematycznych). 3.6.5 BOOST Program wykorzystuje również bibliotekę (a raczej ich kolekcję) boost, która rozszerza możliwości języka C++. Jest ona udostępniona na własnej, open-source’owej, licencji. Boost ma opinię biblioteki o wysokiej jakości i może być stawiana za wzór nowoczesnego projektowania i programowania w C++. Zakres dziedzin implementowanych w booście jest bardzo szeroki. 3.7 AUTORSKIE ROZWIĄZANIA Język C++ nie posiada wbudowanych typów wektorowych – to znaczy typów danych przechowujących więcej niż jedną liczbę (np. pary, trójki, czwórki). A ponieważ tego typu dane są bardzo często używane w aplikacja graficznych (pozycje, wektory prędkości, wymiary), aplikacja definiuje własne typy – Vector2/3/4 – oraz definiuje szereg operacji na nich. Są to szablony klas (więc obsługują różne typy, również zdefiniowane przez użytkownika) i posiadają kilka funkcji, które ułatwiają posługiwanie się nimi: np. do tej samej liczby można odnosić się poprzez zmienna[0], zmienna.x Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 18 3.7. Autorskie rozwiązania oraz zmienna.width albo konstruktory przyjmujące dowolne typy skalarne i wektorowe, które obsługują odpowiednią składnię. Aplikacja korzysta również z autorskiego zaawansowanego loggera. Tradycyjny logger to narzędzie, które zapisuje informacje o działaniu aplikacji przez cały czas jej wykonywania i umieszcza je w pliku (zazwyczaj oznaczonym datą i godziną). Jednak logger użyty w pracy potrafi więcej rzeczy. Po pierwsze zapewnia obsługę błędów poprzez asserty – sprawdza stan odpowiednich zmiennych podczas wykonywania programu i jeśli wykryje błąd, zgłasza to użytkownikowi aplikacji poprzez stosowny komunikat. Wiadomość o błędzie zawiera różne ważne informacje ułatwiające zlokalizowanie błędu, a ponadto sam assert umożliwia debugowanie kodu w odpowiednim miejscu. Po drugie logger zapisuje wszystkie informacje z działania aplikacji i dzieli je na 4 grupy: Debug, Info, Warning i Error. Informacje są również dzielone wg tzw. tagów. Po trzecie narzędzie to zapisuje wszystkie dane do specjalnego pliku HTML w formie strony z tabelą z danymi oraz panelem umożliwiającymi włączanie i wyłączanie kanałów, tagów, plików źródłowych itd… dzięki czemu użytkownik może przeglądać i analizować logi w przeglądarce WWW w łatwy i przejrzysty sposób. Logger ma opcje kompilowania tylko wywołań dla wybranych kanałów, dzięki czemu np. kanał Debug nie jest w ogóle kompilowany w wersji Release, co jest bardzo ważne dla wydajności aplikacji. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.1. Teselacja 19 4 ZAGADNIENIA TEORETYCZNE 4.1 TESELACJA 4.1.1 ZASADA DZIAŁANIA Jak wspomniano w przedmowie pracy, celem teselacji jest zredukowanie ilości informacji przesyłanych z CPU do GPU, gdyż przesył ten jest wąskim gardłem sprzętu w wypadku aplikacji graficznych. Aby to było możliwe, aplikacja wczytuje z dysku i co klatkę przesyła do karty graficznej uproszczoną siatkę modelu (o mniejsze liczbie wierzchołków), a karta graficzna sama generuje dodatkowe wierzchołki, uzyskując szczegółowy model. Oczywiście „uproszczona” i „szczegółowy” to pojęcia względne, zależne jedynie od założeń i potrzeb projektu, jednak aby zastosowanie teselacji dawało wymierną korzyść, szczegółowa siatka powinna mieć przynajmniej kilka razy więcej wierzchołków niż wyjściowa – uproszczona. Ponadto, teselacja sama w sobie to jedynie podział linii, trójkątów lub quadów (ang. quad = quadrilateral – czworokąt) na mniejsze części. Zteselowanie siatki nie wnosi niczego nowego – model dalej wygląda dokładnie tak samo, co prezentuje rysunek 5: Rysunek 5. Teselacja fragmentu siatki i przesunięcie wierzchołków. Pierwszy z lewej rysunek ilustruje fragment siatki modelu przed teselacją. Czerwone punkty to oryginalne wierzchołki modelu. Środkowy rysunek przedstawia efekt samej teselacji – wiele nowych wierzchołków (zielone punkty), które jednak w żaden sposób nie zmieniają kształtu modelu. Widać, że jego krawędź wciąż ma duże ostre załamania i nie prezentuje się dobrze (zakładając, że siatka przybliża jakiś okrągły kształt. Dopiero rysunek z prawej pokazuje, o co naprawdę chodzi w teselacji – sam podział na mniejsze części niczego nie daje, ale odpowiednie przesunięcie nowopowstałych Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 20 4.1. Teselacja wierzchołków pozwala uzyskać bardziej szczegółowy kształt. W tym wypadku wierzchołki zostały ułożone wg krzywej, dzięki czemu nowa krawędź modelu jest znacznie bardziej gładka i okrągła, i lepiej przybliża zaokrągloną krawędź rzeczywistego obiektu. Teselacji używa się nie tylko w celu wygładzenia krawędzi. Można ją też wykorzystać do dodania nowych detali do powierzchni, np. nierówności dla kory drzewa, małych kamyczków na powierzchni kamiennej ścieżki itd… Rysunek 6 ilustruje ten przypadek. Górny obrazek przedstawia gładką powierzchnię uproszczonego modelu, a dolny – powierzchnię szczegółowego modelu z dodanymi wypukłościami. Rysunek 6. Teselacja płaskiej powierzchni i dodanie nierówności. Jak widać, cała korzyść z zastosowania tej techniki, to odpowiednie przesunięcie nowowygenerowanych wierzchołków siatki. Można je albo układać wg pewnych matematycznych reguł (np. różnego rodzaju krzywe), wczytywać ich pozycje z wcześniej przygotowanego pliku (displacement mapping) czy choćby tworzyć losowe przesunięcia (ang. noise – szum), gdyż w niektórych przypadkach dowolne nierówności wyglądają dobrze i są pożądane. Każde podejście ma swoje zalety i wady, i niektóre zostaną opisane w dalszej części pracy. 4.1.2 ETAPY POTOKU RENDERUJĄCEGO Technika teselacji wymaga specjalnego wsparcia ze strony sprzętu oraz biblioteki graficznej. Dlatego DirectX w wersji 11.0 wprowadza 3 nowe etapy do pipeline’u (ang. rendering pipeline - potok renderujący): Hull Shader, Tessellator (teselator) i Domain Shader: Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.1. Teselacja 21 Rysunek 7. Potok renderujący w DirectX 11. [3] Hull Shader jest programowalnym etapem (tzn. programista pisze kod - shader który steruje jego wykonaniem). Jak widać na obrazku 7, Hull Shader występuje po Vertex Shaderze, dlatego HS na wejściu otrzymuje dane zwrócone przez VS. Hull Shader jest podzielony na 2 części: Constant HS i Main HS. Obie części mają dostęp do informacji o prymitywie. Constant HS jest wykonywany raz dla każdego prymitywu i jego głównym zadaniem jest wyliczenie i zwrócenie współczynnika teselacji (ang. Tessellation Factor), który określa stopień podziału prymitywu na mniejsze części przez teselator. Main HS jest wykonywany raz dla każdego punktu kontrolnego (ang. Control Point). Umożliwia on wykonanie na nich operacji w oparciu o informację o całym Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 22 4.1. Teselacja prymitywie i powinien zwrócić na wyjściu pozycję punktu kontrolnego. Współczynniki teselacji są kierowane do teselatora, a punkty kontrolne do Domain Shadera. Teselator nie jest programowalnym etapem - działa automatycznie i na podstawie dostarczonych mu danych dzieli prymitywy na mniejsze części, tworząc nowe wierzchołki. DirectX definiuje 3 sposoby podziału: integer, even-fractional i oddfractional. integer dzieli dany element na równe części (współczynnik teselacji oznacza właśnie liczbę tych części), co oznacza, że wartość wyrażona ułamkiem jest zaokrąglana (w górę) do najbliższej wartości całkowitej. To w efekcie sprawia, że nawet przy płynnej zmianie współczynnika teselacji użytkownik widzi nagłe przeskoki (nagłe pojawianie się/znikanie wierzchołków), co nie wygląda ładnie. Problem ten rozwiązują 2 pozostałe sposoby podziału. W ich wypadku element nie jest dzielony na równe części - jedna z części jest mniejsza (wg wartości ułamkowej) i płynna zmiana współczynnika teselacji powoduje płynną zmianę wyglądu siatki. even i odd wskazują, przy jakich wartościach liczba części jest zwiększana - parzystych lub nieparzystych. Hull Shader zwraca 2 typy współczynników teselacji: dla krawędzi i dla wnętrza prymitywu. Te pierwsze są definiowane dla każdej krawędzi (3 dla trójkąta i 4 dla quada) i określają, na ile części należy podzielić daną krawędź (każda może mieć inny współczynnik). Te drugie są definiowane w zależności od prymitywu (1 dla trójkąta i 2 dla quada: poziomo i pionowo) i definiują, na ile części podzielić wnętrze prymitywu. Należy zwrócić tu uwagę na 2 rzeczy. Po pierwsze "wnętrze" nie jest precyzyjnym określeniem i jest to tylko wskazówka dla teselatora określająca, czy prymityw powinien być bardziej, czy mniej dokładnie podzielony. [4] Po drugie nie dla każdych wartości podział jest możliwy i w takim wypadku teselator dokona najbliższego możliwego podziału. Domain Shader również jest programowalnym etapem. Ten etap jest wykonywany raz dla każdego wierzchołka wygenerowanego przez teselator i ma dostęp do informacji o prymitywie. Domain Shader ma 2 główne zadania. Po pierwsze musi, na podstawie dostarczonych mu punktów kontrolnych i współrzędnych UV, wyliczyć, poprzez interpolację, faktyczne pozycje nowych wierzchołków siatki. Po drugie musi je przekształcić do przestrzeni projekcji, aby DirectX mógł wyliczyć ich prawidłowe pozycje na ekranie. To zadanie Domain Shader przejmuje od Vertex Shadera (który musi je wykonać, jeśli teselacja nie jest aktywna). Dane wyjściowe z Domain Shadera są przekazywane do Geometry Shadera (jeśli jest aktywny) lub Pixel Shadera. Teselator dzieli dany element na mniejsze części, uzyskując nowe wierzchołki siatki, jednak nie ustawia im sam pozycji. To zadanie leży po stronie programisty. Teselator daje jedynie informację, gdzie w dzielonej figurze powinien się znaleźć nowy wierzchołek (wg otrzymanych wytycznych), jednak tylko od programisty zależy jego ostateczna pozycja. W Domain Shaderze programista otrzymuje pozycje wierzchołków figury (trójkąta lub quada) i współrzędne nowego wierzchołka w obrębie tej figury Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.1. Teselacja 23 (teselacja w DirectX obsługuje jeszcze izolinie (ang. isolines), ale one wychodzą poza zakres tej pracy). Dla quadów współrzędne podawane są jako dwie liczby, tradycyjnie oznaczane jako UV. Rysunek 8 obrazuje sytuację: Rysunek 8. Obliczanie pozycji nowego wierzchołka w quadzie. Jak widać, quad ma 4 wierzchołki (v1 - v4), a współrzędne UV biegną wzdłuż boków figury. Pozycje wierzchołków v1 - v4 to pozycje w świecie/scenie i szukana jest pozycja nowego wierzchołka również w świecie/scenie. Natomiast współrzędne UV dotyczą przestrzeni samej figury. Tak naprawdę każda z tych współrzędnych mówi, w jakiej odległości między dwoma bokami figury znajduje się szukany punkt, czyli jest to problem interpolacji liniowej. Dlatego, aby obliczyć pozycję nowoutworzonego wierzchołka, należy wykonać 2 kroki. Najpierw należy dokonać interpolacji liniowej na bokach (v1, v2) i (v4, v3) i wyznaczyć pozycje punktów pomocniczych. Następnie należy dokonać trzeciej interpolacji - pomiędzy pozycjami punktów pomocniczych - uzyskując szukaną pozycję nowego wierzchołka. W przypadku trójkątów taki format współrzędnych oczywiście nie ma zastosowania, dlatego używa się współrzędnych barycentrycznych UVW: Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 24 4.1. Teselacja Rysunek 9. Obliczanie pozycji nowego wierzchołka w trójkącie. Powyższe metody (dla quadów i trójkątów) dotyczą wyliczania pozycji nowych wierzchołków, jednak można przy ich pomocy również znaleźć wektory normalne czy współrzędne tekstur wierzchołków. Przy czym te metody pozwalają jedynie znaleźć pozycje wierzchołków w obrębie teselowanych figur, dlatego po ich wyznaczeniu należy dokonać jakiegoś przesunięcia nowych wierzchołków (wg krzywej, displacement mappingu itd...). 4.1.3 ZALETY I WADY Zalety: Oszczędzanie pamięci i mniejsza ilość danych przesyłanych z CPU do GPU. Możliwość wykonywania części obliczeń na uproszczonych modelach. Automatyczny, ciągły i adaptacyjny LOD. Wady: Konieczność specjalnego przygotowania assetów. Ograniczenia sprzętowe. W zależności od użycia może pogorszyć wydajność zamiast ją poprawić. Pierwszą i główną zaletą teselacji (a zarazem jej istotą) jest ograniczenie rozmiaru danych modeli trzymanych w pamięci. Siatki wczytywane z plików i wysyłane do karty graficznej są uproszczone, więc zawierają (znacznie) mniej geometrii. Jak wcześniej już napisano, przesył danych na linii CPU – GPU jest wąskim gardłem aplikacji 3D, dlatego zmniejszenie rozmiaru przesyłanych znacząco wpływa na poprawę wydajności aplikacji. Ponadto uproszczone siatki modeli zajmują znacznie mniej miejsca w pamięci (zarówno Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.1. Teselacja 25 komputera, jak i karty graficznej), co nie jest tak kluczowym czynnikiem, jak wydajność, ale też jest zaletą. Drugim atutem tej techniki jest możliwość wykonywania niektórych obliczeń na uproszczonych siatkach, zamiast – na szczegółowych. Operacje takie jak animacja na kościach, morph target animation, dynamika ciał miękkich czy generalnie dowolne operacje wykonywane na wierzchołkach modelu mogą być wykonane znacznie mniejszym kosztem, jeśli operuje się na siatce mającej wiele razy mniej wierzchołków. Dotyczy to zarówno obliczeń wykonywanych na CPU przed wysłaniem modelu do karty, jak i tych wykonywanych na GPU w Vertex Shaderze (gdzie siatka jest wciąż jeszcze uproszczona). Trzecią zaletą teselacji jest możliwość uzyskania automatycznego, ciągłego i adaptacyjnego poziomu szczegółowości (ang. Level of Detail – LOD). Level of Detail to technika pozwalająca oszczędzić niepotrzebnej pracy karcie graficznej. Idea jest prosta – modele, które są blisko kamery, powinny być bardziej szczegółowe niż te, które są daleko od kamery, przez co są bardzo małe i słabo widoczne. Przykładowo jeśli kamera znajdowała się bardzo blisko modelu drzewa, to powinno być widać każdą jego gałązkę i liść, ale jeśli kamera znajdowała się bardzo daleko, to powinno być widać jedynie z grubsza przypominający drzewo kształt. Tym bardziej, że z daleka prawdopodobnie bardzo dużo modeli drzew mieściło się w kadrze (np. las) i wyrenderowanie wszystkich przy maksymalnym poziomie szczegółowości zabiłoby wydajność. Tradycyjny LOD zakładał przygotowanie przez grafika kilku wersji tego samego modelu (kilku osobnych siatek), od najbardziej do najmniej szczegółowego i podmienianie ich w locie przez aplikację w zależności od odległości od kamery. Tzn. jeśli obiekt był bardzo blisko kamery – używany był model najbardziej szczegółowy. Gdy, podczas oddalania się kamery od obiektu, odległość przekraczała pewien zdefiniowany próg, aplikacja podmieniała siatkę modelu na następną w kolejności (uproszczoną). Oczywiście przy ponownym zbliżeniu aplikacja wracała do bardziej szczegółowego modelu. Problem z tym podejściem był taki, że grafik miał więcej pracy, bo musiał przygotować kilka wersji modelu, a ponadto przeskoki między kolejnymi wersjami modelu były nagłe i jeśli było to widoczne podczas działania aplikacji, to dawało to zły efekt. Tutaj teselacja daje popis swoich możliwości. Po pierwsze jest automatyczna – grafik przygotowuje tylko jedną wersję modelu (maksymalnie uproszczoną) i np. displacement mapy, a aplikacja sama, na bieżąco, generuje odpowiednie wersje LOD modelu. Po drugie jest to ciągły efekt – nie ma nagłych przeskoków pomiędzy kolejnymi wersjami LOD – manipulując współczynnikiem teselacji (który jest liczbą zmiennoprzecinkową), można płynnie przechodzić od maksymalnie uproszczonego modelu do maksymalnie szczegółowego. Po trzecie, wreszcie, teselacja ta jest adaptacyjna – to znaczy, że sama się dostosowuje do sytuacji w zależności od parametrów, np. odległości od kamery, kąta, pod jakim kamera patrzy na daną Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 26 4.1. Teselacja powierzchnię czy stopnia różnorodności powierzchni w danym miejscu modelu. Drugi wymieniony parametr oznacza, że jeśli dana powierzchnia jest obrócona do kamery bokiem, to stanowi krawędź modelu na ekranie i powinna być mocniej teselowana (aby uzyskać gładką, okrągłą linię) niż środkowe fragmenty modelu, co dobrze widać na rysunku 10. I – tak, możliwe jest zastosowanie różnego współczynnika teselacji dla różnych quadów/trójkątów tego samego modelu. Teselacja w zależności od stopnia różnorodności powierzchni w danym miejscu modelu została zaimplementowana w pracy i zostanie opisana w jej dalszej części. Rysunek 10. Adaptacyjna teselacja w zależności od kąta patrzenia kamery. [5] Jeśli chodzi o wady – zastosowanie tej techniki w aplikacji nie ma wad jako takich – nie utrudnia zastosowania innych technik, niczego nie uniemożliwia i nie pogarsza. Natomiast jej użycie powoduje problemy niezwiązane z działaniem aplikacji. Po pierwsze teselacja wymaga odpowiedniego przygotowania assetów (siatek i tekstur). Modele trzeba tak wykonać, aby siatki miały jak najmniej wierzchołków, ale jednocześnie dobrze się teselowały i w efekcie dawały oryginalną, szczegółową siatkę (którą zapewne grafik najpierw stworzył). Poza samą siatką grafik musi stworzyć specjalnie displacement mapy, które to umożliwią. Poza tym, aby grafik mógł zobaczyć, jak faktycznie jego model będzie wyglądał w grze, musi go za każdym razem umieścić w grze i ją uruchomić albo Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.2. Teselacja Phonga 27 programiści muszą mu przygotować specjalny edytor, który renderuje w ten sam sposób, co gra. Drugim dużym minusem zastosowania tej techniki są jej wymagania sprzętowe. Ograniczenie grona potencjalnych graczy tylko do posiadaczy najnowszego i drogiego sprzętu zwykle jest bardzo niepożądane i zalety samej teselacji nie są wystarczającym uzasadnieniem – o ile aplikacja nie ma wysokich wymagań sprzętowych z innych powodów (zastosowanie kilku innych wymagających technik, próba zachęcenia do gry samą grafiką, marketingowa decyzja o tworzeniu gry na najnowszy sprzęt), to raczej nie powinno się decydować na użycie teselacji. Trzecią rzeczą, o której należy wspomnieć, jest fakt, że teselacja może zarówno poprawić wydajność, jak i ją pogorszyć, w zależności od tego, jak się jej używa. Jeśli stosuje się ją, aby móc używać prostych siatek modeli, które dzięki teselacji i tak będą wyglądać ładnie, albo, jak w przypadku tej pracy, renderuje się duży teren i używa się teselacji do maksymalnego zmniejszenia rozmiaru siatki – to poprawa wydajności aplikacji będzie bardzo znacząca. Jednak jeśli będzie się używać teselacji do modeli, które już są dosyć szczegółowe, tylko po to, aby uzyskać bardzo drobne detale, aby w ten sposób zachwycić graczy, albo używa się jej do takich ekstrawagancji, jak realistyczne włosy, gdzie renderowany jest każdy pojedynczy włos, to nawet przy niskim współczynniku teselacji bardzo łatwo o zabicie wydajności nawet na mocnych kartach graficznych. To jednak nie jest wada per se – czasem takie rzeczy są zamierzone, a chęć zaimponowania niektórym graczom (entuzjastom) usprawiedliwia nawet najwyższe wymagania sprzętowe i niski FPS. Poza tym, technicznie rzecz biorąc, teselacja wciąż tutaj daje ogromną poprawę wydajności – bo wyrenderowanie tych samych rzeczy bez niej byłoby w ogóle niemożliwe (w czasie rzeczywistym). Autor jedynie sygnalizuje to jako potencjalny problem, bo czasem parę dodatkowych szczegółów, których nawet nie widać, może niepotrzebnie obniżać ogólną wydajność aplikacji. 4.2 TESELACJA PHONGA Gdy na siatce dokonano już teselacji i utworzone zostały nowe wierzchołki, należy je, jak już wcześniej wspomniano, odpowiednio przemieścić. Jednym z zastosowań teselacji jest wygładzanie krawędzi okrągłych obiektów w celu uzyskania lepszego estetycznie efektu. Jednak żeby móc znaleźć odpowiednie pozycje dla nowych wierzchołków, trzeba się kierować jakimiś wytycznymi. Jednym z algorytmów zaokrąglania powierzchni siatki jest teselacja Phonga (nie mylić z cieniowaniem Phonga opisanym w dalszej części pracy). Jest to prosta metoda bazująca na wektorach normalnych oryginalnych wierzchołków trójkąta. Cel jej użycia ilustruje rysunek 11: Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 28 4.2. Teselacja Phonga Rysunek 11. Efekt działania teselacji Phonga. [6] Z lewej strony widać oryginalny model, z prawej model po zastosowaniu teselacji. Jak widać na zaznaczonych fragmentach – model uzyskał ładne, okrągłe kształty na brzegach. Ostra, łamana linia brzegowa modelu widoczna z lewej strony jest charakterystyczna praktycznie dla całej grafiki komputerowej (przynajmniej w grach komputerowych) – jest ona skutkiem zbyt niskiej dokładności siatki modelu i jest odwiecznym problemem programistów zajmujących się grami. Z jednej strony chciałoby się użyć jak najdokładniejszej siatki, aby modele zawsze miały okrągłe kształty i dobrze się prezentowały pod każdym kątem, ale z drugiej strony wydajność renderowania takich modeli zawsze stanowiła barierę nie do pokonania. Efektem jest kompromis pomiędzy jakością, a wydajnością, który jednak nie zawsze daje zadowalające rezultaty. Jak widać, teselacja Phonga rozwiązuje ten problem, nie obniżając przy tym znacznie wydajności aplikacji. Rysunek 12. Zasada działania teselacji Phonga dla pojedynczego trójkąta. [6] Rysunek 12 ilustruje główną ideę tej metody – wektory normalne (czarne linie) wyznaczają nachylenie wierzchołków trójkąta, definiując tym samym kształt powierzchni jego wnętrza. Intuicyjnie – jeśli normalne wychylone są na zewnątrz, to powierzchnia jest Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.3. Heightmapa 29 wypukła i nowe wierzchołki teselowanego trójkąta będą przesuwane ku górze (w tym konkretnym przypadku), jeśli normalne skierowane są do wewnątrz, to powierzchnia jest wklęsła i nowe wierzchołki będą przesuwane w dół. Rysunek 13. Ilustracja algorytmu teselacji Phonga. [6] Punkt p to pozycja wierzchołka otrzymana po teselacji sprzętowej (zawiera się w płaszczyźnie trójkąta). Obiekty zaznaczone przerywanymi liniami to płaszczyzny styczne wyznaczone przez wektory normalne trójkąta. u, v i w to współrzędne barycentryczne z teselacji sprzętowej (to dzięki nim został wyznaczony punkt p). W pierwszym kroku algorytmu należy zrzutować punkt p na każdą z 3 płaszczyzn stycznych. W drugim kroku wykonywana jest interpolacja otrzymanych 3 punktów przy pomocy współrzędnych barycentrycznych u, v i w. To w efekcie daje punkt p*, czyli szukaną pozycję nowego wierzchołka siatki. Te dwa kroki wykonywane są dla każdego nowego wierzchołka siatki. Algorytm, jak widać, jest bardzo prosty (a kod implementujący go jeszcze prostszy), ale to jest właśnie jego wielka zaleta – ten kod jest wykonywany dla setek tysięcy wierzchołków powstałych w wyniku teselacji modeli i im jest prostszy, tym szybciej się wykonuje, co ma kluczowe znaczenie. 4.3 HEIGHTMAPA Heightmapa (z angielskiego height map - mapa wysokości) to sposób zapisu terenu jako siatki równo rozłożonych punktów, dla których określamy jedynie wysokość. Pozostałe składowe ich pozycji, szerokość i długość, są wyliczane na podstawie ich pozycji w tablicy i wymiarów terenu. Heightmapy, przynajmniej w grafice komputerowej, tradycyjnie zapisuje się w plikach graficznych, np. bitmapach. Każdy piksel takiego obrazu określa wysokość danego punktu heightmapy. Zwykle koduje się ją powtarzając 3 razy tę samą wartość dla każdego kanału (R, G i B), otrzymując obraz w odcieniach szarości. Rysunek 14 przedstawia przykładowe heightmapy: Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 30 4.3. Heightmapa A B Rysunek 14. Przykładowe heightmapy. Aplikacja używająca heightmapy najpierw wczytuje obraz z pliku, aby otrzymać dwuwymiarową tablicę pikseli. Ponadto gdzieś (w kodzie, osobnym pliku lub w inny sposób) muszą zostać zdefiniowane wymiary terenu (jednostka jest kwestią umowną). Następnie aplikacja generuje prostokątną siatkę wierzchołków, której wymiary mogą, ale nie muszą, odpowiadać rozmiarom obrazu. W tym kroku aplikacja ustawia szerokość i długość dla każdego wierzchołka - pierwszy wierzchołek z brzegu ma szerokość/długość równą 0, a ostatni - równą odpowiedniemu wymiarowi terenu. Wszystkie wierzchołki pośrodku mają pozycje liniowo interpolowane. W kolejnym kroku program iteruje po wszystkich wierzchołkach siatki i ustawia im wysokości, próbkując odpowiednie piksele obrazu - dla skrajnych wierzchołków próbkowane będą skrajne piksele, a pośrodku - znowu wg interpolacji liniowej (oczywiście kroki te można połączyć i wykonać w jednej pętli). Po spróbkowaniu odpowiedniego piksela odczytywany jest dowolny kanał (gdyż wszystkie mają tę samą wartość) i ta wartość jest normalizowana do przedziału (o ile była inna). Następnie wartość ta jest mnożona przez wysokość terenu i zapisywana jako wysokość danego wierzchołka. W praktyce oznacza to, że białe piksele w obrazie oznaczają najwyżej położone punkty w terenie, a czarne - najniżej. W ten sposób dla wszystkich wierzchołków otrzymywana jest pełna informacja o ich pozycjach. Jeśli rozdzielczość siatki odpowiada wymiarom obrazu (ile na ile wierzchołków vs ile na ile pikseli), to każdemu wierzchołkowi odpowiada jeden i tylko jeden piksel. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.4. Mapa obiektów terenu 31 A B Rysunek 15. Tereny wygenerowane z heightmap z rysunku 14. Na rysunku 15 widać tereny wygenerowane z heightmap z rysunku 14. Teren B wygenerowano w 2 wersjach: z lewej strony teren ma wymiary , natomiast z prawej – . To pokazuje, jaką różnicę robią inne proporcje terenu dla tej samej heightmapy. Oczywiście poza wyliczeniem pozycji wierzchołków w heightmapie całą siatkę można przesunąć, obrócić i przeskalować (gdyby była taka potrzeba) - albo w ramach generowania siatki, albo już później - przy renderowaniu na karcie graficznej. Podany sposób wyliczenia pozycji wierzchołków również nie jest jedynym możliwym - zamiast powielać wartość wysokości w każdym kanale można je wykorzystać do zapisania jednej, dokładniejszej wartości (rozdzielając informację na bity wszystkich kanałów). Również inne aspekty można ulepszać i zmieniać wg potrzeb - autor przedstawił jedynie tradycyjne podejście. 4.4 MAPA OBIEKTÓW TERENU „Mapa obiektów terenu” to nie jest oficjalne pojęcie – jest to coś, co autor pracy wymyślił i zastosował w swojej pracy, aby móc łatwo rozmieszczać duże ilości obiektów Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 32 4.4. Mapa obiektów terenu (drzew, skał itd…) w otwartym terenie. Tradycyjnie albo teren powstaje od razu razem z obiektami, które są jego integralną częścią, albo po stworzeniu terenu grafik lub level designer rozmieszczają ręcznie wszystkie obiekty. Ręczne ustawianie (pozycji, skali i rotacji) oczywiście dają pełną kontrolę i możliwość stworzenia lepiej wyglądającego terenu i układów dopasowanych do konkretnych potrzeb. Jednak przy terenie o powierzchni 1 km2 takie podejście wymagałoby mnóstwa pracy, a biorąc pod uwagę, że celem aplikacji jest demonstracja możliwości teselacji, byłby to daremny trud. Ponadto zastosowane podejście umożliwia bardzo łatwą, wygodną i szybką edycję ustawienia obiektów, co nie jest możliwe przy ręcznym ustawianiu każdego obiektu. Mapa obiektów zajmuje się obszarami obiektów danego typu, a nie pojedynczymi obiektami. Pomysł polega na użyciu obrazu, w którym kolorami zamalowane są obszary terenu, które mają być pokryte obiektami danego typu. W tym sensie technika ta stanowi uzupełnienie heightmapy (czyli mapy wysokości) – uzupełnienie o mapę obiektów. Rysunek 16 przedstawia mapę obiektów użytą w aplikacji: Rysunek 16. Mapa obiektów terenu. W tym konkretnym przypadku użyto 3 kolorów do oznaczenia 3 grup obiektów: zielony oznacza kępki trawy, czerwony oznacza rośliny (palmy i małe roślinki), a granatowy oznacza kamienie. Pozycje na mapie obiektów odpowiadają pozycjom na heightmapie – w ten sposób można łatwo zdefiniować, które obszary powinny zostać pokryte jakimi obiektami. Aplikacja wczytuje taki obraz i na całym obszarze danego koloru tworzy obiekty danego typu (oczywiście z odpowiednimi odstępami). Obiekty podzielone są na grupy, co znaczy, że jeden kolor może oznaczać parę różnych obiektów (jak np. w wypadku roślin). Aplikacja w każdym przypadku losuje konkretny obiekt ze wszystkich w grupie. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.4. Mapa obiektów terenu 33 Odstępy między obiektami zostały zdefiniowane w kodzie aplikacji (oczywiście w razie potrzeby można by je wczytywać z pliku). Odstępy jednocześnie definiują całkowitą liczbę obiektów – im mniejsze odstępy, tym więcej obiektów się zmieści na tym samym obszarze. Ważną cechą tej mapy jest możliwość nachodzenia obszarów na siebie. Kolory zostały wybrane nieprzypadkowo – czerwony (FF0000), zielony (00FF00) i niebieski (0000FF) są zapisywane w osobnych kanałach, czyli na osobnych bitach – dzięki temu nie ma kolizji przy nachodzeniu na siebie obszarów. Nachodzenie wizualnie daje efekt mieszania kolorów – przykładowo żółty to obszar z obiektami zarówno „czerwonymi”, jak i „zielonymi”, a biały to obszar z obiektami wszystkich 3 typów. Wspomniano o możliwości łatwej edycji. W tym celu użyto programu obsługującego warstwy (np. Photoshop) – każda warstwa zawiera obszar jednego typu (jednego koloru), a odpowiednie opcje mieszania warstw (ang. blend mode) zapewniają odpowiednie kolory w miejscach, gdzie obszary na siebie nachodzą. Dzięki takiej konfiguracji oraz dzięki wykorzystaniu obrazu jako mapy, edycja dowolnego obszaru obiektów to kwestia jedynie wymazania i/lub pomalowanie odpowiedniego miejsca na mapie. Warstwy można ukrywać, dzięki czemu można skupić się tylko na wybranym typie/wybranych typach obiektów, a ponadto możliwe jest jeszcze jedno udogodnienie – jeśli na samym spodzie doda się dla podglądu używaną heightmapę, to widać od razu, w których miejscach terenu dodawane są nowe obiekty. Oczywiście sposób tworzenia mapy (która jest zwykłym obrazem) jest dowolny i leży w gestii grafika/designera, przedstawiono jedynie propozycję, która sprawdziła się przy tworzeniu tej pracy. Na poniższych rysunkach pokazano każdą warstwę z osobna, podgląd heightmapy oraz całościową kompozycję, na której widać dokładnie, które obszary terenu pokryte są przez jakie obiekty. Rysunek 17. Osobne warstwy z obiektami: roślin, kępek trawy oraz kamieni. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 34 4.5. Displacement mapping Rysunek 18. Heightmapa terenu. Rysunek 19. Mapa obiektów terenu z podglądem heightmapy. 4.5 DISPLACEMENT MAPPING Displacement mapping (z ang. displacement – przemieszczenie, przesunięcie) to metoda dodawania detali do modelu poprzez zmianę pozycji jego wierzchołków na podstawie specjalnego obrazu (tzw. displacement mapy). Jest ona powiązana z technikami normal mappingu (który zostanie wyjaśniony w dalszej części pracy), bump mappingu i parallax mappingu, lecz w odróżnieniu od nich faktycznie modyfikuje siatkę modelu, a nie jedynie symuluje taki efekt. Technika ta jest w zasadzie analogiczna do heightmappingu, gdyż działa na tej samej zasadzie – displacement mapa to też mapa wysokości (tradycyjnie zapisywana w Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.5. Displacement mapping 35 odcieniach szarości), która mówi, o ile przesunąć dany wierzchołek w górę w stosunku do pozycji wyjściowej. Jest ona jednak stosowana do siatek modeli, które mogą mieć dowolne kształty i orientację, dlatego zachowuje się troszkę inaczej. Rysunek 20. Fragment siatki z wektorami normalnymi. Rysunek 21 przedstawia fragment siatki z zaznaczonymi wektorami normalnymi. W przypadku displacement mappingu kierunek „góra” jest wyznaczany właśnie przez te wektory. Jak w przypadku heightmappingu, kolory pikseli mapy przeliczane są na pewną wartość liczbową, jednak w tym wypadku są one interpretowane jako wartość przesunięcia wzdłuż wektora normalnego danego wierzchołka. Kolejny rysunek przedstawia ten sam fragment siatki, jednak tym razem z zaznaczonymi wektorami przesunięć: Rysunek 21. Fragment siatki z wektorami przesunięć wierzchołków. Bladoczerwone punkty to nowe pozycje wierzchołków siatki po zastosowaniu displacement mappingu. W efekcie otrzymujemy nową siatkę z dodanymi detalami: Rysunek 22. Nowy kształt fragmentu siatki. W tym przykładzie może słabo widać korzyść wynikającą z zastosowania tej techniki, jednak chodziło tutaj raczej o pokazanie zasady działania metody. W praktyce Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 36 4.6. Oświetlenie displacement mappingu można użyć do dodania kolców na plecach smoka, zrobienia z płaskiej drogi kamiennej ścieżki z wyraźnie zaznaczonym kształtem każdego kamienia, dodania wypukłości i kształtu do dachówek dachu itd… Oczywiście użycie tej techniki ma sens dopiero w połączeniu z teselacją, gdyż jeśli siatka nie ma dodanych żadnych nowych wierzchołków (a działanie odbywa się jedynie na wierzchołkach wczytanych z pliku modelu), to równie dobrze grafik 3D mógł przesunąć te wierzchołki w edytorze i zapisać już gotowy, poprawiony model do pliku, oszczędzając pracy karcie graficznej podczas renderingu. W przypadku teselacji korzyść jest oczywista – teselator dynamicznie generuje dodatkowe wierzchołki (dzieląc siatkę na dokładniejszą), a displacement mapping nadaje pozycje nowym wierzchołkom, dodając detali do siatki modelu. Ponadto teselacja umożliwia dodawanie szczegółów tylko w tych miejscach i przypadkach, gdy faktycznie daje to wizualną korzyść na ekranie gracza (adaptacyjny LOD). 4.6 OŚWIETLENIE 4.6.1 PODSTAWOWE INFORMACJE Jak już wspomniano wcześniej, programowanie grafiki komputerowej jest sztuką uproszczeń. Upraszcza się rzeczywiste mechanizmy, prawa, wygląd obiektów i relacje między nimi do takiego stopnia, w którym dane przybliżenie rzeczywistości jest zadowalające (tj. wygląda realistycznie, prawdopodobnie lub po prostu ładnie), a jednocześnie daje się policzyć przy pomocy sprzętu komputerowego w krótkim czasie. W tym przypadku – w tzw. „czasie rzeczywistym”, co oznacza, że końcowy obraz jest generowany na bieżąco (nie ma rosnących opóźnień) i wygląda płynnie (najczęściej przyjmuje się granicę 30 klatek/s). Nie inaczej jest w przypadku obliczania oświetlenia obiektów. W świecie rzeczywistym ludzie widzą obiekty dzięki fotonom, które są emitowane lub odbijane przez te obiekty i docierają do ludzkiego oka. Niestety zasymulowanie tego procesu na komputerze jest zdecydowanie zbyt złożone obliczeniowo, aby dało się wykonać w czasie rzeczywistym. Dlatego w aplikacjach 3D często przyjmuje się uproszczony i nie do końca odpowiadający rzeczywistości model. Światło jest dzielone na 3 komponenty: ambient, diffuse i specular. Ambient jest światłem, które zostało tak bardzo rozproszone (poprzez odbijanie się od obiektów), że zdaje się docierać ze wszystkich kierunków. Oświetla ono wszystkie obiekty w zasięgu światła, ze wszystkich stron, niezależnie od położenia światła i obiektu i jest zwykle bardzo słabe (w końcu jest ono wielokrotnie odbite). Jest ono również najmniej złożone obliczeniowo. Diffuse jest „głównym” komponentem – to on najbardziej wpływa na oświetlenie obiektów i nadaje im realistyczny wygląd. Jest to światło, które biegnie z jednego Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.6. Oświetlenie 37 kierunku (źródła światła) i po dotarciu do powierzchni obiektu jest równomiernie odbijane we wszystkich kierunkach (jest jednakowo jasne niezależnie od pozycji obserwatora). Naturalnie powierzchnie obiektów odwrócone od źródła światła nie są w ogóle oświetlone. Specular odpowiada za (najczęściej białe) odbłyski na powierzchni obiektów (np. od słońca w tafli wody). Do jego obliczenia jest brana pod uwagę zarówno pozycja źródła światła, jak i obserwatora (i oczywiście samego obiektu). Ponadto wykorzystywany jest czynnik zwany shininess, który odpowiada za wielkość tych odbłysków. Generalnie specular może być utożsamiany z matowością powierzchni – powierzchnia w pełni matowa nie ma żadnych odbłysków, powierzchnia błyszcząca (np. szkło) je posiada. Dlatego też w części implementacyjnej pracy dla jednych obiektów użyto komponentu specu lar (np. dla liści palm), a dla innych nie (np. dla kory palm). Rysunek 23. Oświetlenie, od lewej: ambient, diffuse, specular i wszystkie razem. [7] 4.6.2 UŻYTE WZORY Te trzy komponenty razem mają symulować działanie rzeczywistych promieni świetlnych. Dają one przekonywujący efekt i do zastosowań rozrywkowych (takich, jak gry komputerowe) są w pełni wystarczające. Jednak na kolor obiektu w danym punkcie wpływ ma oczywiście także materiał, z którego jest on wykonany. Każdy rzeczywisty materiał ma wiele cech, które wpływają na jego wygląd. W aplikacjach 3D przyjmuje się uproszczoną wersję, która posiada kolor (w danym punkcie, cała powierzchnia posiada zwykle wiele kolorów) oraz matowość (parametr shininess). Kolor jest również (tak jak w przypadku światła) rozbijany na komponenty: ambient, diffuse i specular. Jedna uwaga – parametr shininess jest przypisany do materiału, a nie do światła (gdyż odbłyski światła na danej powierzchni zależą od obiektu, a nie od światła). W tej pracy ostateczny kolor obiektu w danym punkcie można wyrazić poniższym wzorem (I – od intensity, czyli natężenie światła): gdzie: oraz oznaczają wpływ każdego z 3 komponentów światła na obiekt. Jak można zauważyć w kodzie shaderów dołączonym do pracy, wartości Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 38 4.6. Oświetlenie komponentu ambitne są różne dla różnych obiektów (a np. woda nie ma go wcale). Wynika to stąd, że celem programisty jest osiągnięcie ładnego wizualnie efektu, a nie poprawnie fizycznego modelu (który i tak jest niemożliwy do osiągnięcia). Istnieje jeszcze taki parametr, jak „attenuation” (z ang. osłabienie, tłumienie), który oznacza siłę oddziaływania światła w funkcji jego odległości od oświetlanego modelu. Jednak ma on zastosowanie tylko do świateł punktowych i reflektorowych (wyjaśnienie niżej), a nie do kierunkowych, które są używane w pracy. Dlatego nie pojawia się on w poniższych wzorach. gdzie: wartość jest dobierana empirycznie i służy osłabieniu tego rodzaju światła (jest to światło wielokrotnie odbite). Niektóre modele oświetlenie zakładają osobne kolory dla każdego komponentu światła (ambient, diffuse i specular), jednak w tej pracy wystarczył uproszczony model z jednym, wspólnym kolorem. gdzie: oznacz wpływ komponentu specular, komponentu diffuse światła, jest wartością jest wartością, z jaką obiekt odbija komponent diffuse światła, jest wektorem normalnym do powierzchni obiektu w danym punkcie jest wektorem wskazującym kierunek od cieniowanego punktu do źródła światła, a jest funkcją zwracającą iloczyn skalarny argumentów. gdzie: jest tzw. pół-wektorem (ang. half-vector), jest funkcją podnoszenia do potęgi, a wartość jest dobierana empirycznie i służy poprawieniu efektu. Halfvector zdefiniowany jest następująco: gdzie: jest wektorem wskazującym kierunek od cieniowanego punktu do obserwatora (kamery). Half-vectora używa się w celu optymalizacji obliczeń. „Oryginalny” wzór na komponent specular jest następujący: gdzie: jest wektorem wskazującym kierunek odbicia promienia światła. We wzorze idea jest taka, że jeśli wektory odbicia i kierunku obserwatora się pokrywają (lub prawie), to w tym miejscu mamy odbłysk (specular). Jednak można zauważyć, że Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.6. Oświetlenie 39 jeśli wektory i się pokrywają, to wektor „pomiędzy” i musi się pokrywać z wektorem normalnym . Na tym się opiera wzór . Za specyfikacją GLSL wektor obliczany jest w następujący sposób: gdzie: wektor jest kierunkiem padania promienia światła, czyli wg powyższych oznaczeń. Jak widać, wyznaczenie wektora wymaga wykonania mniejszej ilości obliczeń niż w przypadku wektora , co daje nam wspomnianą optymalizację. Jak napisano wyżej – każde światło składa się w tym modelu z 3 komponentów: ambient, diffuse i specular. Każdy z nich z kolei składa się z 3 składowych: R, G i B. Podział ten nie jest poprawny fizycznie, ale jest powszechnie stosowany w informatyce i naturalny dla kart graficznych. 4.6.3 TYPY ŚWIATEŁ Światła mają również różne typy. Tradycyjnie w programowaniu grafiki wyróżnia się: kierunkowe (ang. directional lights), punktowe (ang. point lights) i reflektorowe (ang. spotlights). Pierwszy typ oznacza światła tak odległe (nieskończenie odległe), że wszystkie ich promienie są równoległe i biegną w jednym kierunku. Klasycznym przykładem jest światło słoneczne. Drugi typ to światła, które mają swoją pozycję w przestrzeni i emitują promienie we wszystkich kierunkach. Trzeci typ to w zasadzie wariant drugiego – światła te również mają swoją pozycję, ale emitują promienie tylko w określonym zakresie kierunków – w obrębie pewnego stożka, dlatego posiadają również parametr określający kąt rozwarcia tego stożka. Światła kierunkowe nie mają promienia – zasięgu ich działania (przyjmuje się promień nieskończony) ani pozycji – w każdym miejscu na scenie padają pod tym samym kątem. Światła spotlight wymagałyby uwzględnienia w programie kątów rozwarcia stożka. 4.6.4 METODY CIENIOWANIA PIKSELI Na koniec tego rozdziału trzeba jeszcze wybrać metodę cieniowania obiektów. Istnieją 3 typy: oświetlenie „per wielokąt” (ang. per polygon) – płaskie (ang. flat), „per wierzchołek” (ang. per vertex) – Gourauda oraz „per piksel” (ang. per pixel) – Phonga. Cieniowanie płaskie polega na obliczeniu koloru na podstawie wektora normalnego jednego wierzchołka i zastosowanie tego koloru dla całego wielokąta. Cieniowanie Gourauda polega na obliczeniu koloru osobno dla każdego wierzchołka i zinterpolowaniu kolorów pomiędzy wierzchołkami. Cieniowanie Phonga natomiast polega na zinterpolowaniu wektorów normalnych pomiędzy wierzchołkami i – dopiero wtedy – Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 40 4.7. Normal mapping obliczeniu koloru dla każdego piksela. Na rysunku 3. dobrze widać różnice między poszczególnymi typami cieniowania. Rysunek 24. Oświetlenie, od lewej: flat, Gouraud i Phong. [8] Najbardziej popularną metodą była metoda Gourauda, zaimplementowana w większości kart graficznych. Wynikało to z faktu, że do jej zastosowania wystarczała funkcjonalność fixed graphics pipeline. Jednak dzisiaj wszystkie nowe karty graficzne umożliwiają pisanie własnych programów dla shaderów, dlatego metoda Phonga staje się coraz bardziej popularna i prawdopodobnie wyprze zupełnie poprzednią metodę cieniowania. Naturalnie w tej pracy zastosowano cieniowanie Phonga. 4.7 NORMAL MAPPING Gdy wyjaśniono już, jak obliczane jest oświetlenie modeli w grafice komputerowej, można przejść do opisania techniki normal mappingu (mapowania normalnych). Jak wspomniano wcześniej, jest ona związana z displacement mappingiem. Tak naprawdę została ona stworzona dokładnie w celu symulowania efektów displacement mappingu bez konieczności przetwarzania znacznie większej liczby wierzchołków [9]. Jest ona jedną z podstawowych i najstarszych technik stosowanych w grafice komputerowej i umożliwiała ona uzyskanie bardzo efektownych wizualnie rezultatów w czasach, gdy sprzęt komputerowy nie był w stanie przetwarzać takich ilości wierzchołków, jak dzisiaj. Oświetlenie w grafice komputerowej ma kluczowe znaczenie (w zasadzie w świecie rzeczywistym również) – zróżnicowane oświetlenie powierzchni obiektu w zależności od kąta padania światła pozwala ocenić obserwatorowi, jaki kształt ma obiekt w danym miejscu, gdzie są wgłębienia, gdzie wypukłości itd… Gdyby cały obiekt miał jednolity kolor (a przynajmniej tę samą jasność na całej powierzchni), to dla obserwatora wyraźna byłaby wyłącznie sylwetka obiektu i nie byłby on w stanie powiedzieć niczego o jego kształcie. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.8. Frustum culling 41 I na tym właśnie opiera się normal mapping. Ponieważ oświetlenie modeli 3D zależy wyłącznie od kierunku wektora normalnego, technika ta, dla każdego punktu powierzchni, wczytuje z normal mapy kierunek wektora w danym miejscu i podmienia oryginalny wektor. „Dla każdego punktu powierzchni” to znaczy dla każdego piksela zajmowanego na ekranie przez wyrenderowany obiekt – gdyż normal mapping jest stosowany w Pixel Shaderze. Więc chociaż siatka modelu pozostaje bez zmian, nowe wektory normalne używane do obliczenia oświetlenia sprawiają, że powierzchnia wygląda, jakby miała nierówności. Rysunek 25. Zasada działania normal mappingu. Powyższy rysunek ilustruje zasadę działania normal mappingu. Podmienione normalne symulują inna kształt powierzchni (bardziej szczegółowy) niż faktyczny. Szara linia prezentuje symulowany kształt powierzchni. 4.8 FRUSTUM CULLING 4.8.1 BOUNDING VOLUME Aby zdefiniować frustum culling najpierw trzeba wyjaśnić dwa pojęcia. Pierwszym z nich jest bounding volume, czyli bryła brzegowa. Każdy model 3D zajmuje w przestrzeni jakiś obszar. Jednym ze sposobów określania tego obszaru, jest podanie najmniejszej bryły, która zawiera w sobie każdy wierzchołek modelu. Brył brzegowych używa się do przybliżania kształtu modelu w operacjach takich, jak obliczanie kolizji, fizyce 3D, occlusion czy właśnie frustum cullingu. Cechą tych brył jest łatwość sprawdzenia, czy nachodzą one na inne bryły i obiekty. Najczęściej stosuje się prostopadłościany (bounding box) i sfery, rzadziej używa się też walców czy kapsuł (walec z półkulami na końcach). Wybór konkretnej bryły zależy od potrzeb – najszybciej przeprowadza się testy na sferach (wystarczy sprawdzić, czy odległość od środka sfery do testowanego obiektu jest większa lub równa jej promieniowi), jednak inne bryły mogą Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 42 4.8. Frustum culling być wybrane ze względu na lepsze przybliżenie kształtu modelu. Rysunek 26 przedstawia przykładowy model ze swoim bounding boxem: Rysunek 26. Przykład modelu z bounding boxem. Szczególnym przypadkiem bounding boxu jest tzw. axis aligned bounding box (AABB). Jest to najmniejszy możliwy prostopadłościan zawierający w sobie wszystkie wierzchołki modelu, którego boki są równoległe do osi układu współrzędnych. W praktyce oznacza to, że wszystkie wierzchołki jednej ściany tego prostopadłościanu mają jedną wspólną współrzędną. Np. jeśli dana ściana jest równoległa do płaszczyzny XZ układu współrzędnych, to znaczy, że współrzędna X wszystkich wierzchołków tej ściany ma tę samą wartość. Jest to prawdziwe stwierdzenie dla wszystkich ścian. Ta prosta zależność pozwala nie tylko zapisać pełną informację o bounding boxie przy pomocy zaledwie dwóch punktów (o minimalnych i maksymalnych współrzędnych), ale także upraszcza różne algorytmy związane z zawieraniem bryły brzegowej (np. właśnie frustum culling). 4.8.2 FRUSTUM Kamery w grafice komputerowej mają pewien zdefiniowany obszar, który „widzą” – wszystkie modele, które mieszczą się w tym obszarze, w procesie renderowania zostaną wyświetlone na ekranie (w całości lub częściowo). Obszar ten nazywa się frustum i ma kształt piramidy ze ściętym czubkiem. Dokładniej, jest to obszar ograniczony sześcioma płaszczyznami: lewą, prawą, dolną, górną, bliską (ang. near plane) i daleką (ang. far plane). Kąt bocznych oraz górnej i dolnej płaszczyzny zależy od pola widzenia kamery (ang. field of view – FOV) oraz proporcji szerokości do wysokości ekranu, na jaki kamera renderuje obraz (ang. aspect ratio). Poniższy rysunek ilustruje opisywane zagadnienie: Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.8. Frustum culling 43 Rysunek 27. Kamera i jej frustum. [10] 4.8.3 ZASADA DZIAŁANIA Przeciętne aplikacje i gry 3D w każdej klatce renderują dziesiątki, setki albo i tysiące obiektów. Jednak często obiektów na scenie jest jeszcze więcej. Wyrenderowanie każdego obiektu to konkretna praca dla karty graficznej. Dlatego nie ma sensu wysyłać do renderowania obiektów, które w ogóle nie są widoczne w kamerze – ponieważ i tak nie pojawią się na ekranie. Znajdywaniem obiektów „widzianych” przez kamerę zajmuje się frustum culling (ang. culling - ubój selektywny, odstrzał). Metoda ta dla każdego obiektu na scenie testuje, czy jego bryła brzegowa zawiera się (w całości lub częściowo) we frustum kamery. Jeśli wynik testu jest pozytywny, to zakłada się, że obiekt jest widoczny w kamerze i musi zostać wyrenderowany. W przeciwnym wypadku jest pomijany. Ponieważ bryła brzegowa jest jedynie przybliżeniem kształtu obiektu, możliwy jest wynik fałszywie pozytywny – może się zdarzyć sytuacja, w której fragment bryły brzegowej jest widoczny w kamerze, ale cały obiekt przybliżany tą bryłą znajduje się poza frustum. W takim przypadku obiekt jest oczywiście niepotrzebnie renderowany, jednak jest to rzadki przypadek i nie wpływa znacznie na skuteczność metody. W pracy zastosowano axis aligned bounding box z uwagi na łatwość (a zatem i szybkość) sprawdzania, czy zawiera się we frustum kamery. Wystarczy sprawdzić dla każdego wierzchołka bounding boxu i dla każdej płaszczyzny frustum, po której stronie płaszczyzny (zewnętrznej czy wewnętrznej) znajduje się ten wierzchołek. Jeśli choć jeden wierzchołek znajduje się po wewnętrznej stronie wszystkich 6 płaszczyzn, to wynik testu jest pozytywny i obiekt powinien zostać wyrenderowany. 8 wierzchołków bounding boxu i 6 płaszczyzn daje 48 testów dla każdego obiektu. To może się wydawać dużym narzutem obliczeniowym. Jednak sprawdzenie zawierania we frustum faktycznej siatki nawet bardzo prostych modeli wymagałoby obliczeń większych o kilka rzędów wielkości, więc stosunkowo jest to bardzo mała liczba. Z drugiej strony optymalizacja, jaką daje frustum culling, może być ogromna – jeśli spośród tysięcy modeli na scenie w danej klatce widocznych jest tylko kilkadziesiąt, to metoda ta pozwala pominąć renderowanie 90-paru % wszystkich obiektów! Oczywiście liczba ta jest mocno zależna od konkretnej sceny, ale przy dużej liczbie obiektów na scenie zawsze warto rozważyć zastosowanie frustum cullingu. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 44 4.9. Instancing 4.9 INSTANCING Innym przykładem optymalizacji pozwalającej obsłużyć dużą liczbę obiektów w scenie jest instancing. Jest to technika, wspierana sprzętowo przez karty graficzne, służąca do wyrenderowania wielu kopii tego samego obiektu przy użyciu jednego draw callu. [11] Dokładniej – umożliwia ona wyrenderowanie kopii tej samej siatki przy użyciu tego samego materiału. Poszczególne kopie natomiast mogą się różnić między sobą różnymi parametrami, takimi jak pozycja w świecie, skala, rotacja, kolor itd… (każda cecha obiektu, jaką można zróżnicować za pomocą kodu shadera). Normalny draw call wymaga ustawienia siatki modelu oraz materiału – shadera i parametrów wysyłanych do niego z kodu (przede wszystkim macierze transformacji, ale mogą to być dowolne zmienne zdefiniowane przez programistę. Karta graficzna, na podstawie tych danych, jest w stanie wyrenderować jedną kopię obiektu. Aby wyrenderować kolejne, trzeba za każdym razem zmienić parametry dla shadera i wykonać kolejny draw call. Instancing optymalizuje ten proces poprzez utworzenie bufora, w którym zapisuje parametry dla shadera od razu dla wszystkich kopii obiektu (np. ich pozycje), i wysłanie go karcie graficznej. Dzięki temu w pojedynczym draw callu przekazywane są wszystkie dane potrzebne do wyrenderowania wielu kopii obiektu – dla każdego z nich GPU odczytuje parametry dla shadera z odpowiedniego miejsca w buforze i dalej proces renderowania wykonuje się już normalnie. Różnica polega więc na przesłaniu danych łącznie, zamiast na wiele razy, oraz na wykonaniu tylko jednego „zlecenia” renderowania. W praktyce jednak oznacza to duży skok wydajnościowy – draw calle są kosztowne obliczeniowo, a batching operacji (czyli ich wykonywanie seriami zamiast pojedynczo) zawsze daje duże oszczędności w programowaniu grafiki. Zysk jest tym większy oczywiście, im więcej kopii obiektów jest renderowanych. 4.10 SKYBOX Skybox (z ang. dosł. pudełko z niebem) to technika symulowania nieba w otwartych terenach bardzo niskim kosztem. Każdy element widoczny na ekranie w aplikacji 3D to model, który musiał zostać wyrenderowany (ew. jest to kolor, jakim wypełniony zostaje back buffer przed rozpoczęcie procesu renderowania). Dlatego przy otwartych terenach trzeba nie tylko stworzyć ląd i obiekty, wśród których porusza się gracz, ale także zapełnić czymś przestrzeń nad nimi. O ile wypełnienie nieba jednolitym kolorem (lub gradientem) nie jest trudne, o tyle rendering realistycznie wyglądających chmur to temat skomplikowany i posiadający pewien narzut obliczeniowy. A ponieważ niebo i tak stanowi jedynie tło, które jest mało ważne i się nie zmienia, potrzebna jest jedynie metoda „zapełnienia” go sensowną treścią. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 4.10. Skybox 45 Rozwiązaniem jest zastosowanie skyboxu. Nazwa nie jest przypadkowa – jest to sześcian pokryty teksturą nieba, wewnątrz którego znajduje się kamera. Może on być albo tak dużych rozmiarów, żeby cała scena się w nim zawierała, albo może być renderowany na samym początku z wyłączonym zapisem do bufora głębokości. Rysunek 28 przedstawia przykładową teksturę skyboxu w postaci jednego pliku – skyboxy bywają też zapisywane jako 6 osobnych plików. Rysunek 28. Tekstura skyboxu z podpisanymi stronami. Jak wspomniano, jest to sześcian – każda jego ściana stanowi tło dla jednego z sześciu kierunków (podpisane na obrazku). Jeśli kamera znajduje się wewnątrz skyboxu i patrzy na jego ściany pokryte teksturą nieba, to efekt jest taki, jakby gracz patrzył na niebo otaczające świat, po którym się porusza. Co ważne, pozycja skyboxu względem kamery się nigdy nie zmienia. Dzięki temu kamera nigdy nie może się zbliżyć do żadnej z jego ścian (co zniszczyłoby iluzję nieba) ani wyjść poza jego obszar. Jest to prosta sztuczka, która jednak świetnie spełnia swoje zadanie – efekt jest ładny, wiarygodny, teksturami mogą być prawdziwe zdjęcia (i często są), dodając realizmu grafice, a koszt wyrenderowania takiego nieba jest minimalny (6 quadów z bardzo prostym shaderem). Oczywiście nie jest to tak dobry efekt, jak rendering chmur (i słońca), poza tym prawdziwe niebo, w przeciwieństwie do skyboxa, nie jest nieruchome, jednak prostota i wydajność tego rozwiązania sprawiła, że było ono bardzo popularnym rozwiązaniem w starszych aplikacjach i nawet dzisiaj wciąż jest używane wszędzie tam, gdzie potrzebny jest dobry efekt niskim kosztem. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 46 Renderowanie szczegółowego terenu z użyciem teselacji 4.10. Skybox Piotr Martynowicz 5.1. Obsługa aplikacji 47 5 IMPLEMENTACJA 5.1 OBSŁUGA APLIKACJI Jak napisano we wstępie pracy, stworzona aplikacja ma charakter demonstracyjny i przedstawia duży otwarty teren, po którym można się poruszać. Za przykład wybrano wyspę tropikalną, pokrytą roślinnością i skałami oraz otoczoną wodą. Cały teren ma powierzchnię 1 km2, woda sięga nawet dalej. Po wyspie można poruszać się przy pomocy swobodnej kamery, sterowanej myszką i klawiaturą. Sposób poruszania się kamery zapożyczono z gier FPS (First Person Shooter) – ruch myszki obraca kamerą, a odpowiednie przyciski poruszają nią do przodu, do tyłu i na boki (wg aktualnego wektora kierunku kamery). Dodatkowo umożliwiono ruch do góry i w dół (znowu – wg wektora kierunku), co nie jest w FPS-ach możliwe, gdyż tam kamera jest utożsamiona z postacią i porusza się po jakimś terenie. W tym wypadku kamera „lata”, więc dodano ruch góra/dół, aby ułatwić poruszanie się. Obrót kamery w poziomie (ang. yaw) jest nieograniczony (360o zakresu), natomiast wychylenie w pionie (ang. pitch) jest ograniczone do przedziału – to znaczy, że kamera może być co najwyżej skierowana pionowo górę lub w dół, ale nie może wyjść poza ten zakres (bo wtedy obserwowałaby obiekty na scenie do góry nogami). Kamera nie może być przechylana na boki (ang. roll). Takie ograniczenia obrotu kamery również są zapożyczone z FPS-ów. Ponadto, ponieważ teren jest naprawdę duży, dodano opcję przyspieszenia ruchu pod dodatkowym klawiszem. Aby móc dobrze pokazać działanie teselacji (która się dynamicznie zmienia wraz z pozycją kamery), zaimplementowano możliwość włączenia widoku siatki terenu i modeli – na niej doskonale widać, jak tworzone są (i usuwane) wierzchołki siatek obiektów podczas ruchu kamery po scenie. W celach porównawczych dodano również możliwość wyłączenia teselacji dla obiektów (teren jest zawsze teselowany). Szybkie przełączenie modeli teselowanych na nieteselowane pozwala zobaczyć, co tak naprawdę daje technika teselacji i jak różnią się siatki takich modeli. W celach benchmarkowych (pomiaru wydajności aplikacji) umożliwiono wyłączenie synchronizacji pionowej (ang. v-sync). Dzięki temu karta graficzna renderuje tyle klatek na sekundę, ile zdoła, co pozwala ocenić wydajność silnika graficznego oraz porównać wyniki dla różnych widoków kamery (w zależności od liczby widocznych obiektów, stopnia teselacji itd… karta graficzna w różnych klatkach ma bardzo różną ilość pracy do wykonania). Aplikacja sama nie oblicza i nie wyświetla liczby renderowanych klatek, ale istnieją zewnętrzne programy, które to umożliwiają (np. Fraps). Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 48 5.2. Ładowanie modeli i tworzenie materiałów Sterowanie: ruch myszki na boki – obrót kamery w poziomie (yaw) ruch myszki do przodu/do tyłu – obrót kamery w pionie (pitch) W – ruch kamery do przodu S – ruch kamery do tyłu A – ruch kamery w lewo D – ruch kamery w prawo Q – ruch kamery w dół E – ruch kamery w górę Spacja + W/S/A/D/Q/E – przyspieszony ruch kamery F2 – włączenie/wyłączenie synchronizacji pionowej (v-sync) F3 – włączenie/wyłączenie trybu renderowania siatki F4 – włączenie/wyłączenie teselacji obiektów 5.2 ŁADOWANIE MODELI I TWORZENIE MATERIAŁÓW 5.2.1 ELASTYCZNY SYSTEM ŁADOWANIA MODELI Dobry silnik graficzny musi umożliwiać łatwe wczytywanie modeli i materiałów oraz wygodne korzystanie z nich. Jednak w przypadku assetów 3D nie jest to niestety tak proste, jak przy innych assetach. Natura sprzętu i pipeline’u graficznego oraz API zarządzającego nimi, podyktowana wydajnością, tworzy niestety silne powiązania pomiędzy siatkami, materiałami i stanami maszyny renderującej. Przykładowo, każda siatka składa się z listy wierzchołków, które tworzą jej kształt. Niestety definicja konkretnego wierzchołka zależy od użytego shadera. Inny shader może wymagać innej definicji wierzchołka (chodzi tu o jego atrybuty, takie jak: pozycja, wektor normalny, współrzędne tekstur itd…). Poza tym w pojedynczym pliku 3D może być zapisanych wiele różnych obiektów, używających różnych materiałów (pojedyncze obiekty mogą nawet używać wielu materiałów naraz). Zmiana w pliku 3D może pociągać za sobą zmianę materiału/ów, a co za tym idzie – potrzebny jest inny kod aplikacji, który wczyta i przygotuje taki model do renderowania. Napisanie kodu obsługującego poprawnie pojedynczy przypadek (konkretne modele, konkretne materiały) jest stosunkowo prosty. Jednak w komercyjnym silniku taki kod jest kompletnie bezużyteczny – zmiana użytych modeli nie może wymagać zmiany kodu i rekompilacji silnika. Dlatego potrzebne jest elastyczne rozwiązanie, które poradzi sobie z wieloma różnymi przypadkami i zasobami, i nie będzie wymagało zmian w przypadku zmiany assetów. Oczywiście stopień „uniwersalności” rozwiązania bardzo mocno zależy od konkretnych potrzeb i zastosowań – nie ma sensu tracić czasu na Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.2. Ładowanie modeli i tworzenie materiałów 49 projektowanie i implementowanie kodu, który będzie potrafił obsłużyć przypadki, które nigdy nie wystąpią. Takie rozwiązanie zostało stworzone w tej pracy. Ponieważ do wyrenderowania modeli (w uproszczeniu) potrzebne są 2 składniki – siatki oraz materiały, pełny opis modelu wymaga 2 typów plików – siatki są zapisywane przy użyciu jednego z popularnych formatów 3D, a dla materiałów stworzono własny format opisujący ich parametry. Przy wyborze formatu 3D jest pewna swoboda – biblioteka Assimp, używana w projekcie, obsługuje ich wiele, a jedynym wymaganiem dot. konkretnego formatu jest obsługa hierarchii obiektów (relacja rodzic – dzieci). Pliki 3D umożliwiają zapis nie tylko samych siatek, ale też materiałów, animacji, kości, kamer itd… Jednak sposób zapisu materiałów różni się między formatami, praktycznie żaden nie przechowuje tekstur (są one zapisywane do osobnych plików) i żaden nie zapisuje wszystkich informacji, jakich potrzebuje zaimplementowany silnik graficzny (jak choćby przypisanie shaderów czy sposób próbkowania (ang. sampling) tekstur). Dlatego konieczne było stworzenie własnego formatu, który zawierałby te wszystkie informacje oraz umożliwiał łatwą edycję. 5.2.2 PLIKI MATERIAŁÓW Aby zaprojektować dobry format zapisu informacji, trzeba najpierw ustalić, jakie dokładnie informacje będą zapisywane, oraz wybrać jakiś sposób zapisu danych – taki, który jest czytelny, łatwy w ręcznej edycji (gdyż projekt nie zakładał tworzenia specjalnego edytora do plików materiałów) oraz o dobrej strukturze. Pliki materiałów muszą umożliwiać zdefiniowanie i przypisanie konkretnym siatkom materiałów – to jest shaderów i ich parametrów. Dlatego są one podzielone na sekcje – każda z nich definiuje dane konkretnego typu, z których potem można korzystać w innych częściach pliku. Sekcja „materials”, jak sama nazwa wskazuje, definiuje materiały i przypisuje im wszystkie potrzebne parametry oraz nadaje im nazwy. Jest to bardzo ważne, gdyż siatki, w plikach 3D, mają przypisane materiały właśnie po nazwach i to dzięki nim silnik graficzny wie, jakiego materiału użyć dla jakiej siatki. Sekcje te definiują takie rzeczy, jak tekstury, shadery itd…, wszystkie identyfikowane po nazwach, i po wczytaniu pliku są one dodawane do globalnych list zasobów. Dzięki temu w całym programie można ich potem używać – np. raz zdefiniowanej tekstury można użyć dla siatek wczytanych z kilku plików 3D, a zdefiniowane w pliku materiały można przypisać siatkom wygenerowanym proceduralnie. Liczba zasobów każdego typu może być różna (i powinno się dać ją łatwo zmienić), a typy np. parametrów dla shadera mogą być różne. Dlatego potrzebny był format, który posiada dobrą strukturę i jest elastyczny. Dlatego zamiast wymyślać własną notację od zera, autor postawił na sprawdzone rozwiązanie. Jest nim JSON (JavaScript Object Notation). Jest to format tekstowy, który umożliwia zapisywanie danych różnego Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 50 5.2. Ładowanie modeli i tworzenie materiałów typu, tworzenie tablic o dowolnej liczbie elementów oraz organizowanie danych w obiekty i hierarchie, a przy tym posiada minimalny narzut (w przeciwieństwie do np. XML-a). Poniższy listing przedstawia przykładową zawartość pliku materiałów: { "shaders" : [ { "name" : "grass", "filename" : "grass.hlsl", "stages" : [ { "stage" : "vs", "entryPoint" : "VShader", "shaderModel" : "vs_5_0" }, { "stage" : "ps", "entryPoint" : "PShader", "shaderModel" : "ps_5_0" } ] } ], "textures" : [ { "name" : "grasses_0_diffuse", "filename" : "grass_diffuse.png" } { "name" : "grasses_0_displacement", "filename" : "grass_displacement.png" } ], "samplerStates" : [ Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.2. Ładowanie modeli i tworzenie materiałów 51 { "name" : "samplerRepeatLinear", "wrapMode" : "repeat", "filtering" : "mipLinear" } ], "materials" : [ { "name" : "Grass", "shaderPasses" : [ { "shader" : "grass", "constantBuffers" : [ { "name" : "Matrices", "usage" : "default" }, { "name" : "Lights", "usage" : "default" } ], "parameters" : [ { "type" : "texture", "name" : "diffuseTexture", "value" : "grasses_0_diffuse" }, { "type" : "texture", "name" : "displacementMap", "value" : "grasses_0_displacement" }, { "type" : "samplerState", Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 52 5.2. Ładowanie modeli i tworzenie materiałów "name" : "samplerState", "value" : "samplerRepeatLinear" } ] } ] } ], "meshesBuffers" : { "vertexBuffersUsage" : "immutable", "indexBuffersUsage" : "immutable" } } Listing 1. Przykładowy plik materiałów. Nazwy sekcji zostały zaznaczone na czerwono. Pierwszą z nich jest „shaders”. Zawiera ona informację o tym, z jakich plików należy wczytać shadery oraz jakie typy (etapy) shaderów zostały w nich zdefiniowane (Vertex Shader, Pixel Shader itd…). Każdy etap ma zdefiniowaną funkcję główną (ang. entry point), oraz Shader Model. [12] Aktywowanie etapu potoku graficznego jest tak proste, jak dopisanie go do listy. Przykładowo jeśli programista postanowi zrezygnować z Geometry Shadera i ograniczyć się do Vertex Shadera i Pixel Shadera dla danego materiału, to wystarczy usunąć jego definicję z sekcji „shaders” i silnik graficzny sam aktywuje odpowiednie etapy pipeline’u podczas renderingu – plik z kodem źródłowym dalej może zawierać kod Geometry Shadera (np. na wypadek, gdyby miał się przydać później). Równie prosto można włączyć/wyłączyć teselację modelu – wystarczy dodać/usunąć etapy Hull Shader i Domain Shader – silnik sam dostosuje typ danych wejściowych dla potoku graficznego (wierzchołki albo punkty kontrolne) i aktywuje/zdezaktywuje etapy teselacji. Oczywiście to wszystko pod warunkiem, że wybrane etapy i ich kod źródłowy dalej się kompilują i tworzą poprawną całość. W tej sekcji, tak samo, jak w każdej innej (z wyjątkiem ostatniej), można zdefiniować dowolną ilość obiektów. Warunkiem jest tylko wybranie unikalnych nazw. Drugą sekcją jest „textures”. Definiuje one tekstury i podaje ścieżki do plików, w których są one zapisane. Kolejną sekcją jest „samplerStates”. Opisuje ona obiekty, które definiują sposób próbkowania tekstur. Chodzi o sposób wyboru odpowiedniego punktu na teksturze, wg zadanych współrzędnych. Rysunek 29 ilustruje, jak zdefiniowane są współrzędne tekstury: Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.2. Ładowanie modeli i tworzenie materiałów 53 Rysunek 29. Współrzędne UV tekstury. Współrzędne tekstury tradycyjnie oznacza się UV. Punkt oznacza lewy dolny róg tekstury, a prawy górny. To znaczy, że aby pobrać kolor tekstury na samym środku, należy spróbkować punkt . Elementy sekcji „samplerStates” mają dwa parametry. Pierwszy z nich – „wrapMode” – oznacza zachowanie Sammlera dla współrzędnych wychodzących poza zakres . Możliwe są dwie opcje: „repeat” oraz „clamp”. Ta pierwsza oznacza powtarzanie tekstury, czyli np. dla współrzędnych zostanie spróbkowany ten sam punkt, co dla - zachowanie to zaprezentowane jest na rysunku 29. „clamp” natomiast oznacza „przycięcie” współrzędnych do obsługiwanego zakresu, czyli np. dla współrzędnych zostanie spróbkowany ten sam punkt, co dla . Drugi parametr – „filtering” – oznacza sposób obliczenia koloru, gdy współrzędne wskazują punkt pomiędzy dwoma tekselami, czyli tzw. filtrowanie. Tutaj opcji jest wiele i zależą one od konkretnej biblioteki graficznej i jej wersji. Kolejna sekcja to „materials” i stanowi ona sedno tego pliku. To tutaj definiowane są konkretne materiały i przypisywane są im parametry. Materiały mają jeden lub więcej przebiegów (ang. passes), chociaż silnik w tym projekcie obsługuje tylko jednoprzebiegowe materiały. Każdy przebieg ma zdefiniowany shader, constant buffery oraz parametry. Constant buffer to sposób zbiorczego przekazywania parametrów do karty graficznej i dla każdego z nich zdefiniowany jest parametr „usage”, czyli zamierzony sposób użycia bufora. Chodzi o określenie, jak często dane w buforze będą zmieniane (dzięki tej informacji DirectX może zoptymalizować ten proces). Możliwe opcje to „default” – rzadkie zmiany, „dynamic” – częste zmiany oraz „immutable” – dane po utworzeniu bufora nigdy nie mogą być zmienione. Ostatnia część – „parameters” – Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 54 5.3. Teren definiuje parametry dla shadera. Każdy parametr ma podany typ, nazwę oraz wartość. Wszystkie wartości w tej sekcji mogą używać zdefiniowanych wcześniej zasobów – wystarczy podać ich nazwę. Ostatnia sekcja – „meshesBuffers” – pełni inną rolę niż pozostałe. Nie definiuje nowych zasobów, tylko sposób użycia vertex buffera i index buffera (analogicznie jak wypadku constant bufferów w sekcji „materials”), czyli kolejno bufora wierzchołków i bufora indeksów siatki. 5.2.3 ŁADOWANIE MODELI Silnik graficzny ładuje modele, wczytując pliki 3D z siatkami oraz pliki materiałów jako jeden proces. Najpierw, używając Assimpa, ładuje siatki modeli do formatu obsługiwanego przez bibliotekę. Następnie ładuje pliki materiałów, tworząc odpowiednie zasoby (przy okazji wczytując również pliki shaderów i tekstur). Z plików shaderów, wykorzystując mechanizm refleksji, pobiera informacje o formacie wierzchołków siatki oraz o constant bufferach i parametrach tychże shaderów. Mając tę wiedzę, silnik przystępuje do konwertowania danych siatek modeli z formatu Assimpa na własny format wykorzystywany przez silnik, tworząc siatki z wierzchołkami posiadającymi atrybuty wymagane przez konkretne shadery oraz tworzy potrzebne constant buffery z parametrami wymaganymi przez te shadery (konkretne wartości są ustawiane dopiero podczas renderowania). Proces jest automatyczny i spełnia warunek elastyczności. Jeśli przykładowo programista postanowi dodać teksturowanie do materiału swojego modelu, to musi jedynie zdefiniować nową teksturę (albo użyć istniejącej) w pliku materiałów i ustawić oraz ją danemu materiałowi oraz oczywiście dopisać odpowiedni kod do shadera. To wszystko. Silnik, po wczytaniu i utworzeniu tekstury, sam zauważy, że shader wymaga teraz podpięcia tej tekstury oraz że wierzchołki mają nowy atrybut – współrzędne tekstury – i uwzględni to, generując inną siatkę niż poprzednio. Takie rozwiązanie jest bardzo wygodne w użyciu, a zmiany modeli i materiałów wymagają minimum pracy. 5.3 TEREN 5.3.1 WYKORZYSTANIE TESELACJI DO RENDEROWANIA TERENU Jak napisano wcześniej w pracy, teren jest tworzony na podstawie heightmapy jako tablica wierzchołków o różnych wysokościach. Jednak tradycyjnie cała siatka jest generowana przez CPU i przekazywana do karty graficznej, i przy bardzo dużym terenie musiałaby ona posiadać bardzo dużo wierzchołków. Przykładowo dla kwadratowego terenu o boku długości 1 km, jeśli programista chciałby uzyskać dokładność siatki do 1 m [13], musiałaby ona mieć Renderowanie szczegółowego terenu z użyciem teselacji wierzchołków, co nawet dla Piotr Martynowicz 5.3. Teren 55 najmocniejszych kart graficznych w komputerach stacjonarnych jest wartością niebezpiecznie bliską granicy możliwości renderowania w czasie rzeczywistym. Tu z pomocą przychodzi teselacja. CPU przygotowuje uproszczoną siatkę o minimalnej liczbie wierzchołków, a dopiero po przesłaniu jej do karty graficznej GPU generuje dodatkowe wierzchołki. Oszczędność jest dwojaka – po pierwsze poprzez przesyłanie uproszczonej siatki omijane jest wąskie gardło pomiędzy CPU a GPU, a po drugie dzięki adaptacyjnej teselacji duże ilości wierzchołków generowane są tylko blisko kamery, a w pozostałych obszarach siatki jest ich znacznie mniej. To znaczy, że obszar siatki blisko kamery jest generowany z dokładnością np. do 1 m, a obszar, który znajduje się daleko od kamery, może mieć dokładność 10 lub 20 m. W praktyce daje to nie tylko ogromny skok wydajności – użycie teselacji sprawia, że renderowanie w czasie rzeczywistym tak dużych terenów przy zachowaniu dokładności siatki w ogóle jest możliwe. W przypadku terenu renderowanego w tej pracy uproszczona siatka ma rozdzielczość 32 x 32 quady, co daje wierzchołków, co w porównaniu do ok. miliona wierzchołków przy braku teselacji jest ogromną różnicą. Ponadto, przy maksymalnym współczynniku teselacji oferowanym przez DirectX – 64 – daje to maksymalną dokładność siatki wynoszącą m przy boku długości 1 000 m, co jest wartością w pełni zadowalającą. Poniższe rysunki ilustrują działanie teselacji terenu w praktyce: Rysunek 30. Siatka terenu renderowanego przez aplikację, widok z góry. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 56 5.3. Teren Rysunek 31. Efekt działania adaptacyjnej teselacji terenu i wody. Jak widać na rysunku 31, teren (i woda) znajdujący się bliżej kamery jest mocniej teselowany, dając większą dokładność, a teren znajdujący się dalej jest mniej teselowany, gdyż nie potrzebuje on aż takiej dokładności. Czego niestety na statycznych obrazach nie da się pokazać, to to, jak dynamicznie zmienia się siatka terenu podczas przemieszczania kamery po scenie. To automatyczne dostosowywanie siatki do pozycji kamery podczas ruchu stanowi istotę adaptacyjnej teselacji i świetnie pokazuje, co tak naprawdę dzieje się „za kulisami” i jaką pracę wykonuje GPU. 5.3.2 AUTOMATYCZNE TEKSTUROWANIE TERENU Inną ciekawą cechą zaimplementowaną w aplikacji jest sposób teksturowania terenu. Tradycyjnie pokrycie siatki modelu teksturami jest zadaniem wykonywanym ręcznie (z pomocą odpowiednich narzędzi oczywiście) przez grafika 3D. Daje to pełną kontrolę nad ułożeniem tekstur na powierzchni modelu, ale jednocześnie zmiana kształtu siatki może wymagać poprawienia oteksturowania. Jednak w przypadku terenu generowanego z heightmapy można ten proces wykonać proceduralnie (podczas generacji siatki). Autor zdecydował się na pokrycie terenu kilkoma różnymi teksturami symbolizującymi różne elementy terenu: piasek na plaży, trawę na polanach i łagodnych wzniesieniach oraz skały na stromych zboczach. Współrzędne tekstur zależą wyłącznie od szerokości i długości terenu (wysokość i inne Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.3. Teren 57 parametry nie są brane pod uwagę), natomiast wybór konkretnej tekstury (lub kilku w przypadku blendingu (mieszania) tekstur) dokonuje się w shaderze terenu na podstawie cech terenu: stromizny oraz wysokości. Poniższy rysunek ilustruje opisywane zagadnienie: Rysunek 32. Teksturowanie terenu na podstawie jego stromizny i wysokości. Zasady wyboru tekstury są proste. Jeśli dany fragment terenu znajduje się poniżej pewnego ustalonego progu, to wybierany jest piasek (poniżej i tuż nad powierzchnią wody), w przeciwnym wypadku – wybierana jest trawa (teren znajdujący się dalej od wody). Oczywiście zastosowano też pewien obszar pośredni, gdzie obie tekstury są blendowane wg interpolacji liniowej, aby otrzymać gładkie przejście. Drugim warunkiem jest stromizna terenu – jeśli stopień pochylenie terenu jest większy niż pewien ustalony próg, wybierana jest tekstura skał (strome, skaliste zbocza i urwiska), w przeciwnym wypadku – wybierana jest jedna z poprzednich 2 tekstur – piasek lub trawa. Stromizna jest oceniana na podstawie znormalizowanego wektora normalnego danego wierzchołka terenu. Pod uwagę brana jest wartość Y tego wektora, która wynosi 0 dla poziomego terenu i 1 dla pionowego (teoretycznie, gdyż teren idealnie pionowy w heightmapie nie występuje). Rysunek 33 ilustruje problem. Oczywiście tutaj też zastosowano obszar pośredni i blending tekstur. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 58 5.4. Woda Rysunek 33. Fragment terenu z zaznaczonymi wektorami normalnymi, widok od boku. Zaletą użycia shaderów do teksturowania jest fakt, że obliczenia konieczne do wybrania tekstury (i ew. blendowania kilku) dla każdego wierzchołka siatki są wykonywane na GPU, które jest przystosowane do przetwarzania ogromnych ilości danych oraz posiada sprzętowe wsparcie do operacji na teksturach. Wykonywanie ich na CPU byłoby niepotrzebnym obciążaniem procesora komputera, który ma mnóstwo innych zadań. Natomiast plusem samego proceduralnego teksturowania terenu jest brak konieczności poprawek w przypadku zmiany terenu. Jeśli zajdzie taka sytuacja, wystarczy zedytować albo podmienić obraz z heightmapą terenu i shader automatycznie dostosuje oteksturowanie. Zresztą całe zaimplementowane rozwiązanie dla renderowania terenu jest w ten sposób przygotowane. Jedyne, co programista/użytkownik musi zrobić, to przygotować obraz z heightmapą terenu oraz mapę obiektów. Aplikacja sama automatycznie wygeneruje uproszczoną siatkę terenu oraz stworzy instancje i rozmieści odpowiednie obiekty (roślinność, kamienie) na tym terenie, generując bufory dla instancingu. Shadery natomiast same oteksturują teren oraz będą dynamicznie teselować teren i obiekty podczas ruchu kamery. Takie rozwiązanie sprawia, że w przypadku wprowadzania zmian do terenu konieczny nakład pracy jest minimalny. 5.4 WODA Wszystkie opisane zalety teselacji użyte do renderowania terenu zostały również wykorzystane w przypadku wody. Chociaż tutaj zastosowanie teselacji ma trochę mniejsze znaczenie, gdyż cała siatka wody z daleka wydaje się być płaska i duża gęstość siatki nie jest niezbędna. Co nie znaczy, że nic nie daje. Aby woda bardziej przypominała morze otaczające tropikalną wyspę, dodano prostą animację falowania wody. Siatka, podobnie jak w wypadku terenu, stanowi tablicę Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.4. Woda 59 równo rozmieszczonych wierzchołków (szerokość i długość), dla których wartość wysokości obliczana jest ze wzoru: position.y += height * (sin(PI * time + position.x) + sin(PI * time + position.z)); Listing 2. Sposób obliczania wysokości wody w danym punkcie. Jak widać, kod używa funkcji sinus do otrzymania naturalnego kształtu fal. Jej argumentami, poza oczywiście wartością π, jest czas oraz długość i szerokość samego wierzchołka – poziom wody ma być wyższy w jednych miejscach i niższy w innych. Użycie czasu jako argumentu, oczywiście, pozwala wprawić wodę w ruch i uzyskać animację. „height” to parametr definiujący maksymalne wychylenie wody, czyli wysokość fal. Ponadto należy wspomnieć, że z powyższego wzoru zostały usunięte dodatkowe mnożniki (stałe wartości), gdyż ich celem jest jedynie dostosowanie wyglądu wody do konkretnej sceny (są dobierane empirycznie) i jedynie zaciemniałyby wzór. Animacja fal szczególnie ładny dobry efekt daje przy piaszczystych brzegach wyspy, gdzie kolejne fale wpływają kawałek w głąb lądu, po czym się cofają (trochę jak prawdziwe fale). Efekt ten przedstawia poniższy rysunek, chociaż oczywiście trudno przedstawić to za pomocą statycznego obrazu: Rysunek 34. Woda renderowana przez aplikację. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 60 5.5. Obiekty Jeśli chodzi o materiał, to woda oczywiście jest półprzeźroczysta (przy jej renderowaniu włączony jest blending) oraz na jej powierzchni pojawiają się refleksy świetlne. Te ostatnie są osiągnięte poprzez zastosowanie normal mappingu do zasymulowania nierówności na powierzchni wody oraz komponent specular modelu światła. Efekt jest estetyczny i zadowalający na potrzeby tej pracy, choć na pewno daleki od rozwiązań stosowanych w najnowszych grach komputerowych. Jednak dobrze, realistycznie wyglądająca woda, to trudny i złożony temat, który sam w sobie mógłby być tematem pracy dyplomowej. Poza tym naprawdę ładna woda posiada spory narzut wydajnościowy (zależny od ilości geometrii innych obiektów niż sama woda), który w tej pracy byłby problemem. 5.5 OBIEKTY Drugim polem do popisu dla teselacji (po terenie) jest rendering obiektów. Zastosowanie tej techniki pozwala dodać bardzo drobne detale do ogólnego modelu obiektu, czyniąc go znacznie ładniejszym i bardziej efektownym, a dzięki uzależnieniu stopnia teselacji od odległości od kamery wydajność aplikacji pozostaje wysoka (gdyż tak naprawdę tylko kilka najbliższych obiektów jest teselowanych). Dzięki temu możliwe jest stworzenie dużego otwartego terenu pokrytego setkami obiektów (i renderowanie w jednym kadrze wszystkich naraz!), gdzie użytkownik zbliżając się kamerą do każdego obiektu, widzi mnóstwo drobnych detali i wydaje mu się, że wszystkie obiekty są tak szczegółowo renderowane, a przy tym FPS całej aplikacji jest wysoki. Takimi właśnie sztuczkami twórcy gier komputerowych starają się zaimponować graczom i zachęcić ich do kupna. Jeśli chodzi o samo „dodawanie” detali do obiektów, to w zasadzie cała teoria została przedstawiona już w poprzednim rozdziale. Dlatego teraz na podstawie rysunków z wyrenderowanymi obiektami zostanie omówione, w jaki sposób dany efekt został osiągnięty. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.5. Obiekty 61 A B Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 62 5.5. Obiekty C D Rysunek 35. Widok palmy z włączoną i wyłączoną teselacją, widok normalny oraz siatki. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.5. Obiekty 63 Pierwsza rzecz, jaka się rzuca w oczy, to różnica między siatkami modeli na rysunkach C i D. Choć tego nie widać, palmom w C jest renderowana sama siatka (sic!), jednak jest ona tak gęsta, że sprawia wrażenie, jakby model był renderowany normalnie. Siatka w D posiada ok. pół tysiąca trójkątów, natomiast w C, dzięki teselacji – wiele tysięcy. Taka gęstość siatki jest więcej niż wystarczająca i pozwala przedstawić nawet najdrobniejsze detale na powierzchni obiektu. Dla pni palm zastosowano 2 efekty. Po pierwsze teselację Phonga. Patrząc na sam dół pnia na rysunku D, widać, że ma on podstawę w kształcie pięciokąta (cały pień ma taki przekrój poprzeczny), przez co jest on kanciasty i źle wygląda. Ostre załamania widać również w B wzdłuż pnia. Aby wyeliminować ten problem, zastosowano algorytm zaokrąglający powierzchnię – właśnie teselację Phonga. Jak widać w A i C, podstawa i cały pień ma ładne, okrągłe kształty. Drugim efektem jest displacement mapping. Użyto go do dodania pniowi wypustek, aby bardziej przypominał palmę. Displacement mapa wygląda tak (białe elementy to właśnie wypustki na powierzchni pnia): Rysunek 36. Displacement mapa dla pni palm. Choć wypustki tutaj są tylko 3, to poprzez odpowiednie oteksturowanie modeli palm, są one powtarzane przez całą długość pni. Efekt działania displacement mappingu najlepiej widać na rysunkach A i B na palmie po lewej stronie. Jeśli chodzi o liście palm, to tutaj zastosowano jedynie teselację Phonga. Displacement mapping był niepotrzebny, gdyż liście powinny być płaskie. Efekt znowu widać na rysunkach A i B – na tym drugim widać, że liście, mimo dosyć dokładnej siatki modelu, mają ostre załamania i są kanciaste. Teselacja Phonga całkowicie eliminuje ten problem, dając ładne zakrzywione kształty. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 64 5.5. Obiekty A B C Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.6. Niebo 65 D Rysunek 37. Widok kamienia z włączoną i wyłączoną teselacją, widok normalny oraz siatki. W przypadku kamieni również widać ogromną różnicę w siatce z i bez teselacji. Dzięki tak gęstej siatce (która nie byłaby możliwa do osiągnięcia w tradycyjny sposób przy zachowaniu wysokiego FPS-u) można przedstawić każdą wypukłość i nierówność skały. Oczywiście tutaj również zastosowano teselację Phonga, aby zamienić kanciaste kształty w ładnie zaokrąglone brzegi bryły. Nie używano jednak displacement mappingu, gdyż nie był konieczny. Chociaż oczywiście, jakby ktoś chciał, można to zrobić, aby kamień miał jeszcze więcej wgłębień, wypukłości i załamań. Dodano za to normal mapping, aby za jego pomocą zasymulować nierówności na powierzchni skały – w połączeniu z oświetleniem daje to wrażenie, jakby model był bardzo szczegółowy, i sprawia, że realistycznie wygląda. 5.6 NIEBO Niebo w tej pracy jest wykonane przy użyciu skyboxa. Tekstura ze zdjęciami nieba i chmur z różnych stron rozpięta na sześcianie otaczającym całą scenę daje wrażenie, jakby nad wyspą faktycznie unosiły się chmury, i sprawia, że całość bardziej przypomina piękny słoneczny dzień. W zasadzie w samym skyboxie nie ma niczego niezwykłego, natomiast warto wspomnieć o jednej prostej optymalizacji zastosowanej tutaj. Tradycyjnie skybox posiada 4 ściany po bokach, jedną na górze oraz jedną na dole (czasem skyboxy przedstawiają również teren znajdujący się bardzo daleko i dolna ściana pokryta jest wtedy teksturą np. ziemi). Jednak w tym wypadku cały teren jest modelem, dlatego dolna ściana nie jest potrzebna (więc skybox ma tylko 5 ścian). Jeśli chodzi o liczbę trójkątów (2 mniej) jest to żadna oszczędność, natomiast różnica pojawia się w wywołaniach Pixel Shadera. Ich liczba zależy wprost od liczby pikseli zajmowanych na ekranie przez dany trójkąt (co z kolei zależy od ustawienia kamery). W skrajnym przypadku, gdy kamera patrzy w dół, Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 66 5.7. Użyte modele dolna ściana skyboxa może rozciągać się nawet na całe okno aplikacji (niezależnie od tego, czy jest zakrywana przez teren, czy nie), co w przypadku wymiarów 800 x 600 oznacza 480 000 wywołań Pixel Shadera. Usunięcie dolnej ściany daje więc w takim wypadku dokładnie taką oszczędność. 5.7 UŻYTE MODELE Aplikacja używa kilku modeli udostępnionych za darmo: rośliny, autor: killst4r, źródło: http://www.turbosquid.com/FullPreview/Index.cfm/ID/490485 kamienie, autor: Veleran, źródło: http://www.turbosquid.com/FullPreview/Index.cfm/ID/561881 trawa – model własny, tekstura: autor: Metadsign, źródło: http://www.turbosquid.com/FullPreview/Index.cfm/ID/662021 5.8 WYDAJNOŚĆ Gdy omówiono już, jakie korzyści daje teselacja, można przeanalizować, jak to się przekłada na poprawę wydajności. W zaimplementowanej aplikacji uproszczona siatka terenu, jak już wcześniej wspomniano, ma Maksymalny współczynnik teselacji używany dla terenu wynosi teren był w takim stopniu teselowany, to finalna siatka miałaby wierzchołków. , więc gdyby cały wierzchołków. Jednak ponieważ zastosowano adaptacyjny Level of Detail, faktyczna siatka wygenerowana przez GPU będzie miała zmienną liczbę wierzchołków i będzie gdzieś pomiędzy 1 tysiącem a 4 milionami (prawdopodobnie najczęściej kilkadziesiąt – kilkaset tysięcy, chociaż ciężko oszacować tę wartość). Pojedynczy wierzchołek ma 2 atrybuty: pozycję zapisywaną w zmiennej typu float3 oraz współrzędną tekstury zapisywaną w zmiennej typu float2. To daje bajtów na wierzchołek. Zatem uproszczona siatka terenu zajmuje bajtów, czyli ok. 21.3 kB. Gdyby nie używać teselacji, to trzeba by wygenerować siatkę o maksymalnej, wcześniej wspomnianej, liczbie wierzchołków. Jej objętość w pamięci wynosiłaby więc bajtów, czyli ok. 80.1 MB. Natomiast, jak wspomniano, siatka powstała w wyniku teselacji ma zmienne rozmiary, ale dzięki niskim współczynnikom teselacji dla części siatki oddalonych od kamery, jej rozmiary nigdy nie zbliżą się do maksymalnych. Na potrzeby obliczeń niech jej średni rozmiar wynosi wierzchołków. To daje bajtów, czyli ok. 1.9 MB. Różnice są kolosalne. Siatka uproszczona jest ok. 3 800 RAZY mniejsza niż siatka bez użycia teselacji. Nie tylko jest to bardzo duża oszczędność pamięci (zarówno CPU, jak i GPU), ale co najważniejsze jest to ogromna optymalizacja, jeśli chodzi o wąskie Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 5.8. Wydajność 67 gardło, jakim jest przesyłanie danych z CPU do GPU. Tak ogromna, że nawet wygenerowanie nowych wierzchołków i wyliczenie dla nich pozycji (poprzez np. teselację Phonga albo displacement mapping) wciąż zajmują mniej czasu. Dzięki temu narzut związany z techniką teselacji jest z nawiązką nadrabiany przez ominięcie wąskiego gardła i w ostatecznym rozrachunku teselacja wciąż daje duży wzrost wydajności. Tym bardziej, że dzięki adaptacyjnemu LOD-owi generowana siatka i tak ma znacznie mniej wierzchołków niż ta maksymalna. Jak to się przekłada na szybkość działania aplikacji i liczbę klatek na sekundę? Niestety konkretnych liczb autor nie jest w stanie przytoczyć, gdyż wyrenderowanie tak dużego terenu bez teselacji, przy dokładności terenu do 0.5 m, jest niemożliwe w czasie rzeczywistym – przynajmniej, jeśli poza terenem renderowane są również obiekty, których w całej scenie są tysiące, a ponieważ wszystkie one są teselowane, tutaj również występuje spora oszczędność. Sam teren przy 4 milionach wierzchołków na sprzęcie autora (średniej klasy laptop) prawdopodobnie renderowałby się przy bardzo niskim FPSie (15? 5?). Natomiast próba wyrenderowania setek albo tysięcy obiektów przy tak gęstych siatkach, jakie daje teselacja, skończyłaby się renderowaniem pojedynczej klatki przez wiele sekund. Na koniec trzeba wspomnieć o ogólnej wydajności zaimplementowanej aplikacji. Renderuje ona teren o powierzchni 1 km2 pokrytego tysiącami obiektów. Dzięki frustum cullingowi oraz instancingowi, bez których to w ogóle nie byłoby możliwe, wydajność jest dobra i aplikacja chodzi płynnie. W zależności od kadru kamery, FPS wynosi od 20 – 30 do nawet 90 (Intel i5 2.4 GHz, HD Mobility Radeon 5730, 4 GB RAM). Jak na ilości obiektów i geometrii na scenie to są w pełni zadowalające wyniki. Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 68 Renderowanie szczegółowego terenu z użyciem teselacji 5.8. Wydajność Piotr Martynowicz 6.1. Realizacja celów 69 6 PODSUMOWANIE 6.1 REALIZACJA CELÓW Temat pracy został zrealizowany – stworzono aplikację desktopową prezentującą zalety teselacji poprzez renderowanie przykładowego dużego otwartego terenu pokrytego tysiącami obiektów. Program pozwala użytkownikowi na poruszanie się kamerą po scenie i oglądanie efektów działania teselacji. Aplikacja posiada wszystkie zaplanowane funkcjonalności oraz, co ważne, cechuje się bardzo dobrą wydajnością. Ponadto spełnia ona swoją funkcję demonstracyjną – pokazuje w praktyce zastosowanie teselacji do renderowania terenu oraz obiektów i pokazuje korzyści płynące z takiego rozwiązania. Jednak, jak to zawsze bywa w przypadku silników graficznych, zawsze można wiele rzeczy ulepszyć i dodać nowe funkcje. 6.2 MOŻLIWE USPRAWNIENIA I PERSPEKTYWY ROZWOJU Wydajność, jak wspomniano, jest dobra, ale oczywiście zawsze istnieje pole do ulepszeń. Pierwsze, co przychodzi na myśl, to wykorzystane modele obiektów. Autor nie jest grafikiem 3D i nie był w stanie sam wymodelować roślinności i skał. Modele znalezione w internecie są dobrej jakości, ale nie były tworzone z myślą o teselacji i mogłyby zostać zoptymalizowane w tym kierunku – mogłyby mieć mniej trójkątów (szczególnie pnie palm), gdyż 64-krotna teselacja i tak dałaby wystarczającą gęstość siatki. A mniej geometrii do przetworzenia, to oczywiście mniej pracy dla karty graficznej i wyższy FPS. Innym pomysłem na optymalizację jest użycie drzewa ósemkowego (ang. octree) przy frustum cullingu. Pozwala ono na szybkie odrzucenie całych obszarów z obiektami na scenie, przyspieszając proces szukania widocznych obiektów. Być może również przejście z bounding boxów na bounding sphere’y dałoby poprawę wydajności (gdyż testowanie sfery jest znacznie szybsze niż prostopadłościanu), ale jest to coś, co trzeba by sprawdzić w praktyce (czasem niektóre optymalizacje okazują się wcale nie poprawiać wydajności). Oczywiście to są tylko przykłady, optymalizacji wydajności można szukać w bardzo wielu miejscach i możliwości jest sporo. Poza wydajnością na pewno można by poprawić stronę graficzną renderowanej sceny. Przede wszystkim przydałaby się porządna woda, implementująca takie efekty jak odbijanie obiektów na jej powierzchni, refrakcja, kaustyka, może jakaś piana morska przy brzegach wyspy itd... Jeśli chodzi o teren, to na pewno przydałoby się użycie techniki zwanej detail mappingiem. Polega ona na użyciu specjalnej, szczegółowej tekstury np. trawy w miejscach znajdujących się blisko kamery oraz innej, mniej szczegółowej, dla miejsc bardziej oddalonych (oczywiście przejście między jedną a drugą jest płynne). Daje Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 70 6.2. Możliwe usprawnienia i perspektywy rozwoju to bardzo ładne efekty i sprawia wrażenie, jakby tekstura całego terenu była taka szczegółowa. Ponadto całej scenie bardzo by pomógł shadow mapping (technika renderowania cieni). Inny element, któremu przydałoby się ulepszeniem, to niebo. Zamiast skyboxa można by zrobić prawdziwe (w sensie jako osobne obiekty) chmury i słońce. Można nawet pójść dalej i zrobić tzw. cykl dnia i nocy (zmiana kolorów nieba i terenu/obiektów na podstawie godziny panującej w świecie gry oraz rotacja słońca i księżyca) czy zaimplementować różne warunki pogodowe (słońce, deszcz, śnieg, burzę, mgłę). Możliwości jest bardzo wiele. Scenie nie zaszkodziłoby też ożywienie jej – animowane, poruszające się po wyspie zwierzęta, latające nad nią ptaki czy ryby pływające w przybrzeżnych wodach. Do tego animacje kołysania się palm i trawy na wietrze i/lub dymiący wulkan. Ponadto w większości współczesnych gier często stosuje się też różne efekty postprocess: bloom (rozmycie w najjaśniejszych miejscach obrazu symulujące podobny efekt obserwowany w świecie rzeczywistym), blur lub motion blur (rozmycie obiektów w ruchu), Screen Space Ambient Occlusion (SSAO), Depth of Field (DoF) i wiele innych. Przy czym to są wszystko raczej smaczki, mające na celu zrobienie wrażenia niż dodanie wartości merytorycznej. Jeśli chodzi o samo użycie teselacji, to tutaj raczej niewiele można poprawić – można oczywiście zastosować ją do jeszcze jakichś innych obiektów (których teraz w scenie nie ma), ale raczej nie wprowadzi to już niczego naprawdę nowego w temacie. Natomiast rozwiązania przedstawione i zaimplementowane w tej pracy z pewnością można by zastosować w komercyjnych grach i programach, poprawiając ich wydajność i jakość grafiki. Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 6.2. Możliwe usprawnienia i perspektywy rozwoju 71 7 PRZYPISY Zakładając użycie tego samego materiału dla wszystkich trójkątów modelu. Ilość pracy wykonywanej przez kartę graficzną zależy od wielu czynników, ale w prostym przypadku dla pojedynczej siatki – 2 razy więcej trójkątów to 2 razy więcej pracy. ↑ [2] Można oczywiście stworzyć własną bibliotekę przekształcającą dane modeli 3D na obraz na ekranie, ale jest to zadanie bardzo niskopoziomowe, trudne i wymagające dużego nakładu pracy (a osiągnięta wydajność prawdopodobnie i tak będzie daleka od tej oferowanej przez istniejące rozwiązania). ↑ [3] Źródło: MSDN, http://msdn.microsoft.com/enus/library/windows/desktop/ff476882%28v=vs.85%29.aspx ↑ [4] Oczywiście teselator jest maszyną, która działa wg ściśle ustalonych reguł, jednak konkretny algorytm podziału prymitywów wg zadanych współczynników teselacji nie jest podany nigdzie w dokumentacji. Dlatego przedstawiona została ogólna zasada, którą powinien kierować się programista. ↑ [5] Autor: Bay Raitt, źródło:, Tariq S., NVIDIA, GDC09: D3D11 Tessellation, http://developer.download.nvidia.com/presentations/2009/GDC/GDC09_D3D11Tes sellation.pdf ↑ [6] Źródło: Boubekeur T., Alexa M., Phong Tessellation, http://perso.telecomparistech.fr/~boubek/papers/PhongTessellation/PhongTessellation.pdf ↑ ↑ ↑ [7] Źródło: Wikipedia, http://en.wikipedia.org/wiki/Phong_shading ↑ [8] Źródło: Strona Paula Heckberta, http://www.cs.cmu.edu/~ph/nyit ↑ [9] W zasadzie normal mapping ma ogólnie na celu symulowanie efektów oświetlenia siatek o znacznie większej liczbie wierzchołków niż faktyczna – to, czy grafik komputerowy wymodelował szczegółową siatkę, czy szczegóły są dodawane w locie poprzez displacement mapping, nie ma znaczenia. ↑ [10] Źródło: Lighthouse3d.com, http://www.lighthouse3d.com/tutorials/view-frustum-culling/ ↑ [11] Draw call to pojedyncze „zlecenie” wyrenderowania obiektu/ów dla karty graficznej (wszystkie inne wywołania API jedynie przygotowują wszystko do renderowania). Liczba Draw calli ma duże znaczenie, jeśli chodzi o wydajność – im większa ich liczba, tym gorsza wydajność. Nawet wyrenderowanie tej samej liczby obiektów przy pomocy mniejszej liczby draw calli daje poprawę wydajności. ↑ [12] Shader Model to zestaw funkcji i możliwości shaderów obsługiwanych przez konkretne karty graficzne zdefiniowany przez DirectX. W chwili pisania pracy najwyższą wersją było 5.0. ↑ [1] Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji 72 6.2. Możliwe usprawnienia i perspektywy rozwoju [13] Przez dokładność siatki autor ma tu na myśli jej gęstość – przykładowo dokładność do 1 m oznacza, że kolejne wierzchołki w danym rzędzie znajdują się w odległości 1 m od siebie. ↑ Renderowanie szczegółowego terenu z użyciem teselacji Piotr Martynowicz 6.2. Możliwe usprawnienia i perspektywy rozwoju 73 8 BIBLIOGRAFIA 1. [Boost] Dokumentacja biblioteki boost, http://www.boost.org/doc 2. [Boubekeur08] Boubekeur T., Alexa M., Phong Tessellation, http://perso.telecomparistech.fr/~boubek/papers/PhongTessellation/PhongTessellation.pdf 3. [MSDN] Microsoft Developer Network, http://msdn.microsoft.com/en-us/library/ms123401.aspx 4. [Sawicki07] Sawicki A., Precompiled Headers w Visual C++, http://www.asawicki.info/productions/artykuly/PrecompiledHeaders.php5 5. [Story10] Story J., Cebenoyan C., Tessellation Performance, http://developer.download.nvidia.com/presentations/2010/gdc/Tessellation_Performa nce.pdf 6. [Valdetaro10] Valdetaro A., Nunes G., Raposo A., Feijó B., Understanding Shader Model 5.0 with DirectX 11, http://www.sbgames.org/papers/sbgames10/computing/tuto_comp1.pdf 7. [Wikipedia] Wikipedia, http://en.wikipedia.org, http://pl.wikipedia.org Piotr Martynowicz Renderowanie szczegółowego terenu z użyciem teselacji