<#KEYWORDS:[WORDS:<Grafika 3D ogólnie. PART II >]#><#SCROLLS:[BYLINE:1][BYPAGE:1][BYTWOPAGES:0]#><#JLIMIT:[C1LMARGIN:10][C1RMARGIN:630][C2LMARGIN:10][C2RMARGIN:300]#>
<#JUSTIFY:[TYPE:2]#><#FONT:[BOLD:1][FACE:<arial.ttf>][AALIASED:1][SIZEX:12][SIZEY:16][COLOR:0xFFFFFF40]#>Grafika 3D ogólnie. PART II
<#FONT:[FACE:<arial.ttf>][BOLD:0][SIZEX:10][SIZEY:12 ][COLOR:0xFFFFFFFF]#><#TITLE:[CAPTION:<Grafika 3D ogólnie. PART II>][AUTOSCROLL:1]#>
    
    Witam po raz kolejny. Jeśli kojarzycie poprzedni odcinek <#FONT:[COLOR:0xFF6B8DFD]#>"porad śmieciowego kodera"<#FONT:[COLOR:0xFFFFFFFF]#>, to pewnie pamiętacie, że zajmowaliśmy się w nim podstawami grafiki 3d, czyli perspektywą, wypełnianiem trójkąta i liczeniem dla niego oświetlenia. Obiecałem, że następnym razem napiszę co nieco o texturowaniu, korekcji perspektywy i clippingu. Mam nadzieje, że uda mi się w tym artku <#FONT:[COLOR:0xFF6B8DFD]#>"wykończyć"<#FONT:[COLOR:0xFFFFFFFF]#> problematykę podstaw grafiki trójwymiarowej i już od następnego razu będziemy mogli zająć się usprawnianiem podstawowych wzrów, które za chwilę przedstawię.

  <#FONT:[COLOR:0xFFEDEF61]#>TEXTUROWANIE.<#FONT:[COLOR:0xFFFFFFFF]#>
    
    Ostatnio skończyliśmy na goraudzie i phongu. Pierwsza z tych metod polegała na interpolacji na całym trójkącie danej wartości <#FONT:[COLOR:0xFFB8AA98]#>(tu: koloru)<#FONT:[COLOR:0xFFFFFFFF]#>, przy użyciu stałej delty. Podobnie dzieje się w przypadku teksturowania. Co prawda sposób przeze mnie przedstawiony jest jak najbardziej podstawowym i jak najbardziej wolnym, nie mniej jednak nie możemy zabrać się za optymalizację czegoś, czego nie znamy. Tak więc, chcąc oteksturować obiekt musimy każdemu wierzchołkowi przypisać pewien koordynat tekstury <#FONT:[COLOR:0xFFB8AA98]#>(u, v)<#FONT:[COLOR:0xFFFFFFFF]#>. Następnie wartości te interpolujemy po całym trójkącie używając metody identycznej do tej z gorauda. Potem pobieramy kolor punktu z tekstury o współrzędnej u1, v1 <#FONT:[COLOR:0xFFB8AA98]#>(wyliczone wartości)<#FONT:[COLOR:0xFFFFFFFF]#> i wrzucamy na ekran. Jak widać, metoda ta jest straaasznie wolna i nawet przy zastosowaniu stałej delty zżera strasznie dużo mocy obliczeniowej. Właśnie dlatego wymyślono mnóstwo sposobów na zoptymalizowanie tego procesu. Ale o tym w następnym artku. 

     Teoretycznie tak wypełniony trójkąt moglibyśmy zostawić już w spokoju, ale jak się okazuje to co widzimy na ekranie niekoniecznie musi być miłe dla oka, a już na pewno nie zamierzone. Dlaczego? Otóż dlatego, że trójkąty rysujemy z zastosowaniem perspektywy i o ile interpolowane wartości koloru jakoś to znoszą, o tyle koordynaty tekstur nie zachowują się tak jak powinny. Rysowane trójkąty w zdecydowanej większości nie są prostopadłe do ekranu, właśnie dlatego przy wyliczaniu wartości u i v musimy użyć metody zwanej korekcją perspektywy. Polega ona na tym, że poza interpolowaniem wartości u, v na trójkącie, uzależniamy je jeszcze od wartości <#FONT:[COLOR:0xFF00FF00]#>z<#FONT:[COLOR:0xFFFFFFFF]#>, czyli odległości punktu od obserwatora. Uff... wydaje się zagmatwane, ale wcale tak nie jest. W praktyce wygląda to tak, że nie interpolujemy po trójkącie wartości u i v, tylko odpowiednio:

    u1 = u / z
    v1 = v / z

