background image

1 

DirectX ▪ Cell Shading 

Po lekcji teoretycznej czas pokazać, co możemy zrobić w praktyce. Jak już wiemy tak naprawdę cieniowanie kreskówkowe 
nie stanowi wielkiego problemu i cały ambaras polega w zasadzie na odpowiednim przygotowaniu tekstur, których użyjemy 
w przykładzie. Żeby za dużo nie kombinować oprzemy się na tych, z lekcji teoretcznej. 
Co do samego kodu to w zasadzie dwie główne rzeczy będą nam potrzebne - a mianowicie odpowiednia struktura 
wierzchołka i vertex shader, który policzy co trzeba. 
W cieniowaniu kreskówkowym w zasadzie nie używa się tekstur - przynajmniej darmo wypatrywać czegoś podobnego w 
znanych komiksach czy bajkach rysunkowych. My aby sobie nie zaciemniać przykładu także ograniczymy się tylko do 
mechanizmu samego cieniowania i nadawania bryłom konturów. A tekstury przecież możecie nałożyć sobie sami, bo 
robiliśmy to już chyba dziesiątki razy. 
A więc po pierwsze struktura wierzchołka:  

struct SVertex 

    float        x, y, z;        // position 
    float        nx, ny, nz;        // normal 
    float        u1, v1;            // firts texture channel 
    float        u2, v2;            // second texture channel 
};

 

Oczywiście pozycja, normalna która tutaj okaże się niezbędna (w końcu wyliczamy oświetlenie) no i dwa zestawy 
współrzędnych tekstur - jeden dla tekstury cieniującej a drugi dla tekstury definiującej kontury obiektu. Tak naprawdę to 
wszystko jest dla nas już w tym momencie zupełnym banałem, więc nawet nie powinienem tego opisywać, no ale dla 
porządku niech wam będzie. 
Inicjalizację urządzenia, ładowanie geometrii i tekstur także pomijamy, bo mamy ważniejsze sprawy - a mianowicie druga 
decydująca w naszym przykładzie sprawa, czyli vertex shader: 

vs.1.1 
 
dcl_position    v0 
dcl_normal        v3 
dcl_texcoord0    v7 
dcl_texcoord1    v8 
 
; c0-c3 - world transform matrix 
; c4-c7 - view*proj transform matrix 
; c8 - eye position 
; c9 - light vector 
 
def c10, 0.0f, 1.0f, 0.0f, 0.0f 
 
; position to world 
dp4 r0.x, v0, c0 
dp4 r0.y, v0, c1 
dp4 r0.z, v0, c2 
dp4 r0.w, v0, c3 
 
; and final 
dp4 oPos.x, r0, c4 
dp4 oPos.y, r0, c5 
dp4 oPos.z, r0, c6 
dp4 oPos.w, r0, c7 
 
; normal to world 
dp3 r1.x, v3, c0 
dp3 r1.y, v3, c1 
dp3 r1.z, v3, c2 
 
; normalize normal 
dp3 r1.w, r1, r1 
rsq r1.w, r1.w 
mul r1, r1, r1.w 
 
; output color 
mov oD0, c10 
 
; and toon textures 
dp3 oT0.x, r1,-c9 
 
; eye vector 
sub r2, c8, r0 

background image

2 

DirectX ▪ Cell Shading 

dp3 r2.w, r2, r2 
rsq r2.w, r2.w 
mul r2, r2, r2.w 
 
; outline texture 
dp3 oT1.x, r1, r2

 

Jak widać shader nie jest trudny bo i być nie może. Dla zasady jednak postarajmy sobie zobaczyć poszczególne kawałki, 
żeby w razie wątpliwości nie stresować się. 

; position to world... 
dp4 r0.x, v0, c0 
dp4 r0.y, v0, c1 
dp4 r0.z, v0, c2 
dp4 r0.w, v0, c3

 

Najpierw oczywiście manewrujemy naszym obiektem w świecie, czyli mnożymy pozycję wejściową wierzchołka poprzez 
macierz świata umieszczoną w rejestrach 

c0

 do 

c3

. Wynik zostawiamy w rejestrze 

r0

, ponieważ jeszcze się nam przyda. 

; ...and final 
dp4 oPos.x, r0, c4 
dp4 oPos.y, r0, c5 
dp4 oPos.z, r0, c6 
dp4 oPos.w, r0, c7

 

Żeby jednak za dużo nie mieszać od razu też wyślemy nasz wierzchołek na ekran, czyli przepuszczamy przez przemnożone 
przez siebie macierze widoku i projekcji, co w sumie wrzuci nam go na ekran. 

; normal to world 
dp3 r1.x, v3, c0 
dp3 r1.y, v3, c1 
dp3 r1.z, v3, c2

 

