Zdążyliśmy się już przyzwyczaić do tego, że w programowaniu grafiki bardzo często wykorzystujemy zastane mechanizmy w zupełnie inny sposób niż ten formalnie założony. I tak: tekstury nie muszą być tylko bitmapami nakładanymi na geometrię, ich rzekome współrzędne mogą tak naprawdę przechowywać pozycję 3D lub normalne, a piksele nie muszą wcale zawierać danych o kolorach.
Analogicznie potok renderowania nie musi też produkować pikseli do wyświetlenia na ekranie! Zamiast tego możliwe jest, aby dokonywał on najpierw wyliczania pewnych właściwości materiałów dla każdego piksela na powierzchni ekranu i to właśnie te informacje zapisywał jako wynikowy “kolor” w teksturze typu Render Target. Później byłyby one wykorzystywane do obliczeń związanych np. z oświetleniem i cieniowaniem sceny. Zaletą takiego podejścia (znanego powszechnie jako Deferred Shading) jest między innymi oszczędzenie sobie wielokrotnego przetwarzania wierzchołków przez vertex shader, np. wtedy, gdy potrzebnych jest wiele przebiegów dla wielu świateł. Z drugiej strony w ten sposób znacznie bardziej obciąża się jednostki Pixel Shader, których liczba w starszych karta jest stała i niezbyt duża. Korzyścią jest jednak uproszczenie całego procesu renderowania, nawet jeśli z powyższego opisu nieszczególnie to wynika ;-)
Pozostaje jeszcze pewien drobny szkopuł. Otóż materiały mają wiele parametrów, które często są złożone, i nie da się ich wszystkich upakować w pojedynczym pikselu, składającym się jedynie z czterech wartości zmiennoprzecinkowych. Dlatego też stosuje się tutaj “wielokrotne cele renderowania” (Multiple Render Targets – MRT), a więc produkowanie przez potok więcej niż jednej wartości koloru naraz. Zwykle po prostu każdy parametr materiału jest zapisywany osobno, do innej powierzchni typu Render Target. Rozróżnienie odbywa się na poziomie pixel shadera. Zamiast zwracać jedną wartość o semantyce COLOR0
(która domyślnie trafia do bufora ramki, czyli – w przybliżeniu – na ekran), może on wypluć także COLOR1
, COLOR2
i tak dalej:
Rezultaty te chcielibyśmy rzecz jasna odebrać i zachować, ale w DirectX wystarcza do tego zwykła metoda SetRenderTarget
, której podajemy powierzchnię działającą jako Render Target o danym indeksie. Są tutaj oczywiście pewne obostrzenia (łącznie z tym najbardziej oczywistym – rozmiaru powierzchni).
Największym mankamentem jest jednak to, że jedynie stosunkowo nowe karty (np. nVidii od serii 6) obsługują MRT. Można to sprawdzić, czytając wartość pola NumSimultaneousRTs
struktury D3DCAPS
, które da nam – niezbyt imponującą, bo wynoszącą zwykle 4 lub 8 – maksymalną liczbę Render Targets podpiętych jednocześnie. W tak niewielkiej ilości (zwłaszcza, jeśli równa jest ona nawet mniej, czyli… 1 :]) może być niełatwo zmieścić wszystkie potrzebne informacje o materiałach.
Ale w programowaniu grafiki zwykle bywa tak, że najlepiej jest wybierać techniki, które jeszcze wydają się nowe. Wtedy bowiem jest całkiem prawdopodobne, że w chwili kończenia projektu wsparcie dla nich będzie już powszechne. Zważywszy na to, że w moim przypadku ciężko jest powiedzieć, kiedy pisanie właściwego potoku renderowania zdołam chociaż zacząć – o zakończeniu nie wspominając – techniki typu deferred zdają się być całkiem rozsądnym wyborem do rozważenia :)
W potocznym rozumieniu tekstura to taki obrazek, który nakłada się obiekt trójwymiarowy, aby w ten sposób imitować wygląd jego powierzchni. Rzeczywiście, dawno temu była to ich jedyna funkcja. Tego rodzaju tekstury (nazywane teksturami rozproszenia) są oczywiście nadal niezbędne. Obok nich powstało jednak całe mnóstwo innych rodzajów tekstur, które są wykorzystywane podczas renderowania scen 3D.
Wśród nich są na przykład takie, które zawierają pewne niezbędne informacje na temat obiektów na scenie – nie tylko zresztą geometrii. Są to chociażby:
Mapy wysokości (height maps). To czarno-białe tekstury, używane do modelowania terenu. Jasność konkretnego piksela odpowiada wysokości terenu w danym punkcie. Taka tekstura musi być naturalnie przetworzona na odpowiednie trójkąty (co specjalnie trudne nie jest), ale jej używanie zamiast innych reprezentacji ma dwie wyraźne zalety. Po pierwsze, umożliwia regulowanie stopnia szczegółowości (Level of Detail, LoD) wyświetlanego terenu. Po drugie, mapy wysokości są bardzo łatwe do wykonania za pomocą dowolnego programu graficznego nawet przez średnio uzdolnionego w tym kierunku kodera :)
Mapy normalnych (normal maps) obrazują z kolei wektory normalne punktów powierzchni. Pomysł jest bardzo prosty: kolor każdego piksela w formacie RGB odpowiada normalnej o współrzędnych XYZ. Ponieważ współrzędne te są ograniczone (długość normalnej to zawsze 1), mogą być zapisane jako kolor. Mapa normalnych jest potem wykorzystywana przy obliczeniu oświetlenia per-pixel.
Tego rodzaju tekstury są przygotowywane wcześniej i obok modeli, tekstur rozproszenia i innych danych stanowią informacje umożliwiają renderowanie sceny. Oprócz nich w trakcie samego rysowania wykorzystywane są też inne tekstury, tworzone na bieżąco i zwykle niezachowywane na później. Wśród tych efemerycznych tekstur mamy na przykład:
Mapy odbić środowiskowych (environmental maps) służą do symulowania przedmiotów o powierzchniach lustrzanych. Podobnie jak wyżej, wymagają osobnego przebiegu renderowania, i to często nawet niejednego (jak w przypadku map sześciennych). Tak powstałe obrazy odbić są potem nakładane na przedmiot, który dzięki temu sprawia wrażenie, jakby odbijał w sobie resztę sceny.
Potencjalnych i aktualnych zastosowań tekstur jest o wiele więcej. Ale już na tych przykładach widać, że tekstury tak naprawdę nie są obrazkami, a jedynie zbiorami jakichś informacji, które tylko z konieczności są zapisywane w postaci kolorów pikseli. Być może niedługo staną się one pełnoprawną “pamięcią operacyjną” kart graficznych, którą można będzie np. alokować i zwalniać w kodzie shaderów. Jak dotąd możliwy jest ich odczyt oraz w pewnym stopniu zapis (zależnie od modelu shaderów), ale kto wie – może wkrótce doczekamy się czegoś więcej?…