gdzie z to po prostu z danego wierzchołka, a u i v to koordynaty tekstur na tymże. Do tych dwu wartości dochodzi jeszcze trzecia:

    z1 = 1 / z

która też jest interpolowana po całym trójkącie. I teraz dla każdego punktu na trójkącie liczymy odpowiednio u i v tekstury w następujący sposób.

    u2 = u1 / z1
    v2 = v1 / z1

Mamy teraz u2, v2 - czyli współrzędne tekstury dla danego punktu trójkąta. Jak widać nie jest to tak strasznie zagmatwane. Jest za to bardzo <#FONT:[COLOR:0xFF6B8DFD]#>"mocożerne"<#FONT:[COLOR:0xFFFFFFFF]#>, bo nawet po zamianie dzielenia na mnożenie daje to nam dwa mnożenia + dzielenie, dlatego można np. liczyć wartości u i v w inner loopie <#FONT:[COLOR:0xFFB8AA98]#>(czyli linii poziomej trójkąta)<#FONT:[COLOR:0xFFFFFFFF]#> co 8 bądź 16 punktów, a potem interpolować je liniowo. Możemy też oczywiście teksturować trójkąty bez korekcji perspektywy, ale wtedy musimy liczyć się z tym, że scenka nie będzie wyglądała zbyt ciekawie. 

    Dochodzi jeszcze jedna sprawa. Jak połączyć cieniowanie <#FONT:[COLOR:0xFFB8AA98]#>(oświetlenie)<#FONT:[COLOR:0xFFFFFFFF]#> z teksturowaniem. Dawniej, w trybach paletowych, było to bardzo trudne do zrobienia bez ustawienia odpowiedniej palety. Dziś cały problem polega na wymnożeniu jednej wartości przez drugą, czyli:

    col = tex * light

    gdzie:

    col - kolor punktu na ekranie (16, 24, bądź 32 bit)
    tex - kolor pobrany z tekstury
    light - wartość natężenia światła z przedziału (0,1)

