background image

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? 

background image

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-

background image

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 ( 01 ) do (-11 ), 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 

background image

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ę ;).