1
DirectX ▪ Dot3 Env.
Technika DOT3 daje w dzisiejszej grafice 3D potężne możliwości biorąc pod uwagę możliwości wykorzystania Vertex i
Pixel Shaderów. Podstawowym efektem, od którego zaczyna się przygodę jest oczywiście bumpmapping, czyli mapowanie
nierówności. I wszystko układałoby się naprawdę pięknie, no ale... zawsze jest przecież jakieś ale :). Ale czy musimy się tym
przejmować - zobaczymy w tym artykule.
Problem, którym zajmiemy się tym razem będzie oczywiście związany z mapowaniem nierówności i techniką DOT3. Jak
doskonale wiemy większość naszych efektów to proste, wręcz ordynarne sztuczki głównie z wykorzystaniem tekstur, dające
jednak efekty dosyć okazałe. Jednak czasem nadmiar takich sztuczek może doprowadzić do problemów, z którymi trzeba
będzie sobie dzisiaj dla przykładu poradzić. Otóż wróćmy myślami do dwóch artykułów z naszego kursu. Jeden o
mapowaniu nierówności za pomocą techniki DOT3 a drugim niech będzie artykuł, o którym pewnie już zdążyliście
zapomnieć a mianowicie mapowanie środowiskowe sześcienne. Obydwa omawiane w tych artykułach efekty opierały się na
zabawach z teksturami. W pierwszym przypadku było to nietypowe zastosowanie tekstury jako nośnika danych o wektorach
odpowiedzialnych raczej za geometrię obiektu, w drugim kombinowaliśmy z wyliczaniem własnych współrzędnych
mapowania tekstury.
Jak było widać w przykładzie o mapowaniu nierówności wyszło nam to całkiem nieźle - bryła wydawała się być dosyć
naturalnie zniekształcona zgodnie z naszą mapą normalnych i ziemia nie była już płaska jak deska. I można by się tym nawet
zadowolić ale oto nagle... Okazało się pewnego pięknego dnia, że ktoś zażyczył sobie piękny, wypukły obiekt, który
jednocześnie miał odbijać pięknie całe swoje otoczenie i to jeszcze czynić to w czasie rzeczywistym. Ano pomyślelibyśmy,
że nic trudnego - przecież potrafimy robić mapowanie sześcienne, które nadaje się do tego idealnie. Wystarczy wykorzystać
przykład mapowania wypukłości, dodać do tego wyliczenie współrzędnych tekstury sześciennej, nałożyć ową teksturę na
obiekt i gotowe. Jak pomyślał tak zrobił - ale powiem szczerze, że efekt nie jest zbyt oszałamiający na początek. Niby obiekt
wypukły, mapa się nakłada no ale sami zobaczcie jak to wygląda:
Gdzieś tam pod teksturą mapy sześciennej przebija się niby wypukłość naszej kochanej ziemi, no ale już ten widok powinien
nas mocno zaniepokoić. Widać od razu, że nie jest tak jak powinno. Mapowanie sześcienne jak pamiętamy było wyliczane na
podstawie normalnych wierzchołków. Współrzędne wektora refleksu wyliczone ze znanego wzoru:
d = V·N
R = V-2·d·N
stanowiły jednocześnie współrzędne, z których pobierano teksturę sześcienną i nakładano na bryłę. W przypadku
wierzchołków działało to znakomicie no ale jak wiemy tutaj posługujemy się normalnymi ale dla pikseli, a nie dla
wierzchołków. Na upartego można by się powyższym efektem zadowolić bo coś tam widać, ale my jesteśmy oczywiście
ambitni i nie możemy sobie pozwolić na środki zastępcze. Pokombinujmy więc co zrobić, żeby to wyglądało lepiej. Pierwsze
co przychodzi nam do głowy to liczyć wektor odbicia z normalnymi zapisanymi w mapie normalnych a nie tymi do
wierzchołków - no i całkiem słuszne, ale pojawia się równie słuszne pytanie - jak?
Okazuje się, że tym razem już bez wydatnej pomocy Pixel Shadera się niestety nie obejdzie. Ponieważ normalne mamy
zapisane w teksturze, jasne się staje od razu, że cała operacja obliczania wektora refleksu musi się odbyć w Pixel Shaderze
tym razem - Vertex Shader niestety tutaj zupełnie się nie nadaje. Pozostaje kwestia - czy da się policzyć coś takiego za
pomocą Pixel Shadera? Patrząc na zestaw instrukcji dostępnych dla tego wynalazku jasne staje się, że tak. I po prawdzie
problemem nie jest sama operacja wyliczenia wektora refleksu ale przesłania odpowiednich danych do shadera. To, czego
będziemy potrzebować to wysłać w jakiś cudowny sposób do Pixel Shadera wektor od wierzchołka do oka, który posłuży do
wyliczenia wektora refleksu. Ale żeby nie było za łatwo dodatkowo wektor ten musi być umieszczony w przestrzeni naszej
tekstury normalnej - podobnie jak w przypadku zwykłego mapowania wypukłości przenosiliśmy wektor światła do
przestrzeni tej tekstury. Tutaj można się załamać na pierwszy rzut oka, no bo w jaki sposób po obliczać i powysyłać takie
dane do shadera który zajmuje się pikselami?
2
DirectX ▪ Dot3 Env.
Zacznijmy od wektora od oka do wierzchołka - to akurat nie jest problem, bo ten możemy wyliczyć w prosty sposób w
Vertex Shaderze:
; calculate eye vector and put it to texture
sub r1.xyz, r0.xyz, c8.xyz ; from eye to vertex
dp3 r1.w, r1.xyz, r1.xyz ; normalize eye vector
rsq r1.w, r1.w
mul r1.xyz, r1.xyz, r1.w
Zakładając, że pozycję oka mamy w stałej shadera odejmujemy od pozycji oka przekształconą pozycję wierzchołka i
normalizujemy ją (jak zwykle zresztą). Ta operacja jest w miarę prosta i nie wymaga raczej szerszego omówienia. Wiemy, że
potrzebujemy tego wektora w przestrzeni tekstury, więc czym prędzej powinniśmy przemnożyć go przez przekształcone do
przestrzeni świata wektory definiujące lokalny układ współrzędnych tekstury - ale tutaj STOP! Ku uciesze wszystkich my tej
operacji nie dokonamy dzisiaj w Vertex ale w Pixel Shaderze! Tak, tak proszę państwa - dzisiaj mnożenie macierzowe
wektorów będzie się odbywać zupełnie nie tam, gdzie się tego spodziewamy :), ale za to zobaczymy naprawdę jak potężnym
narzędziem jest Pixel Shader.
Aby dokonać mnożenia wektora przez macierz potrzebujemy owych dwóch składników oczywiście - i teraz najbardziej
interesujące. Jak przesłać te dane do Pixel Shadera? Aby się tego dowiedzieć należy najpierw dobrze zrozumieć samą ideę
działania tego elementu karty graficznej. Pixel Shader operuje oczywiście na pikselach, które zostają pobierane z tekstur. Ale
to nie piksele są wysyłane przez rejestry wejściowe shadera - co może nieco zaskakiwać. Jak pamiętamy rejestrami
wejściowymi w przypadku pikseli były tzw. rejestry tekstur oznaczane jako "tx", gdzie "x" oznaczał numer poziomu
tekstury. W rzeczywistości w tych rejestrach są przemycane do pixel shadera nie kolory ale... współrzędne mapowania. Pixel
Shader używając tych współrzędnych "sampluje" teksturę, czyli pobiera z odpowiedniego miejsca mapy kolor posługując się
tymi współrzędnymi. Skoro więc są to współrzędne to już mamy ideę. Wystarczy dla każdego wierzchołka w Vertex
Shaderze wyliczyć wszystko co potrzebne, umieścić to we współrzędnych tekstur a potem to uda się do Pixel Shadera już
samoczynnie.
Reasumując - wszystko co potrzebujemy wysłać to:
- trzy wektory definiujące dla każdego wierzchołka lokalny układ współrzędnych tekstury zawierającej normalne. Te wektory
muszą oczywiście wcześniej zostać przekształcone przez macierz świata obiektu aby poruszały się wraz z obiektem,
- wektor łączący oko z wierzchołkiem, który po przekształceniu przez lokalną macierz tekstury normalnych zostanie odbity
od normalnej zapisanej w mapie normalnych i ten odbity wektor będzie stanowił jednocześnie współrzędne dla sześciennej
mapy środowiska.
W ten właśnie sposób mapa sześcienna zostanie zniekształcona zgodnie z mapą normalnych a nie z normalnymi
wierzchołków i odda nam wiernie wygląd lśniącego obiektu wypukłego. Czas więc zabrać się ostro do pracy:
; move u to world space
dp3 r2.x, v8, c0
dp3 r2.y, v8, c1
dp3 r2.z, v8, c2
; move v to world space
dp3 r3.x, v9, c0
dp3 r3.y, v9, c1
dp3 r3.z, v9, c2
; move uv to world space
dp3 r4.x, v10, c0
dp3 r4.y, v10, c1
dp3 r4.z, v10, c2
Przenosimy najpierw wektory lokalnego układu mapy normalnych do przestrzeni świata obiektu i umieszczamy w rejestrach
tymczasowych, bo będziemy musieli je jeszcze wykorzystać. Teraz nastąpi pierwszy krok przesyłania tych danych do Pixel
Shadera - jak powiedziałem tak naprawdę shader ten posługuje się współrzędnymi tekstur, więc umieśćmy sobie te trzy
wektory jako takie współrzędne. Dla trzech wektorów potrzebujemy trzech poziomów tekstur:
mov oT1.xyz, r2.xyz
mov oT2.xyz, r3.xyz
mov oT3.xyz, r4.xyz
Pierwsza rzecz poza nami - w tym momencie mamy już lokalny, przekształcony układ mapy normalnych w Pixel Shaderze.
Zatem połowa sukcesu. Kilkadziesiąt linii powyżej wyliczyliśmy także znormalizowany wektor wierzchołek-oko, który
znajduje się w rejestrze
r1
. Teraz możemy go wysłać do Pixel Shadera. Ale nie, nie użyjemy do tego kolejnego poziomu
tekstur z dwóch ważnych powodów. Po pierwsze - im mniej poziomów wykorzystamy tym lepiej, bo więcej sztuk hardware-
3
DirectX ▪ Dot3 Env.
u będzie mogło nasz efekt wyliczyć. Po drugie, jak zobaczymy za moment, kosmiczna konstrukcja, którą zastosujemy w
Pixel Shaderze będzie wymagała takiego a nie innego układu danych. Czas więc odsłonić kolejną tajemnicę - umieszczamy
wyliczony wektor z
r1
w rejestrach współrzędnych tekstur:
mov oT1.w, r1.x
mov oT2.w, r1.y
mov oT3.w, r1.z
Trzy pierwsze dane współrzędnej tekstury zajmują wektory lokalnego układu mapy normalnych, natomiast ostatni element to
znormalizowany wektor oko-wierzchołek. I w zasadzie na tym rola naszego Vertex Shadera się kończy. Oczywiście przelicza
on jeszcze wektor światła i umieszcza w
oD0
i dokonuje wszystkich niezbędnych wyliczeń i wypełnień rejestrów
wyjściowych, ale to pamiętamy już doskonale z mapowania nierówności i poprzednich tutoriali i nie ma sensu tego
tłumaczyć. Nas natomiast najbardziej interesuje dzisiaj Pixel Shader:
ps.1.1
; texture instructions
tex t0
; arithmetic instructions
texm3x3pad t1, t0_bx2
texm3x3pad t2, t0_bx2
texm3x3vspec t3, t0_bx2
; output
mov r0, t3
Rozkazy naprawdę na pierwszy rzut oka mogą przyprawić o mały ból głowy już samą nazwą. Na pocieszenie można dodać,
że ich działanie jednak da się dosyć rozsądnie wytłumaczyć i na końcu dojdziemy co i jak. Jak zwykle rozpoczynamy od
spisu instrukcji w dokumentacji. Idąc po kolei napotykamy całą serię tworów zaczynających się od "texm" a uzupełnionymi
różnymi numerkami i końcówkami. Niemal we wszystkich przypadkach opis wspomina o głównym zadaniu owych instrukcji
- czyli mnożeniu macierzowym. Jak na wynalazek mający się zajmować przekształcaniem pikseli trzeba przyznać, że te
rozkazy są dosyć dziwne. Jak się jednak okaże w dalszym ciągu, ten co projektował Pixel Shader dobrze wiedział co robi - bo
te rozkazy w tym, konkretnym przypadku uratują nam skórę.
Jak pamiętamy z części poświęconej Vertex Shaderowi wykombinowaliśmy sobie tam, że wektory układów lokalnych dla
tekstury zawierającej wektory normalne zostały zachowane w trzech poziomach tekstur w ich trzech pierwszych
współrzędnych (x, y i z ) oraz znormalizowany wektor oka jako ostatnie współrzędne na tych samych poziomach. To, co
podkreśla dokumentacja w przypadku każdej z instrukcji to to, że nie można używać ich pojedynczo - każda z nich ma sens,
jeśli występuje w parze z innymi. Wiedząc jak wygląda mnożenie macierzowe ma to jakiś sens - w sumie macierze to kilka
wierszy i kolumn i jednym rozkazem załatwić się tego nie da. To, co jest charakterystyczne w tego typu rozkazach widać w
naszym przykładzie - ponieważ dokonujemy transformacji wektora przez macierz, a będzie to macierz obrotu całą sprawę
załatwiamy przez macierz 3x3. W Pixel Shaderze będziemy dokonywać takiego mnożenia za pomocą trzech linii, z tym że ta
ostatnia będzie dokonywać niejako finalnej obróbki danych i ewentualnie dokonywać dodatkowych operacji.
No ale - w czym leży istota przedstawionego powyżej rozwiązania? Otóż, aby nałożyć efektownie sześcienną mapę
środowiska na obiekt potrzebujemy wektora odbicia, którego współrzędne będą jednocześnie współrzędnymi mapowania
tekstury. Wektor odbicia potrafimy sobie policzyć mając normalną. Normalne mamy, tyle że tym razem zapisane w teksturze
na poziomie 0. Wystarczy więc odbić wektor według normalnej i gotowe... no prawie. Pamiętamy o tym, że aby wszystko
działało poprawnie to musi się to odbywać w określonej przestrzeni. Tutaj więc trzeba zrobić jedną ważną rzecz - przenieść
wektory normalne zapisane w teksturze do przestrzeni tej właśnie tekstury. Przestrzeń tę mamy zapisaną jako trzy wektory w
trzech poziomach Pixel Shadera -
t1
,
t2
i
t3
. Wektory normalne są zapisane jako kolory w
t0
. Aby pomnożyć teraz wektor
normalny przez macierz zawartą w
t1
,
t2
i
t3
trzeba zawczasu przenieść kolor z zakresu ( 0, 1 ) do (-1, 1 ), co czynimy za
pomocą modyfikatora
_bx2
. Uważni obserwatorzy po przyjrzeniu się kodowi źródłowemu shadera zauważą pewną ważną
rzecz - otóż rozkazy jak na mnożenie wyglądają dosyć dziwnie - no bo składników mnożenia jest dwa a wynik? No właśnie,
pytanie gdzie przechowywany jest wynik? W powyższym kodzie w ogóle nie widać, do jakiego rejestru my przesyłamy
wynik naszego mnożenia. I można powiedzieć, że tak naprawdę tego rejestru po prostu... nie ma! A dokładniej mówiąc
wynik mnożenia
t0
, przez
t1
,
t2
i
t3
jest przechowywany gdzieś w tymczasowej pamięci Pixel Shadera, do której nie
mamy dostępu. Dzieje się tak dlatego, ponieważ to nie koniec naszych działań. Wszystko za sprawą instrukcji
texm3x3vspec
, która nie tylko dokonuje mnożenia macierzowego, ale na samym końcu robi jeszcze jedną, bardzo ważną
rzecz. Jak pamiętamy doskonale w ostatnich rejestrach
t1
,
t2
i
t3
a konkretnie w części "w" mamy przechowywany wektor
od oka do wierzchołka. Instrukcja
texm3x3vspec
wykorzystując tą daną i mając w tymczasowej pamięci przekształcony
wektor normalny obliczy bez naszego udziału wektor refleksu według doskonale nam znanego wzoru i umieści jego wynik w
rejestrze
t3
. No i to jest woda na nasz młyn - bo to właśnie chcieliśmy uzyskać. Mając wektor refleksu możemy użyć jego
współrzędnych do pobierania z tekstury sześciennej odpowiednich pikseli. No i nic innego Pixel Shader nie robi w swojej
4
DirectX ▪ Dot3 Env.
ostatniej instrukcji - po prostu sampluje teksturę na poziomie
t3
używając współrzędnych wektora refleksu i nakłada tę
teksturę na bryłę - a efekt możecie sobie porównać z powyższym, gdzie wyliczaliśmy współrzędne dla mapy na podstawie
normalnych wierzchołków:
Jak widać efekt jest o niebo lepszy i nie może być inny. Cały proces o wiele bardziej przypomina to, co dzieje się naprawdę
w rzeczywistym świecie niż to uproszczenie, którego dokonaliśmy powyżej. Trzeba sobie zapamiętać tę oczywistą zasadę, że
im wierniej oddamy proces, który odbywa się przy osiąganiu danego efektu za pomocą przekształceń matematycznych tym
ten efekt symulowany będzie lepiej i wierniej. Niesie to ze sobą także tę dodatkową korzyść, że zamiast głowić się jak
osiągnąć dany efekty wystarczy przeanalizować jak to się dzieje naprawdę i będzie nam prościej osiągnąć zamierzone efekty.
A jak już będziemy mieli efekt w ręce to wtedy można myśleć o niezbędnych w większości przypadków optymalizacjach.
W przykładzie zastosowałem jeszcze prosty manewr wieloprzebiegowego teksturowania, który ma umożliwić mniej
zaawansowanemu sprzętowi odtworzenie efektu w całej okazałości. Moglibyśmy jeszcze nałożyć na obiekt teksturę diffuse w
jednym przebiegu umieszczając ją na dodatkowym, piątym już poziomie tekstury, ale większość sprzętu dzisiaj popularnego
obsługuje raczej cztery, więc aby inni też mogli się pocieszyć dodałem dodatkowy przebieg i użyłem ponowne tych samych
poziomów tekstur, tylko tym razem oczywiście wyłączając Pixel Shader aby nie komplikował nam sprawy. I końcowy efekt
wygląda następująco:
Oczywiście dobór sposobu mieszania tekstur można zmieniać i możecie sobie sami dobrać tak, aby wam pasował - bardziej
błyszczące będzie to wszystko, albo w ogóle jakieś "dzikie" efekty uzyskacie. No ale tutaj zostawiam was z waszą
wyobraźnią i rodzajami mieszania.
Mam nadzieję, że trochę wam się wyjaśni po dwóch artykułach istota mapowania z zastosowaniem operacji dot3. Już po
dwóch przykładach widać jaką mocą dysponuje ta technika i z nią związana jest najbliższa przyszłość grafiki 3D czasu
rzeczywistego. Z całą pewnością większość liczących się nadchodzących produkcji w dziedzinie gier będzie dysponować
takimi możliwościami i będzie opierać swoje efekty wizualne na przedstawionych fragmentach shaderów. Dzięki tym
artykułom i wasze dołączą do tego grona i nie będą odstawać od konkurencji - czego sobie i wam życzę ;).