Oczywiście, żeby policzyć oświetlenie podczas manewrowania obiektem po scenie musimy także manewrować normalnymi 
wierzchołków, które muszą pokonać dokładnie tę samą drogę co wierzchołki. Normalne zapamiętujemy jak zwykle już w 
rejestrze tymczasowym 

r2

; normalize normal 
dp3 r1.w, r1, r1 
rsq r1.w, r1.w 
mul r1, r1, r1.w

 

I robimy oczywiście bardzo ważny manewr, o którym nigdy nie powinniśmy zapominać - czyli normalizujemy wszelkie 
wektory przed obliczeniami oświetlenia. 

mov oD0, c10 

 

Do wyjściwoego rejestru koloru wysyłamy jakiś dobrany przez nas kolor diffuse obiektu, ponieważ akurat w tym przypadku 
nie zajmujemy się materiałami wczytanymi bezpośrednio z modelu, no ale przecież nie w tym rzecz. W tym przykładzie 
wymyśliłem sobie obiekt zielony, ale możecie sobie zdefiniować jaki chcecie tak naprawdę. Jak ktoś będzie robił finalną 
aplikację to oczywiście musi sobie opracować mechanizm wysyłania kolorów materiałów przez stałe shadera.  

; and toon textures 
dp3 oT0.x, r1,-c9

 

Teraz nastąpi właściwy bajer w naszym przykładzie czyli pocieniowanie naszego obiektu za pomocą tekstur. Jak widać 
powyżej, w kolorze diffuse nie zawieramy żadnej warotści wyliczonej na podstawie normalnej obiektu a jedynie stałą 
wartość koloru. Gdybyśy nie pocieniowali obiektu w inny sposób to dostalibyśmy na ekranie coś zupełnie bez wyrazu i głębi. 
W rejestrze 

c9

 wpada do shadera wektor światła znajdującego się na scenie. Odwracamy go (za pomocą znaku "-") aby móc 

wyliczyć własćiwą warość iloczynu skalarnego pomiędzy nim a normalną. Minus jest po to, aby kąt pomiędzy normalną a 
światłem mieścił się w przedziale od 0 do 180 stopni. Wynikiem iloczynu skalarnego jak wiemy będzie liczba - cosinus kąta 
pomiędzy wektorami. Im większy kąt tym mniejsza wartość iloczynu i tym mniejsza liczba. Większy kąt oznacza nie mniej 
ni więcej to, że światło pada na daną normalną pod większym kątem, co oznacza coraz mniej światła. Nasza wartość wędruje 
zatem do rejestru tekstury, który zawiera współrzędne odpowiedzialne za mapowanie tekstur na poziomie nr 0 a tam znajdzie 
się nasza tekstura cieniująca. I jeśli tekstura będzie skonstruowana w sposób odpowiedni (dla małych wartości mapowania 

background image

3 

DirectX ▪ Cell Shading 

będzie ciemniejsza, dla dużych jaśniejsza) to otrzymamy poprawne cieniowanie, co wiemy już z teorii. Ponieważ nasza 
tekstura będzie miała znaczące zmiany wartości tylko w jednym wymiarze, więc w tym momencie druga współrzędna nie ma 
tak naprawdę znaczenia i możemy ją ustawić dowolnie, ponieważ dla każdej wartości y, współrzędna x będzie wyglądała tak 
samo.  

; eye vector 
sub r2, c8, r0 
dp3 r2.w, r2, r2 
rsq r2.w, r2.w 
mul r2, r2, r2.w

 

Pozostało nam zatem wyliczyć już tylko dane dla tekstury wyznaczającej kontur naszego obiektu. Jak pamiętamy z lekcji 
teoretycznej potrzebny nam będzie wektor łączący oko z przekształconym w przestrzeni świata wierzchołkiem. Wierzchołek 
mamy w rejestrze 

r0

, pozycję oka w stałej shadera 

c8

. No i wyliczamy taki wektor odejmując pozycję wierzchołka od 

pozycji oka i umieszczamy w tymczasowym rejestrze 

r2

. Oczywiście dany wektor normalizujemy, aby uniknąc przykrych 

niespodzianek.  

; outline texture 
dp3 oT1.x, r1, r2 

 

Na koniec robimy podobny manewr co poprzednio, czyli na podstawie iloczynu wektorowego będziemy wyliczać 
mapowanie dla tekstury. Tym razem iloczyn wyliczymy pomiędzy wektorem normalnym a wektorem, który wyliczyliśmy 
przed chwilą - łączącym oko z wierzchołkiem. Jeśli ten iloczyn będzie mały - znaczy się, że wektory są położone niemal 
prostopadle co prawdopodobnie oznacza krawędź, a przynajmniej można odnieść takie wrażenie patrząc na obiekt. Jeśli 
iloczyn będzie bliski zeru, znaczy że wektory leżą niemal w jednej linii i krawędź toto na pewno nie jest. Tak więc wyliczona 
wartość wędruje na poziom nr 1, gdzie panoszy się tekstura odpowiedzialna za pocieniowanie naszej krawędzi.  

