Torment ‒ mój jest ten kawałek podłogi
To pierwsza aktualizacja Grimuaru w całości poświęcona modelowaniu interaktywnej podłogi. W bardzo technicznym opisie Nathana Fabiana poznamy innowacyjny system ruszających się płaszczyzn.
Z tej strony Nathan Fabian. Na co dzień pracuję dla laboratorium Departamentu Energii, składając obrazki z miliardów elementów skończonych. Po godzinach param się projektowaniem gier, między innymi jako konsultant swojej firmy Longshot Studios. Jestem jednym z donatorów Tormenta, a z jego twórcami współpracuję na pół etatu już niemal od roku. Pracuję głównie nad systemem animacji, teraz jednak chciałbym poruszyć temat innego wyzwania, jakiego się podjąłem. Będzie nieco technicznie, ale z pewnością spodoba Wam się końcowy efekt mojej pracy.
Wyobraźcie sobie słup, taki najzwyczajniejszy w świecie. Chodzi mi właściwie o jego model komputerowy, składający się z kilkudziesięciu polygonów (dość typowe dla słupa). Waszym zadaniem jest teraz jego pomnożenie – stworzenie ze 100×100 słupów dynamicznej podłogi, której każdy fragment może się przemieszczać w górę i w dół, zmieniając tym samym jej kształt. Pojedynczy słup musi przekształcić się w megastrukturę składającą się z 10 000 słupów.
„Żaden problem”, możecie powiedzieć. „Mamy wystarczającą moc i kod!” Tworzycie zatem pętlę, budujecie 10 000 słupów i… Wasza karta graficzna staje w płomieniach (nie dosłownie). Nie tego zaklęcia szukaliśmy.
Nowoczesne karty graficzne mają niesamowitą moc i mogą renderować setki milionów trójkątów na sekundę. Dla kogoś, kto wychował się na książkach i artykułach Michaela Abrasha, to wręcz nierozróżnialne od magii. Za moich czasów rozdzielczość 320×240 była sporym osiągnięciem („Patrzcie! Kwadratowe piksele!”).
Co poszło nie tak przy rzucaniu zaklęcia? Co nie spodobało się diabelskiej skrzynce?
Draw calls.
Pojęcia draw calls używa się do opisania poleceń przesyłanych karcie graficznej przez procesor. Komunikacja obu układów scalonych w obrębie płyty głównej przypomina nawoływanie się w pomieszczeniu, w którym panuje hałas; przekazanie wiadomości wymaga powtórzeń i czasu. Z początku poleciliśmy wykonanie 10 000 draw calli. To o jakieś 9999 za dużo, jeżeli chcemy wyrenderować tylko jedną strukturę. Musimy poszukać lepszej metody.
Na szczęście istnieje rozwiązanie tego problemu – instancjonowanie. Nie można co prawda wykorzystać tej metody bez odpowiedniej konfiguracji na systemach, które wspierają tylko Shader Model 3 (starsze wersje Windowsa, OSX i Linuxa). Dobra wiadomość jest taka, że Unity – silnik, na którym pracujemy – posiada przydatny w tym zakresie wsadowy system przetwarzania, zarówno statyczny, jak i dynamiczny. Tworzy on z niewielkich obiektów składających się z polygonów większą całość. Niestety jednak nie sprawdzi się to w tym konkretnym przypadku.
Co ważniejsze, nie byłoby dobrze polegać na takim kodzie. Na warsztatach pisarskich otrzymasz radę: „Pisz o tym, o czym wiesz”; podobnie jest w programowaniu. Powinno się zawrzeć w kodzie to, co już się wie, a resztę wywnioskować z algorytmów. W tym przypadku chcemy konkretnej rzeczy: podłogi złożonej z 100×100 słupów. Grupując obiekty w ten sposób, zredukujemy liczbę przesyłanych poleceń. Ponieważ silnik Unity ogranicza liczbę wierzchołków siatki do 64 000, podzielimy ją na kilka mniejszych sekcji.
To już wszystko… no, prawie.
Nasza podłoga jeszcze nie może się poruszać w górę i w dół. Teoretycznie można by ponawiać proces przetwarzania za każdym razem, gdy porusza się jeden ze słupów. To jednak znacznie spowolniłoby animację, a my chcemy przecież, by miała ona miejsce w każdej klatce.
Szybka wymiana zdań z zespołem od efektów specjalnych: „Chcemy anim…” „Tak.” „Dobra.”
Jak się okazuje, możemy przemieszczać wierzchołki, nie wykraczając poza funkcje Shader Model 3. Musimy tylko trochę wspomóc się teksturą. Wartości kolorów pikseli na kanale czerwonym zdefiniują wysokość (w zakresie od 0 do 255, gdzie 0 oznacza najniższą pozycję, kolor czarny, a 255 – najwyższą, kolor jaskrawoczerwony), która reprezentuje każdy zakres wysokości z dyskretyzacją do 256 progów. A tak wygląda to w shaderze wierzchołków:
float4 lkup = float4((v.texcoord1.xy + _DisplacementOffset.xy) * _DisplacementScale.xy, 0.0f, 0.0f);
float4 dispV = tex2Dlod (_DisplacementTex, lkup);
float dis = (dispV.x + dispV.y/256 + dispV.z/65536) * _VerticalScale + _VerticalOffset;
Dzięki wykorzystaniu dodatkowych kanałów koloru (poza czerwonym) i przemnożeniu wartości przemieszczenia na procesorze (a następnie podzieleniu przez tę samą wartość na karcie graficznej) możemy uzyskać znacznie większą precyzję, przewyższającą 256 poziomów dyskretyzacji. Przy użyciu czerwonego, zielonego i niebieskiego kanału otrzymamy nawet 256^3 wartości. Jeśli dołączymy do tego kanał alfa, możemy uzyskać nawet 256^4 wartości, ale prawdopodobnie nie będziemy potrzebowali ich aż tak wielu. Zawsze możemy zarezerwować czwarty kanał na potencjalne dodatkowe informacje.
Do animacji podłogi będziemy tylko potrzebowali zmienić teksturę lub pozmieniać wartości kolorów pikseli składających się na tę teksturę. Wiąże się to z dodatkowym draw callem, ale na szczęście tylko jednym – nie dziesięcioma tysiącami, tak jak na początku.
Teraz to już wszystko… prawie.
Musimy jeszcze dopasować światło shadera do reszty naszego fantastycznego potoku graficznego. Choć nie jest to szczególnie skomplikowane, wymaga zduplikowania kodu światła w nowym shaderze. Dzięki temu słupy mogą zostać poprawnie umieszczone zanim nastąpią obliczenia odpowiadające za oświetlenie.
Kiedy już uporządkujemy to wszystko, otrzymamy wyrenderowaną dynamicznie poruszającą się podłogę.