Przy czym pamiętać musimy, aby przy trzech składowych RGB, wymnażać przez ostatnią wartość każdy kolor osobno.

    <#FONT:[COLOR:0xFFEDEF61]#>Z - BUFFER<#FONT:[COLOR:0xFFFFFFFF]#>

    No tak. Wspomniałem w poprzednich dwóch działach o interpolacji wartości koloru i koordynatów tekstur po całym trójkącie, a tu dochodzi nam trzecia wartość, którą musimy równomiernie <#FONT:[COLOR:0xFF6B8DFD]#>"rozpracować"<#FONT:[COLOR:0xFFFFFFFF]#> na całej długości i szerokości trójkąta. Mowa o wartości Z. Po co nam ona? Ano po to, że podczas rysowania trójkątów musimy sprawdzać, które z nich są widoczne, które częściowo zasłonięte, a które przysłaniają nam inne. Mówiąc krótko, musimy posortować trójkąty tak, żeby to co z tyłu nie wyłaziło nam na przód :). Żeby nam się to udało, należy uporządkować trójkąty od najbliżej leżącego do najdalej oddalonego od obserwatora. Nie piszę tu, żeby uporządkować wartości Z rosnąco, bądź malejąco, ponieważ zależy to od układu współrzędnych, jaki sobie przyjmiecie w swoim silniku. Generalnie zakłada się, że wraz z oddaleniem od obserwatora wartości z ulegają zwiększeniu. Ale już np. w <#FONT:[COLOR:0xFF00FF00]#>OpenGL'u<#FONT:[COLOR:0xFFFFFFFF]#> oś Z skierowana jest w stronę obserwatora, co oznacza, że dodatnie wartości znajdują się poza obszarem widzenia <#FONT:[COLOR:0xFFB8AA98]#>(za kamerą)<#FONT:[COLOR:0xFFFFFFFF]#>, a im mniejsze z tym dalej obiekt od obserwatora. Aby posortować trójkąty możemy po prostu zsumować wartości współrzędnych z każdego z wierzchołków, a następnie na tej podstawie zrobić sortowanie. Po tym wszystkim możemy już rysować trójkąty od najdalszego do najbliższego, mając pewność, że te <#FONT:[COLOR:0xFF6B8DFD]#>"z tyłu"<#FONT:[COLOR:0xFFFFFFFF]#> i tak będą przykryte przez te <#FONT:[COLOR:0xFF6B8DFD]#>"z przodu"<#FONT:[COLOR:0xFFFFFFFF]#>. Jest to pewne rozwiązanie, ale skrajnie nieefektywne, ponieważ rysujemy nawet te ścianki, które w finalnej wersji nie są widoczne, co bardzo spowalnia działanie engine'u. Lepszym sposobem sortowania trójkątów co do odległości, jest rysowanie <#FONT:[COLOR:0xFF6B8DFD]#>"od przodu do tyłu"<#FONT:[COLOR:0xFFFFFFFF]#>, czyli najpierw rysowanie trójkątów znajdujących się najbliżej, a potem coraz dalej, przy czym na każdym punkcie trójkąta sprawdzamy czy jest on dalej czy bliżej od tego, który jest już na ekranie w tym miejscu. Właśnie do tego potrzebna jest nam interpolowana wartość z. Mając ją możemy podczas rysowania sprawdzić w odpowiednim buforze, odpowiadającym wielkością naszemu buforowi ekranu, czy powinniśmy zastąpić dany pixel, znajdujący się <#FONT:[COLOR:0xFF6B8DFD]#>"na ekranie"<#FONT:[COLOR:0xFFFFFFFF]#> czy też nie. Jeśli to co mamy narysowane jest bliżej od tego, który właśnie sprawdzamy, to nie rysujemy nic, jeśli natomiast okaże się, że rysowany pixel jest bliżej od tego, który jest na ekranie, to zastępujemy pixel nowym, a w buforze z wartościami Z wpisujemy nową wartość. Właśnie ten bufor, zwany Z - <#FONT:[COLOR:0xFF00FF00]#>BUFFEREM<#FONT:[COLOR:0xFFFFFFFF]#> bądź też <#FONT:[COLOR:0xFF00FF00]#>DEPTH BUFFEREM<#FONT:[COLOR:0xFFFFFFFF]#> przechowuje wartości o odległości każdego punktu znajdującego się na ekranie od obserwatora <#FONT:[COLOR:0xFFB8AA98]#>(kamery)<#FONT:[COLOR:0xFFFFFFFF]#>. Właśnie po to interpolujemy wartość Z po trójkącie, żeby DLA KAŻDEGO PIXELA sprawdzić czy powinniśmy go rysować czy też nie.

    <#FONT:[COLOR:0xFFEDEF61]#>CLIPPING<#FONT:[COLOR:0xFFFFFFFF]#>

    Właściwie jest to problem, od którego powinno się zacząć pisząc własny engine. Dlaczego? Ano dlatego, że jeśli okaże się, iż rysowany trójkąt wychodzi poza ekran, to wszystko nam się skaszani. Aby temu zapobiec musimy sprawdzić, czy dany trójkąt jest przez nas widoczny, a jeśli choć trochę wychodzi poza ekran, należy go <#FONT:[COLOR:0xFF6B8DFD]#>"obciąć"<#FONT:[COLOR:0xFFB8AA98]#> (clip)<#FONT:[COLOR:0xFFFFFFFF]#>. Na samym początku musimy zdefiniować sobie tzw. <#FONT:[COLOR:0xFF6B8DFD]#>"piramidę widzenia" <#FONT:[COLOR:0xFFB8AA98]#>(z angielskiego to będzie "fustrum")<#FONT:[COLOR:0xFFFFFFFF]#>. Następnie sprawdzamy, czy dany trójkąt należy do naszej piramidy i jeśli tak dołączyć go do listy rysowanych trójkątów. Jeśli tylko częściowo należy on do niej, to dołączamy go do listy trójkątów, na których zastosujemy jeszcze clipping 2D. Aby sprawdzić czy ściana mieści się w naszym <#FONT:[COLOR:0xFF6B8DFD]#>"fustrumie"<#FONT:[COLOR:0xFFFFFFFF]#> możemy zastosować algorytm Sutherlanda - Hodgmana, którego nie będę tu opisywał, bo zrobił to już Biki w trzecim Dragonie <#FONT:[COLOR:0xFFB8AA98]#>("Obcinanie 3D")<#FONT:[COLOR:0xFFFFFFFF]#>. Nie ukrywam, że samemu przerabiając ten temat korzystałem z pomocy wspomnianego tekstu, więc mógłbym po prostu przepisać go stamtąd, tylko po co? Zainteresowanych odsyłam do Dragona.

    Innym sposobem na sprawdzenie, czy dany trójkąt jest do narysowania czy do wyrzucenia, jest po prostu zrzutowanie go przez perspektywę i sprawdzenie, czy jego wartości mieszczą się na naszym ekranie. Jeśli nie to wywalamy, jeśli tylko częściowo to stosujemy clipping 2D. To jest już stosunkowo proste, gdyż polega na odpowiednim zmodyfikowaniu procedury rysującej trójkąt. Jeśli trójkąt ucinamy od góry, to po prostu pomijamy rysowanie kilku linii poziomych, jeśli na dole - rysujemy bez kilku ostatnich linii. Jeśli ucinamy z prawej to rysujemy punkty w linii poziomej tylko do określonej wartości x, jeśli z lewej - pomijamy wszystkie punkty aż do określonej wartości x. Prawda, że proste?

    Wracając jeszcze do drugiego sposobu... przy jego pomocy możemy wywalić wszystkie trójkąty leżące <#FONT:[COLOR:0xFF6B8DFD]#>"z boku", "na górze"<#FONT:[COLOR:0xFFFFFFFF]#> lub <#FONT:[COLOR:0xFF6B8DFD]#>"pod spodem"<#FONT:[COLOR:0xFFFFFFFF]#>. A co z trójkątami leżącymi <#FONT:[COLOR:0xFF6B8DFD]#>"w głębi"<#FONT:[COLOR:0xFFFFFFFF]#>? Po prostu sprawdzamy, czy ich wartości mieszczą się w określonym przedziale <#FONT:[COLOR:0xFFB8AA98]#>(0,Z)<#FONT:[COLOR:0xFFFFFFFF]#>, przy czym większe od zera są zawsze <#FONT:[COLOR:0xFFB8AA98]#>(zakładając, że wyrzuciliśmy wcześniej trójkąty leżące za kamerą)<#FONT:[COLOR:0xFFFFFFFF]#>, więc sprawdzamy tylko czy ich wartości są mniejsze od ustalonego Z, które możemy dobrać sobie sami odpowiednio do potrzeb. A żeby nie było, że łatwo to powiem Wam, że to jeszcze nie koniec paprania się z obcinaniem, bo po sprawdzeniu i wyrzuceniu trójkątów <#FONT:[COLOR:0xFF6B8DFD]#>"z głębi"<#FONT:[COLOR:0xFFFFFFFF]#> może się okazać, że scenka nagle nam się <#FONT:[COLOR:0xFF6B8DFD]#>"urywa"<#FONT:[COLOR:0xFFFFFFFF]#>. Aby temu zapobiec możemy zastosować stopniowe <#FONT:[COLOR:0xFF6B8DFD]#>"przyciemnianie"<#FONT:[COLOR:0xFFFFFFFF]#> trójkątów leżących dalej, czyli to, na co potocznie mówi się...

    <#FONT:[COLOR:0xFFEDEF61]#>MGŁA<#FONT:[COLOR:0xFFFFFFFF]#>

    Wydawać by się mogło, że jest to problem dosyć złożony, ale jak się okazuje jest to jedna z najprostszych rzeczy do zakodowania. Musimy tylko do wartości natężenia światła dodać jeszcze jeden współczynnik, który zależny będzie od odległości - znaczy się od z. I tak, definiujemy sobie dwie płaszczyzny z, które będą oznaczały początek <#FONT:[COLOR:0xFF6B8DFD]#>"mgły"<#FONT:[COLOR:0xFFFFFFFF]#> i jej koniec <#FONT:[COLOR:0xFFB8AA98]#>(najczęściej ta sama wartość która określa Zclip sceny)<#FONT:[COLOR:0xFFFFFFFF]#>. I teraz dla każdego wierzchołka leżącego między tymi dwoma płaszczyznami liczymy odpowiednią wartość natężenia <#FONT:[COLOR:0xFF6B8DFD]#>"mgły"<#FONT:[COLOR:0xFFFFFFFF]#> i sprowadzamy ją do przedziału <#FONT:[COLOR:0xFFB8AA98]#>(0,1):<#FONT:[COLOR:0xFFFFFFFF]#>

    radius = (z - początek_mgły) / (koniec_mgły - początek_mgły)