Shader powinien działać bez zarzutu, czas więc zobaczyć co się dzieje w aplikacji. Jak wspomniałem, wszelkie ładowania, 
inicjalizacje itp. darujemy sobie, bo szkoda tylko miejsca a przejdziemy do sedna sprawy, czyli renderingu naszej bryły. W 
przykładzie pobawiłem się troszkę i prezentuję trzy różne możliwości renderingu: 

case eOutline: 

     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); 
     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); 
     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT ); 

 

 

Pierwszym trybem jest rendering samych krawędzi obiektu. Jak widać na rysunku krawędzie rysują się w dosyć 
charakterystyczny sposób i widać dodatkowo w jaki sposób jest zbudowana siatka obiektu. Taki a nie inny efekt zawdzięczać 
trzeba odpowiednio spreparowanej teksturze krawędzi, która zawiera zbiór odpowiednio przygotowanych mipmap. Tekstura 
ta jest w formacie *.dds, który jest charakterystyczny dla pakietu Direct3D. Może on zawierać nie tylko jedną teksturę, ale 
całe zestawy łącznie ze wszystkimi poziomami mpmap. W pakiecie Direct3D jest narzędzie, dzięki któremu możecie nawet 
poznać budowę takiej tekstury i obejrzeć jej posczególne elementy, no ale po szczegóły to dzisiaj odsyłam do SDK - to nie 

background image

4 

DirectX ▪ Cell Shading 

temat na teraz. Wracając zaś do naszego przykładu to zaby zobaczyć tylko krawędzie obiektu z nałożonym na obiekt kolorem 
diffuse po prostu modulujemy je na poziomie nr 1, ponieważ właśnie tam znajduje się tekstura konturu.  

case eShade: 

     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE ); 
     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); 
     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT ); 

 

 

Drugim przypadkiem renderingu dostępnym w przykładzie jest rendering wykorzystujący samo cieniowanie za pomocą 
tekstury cieniującej, umieszczonej na poziomie 0 bez uwzględniania konturów obiektu. Aby uwidocznić taki a nie inny 
sposób po prostu mieszamy na poziomie nr 0 kolor difuuse i kolor tekstury cieniującej a następnie przepuszczamy ten 
rezultat na poziom 1, który w naszym przypadku oczywiście zawsze jest poziomem wyjściowym.  

case eFinal: 

     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE ); 
     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); 
     g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE ); 
     g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT ); 

 

 

I na sam koniec oczywiście efekt w pełnej krasie czyli jednocześnie i cieiowanie i wyciąganie krawędzi. Na każdym 
poziomie nie robimy nic innego, tylko modulujemy kolory przychodzące z poziomów poprzednich.  

background image

5 

DirectX ▪ Cell Shading 

No i to w zasadzie byłoby tyle, jeśli chodzi o rendering kreskówek za pomocą Direct3D. Można powiedzieć, że łatwizna i 
faktycznie, jeśli chodzi o kod żródłowy to raczej tak to właśnie wygląda na naszym poziomie znajomości zagadnień grafiki. 
Inna sprawa to odpowiednio spreparowane tekstury, które akurat w tym przypadku grają główne role. Jeśli na przykład 
tekstura konturu byłaby nie taka, nie zawierałaby odpowiednio spreparowanych poziomów mipmap to zapewne kontur nie 
byłby tak wyraźny i może trudno byłoby go wogóle dostrzec. Tekstura cieniująca natomiast jest przygotowana pod kątem 
cieniowania kreskówkowego - gdybyśmy na przykład zrobili z niej ładny, ciągły gradient, to otrzymalibyśmy tradycyjne 
cieniowanie Gourauda zamiast kilku poziomów oświetlenia i charakterystycnego efektu. 
Pragnę także zwrócić waszą uwagę na możliwości, jakie ze sobą niesie ta technika. Spróbójcie zresztą sami trochę 
poeksperymentować, zwłaszcza z teksturą cieniującą. Dodajcie do niej kanał alfa, włączcie przeźroczystość i twórzcie. 
Poprzestawiajcie kolory cieniujące, może dajcie zupełnie inne odcienie niż czarne. Może wpadnie wam do głowy jakiś fajny 
efekt z teksturą konturu na przykład? 
Na wszystkie ciekawe pomysły oczywiście czekamy z niecierpliwością, może jakieś dema albo najlepiej efekty do naszego 
browsera :). Teraz powinno być łatwiej napisać cokolwiek, więc bardzo zachęcam ;).