Sporo czasu minęło od notki związanej z połączeniem materiałów z meshami, ale nie był to czas do końca stracony (choć mamy wakacje). Od około tygodnia (a może i nawet dłużej) męczyłem bowiem się z ładnym modułem oświetlenia w Scavie, który:
- Jest implementacją oświetlenia per fragment (plus pewne dodatki, których na razie nie ma)
- W prosty sposób umożliwia dodawanie źródeł światła na scenę
- Dokładniej – w taki, żebym nie zapomniał jak to się robi
- Źródeł światła może być tyle, na ile pozwala dana technologia oraz możliwości komputera (domyślnie – nieskończenie wiele)
- W jednym momencie scena może być oświetlana zarówno przez światła punktowe, kierunkowe jak i stożkowe
- Ma to po prostu działać
Dziwnym trafem udało się te postulaty zrealizować. Ale nie powiem, żeby było to strasznie proste, chociażby z tego względu, że trzeba było dopisać mnóstwo kodu, a poza tym klasa managera sceny nie wygląda już tak klarownie, gdyż pojawiły się odwołania do parametrów shadera (napisanego w języku Cg).
Zacznijmy może od górnej warstwy kodu, czyli tzw. klas pomocniczych.
Klasa CShader
Jak się można domyślać, niezbędne stało się stworzenie jakiegoś interfejsu, który umożliwiałby łatwiejsze posługiwanie się shaderami. Poza tym, była też osobista motywacja – we frameworku Yesta nie było to do końca przemyślane (jak większość zresztą) i każda klasa, która miała do czynienia z programami na GPU (na przykład oświetlenie, niebo, trawa, woda) same sobie implementowały wszystkie metody i je powtarzały z nieznacznymi modyfikacjami. Dlatego niemal od razu zdecydowałem się zadbać o odpowiednie mechanizmy rządzące shaderami, stąd też mamy klasę CShader (klasa abstrakcyjna) oraz jej dwie pochodne – CShaderCg (dla shaderów napisanych w języku Cg) oraz nieco mniej rozbudowaną CShaderGLSL (analogicznie dla GLSL-a – mniej rozbudowaną, gdyż rzadziej korzystam z GLSL-a, zatem to raczej kwestia czasu). Co istotne, klasy dają się w prosty sposób rozszerzać (inaczej – komplikować implementację) poprzez kolejne funkcje, które jedynie ułatwiają dalsze pisanie kodu i hermetyzację (wraz z hasłem “niech żyje obiektowość, oj oj”).
Każdy obiekt shadera to tak naprawdę kontekst wraz z programami cieniującymi wierzchołki oraz fragmenty (póki co nie interesowałem się shaderami geometrii). Tutaj warto wspomnieć, że ktoś, kto chce sam manipulować shaderami, musi pamiętać o odpowiednich inicjalizacjach czy ręcznych kompilacjach (jeśli takie sobie ustawi). Na szczęście (dla mnie), domyślnie jest wszystko ustawiane automatycznie, a jedynie w specjalnym przypadku (który oczywiście dotyczył mnie w sprawie dynamicznych tablic) trzeba samemu specjalnie poustawiać. Tym niemniej, jest to możliwe. Wszystkie parametry podzielone są na: parametry najzwyklejsze, parametry macierzowe oraz parametry teksturowe. Wszystkie te rodzaje są w osobnym mapach, które ułatwiają wstawianie (klucz, pod jakim wstawiamy jest taki sam jak nazwa parametru w shaderze) oraz wyszukiwanie. Także shadery mamy załatwione.
Klasa CLight
Z drugiej strony musimy pamiętać, iż czeka nas konfrontacja ze światłem. Nie polega to bynajmniej na obsłudze i liczeniu długości tuneli, w których znajdziemy to światełko ani liczeniu zużycia świeczki, ale musimy skupić się na (niespodzianka) prawdziwych źródłach światła. Podobnie jak w przypadku CShader, mamy tutaj do czynienia z jedną klasą abstrakcyjną (CLight) oraz pochodnymi: CPointLight (światło punktowe – umiejscowione w jednym miejscu i świecące we wszystkich kierunkach (przykład – pochodnia w Wielkim Mistrzu)), CSpotlight (światło stożkowe, pochodne z CPointLight, świecące w dodatku w jednym konkretnym kierunku (przykład – latarka, której nie da się wymacać w ciemnym pomieszczeniu)) oraz CDirectionalLight (światło kierunkowe – nieskończenie oddalone, oświetlające równomiernie w zadanym kierunku (przykład – Księżyc, który jest bardziej potrzebny od Słońca)). Każdy rodzaj światła ma własne parametry, które mogą być ustawiane oraz pobierane i właściwie to wszystko – CLight z dziećmi nie są zbyt inteligentnymi stworzeniami. Zastosowałem jednak jeszcze jeden mały trik (który jest pewnie każdemu znany), a mianowicie każda klasa pochodna reprezentuje pewną odmianę zwykłego światła (w tym przypadku światła, w innym shadera, efektu czy nutrii), zatem ma swój typ, który można pobrać za pomocą GetType. W czym to nam pomaga? Otóż, nasze światełka będą przechowywane w mapie <std::string, CLight*>. Sprecyzuję – wszystkie nasze światełka, niezależnie, jakiego typu są. Czasami jednak musimy dojść do tego, do czego przypisać dany obiekt i owszem, można próbować Cudów Na Kiju (CNK) z sizeof czy innymi zabawkami, ale prościej po prostu pamiętać tę informację w każdym obiekcie i sprawdzić, że “oho – obiekt zapisany pod kluczem “flashlight” ma typ LT_SPOTLIGHT, więc jest światłem stożkowym. A do czego nam to będzie potrzebne – napiszę dalej.
Klasa CEffect
Pozostała jeszcze jedna cegiełka w naszej drabince (a nie, to szczebelek, a nie cegiełka). Trzeba przyznać, że głupio byłoby nakazać programiście samemu zarządzać shaderem w sytuacji, gdy moduł oświetlenia ma działać niemalże automatycznie i niezauważalnie (oprócz oczywiście samego rezultatu działania światła). Dlatego też powstał typ obiektów, które są pośrednikami pomiędzy zleceniodawcą a shaderem, trzymają większość potrzebnych informacji i ogólnie sprawiają dobre reprezentacyjne wrażenie – są to twory nazwane przeze mnie efektami (nie mylić z plikami efektów .fx i podobnymi). Z założenia klasa CEffect – podobnie jak wcześniej to było z CShader oraz CLight – będzie miała swoje dzieci, które będą tymi właściwymi efektami. Póki co jest tylko CLightingEffect, który jest klasą oświetlenia i tak naprawdę jest obiektem Standardowego Modułu Oświetlenia (SMO – w innych kręgach System Masowej Obsługi). To właśnie ten obiekt zawiera w sobie mapę świateł, udostępnia obiekt shadera, ale tak prawdę mówiąc – w przypadku samego modułu prawie całość operacji przypada na manager sceny, gdyż to on zawiera szczegółowe informacje gdzie czego użyć. Fakt, wygląda to trochę jak pomieszanie country z metalem (nierdzewnym) i być może będzie trzeba to zmienić, gdyż w CSceneManager zrobił się mały bałagan. Czyżby kolejna klasa pochodna tylko-dla-SMO (mamy oświetlenie, które sobie sam może zdefiniować użytkownik, ale także SMO zarządzane przez sam silnik)? W sumie nie jest to nawet takie głupie…
Widzicie jaką inspiracją jest pisanie notek na devbloga?
Teraz słowo o tym jak to tak naprawdę przebiega. Domyślnie nie ma żadnego oświetlenia, jednak większość będzie chciała dodać takowe na własną scenę. Załóżmy, że jest to zwykłe, białe światło punktowe bez skomplikowanego równania tłumienia i umieszczone na pozycji (0, 0, 0). Zatem tadam:
GetSceneManager().AddLight(“punkcik1”, LT_POINT);
I to wystarczy – od tej pory SMO już wie, że jest jakieś światło i trzeba odkurzyć program shadera. Jak widać, całość została osadzona w managerze sceny – oświetlenie tyczy się całej sceny, więc uznałem, że tak jest miejsce SMO. W dodatku są parametry, które nie wchodzą w skład efektu – mam na myśli kolor światła otaczającego oraz pozycję oka kamery. Jednak głównym składnikiem oświetlenia są oczywiście źródła światła, zatem dodanie choćby jednego uruchamia całą lawinę związaną z generacją efektu i inicjalizacją shadera. Finał tej zabawy odbywa się w momencie pierwszego renderowania sceny (niestety, założyłem póki co, że wszystkie światła są znane od początku), gdzie shadery są kompilowane… A właśnie – kompilacja. Wspominałem o tym, że zostało umożliwione kompilowanie shaderów ręcznie (w skrócie – wrapper na funkcję cgSetAutoCompile) i to z bardzo konkretnego powodu. Jedno z założeń mówi “Źródeł światła może być tyle, na ile pozwala dana technologia oraz możliwości komputera (domyślnie – nieskończenie wiele) “. Oznacza to również, iż shader nie ma sztywnej wielkości tablicy świateł – należy ją ustalić w locie. Wcześniej nawet nie wiedziałem, że takie coś jest możliwe, ale nawet się uśmiechnąłem (a to się rzadko zdarza, wierzcie mi), jak przeczytałem w dokumentacji o “unsized arrays” (pod linkiem w przykładzie 32-3). Oczywiście, okazało się, że tak prosto nie będzie – materiały w sieci nie są zbyt bogate i po wielu bezowocnych próbach udało mi się w końcu doprowadzić proces runtime’u do stanu używalności (pamiętajcie, najpierw inicjalizacja wielkości tablic, potem inicjalizacja elementów tablicy, a dopiero na końcu kompilacja). Tak czy inaczej, w końcu się jednak udało i pod tym względem shader (prawie 200-linijkowy) zyskał na uniwersalności.
Chciałem również umożliwić inną rzecz – osobne shadery/efekty dla poszczególnych meshów. Istnieje bowiem możliwość chwilowego zastąpienia głównego shadera sceny shaderem konkretnym dla danego obiektu, który działa tylko dla niego.
Podsumowując – zagadnienie SMO niewątpliwie ciekawe i bardzo nerwowe (zarówno pozytywnie ('”ojej, uda się”) jak i negatywnie (“no ja łapię nogi motyli!”)), ale niewątpliwie wprowadził nieco zamieszania do kodu managera łamiąc parę punktów zasad obiektowości. Czy uda się to jakoś okiełznać? Czy zostaną wprowadzone nowe modyfikacje? Czy Rysiek z Klanu będzie umiał napisać cRPG-a opierając się na Scavie? O tym w następnych odcinkach cyklu “Jak nie robić silnika”.
Pozdrawiam i dziękuję - SceNtriC
0 komentarze:
Prześlij komentarz