Oczywiście <#FONT:[COLOR:0xFF00FF00]#>koniec_mgły - początek_mgły<#FONT:[COLOR:0xFFFFFFFF]#> wyliczamy zaraz na początku. Otrzymany radius dodajemy po prostu do wartości natężenia światła dla każdego wierzchołka. Akurat w tym przypadku wraz ze zwiększaniem odległości mgła będzie coraz jaśniejsza, ale możemy przecież zrobić <#FONT:[COLOR:0xFFB8AA98]#>(1 - radius)<#FONT:[COLOR:0xFFFFFFFF]#> i mamy coś zgoła innego. Możemy także zamiast interpolować wartości liniowo dodać do każdej z nich określoną wartość, bądź wyliczyć sinus. Wszystko zależy od wyobraźni.

    <#FONT:[COLOR:0xFFEDEF61]#>ENVIROMENT<#FONT:[COLOR:0xFFFFFFFF]#>
    
    O jejku. Ale mi się rozpisało. Okej, ostatnia rzecz na dzisiaj to prostu enviroment mapping - czyli <#FONT:[COLOR:0xFF6B8DFD]#>"mapowanie środowiskowe"<#FONT:[COLOR:0xFFFFFFFF]#> czy jakoś tak. W naszym przypadku będzie to troszkę fake'owane. A zrobimy to tak... Wyobraźcie sobie kulę. Załóżmy, że chcemy, aby odbiło się w niej to, co stoi za nami i obok nas i potraktujmy to jako teksturę. Punkt kuli znajdujący się najbliżej nas będzie odbijał to co jest w centrum tekstury, a punkty znajdujące się <#FONT:[COLOR:0xFF6B8DFD]#>"po bokach"<#FONT:[COLOR:0xFFFFFFFF]#> odbijać będą <#FONT:[COLOR:0xFF6B8DFD]#>"krańcowe"<#FONT:[COLOR:0xFFFFFFFF]#> rejony tekstury. Jasne? Mam nadzieje, że tak :). A teraz zastanówmy się jak to się ma do naszego obiektu i od czego możemy uzasadnić wartość u i v tekstury. Zakładam, że oś Z skierowana jest w stronę obserwatora <#FONT:[COLOR:0xFFB8AA98]#>(jak w OpenGL'u)<#FONT:[COLOR:0xFFFFFFFF]#>. Mamy więc punkt najbliżej nas. Wartość wektora normalnego w tym punkcie wynosi <#FONT:[COLOR:0xFFB8AA98]#>(0,0,1)<#FONT:[COLOR:0xFFFFFFFF]#>. Punkty krańcowe <#FONT:[COLOR:0xFFB8AA98]#>(góra, dół i boki)<#FONT:[COLOR:0xFFFFFFFF]#> mają normale o wartościach <#FONT:[COLOR:0xFFB8AA98]#>(1,0,0), (0,1,0), (-1,0,0), i (0,-1,0)<#FONT:[COLOR:0xFFFFFFFF]#>. Można z tego wywnioskować, że koordynaty tekstur dają się uzależnić od wartości wektorów normalnych. I tak:

    u = (normal.x + 1) / 2
    v = (normal.y + 1) / 2

