Staje się jasność – implementacja modeli oświetlenia.

Hej!

Poprzednia lekcja wyjaśniła nam większość teoretycznych zagadnień związanych z oświetleniem. Dzisiaj więc, możemy zająć się praktyczną stroną problemu.

Tak więc, nie przedłużając niepotrzebnie wstępu, zajmijmy się kodem shadera odpowiedzialnym za oświetlenie:

float3 lightDir = float3(1,0,0);

float3 cameraPos = float3(0,0,-3);

No cóż, jeszcze nie. Zanim dojdziemy do właściwego oświetlenia, warto opowiedzieć co nieco o dwóch stałych które deklarujemy w shaderze. cameraPos to pozycja kamery, która w naszych przykładach jest stała, jednak o wiele ciekawsza jest wartość lightDir. Jest to wektor kierunku światła. Aby opowiedzieć coś o nim, musimy rozważyć skąd może padać światło i w jaki sposób.

Otóż w prawdziwym świecie mamy zasadniczo jeden typ źródeł światła: źródła punktowe (Point Light), Światła takie posiadają pozycję, gdyż są umieszczone w przestrzeni, nie posiadają zaś kierunku – gdyż świecą w każdą stronę – jak żarówka w lampie na suficie.

image

 

Ale ale, wyobraźmy sobie latarkę. Owszem, w środku znajduje się lampeczka (pointlight), jednak przez odbłyśnik snop światła kierowany jest w konkretną stronę:

image

Jako, że symulowanie takiego zjawiska przez faktyczne liczenie odbić zapewne nie byłoby specjalnie wydajne, a pewnie część pamięta małą latareczkę w ciemnych tunelach świata Half Life 1 – a były to lata 90, czyli czasy prehistoryczne w rozwoju grafiki 3D. Mądrzy ludzie wymyślili więc coś takiego jak światło reflektorowe – w naszej ulubionej semantyce angielskiej Spot Light, o którym powiemy za moment.

No, ale jako że każdy lubi co nieco pooptymalizować, wyjrzyjmy na chwilę przez okno. Jeśli mamy trochę szczęścia i czytamy ten tekst za dnia, powinniśmy ujrzeć słońce. Nasza gwiazda wszak ma i pozycję, i świeci w każdą stronę – jest więc klasycznym przykładem Point Lighta, czyż nie?

Otóż, słońce przez wielu uznawane jest za klasyczny przykład, ale tak zwanego światła kierunkowego (Directional). Wszak pozycja słońca w jednostkach świata gry ( ;) ) jest tak odległa od naszej, że nie ma praktycznie znaczenia! Znaczenie ma kierunek – a ten może być określony jako stała. Na przykład taka, jaka jest zadeklarowana w naszym kodzie.

Z tą podstawą teoretyczną, możemy rzucić się w wir naszego shadera:

void vsmain(in float3 pos : POSITION,

            in float3 normal : NORMAL,

            in float2 texc : TEXCOORD0,

 

            out float4 oPos : POSITION,

            out float2 oTexc : TEXCOORD0,

            out float3 oNormal : TEXCOORD1,

            out float3 oCamDir : TEXCOORD2)

{

    oPos = mul(float4(pos,1), worldViewProj);

    oTexc = texc;

    oNormal = mul(float4(normal,1), World);

    oCamDir = cameraPosmul(float4(pos,1), World);

}

Jak widać, Vertex Shader nie zmieił się dużo – dodaliśmy dwie nowe wartości wyjścia, które sobie omówimy.

Cel istnienia oCamDir powinien być jasny – mówiliśmy o nim w poprzedniej lekcji, przy okazji opisywania wzorków. Co może mylić, to sposób obliczania jego wartości – który to posłuży nam za przykład prawa, które jest bardzo ważne, a wręcz zasadnicze, a dbanie o poprawność tego typu obliczeń zaoszczędzi w przyszłości godzin debuggowania kodu shaderów.

Oto więc, ważnym jest żeby każde obliczenie wykonywać w tej samej przestrzeni. Zadeklarowaliśmy wektor cameraPos jako wektor w przestrzeni świata, wektor pos zaś jest w przestrzeni modelu – obliczanie wektora o początku w jednym a końcu w drugim byłoby bez sensu, gdybyśmy nie przekształcili ich do jednego układu współrzędnych.

Jak widać, podobnie robimy z normalną. Ale ale – normalna – skąd to się wzięło? Jak wspominałem wcześniej, normalna to wektor prostopadły (czyli skierowany pod kątem prostym) do powierzchni.

image 

Ale ale, w Vertex Shaderze nie mamy dostępu do powierzchni, a jedynie do wierzchołków. Przypomnijmy więc sobie, tymczasem, lekcję numer 3 – tę oto. Sama lekcja nie przedstawia dla nas w tej chwili niczego czego byśmy nie znali, jednak obejrzyjmy obrazek, który był jej finałem:

image

Jak pamiętamy, nadawaliśmy wierzchołkom poszczególne kolory – czerwony, niebieski i zielony. Jednak, podczas rasteryzacji kolory nam się zmieszały! Oto działanie tak zwanych interpolatorów. Każdy z parametrów przekazywanych do Pixel Shadera przez Vertex Shader, jest de facto interpolowany liniowo (lub inaczej, ale inne interpolatory używane są stosunkowo rzadko), jako średnia wartość tego parametru dla trzech wierzchołków składowych. W takim razie – normalna powierzchni uzyskuje się „sama”, przez interpolację trzech normalnych z trzech wierzchołków – wszystko automagicznie!

