Często dostaję pytania…

Categories Programowanie

Często dostaję pytania dotyczące mojego silnika do gier o takiej treści: „Czym on się różnicy od innych silników?”. Raz został on nawet porównany do Maluch Racer. Przez dwa ostatnie miesiące siedziałem nad elementem, który odróżni ten silnik, od innych dostępnych na rynku silników do gier i będzie miał coś co będzie wywoływało efekt „WOW”. Robiłem wszystko, żeby można było powiedzieć, że ma on coś, co w innych silnikach nie jest dostępne na takim poziomie i mam nadzieję, że to co udało mi się osiągnąć można za takie coś uznać.

Elementem, który zajął mi tyle czasu jest trawa. Może nie brzmi jak coś trudnego, ale zaufajcie mi, że jest. Spędziłem nad tym sporo czasu po godzinach i na dzień dzisiejszy jestem w stanie powiedzieć, że dotarłem do momentu, gdzie jestem dumny z tego co udało mi się osiągnąć. Efekt możecie zobaczyć na filmiku, który dołączyłem do tego wpisu na mikroblogu i został niestety nieładnie skompresowany przez algorytmy YT (-‸ლ). Opowiadam też tam krótko o elementach, które pokazuje na filmię. Jako, że mówię na tym filmie w języku angielskim, to czuję, że będzie śmiesznie, ale cóż… starałem się, ale przyznaję się, że ostatnio mój angielski się dość znacząco pogorszył (╥﹏╥).

Skoro wiemy już z czym mamy do czynienia to przejdźmy do samego mięska i zakończmy to moje biadolenie w związku z angielskim. Moim celem podczas dodawania do procesu renderowania w silniku nowego elementu było to, aby na takim PC jak mój, czyli GTX 960, i5 4460, 12 GB RAM silnik ten chodził przynajmniej w 30 FPS. Była to taka wartość graniczna poniżej której nie mógł spadać klatkarz.

Na samym początku zacząłem od wymodelowania trawy. Na starcie wymodelowałem pojedyncze źdźbła trawy, a następnie wyrenderowałem to do tekstury (bake to texture). Potem tą tekturę użyłem na zwykłym prostokącie (Quad), który poobracałem pod różnymi kątami, aby stworzyć z tego kilka kępków trawy. Jak się potem okazało musiałem do tego podchodzić drugi raz, bo model, który stworzyłem był zbyt duży i na terenie, gdzie było dużo nachylenie wyglądał on niepoprawnie. Nowy model, który był bardziej zbity, rozwiązał ten problem.

Następnie rozpoczeła się przeprawa przez szambo. Wyciągnąłem z szafy strój szambonurka i zanurkowałem. Dotarłem do miejsca, o których praktycznie nic nie można znaleźć w internecie i to nawet w oficjalnej dokumentacji Microsoftu, która dotyczy DirectX 11. Wiedziałem co chcę osiągnąć i nawet wiedziałem mniej więcej jakie funkcje w DirectX muszę wywołać, ale nie miałem bladego pojęcia co ja mam do nich przekazać.

Moim celem była redukcja użycia CPU w procesie renderowania trawy. Jego użycie miało być ograniczone do minimum. Znalazłem w jednej prezentacji, że jest to możliwe, gdy wykorzystamy tzw. „Indiriect Rendering”. Nie ściągając mojego ubrania roboczego zanurkowałem w kolejne szambo. Zanim jednak miałem wykorzystać tą technikę renderowania postanowiłem rozwiązać inne problemy.

Na samym początku potrzebowałem wygenerować pozycję, rotację i inne dane, które będą używane do umiejscowienia trawy na ternie. Nie mógł tego robić CPU, bo było by to niewydajne. Dlatego stworzyłem kamerę ortagonalną, która patrzy w dół i porusza się po siatce za kamerą gracza. Następnie odrzuciłem obszary, gdzie trawy nie powinno być. Jak? Utworzyłem sobie teksturę, gdzie białe obszary oznaczały, że tutaj trawa jest potrzebna, a czarne, że tutaj trawy ma nie być. Następnie na wyjściu Pixel Shader mógł zapisywać tą informację. Nie zapisywał jej jednak po prostu do tesktury! Nie nie! Główna funkcja Pixel Shadera była typu void, a zamiast tego wynik był zapisywany do AppendStructuredBuffer. Dzięki temu mogłem w Pixel Shaderze zapisywać dużo więcej informacji, niż tylko kolor.

Następnie utworzyłem kolejny shader – Compute Shader. Ten shader odpowiedzialny jest za przygotowanie argumentów do funkcji „DrawIndexedInstancedIndirect”, która następnie jest wywoływana i rozpoczyna renderowanie trawy za pomocą specjalnego Vertex oraz Pixel Shadera. Dane do pozycji tej trawy są pobierane z wygenerowanego wcześniej AppendStructureBuffera, a część obliczeń przerzucona jest na GPU. Świetnym przyładem jest tutaj macierz świata, która jest wyliczana dla każdego wierzchołka na podstawie danych z AppendStructuredBuffer oraz identyfikatora instacji (SV_InstanceID).

Możece teraz myśleć „O czym ten koleś gadasz?”. To w pełni zrozumiałe. Przeszedłem przez ten temat renderowania trawy bardzo szybko i częściowo nie opisując niektórych elementów. Wynika to z tego, że nie ma możliwości na wykopie wrzucać obrazków krok po kroku, a tej tematyki nie da się omówić za pomocą samego (suchego) tekstu. Także, jeżeli kogoś zainteresowało to wprowadzenie do tematyki, to zapraszam do mojego pełnego artykułu na ten temat, gdzie każdy element opisuje w najmniejszym detalu, dodaję do tego obrazki z wytłumaczeniem jak co działa i kawałki kodu, które faktycznie są wykorzystywane w silniku. Zresztą jak ktoś chce zobaczyć kod źródłowy to nie ma problemu, ponieważ jest on dostępny tutaj. Natomiast wersja wykonywalna dostępna jest tutaj. Jeżeli macie jakieś pytania to zapraszam do komentowania i to szczególnie, jeżeli po przeczytaniu artykułu macie jakieś wątpliwości i chcielibyście czegoś więcej się dowiedzieć. Sam wiem jak ciężko jest znaleźć informacje na ten temat, także każdemu chętnie pomogę.

#programowanie #gamedev #gry #directx #grafikakomputerowa #physx #clusek