W ten oto sposób możemy, <#FONT:[COLOR:0xFF6B8DFD]#>"odbić"<#FONT:[COLOR:0xFFFFFFFF]#> środowisko na obiekcie. Jest to sposób <#FONT:[COLOR:0xFF6B8DFD]#>"oszukańczy"<#FONT:[COLOR:0xFFFFFFFF]#>, gdyż zakładamy, że to co chcemy odbić jest tylko na jednej teksturce <#FONT:[COLOR:0xFFB8AA98]#>(a przecież chcemy widzieć całe otoczenie)<#FONT:[COLOR:0xFFFFFFFF]#>, jednakże stosunkowo szybkie, ładnie wygląda i czasami wystarcza. Prawdziwy enviroment polega na tym, że robimy niejako <#FONT:[COLOR:0xFF6B8DFD]#>"zrzut"<#FONT:[COLOR:0xFFFFFFFF]#> wszystkich sześciu stron otoczenia, nakładamy na sześcian, liczymy wektor od liczonego wierzchołka do punktu kamery, obliczamy wektor odbicia, sprawdzamy przez która ścianę wygenerowanego cube'a on przechodzi, a następnie pobieramy z danej tekstury wartość u i v. To tylko ogólny zapis, bo w praktyce jest to znacznie bardziej skomplikowane. Na dzisiejszych kartach jest to obsługiwane sprzętowo i nazywa się Cube Mapping. Jak ktoś ma GeForce'a albo Rodeona to można poeksperymentować, bo efekty naprawdę fajne.

   A jak już jesteśmy przy oszukańczym enviromencie, to dodam, że za jego pomocą możemy zrobić oszukańczego phonga. Zamiast zwyczajnej tekstury dajemy teksturę <#FONT:[COLOR:0xFF6B8DFD]#>"latarki" <#FONT:[COLOR:0xFFB8AA98]#>(taką jak do bumpa)<#FONT:[COLOR:0xFFFFFFFF]#> i jazda. Co prawda też jest to troszkę ograniczone, gdyż w tym przypadku zakładamy, że światło umiejscowione jest w pozycji <#FONT:[COLOR:0xFF6B8DFD]#>"za nami"<#FONT:[COLOR:0xFFFFFFFF]#>. Nie mniej jednak, możemy pobawić się w odpowiednie obracanie normali. Ciekawe co wyjdzie?  :). Pozdrawiam.

<#FONT:[BOLD:1]#><#FONT:[COLOR:0xFFFFFF40]#>pienia/msm<#FONT:[BOLD:0]#><#FONT:[COLOR:0xFFFFFFFF]#>