Nie powiedzieliśmy jeszcze o tym jak liczymy normalne – ale omówimy to przy okazji oglądania kodu aplikacji. Na razie spójrzmy to, co powinno stanowić temat dzisiejszej lekcji – Pixel Shader:

float4 psmain(float2 texc : TEXCOORD0,

              float3 norm : TEXCOORD1,

              float3 CamDir : TEXCOORD2) : COLOR

{

    float3 R = reflect(lightDir, norm);

    float3 H = normalize(-lightDir + CamDir);

 

    float4 diffuseLambert =

        saturate(dot(normalize(norm), lightDir));

 

    float4 specularBlinn =

        pow(dot(H, normalize(norm)), 16);

 

    float4 specularPhong =

        pow(dot(normalize(R), normalize(CamDir)), 16);

 

    return diffuseLambert + /*specularBlinn*/ specularPhong;

}

Jak widzimy, nie ma tu niczego co nie pokrywałoby się z poprzednią lekcją. Wartości każdego z typów oświetlenia liczymy dokładnie według wzorów podanych ostatnio. Warto jednak wspomnieć o paru rzeczach – przede wszystkim, trzeba pamiętać czym jest iloczyn wektorowy. Aby wyniki były poprawne, nie może być brana pod uwagę długość wektorów – a więc oba argumenty funkcji dot powinny zostać znormalizowane. Wektora lightDir nie normalizujemy, gdyż zadeklarowaliśmy go już jako wektor jednostkowy. Jest to jedna z zasad optymalizacji shaderów – nigdy nie normalizować wektora który już ma długość 1. Używamy także instrukcji saturate. Służy ona do obcięcia wartości argumentu do zakresu 0..1. Jest ona o tyle fajna, że nie wymaga dodatkowej mocy obliczeniowej i nie zajmuje ani jednej instrukcji arytmetycznej, gdyż jej rozwinięcie do assemblera jest tzw. modyfikatorem, a nie wywołaniem.

Jak widać, omawiamy przykład światła kierunkowego. Dodanie światła Point nie jest skomplikowane – wystarczy za kierunek podstawić znormalizowany wektor między pozycją światła a pozycją wierzchołka (w tej samej przestrzeni!). Zaimplementowanie światła typu spot niech będzie zaś zadaniem domowym dla ambitnych. Gwoli podpowiedzi, potrzebne są trzy parametry światła – pozycja, kierunek, oraz kąt, poza którym nie ma już światła:

image

Od tego momentu, obliczenie oświetlenia dla danego punktu na powierzchni nie powinno stanowić problemu.

Teraz natomiast, nadszedł czas aby obejrzeć nowy kod, który pojawił się w naszej aplikacji:

void CreateMesh(char* path)

{

    D3DVERTEXELEMENT9 VertexDecl[] =

    {

        {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},

        {0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0},

        {0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},

        D3DDECL_END()

    };

 

    LPD3DXBUFFER materialsBuffer;

    ID3DXMesh* tmp;

    D3DXLoadMeshFromX(path, D3DXMESH_SYSTEMMEM, pDev, NULL,

        &materialsBuffer, NULL, &numMaterials, &tmp);

 

    tmp->CloneMesh(D3DXMESH_MANAGED, VertexDecl, pDev, &pMesh);

 

    D3DXComputeNormals(pMesh, NULL);

    //…

}

Nas w tym kodzie zainteresują dwie rzeczy – te nowe. Przede wszystkim dodajemy semantykę NORMAL do Vertex Declaration, ale to już jest przez nas znane. Z tym, że „miejsce” stworzone przez tę semantykę trzeba teraz wypełnić: zrobi to za nas funkcja o nazwie D3DXComputeNormals. Jak wspominałem, normalne trzeba obliczyć – jest to zadanie nietrywialne, więc posługujemy się biblioteką D3DX. Warto wspomnieć, że do podobnych zadań stworzona została biblioteka NvMeshMender firmy NVIDIA i dziesiątki podobnych. W każdym razie, my jako programiści, nie musimy raczej martwić się o normalne – gdyż często są one tworzone w programach graficznych.

 

Tak oto dobrnęliśmy do końca tej lekcji. W następnej części poznamy jeszcze inne zastosowanie tekstur, oraz pobawimy się trochę normalnymi, czyli omówimy normalmapping.

Do tej lekcji dołączony jest oczywiście kod źródłowy.

image


komentarzy 8 to “Staje się jasność – implementacja modeli oświetlenia.”

  1. […] ani nad komputerem w ogólności. Tym niemniej, napisałem nową część kursu Direct3D – Staje się jasność – implementacja modeli oświetlenia – traktującą o implementacji oświetlenia diffuse, oraz specular (wg. Phonga i […]

  2. Kiedy można się spodziewać kolejnej części? Może jakiś Shadow Mapping?

  3. Tak jakoś nie wiem. I będzie też mapping, ale normal. ;P

  4. Przed chwilą skorzystałem z Twojego kodu. Działa. Dzięki :)

  5. Świetny tutek, z niecierpliwością czekam na normal mapping :)

  6. Hej, ściągnąłem kod i przy kompilacji w VS2008 otrzymałem następujący błąd: Nazwa ‚fxc’ nie jest rozpoznawana jako polecenie wewnętrzne lub zewnętrzne, program wykonywalny lub plik wsadowy.Jak można zaradzić temu problemowi?

  7. Musisz ściągnąć i poprawnie zainstalować SDK – a jeśli już masz, to poszukaj gdzieś tam .bat-a który ustawia ścieżki. :)

  8. Ja mam następujący problem:

    First-chance exception at 0x003C12B0 in 1.exe: 0xC0000005: Access violation reading location 0xCCCCCCCC.

    Jak temu zaradzić ?

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

 
%d bloggers like this: