poniedziałek, 29 grudnia 2008

Garść refleksji o narzędziach…

screen Jako że ostatnimi czasy coraz bardziej denerwuje mnie trzeci program na zaliczenie, muszę zrobić małą odskocznię. Tą ucieczką okazała się kolejna notka na blogu – tym razem o niczym (no dobra, prawie) związanym z frameworkiem…

…ale nie myślcie, że nie będzie traktować o programowaniu. Jednakże nie w sensie szczegółowym, ale ogólnym. Konkretnie o narzędziach, jakich używań przy tworzeniu oprogramowania. Wprawdzie jest to wybór być może mniej ważny niż chociażby używane technologie, gdyż i tak większość ludzi wybiera te programy, które im najbardziej pasują i są najprostsze w obsłudze. Tym bardziej bolała mnie przesiadka z Dev-C++ na Visual C++ – jednak jak widać, nawet ja opanowałem taki kombajn jak dziecko Microsoftu, a więc da się. Dobrze, miałem nie narzekać (zadanie prawie że dla mnie niewykonalne…).

W każdym razie – krótka wędrówka po programach, których używam, aby dręczyć znajomych na Gadu-Gadu screenami i pytaniami w stylu “Jak sądzisz, pełnia Słońca, czy szalony surrealizm i "ja pierdzielę, ale daje po oczach"?” (pozdrawiam Krzyśka i Shirou Noira).

1. Środowisko programistyczne i kompilator, czyli Pełen Serwis W Jednym Miejscu I Jak Się Rozleci, To Jest Zmartwienie (w skrócie – PSWJMIJSR,TJZ)

Tytuł tego punktu jest mocno prześmiewczy – w końcu 99% programistów używa (do nowoczesnych języków programowania) zintegrowanych środowisk programistycznych (IDE) i nie bawi się w pisanie kodu w edytorze tekstowym, a następnie kompilowaniu za pomocą konsoli systemowej (nie wiem, jak wyznawcy Linuxa, choć nie sądzę, aby na siłę bawili się komendami tekstowymi). Generalnie używam Jedynego Słusznego Microsoft Visual C++ 2008 Express Edition. Być może niektórzy kojarzą, że na przykład Graphratora pisałem za pomocą Dev-C++ – szczerze mówiąc nadal używam tego drugiego programu (a właściwie trzeciego, bo w środek wmieszał się Graphek), ale tylko do testowania pewnych rozwiązań językowych – możliwość polimorfizmu, niektóre metody itd. W każdym razie – coraz rzadziej. Jakoś nie przekonałem się do Code::Blocks, którego większość używa jako substytut Visuala (czy Eclipse) – być może jakbym zaczynał od C::B a nie Deva, to sytuacja wyglądałaby nieco inaczej. Ale i tak C::B jest brzydkie, w moim skromnym mniemaniu.

Czasami jednak stosuję inny program do pisania kodu – i to nie tylko C++. Jest to Notepad++, który często przywołuję w notkach (a ostatnio pisał o nim Gamer.cpp), doskonale sprawujący się przy pisaniu stron w HTML-u, PHP czy shaderów (o tym za chwilę). Często bywa tak, że piszę kod klasy w Zplusowanym Notatniku, a następnie kompiluję w Visualu i czytam logi z nieudanej kompilacji. Bywa wygodniej.

Oczywiście, czym by było IDE bez odpowiedniej personalizacji – każdy ma tutaj inną filozofię. Ja akurat wyłączyłem większość okienek i zakładek, zostawiając tylko Output, Breakpoints oraz Solution Explorer. I to mi całkowicie starczy – nie używam jakiś niemożliwych funkcji w tym programie, choć być może będzie się to zmieniało z czasem. Przynajmniej mam taką nadzieję.

2. Program do shaderów, czyli Edytor Do Tych Cholernych Programików Dla GPU (w skrócie - EDTCPDG)

Shadery przez dłuższy czas były dla mnie czarną magią – tutaj się a wiele nie zmieniło. Aczkolwiek kiedyś nawet pisanie ich oraz szukanie błędów było ogromnym problem, gdy robiłem to w Notepad++. Od jakiegoś czasu używam jednak CgEddie – programu napisanego przez SirMike’a należącego do społeczności Warsztatu i muszę przyznać, że sprawuje się znakomicie – taki szybki, mały, prosty, a skoczny i żwawy. Oferuje dokładnie to, czego pragnę – analizę kodu shadera i możliwość efektywnej poprawy. Wielkie wyrazy szacunku dla autora i pozdrowienia.

Są oczywiście bardziej wyspecjalizowane aplikacje – chociażby NVIDIA FX Composer. Muszę jednak przyznać, że możliwości dawane przez aplikacje nie są zawsze współmierne z efektywnością. A konkretnie ze zrozumieniem interfejsu – w końcu spasowałem i wróciłem do CgEddie.

3. Grafika, czyli Jak Obrobić Tekstury I Inne Pierdoły (w skrócie – JOTIIP)

Również w tym względzie nie będę specjalnie oryginalny i przyznam, że do tworzenia map normalnych, wypukłości i innych efektów używam znanego i lubianego GIMPa, który oferuje opcji hop hop i ciut ciut. Zauważyłem jednak, że nie zawsze GIMP prawidłowo zapisuje pliki w formacie TGA – trzeba je niekiedy nadpisać za pomocą IrfanView. Ogólnie nie mam w tej kwestii za wiele do powiedzenia – te programy większość zna (nawet jeśli nie jest programistą), więc zbyteczne jest wykładanie wszystkich opcji takich kombajnów – zwłaszcza, że używam może z 10%.

Jak wspominałem na początku, za dużo tych punktów nie było – raptem trzy. Cel został jednak osiągnięty – odsapnąłem od niekiedy nudnego programowania, a być może kogoś zachęciłem do skorzystania z jakiegoś narzędzia.

Na wszystkie pytania tradycyjnie odpowiem w komentarzach.

Pozdrawiam i dziękuję – SceNtriC.

niedziela, 21 grudnia 2008

Garść narzekań frameworkowych i LaTeXowych

Kolejny tydzień za pasem – na szczęście, ostatni w tym roku kalendarzowym, podczas którego należało uczestniczyć w zajęciach. W tym czasie zdarzyło się między innymi tak, iż jako podgrupa mamy przerobić prezentację przygotowaną w programie PowerPoint na LaTeXa – oczywiście, siłą komunalnej sprawiedliwości, dostaliśmy po kilka slajdów na łebka. Z mojego punktu widzenia ważniejsze jest jednak to, że wreszcie miałem motywację, aby bliżej przyjrzeć się samemu LaTeXowi – narzędziu, o którym słyszałem już wiele dobrego, a do którego jakoś nie miałem chęci sam podejść. Niedoinformowanym objaśniam, iż język ten, będący rozwinięciem TeXa, służy do profesjonalnego składu tekstu, ale także do tworzenia nowoczesnych prezentacji multimedialnych, które swoją estetyką i efektywnością doprowadzają kolejne rzędy studentów na wykładzie do ekstazy. Choć jest to być może efekt licznika slajdów na górnym marginesie i cichego pojękiwania “o, już tak niewiele zostało do końca…”

W każdym razie, postanowiłem dobrze wykorzystać ostatni weekend przed świętami i spróbować chociaż zacząć moją “działkę” prezentacji. W tym celu, zainstalowałem polecany przez prowadzących zajęcia MikTex oraz edytor LEd, a następnie godzinę męczyłem się z instalowaniem pakietów obsługujących między innymi Beamera (nakładka umożliwiająca tworzenie slajdów) oraz polskiej pisowni. Sam edytor okazał się jednak dosyć bezkonfliktowy, choć początki były standardowe – od stanu “dodam tutaj backslash i zadziała” poprzez “ale dlaczego nie działa?” aż do “cholerne, lateksowe wymię chaosu”. Końcem końców, udało mi się wypłodzić praktycznie całą część, jaka została mi przydzielona – zostały tylko kosmetyczne poprawki. Ogólnie muszę przyznać, że mimo irytującej miejscami składni i zaciemnienia kodu oraz – moim skromnym zdaniem – mącących wydruków kompilatora, sam proces tworzenia tekstu bywa fascynujący. Zwłaszcza, jak zobaczy się efekt swojej pracy w formie ładnego dokumentu PDF. Dlatego też, postaram zaprzyjaźnić się z LaTeXem na dłużej, chociażby przy pisaniu jakiś artykułów – i to nie tylko dlatego, że będę miał z tego zaliczenie na uczelni.

Zostawmy jednak na razie nowinki (dla mnie) i przejdźmy do tego, co udało mi się zaimplementować w frameworku przez ostatni tydzień. Wiele tego nie było (tak, znowu przez sprawy uczelniane), ale zawsze jest coś nowego.

Poprawki

Zostały one poczynione na dwóch polach, przy okazji implementacji parallax mappingu – przy śledzeniu ruchów myszki oraz przy teksturowaniu. Co do pierwszego, od teraz klasa CCore umożliwia dokładne i aktualne określenie współrzędnych punktu w którym znajduje się magiczna strzałeczka z odcinkiem (fachowo – kursor). Niby rzecz oczywista, ale dopiero przy okazji tego efektu potrzebowałem tego mechanizmu do obracania ścianki, na której testowałem wypukłość i zastanawiałem się przez tydzień, dlaczego nie działa. Natomiast managery tekstur wzbogaciły się o możliwość ustawiania środowiska, filtracji oraz zawijania tekstur – niby skromne rzeczy, będące tak naprawdę nakładką na OpenGL, ale okazały się wręcz zbawienne przy ustawieniach, jakie domyślnie i lekkomyślnie przyjąłem dla procesu generowania tekstur (czyli filtracja specjalnie dla mipmap, teksele zamieniające wszystko co się rusza i rozciąganie obrazków na płaszczyźnie). Teraz to wygląda lepiej.

Steep Parallax Mapping

Przy okazji poprzedniej notki wspominałem o rozbudowanej wersji paralaksy – Parallax Occlusion Mapping, czyli steep parallax mapping. Z tego, co się dowiedziałęm, polega ona na wysyłaniu promienia i kilkukrotnym mapowaniu, przez co efekt powinien być bardziej realistyczny. Może i jest – u mnie jakoś to nie do końca działa i zawiesza czasami sterownik ekranu. Tym zajmę się jednak kiedyś, przy okazji.

Anizotropia

Tajemniczo brzmiąca nazwa, nieprawdaż? Według Jedynego Słusznego Dostarczyciela Wiedzy, anizotropia to różne cechy jednego ciała w zależności od kierunku. W grafice można zauważyć po prostu różne rzutowanie tekstury i oświetlenie ciała w zależności od kierunku padania światła. I właściwie, nie ma tutaj o czym więcej mówić, gdyż sam efekt nie jest morderczy, a jego przykładowa implementacja znajduje się na niezawodnej stronie UGP. Jeśli ktoś wyrazi chęć, mogę coś więcej o tym napisać.

To właściwie tyle – niewiele, aczkolwiek mam nadzieję, że coś dojdzie w najbliższym czasie. W pierwszej kolejności muszę się jednak zająć sprawami uczelnianymi, “świętowymi” i tymi, które wiążą się z innym projektem. Zatem – do przeczytania.

Pozdrawiam i dziękuję – SceNtriC.

niedziela, 14 grudnia 2008

Co nieco o paralaksie…

Od pewnego czasu, zepchnąłem tworzenie frameworka na drugi albo i nawet trzeci plan na mojej liście priorytetów. Do tego stopnia, że prawie w ogóle nie notuję w kalendarzu żadnych scr000zapisków dotyczących dalszych pomysłów na rozwiązywanie problemów, a jak już, to nie określam dokładnie daty. Oczywiście, jako że nie jestem osobą o specjalnie silnej woli, dlatego programowanie frameworka zwykle kontynuuję każdego dnia. Sprawia mi to jednak tyle radości, że aż chce się łamać harmonogram. Mam też jednak nadzieję, że powoli, bo powoli dojdę do wszystkiego, co tak naprawdę przepisuję z tutoriali i kursów - bądź co bądź, ten powoli rozszerzający się projekt zwany Yestą nie jest obrazem moich umiejętności programistyczno-projektowych, choć się staram. Ech, ale przestaję marudzić - są inne tematy ku temu.

A w tej notce chciałbym się zająć pewnym efektem, który ostatnio spędzał mi sen z powiek i nadal spędza, choć już nieco mi się rozjaśniło. Mam jednak nadzieję, że opisując kolejne etapy powstawania Parallax Mappingu (bo o nim mowa) także mnie uda się zrozumieć proces. Wiem, że to herezja - pisanie sposobu działania czegoś, czego się do końca samego nie rozumie. Myślę jednak, że każdy coś z tego wyniesie - Czytelnicy pewną wiedzę dzięki kawałkom kodu źródłowego, a ja inną wiedzę dzięki staraniu się zamglenia mojej niewiedzy. A przy okazji wypróbuję mechanizm pisania ładnych kodów źródłowych z użyciem Windows Live Writera (dzięki gamer.cpp, pozdrawiam). Przepraszam zatem już na wstępie, jeśli układ strony będzie lekko niestrawny – dopiero się uczę obsługi tegoż narzędzia (a prezentuje się bardzo porządnie, jak na razie).

Może na sam początek przywołam definicję samego efektu z Wikipedii.

Parallax mapping (also called offset mapping or virtual displacement mapping) is an enhancement of the bump mapping or normal mapping techniques applied to textures in 3D rendering applications such as video games. To the end user, this means that textures such as stone walls will have more apparent depth and thus greater realism with less of an influence on the performance of the simulation. Parallax mapping was introduced by Tomomichi Kaneko et al in 2001.

Parallax mapping is implemented by displacing the texture coordinates at a point on the rendered polygon by a function of the view angle in tangent space (the angle relative to the surface normal) and the value of the height map at that point. At steeper view angles the texture coordinates are displaced more, and so give the illusion of depth due to parallax effects as the view changes.

Parallax mapping described by Kaneko is a single step process that does not account for occlusion. Subsequent enhancements have been made to the algorithm incorporating iterative approaches to allow for occlusion and accurate silhouette rendering.

Jak można wywnioskować, całość sprowadza się do takiego operowania współrzędnymi tekstury, aby całość dawała efekt głębi. Dlaczego zatem nazwa wzięła się od paralaksy? Odpowiednia strona na Wikipedii to poniekąd wyjaśnia - całość opiera się na wykorzystaniu wrażenia różnych odległości przedmiotu od oka na innych planach. Brzmi skomplikowanie, a moje wyjaśnienie pewnie jeszcze dodatkowo to komplikuje. Mam jednak nadzieję, że każdy chociaż intuicyjnie wie, o co chodzi. Natomiast jeśli ktoś ma tutaj jakieś uwagi - koniecznie proszę zawrzeć to w komentarzu, to przy okazji ja również zweryfikuję swoje refleksje.

W artykule będę opierał się na wersji przedstawionej na tej stronie . Z pewnych względów, przykład na UGP okazał się niewystarczający, jednak zamieszczam również ten odnośnik.

Ogólnie, według mnie, plan działania przedstawia się następująco na szalonym obrazku narysowanym w Paintcie:

scr001

Mając pozycję oka kamery, obliczamy kierunek wektora i jego pozycję padania na płaszczyznę. Wtedy następuje rzutowanie na podstawie mapy wysokości i obliczenie końcowej współrzędnej tekstury, która będzie się przesunięta o pewien offset w stosunku do "właściwej" pozycji. Brzmi nieskomplikowanie w sumie matematycznie - z podobnymi rzeczami mają do czynienia uczniowie liceum na lekcji fizyki w trzeciej klasie przy odbiciu promieni w cieczy i takich tam. Przyznam jednak, że nie lubiłem tych tematów - zawsze byłem bardziej matematykiem aniżeli fizykiem, ale jako że lubię się też wkopywać w różne dziwne rzeczy tylko po to, aby zdobyć potrzebną wiedzę i doświadczenie, to zająłem się grafiką.

Od razu też powiem, że istnieje nowsza, lepsza i bardziej realistyczna wersja tej techniki zwana Parallax Occlusion Mappingiem lub Steep Parallaxem. Mimo zwiększonej pamięciożerności wydaje się dużo lepszym wyborem dla nowoczesnych gier. Różni się wysyłaniem promienia i opieraniu się na parokrotnych obliczeniach w celu większego "displacingu" (przefałdowania, przemieszczenia). Jednocześnie może być wykorzystane do stworzenia efektu futra. Nieco później także się tym zajmę w oparciu o przykład ze strony przytoczonej wcześniej. Natomiast, jeśli chodzi o porównanie efektów mapowania wypukłosci, jest ono dokonane tutaj.

Zajmijmy się najpierw implementacją shaderów odpowiedzialnych za efekt. Proponuję na początek program do obróbki wierzchołków i jego szkielet na warsztat.

   1: struct VertexIn
   2: {
   3:     float4 Position      : POSITION;
   4:     float2 TexCoord      : TEXCOORD0;
   5:     float3 Normal        : NORMAL;
   6:     float3 Tangent       : COLOR;
   7: };
   8:  
   9: struct VertexOut
  10: {
  11:     float4 Position      : POSITION;
  12:     float2 TexCoord      : TEXCOORD0;
  13:     float3 TanEyeVec     : TEXCOORD1;
  14:     float3 TanLightVec   : TEXCOORD2;
  15: }
  16:  
  17: VertexOut main (VertexIn In, uniform float4x4 ModelViewProj, uniform float4x4 WorldMatrix, uniform float3 EyePosition, uniform float3 LightPosition)
  18: {
  19:     VertexOut Out;
  20:     //...
  21: }


Widać dokładnie, jakie wartości rejestrów będziemy potrzebować i jakie umieścimy w nich po zakończeniu działania vertex shadera. Wartości, które musimy podać “ręcznie” to kolejno: macierz projekcji i modelowania (ModelViewProj), odrócona macierz modelowania (WorldMatrix), pozycja oka kamery (EyePosition) oraz pozycję źródła światła (LightPosition). Od razu przestrzegę też przed jedną rzeczą, która zdarzyła mi się przy formatowaniu fragment shadera – jeśli podamy czteroelementową tablicę dla argumentu, który jest zdefiniowany jako float3, to w większości przypadków nie dostaniemy dobrych wyników. Niby oczywiste, ale tym sposobem męczyłem się z współczynnikiem światła odbijającego (specular). Zatem szkielet pierwszego programu mamy. Dodajmy zatem funkcjonalność w funkcji main (czyli w miejscu komentarza jednolinijkowego z metaforycznym przesłaniem “…”. Ale po kolei – najpierw podstawowe obliczenia, które nawet ja rozumiem. Czyli zaliczyłem pierwszy sukces.

   1: Out.Position = mul(ModelViewProj, In.Position);
   2: Out.TexCoord = In.TexCoord;
   3:     
   4: float4 eyePosition = mul(WorldMatrix, float4(EyePosition.x, EyePosition.y, EyePosition.z, 1));
   5: eyePosition /= eyePosition.w;
   6:     
   7: float4 objectLightPosition = mul(WorldMatrix, float4(LightPosition.x, LightPosition.y, LightPosition.z, 1.0f));
   8: objectLightPosition *= objectLightPosition.w;


Obliczenie pozycji wierzchołka oraz współrzędnej tekstury – w miarę proste. To drugie przyjmujemy bez zmian, natomiast pozycję wierzchołka obliczamy mnożąc wejściową wartość przez macierz modelowania i projekcji. Następnie w dosyć mętnie wyglądający sposób obliczamy wartości wektorów oka kamery i pozycji światła. Ktoś zapyta “po co obliczamy to jeszcze raz, skoro podajemy to na wejściu?”. Pewnie, że moglibyśmy dalej ciągnąć program na argumentach, ale są przynajmniej trzy powody, dlaczego tak się nie dzieje. Po pierwsze – przejrzystość kodu. Jakoś generalnie nie spotykam się z shaderami, które operują dalej na argumentach podanych “ręcznie” (uniformach), więc musi być w tym jakaś logika. Drugi powód wynika po części z trzeciego i zakłada fakt, że operujemy na macierzach 4x4 oraz wektorach o czterech współrzędnych. Klasę CLighting oparłem jednakże na argumentach trójelementowych, zatem należy utworzyć nowe wektory, identyczne z podanymi wraz z czwartym elementem 1, przez który normalizacja wygląda dosyć dziwnie, ale formalnie rzecz biorąc – jest konieczna. Trzeci powód to taki, że musimy przemnożyć te wektory przez macierz świata (nie wiem, czy nie popełnię teraz herezji, ale to się chyba fachowo określa “przejść na płaszczyznę tangensową (tangent space)” – tutaj proszę o skorygowanie mnie, jeśli się mylę). Zatem, posiadając już przetransformowane wektory, możemy przejść dalej, do ciekawszych obliczeń.


   1: float3 tangent = In.Tangent;
   2: float3 binormal = cross(In.Normal, tangent);
   3:     
   4: float3 eyeVec = normalize(eyePosition - In.Position);
   5: Out.TanEyeVec = 0.5 + eyeVec / 2;


To może jeszcze koniecznie nie są te bardzo ciekawe działania, ale równie potrzebne i fundamentalne jak powyższe. Co to jest wektor normalny i jak się go oblicza (przypominam – iloczyn wektorowy dwóch wektorów prostopadłych do siebie opartych na bokach wielokąta, do która wektor normalny jest liczony), wspominałem bodajże przy okazji artykułu o kolizjach. Wypada teraz dokładniej określić wektory tangent i binormal (błędnie określany czasami przeze mnie jako bitangent – przepraszam zainteresowanych). Tangent to iloczyn wektorowy obliczonego przed chwilą (a podawanego w shaderze przez rejestr, podobnie zresztą jak tangent) wektora normalnego oraz jednego z wektorów na boku wielokąta. Binormal to kolejny “cross product”, tym razem wektorów normalnego i tangenta. Tak uzbrojeni, możemy przejść dalej, do obliczenia wektora widzenia, czyli znormalizowanej różnicy “tych śmiesznych kresek ze strzałkami” obliczonej powyżej pozycji oka kamery i wejściowej pozycji wierzchołka. Następnie uzyskujemy trzecią zwracaną wartość, w rejestrze TEXCOORD1, czyli TanEyeVec, gdyż jest to eyeVec ograniczony do przedziału [0, 1]. Czas na ostatni etap pisania vertex shadera.


   1: float3 lightVec = objectLightPosition - In.Position;
   2: float3 tanLightVec;
   3: tanLightVec.x = dot(tangent, lightVec);
   4: tanLightVec.y = dot(binormal, lightVec);
   5: tanLightVec.z = dot(In.Normal, lightVec);
   6: Out.TanLightVec = 0.5 + normalize(tanLightVec) / 2;
   7:  
   8: return Out;


W podobny sposób jak wyżej obliczamy lightVec, czyli wektor światła – tym razem nie ma potrzeby jego normalizowania już w tym miejscu, stanie się to dopiero potem. Następnie obliczamy kolejne współrzędne pomocniczego wektora tanLightVec jako iloczyn skalarny kolejnych wektorów oraz lightVec. Mimo iż sama idea nie jest do końca jasna, dlaczego akurat takie wektory, dlaczego iloczyn skalarny i dlaczego nie wziąłem się za prostsze rzeczy, ale myślę, że kod jest rozpisany w miarę przystępnie. Na końcu obliczamy wartość czwartego parametru wyjściowego (w TEXCOORD2), czyli TanLightVec (zwracam uwagę na wielkość początkowej litery, jest to dodatkowy znacznik oprócz oczywiście identyfikator obiektu Out). W tym miejscu normalizujemy tan LightVec i ograniczamy jego zakres w identyczny sposób jak poprzednio TanEyeVec. Operacje kończą się oczywiście zwróceniem obiektu.

Przeanalizujmy szybko naszą obecną sytuację – po zakończeniu pracy vertex shadera, uzyskaliśmy cztery rejestry wypełnione danymi, które potrzebujemy dalej – pozycję wierzchołka w POSITION, współrzędne tekstury w TEXCOORD0, tangens wektora widoku w TEXCOORD1 oraz tangens wektora światła w TEXCOORD2. Posiadając te informacje możemy przystąpić do tworzenia szkieletu fragment shadera.

   1: struct FragmentIn
   2: {
   3:     float4 Position     :    POSITION;
   4:     float2 TexCoord     :    TEXCOORD0;
   5:     float3 TanEyeVec    :    TEXCOORD1;
   6:     float3 TanLightVec  :    TEXCOORD2;
   7: };
   8:  
   9: struct FragmentOut
  10: {
  11:     float4 Color        :    COLOR;
  12: };
  13:  
  14: FragmentOut main (FragmentIn In, uniform sampler2D Tex, uniform sampler2D Tex1, uniform sampler2D Tex2, uniform float Scale, uniform float Bias, uniform float3 Ka, uniform float3 Kd, uniform float3 Ks, uniform float Shininess, uniform float AmbientCoeff, uniform float DiffuseCoeff, uniform float SpecularCoeff)
  15: {
  16:     FragmentOut Out;
  17:     //...
  18: }

Strukturę FragmentIn można było przewidzieć na podstawie VertexOut z poprzedniego programu. Zwykle wynikiem działania fragment (pixel) shadera jest wartość koloru w danym punkcie zapisana w rejestrze COLOR – tak jest też tym razem. Gwałtowną zmianę możemy jednak zauważyć przy ilości podawanych uniformów. Tex to identyfikator tekstury “zwykłej”, którą będziemy widzieć na ekranie (oczywiście, ulepszoną przez shader). Tex1 to mapa wysokości tekstury zwykłej i określa głębokość tekstury – jest to jedna z rzeczy najmocniej różniących zwykły bump mapping i parallax. Tex2 to mapa normalnych. Scale to skala wypukłości, jaką chcemy uzyskać, natomiast dodatnia wartość Biasa pozwala uzyskać te przesunięcia, na których nam zależy. Kolejne parametry Ka, Kd, Ks oraz Shininess to wartości znane z implementacji Per Pixel Lighting – odpowiednio, współczynniki materiałowe dla światła otaczającego (ambient), światła rozproszonego (diffuse), światła odbijającego (specular) oraz współczynnik mocy odblasku (shininess). Kolejne parametry (AmbientCoeff, DiffuseCoeff, SpecularCoeff) to dodatkowe współczynniki liniowe dla odpowiednich rodzajów światła. Uch, trochę nudne to było – przejdźmy zatem do implementacji.


   1: float2 texUV;
   2:  
   3: float height = tex2D(Tex1, In.TexCoord).r;
   4: height = height * 2 * Scale - Scale;
   5: float3 tanEyeVec = normalize((In.TanEyeVec - 0.5) * 2);
   6:  
   7: if (Bias > 0)
   8: {
   9:     texUV = In.TexCoord + (tanEyeVec.yx * height);
  10: } 
  11: else 
  12: {
  13:     texUV = In.TexCoord;
  14: }

Na początku deklarujemy sobie dwueelementowany wektor dla współrzędnych tekstury. Następnie dochodzi do wykorzystania mocy mapy wysokości – obliczamy wysokość, jaka następuje poprzez “stekselowanie” Tex1 oraz współrzędnej tekstury jaką mamy na wejściu. Dokładniej, zależy nam na tylko jednej współrzędnej struktury jaką uzyskamy po wykonaniu działania – r, czyli trzeci parametr stanowiący głębokość. Następnie dokonujemy dalszego powiększania wysokości danego punktu na podstawie skali, jaką podaliśmy. Obliczamy też znormalizowany i odpowiednio zoperowany wektor tangensowy widoku. Jeśli jako parametr Bias podaliśmy liczbę dodatnią, uzyskujemy parallax – współrzędne wyjściowe tekstury będą zależy nie tylko od wejściowych, ale także od iloczynu obliczonego przed chwilą wektora i wysokości. To przesunięcie jest reprezentowane przez offset na rysunku poglądowym na wstępie tej notki.


   1: float3 normal = (tex2D(Tex2, texUV).rgb - 0.5) * 2;
   2: normal = normalize(normal);
   3:  
   4: float3 tanLightVec = normalize((In.TanLightVec - 0.5) * 2);
   5:  
   6: tanLightVec.x = - tanLightVec.x;

Musimy obliczyć też wektor normalny, co następuje już z użyciem wartości wektora texUV obliczonego wcześniej. Weryfikowany jest także tangens wektora światła. Dlaczego negujemy pierwszą współrzędną? Ze względu na to, iż zależy nam na kierunku do światła, a nie od. Kolejne fragmenty kodu dotyczą już obliczania wpływu światła.

   1: float diffuse = max(dot(tanLightVec, normal), 0.0) * DiffuseCoeff;
   2:  
   3: float3 tanHalf = normalize(tanLightVec + tanEyeVec);
   4: float specular = max(dot(tanHalf, normal), 0.0);
   5: specular = pow(specular, Shininess) * SpecularCoeff;
   6:  
   7: float3 ambient = Ka * AmbientCoeff;

Po kolei obliczymy teraz wszystkie podstawowe składowe światła łączące się na końcowy kolor piksela. Nie różni się to wiele od mechanizmu w per pixel – dla diffuse bierzemy większą wartość od iloczynu skalarnego tanLightVec i wektora normalnego oraz zera i mnożymy przez współczynnik liniowy. Podobnie postępujemy przy specular z tą różnicą, iż najpierw obliczamy pomocniczy wektor będący złożeniem tanLightVec i tanEyeVec. Dodatkowo, specular podnosimy do potęgi Shininess, czyli stopnia odblasku. Przy obliczaniu światła otaczającego nie potrzeba już takich wygibasów – jest to iloczyn współczynników tablicowego i liniowego dla tego rodzaju oświetlenia.


   1: float3 texColor = tex2D(Tex, texUV).rgb;
   2:  
   3: Out.Color.rgb =  texColor * (ambient + Kd * DiffuseCoeff * diffuse) + Ks * SpecularCoeff * specular;
   4: Out.Color.a = 1.0f;
   5:  
   6: return Out;

Pozostało nam już jedynie obliczenie wartości teksela na podstawie podstawowej tekstury i “nowych” współrzędnych texUV oraz złożenie ostatnio otrzymanych składników w jeden wynik, będącym wartością danego piksela. Czynimy to poprzez przemnożenie texColor i światła ambient oraz diffuse, a następnie dodania speculara.

Jeśli napisaliśmy dobrze shader i kod, który komunikuje się z językiem CG z poziomu aplikacji, powinniśmy uzyskać żądany efekt Parallax Mapping.

Cóż, dobrnęliśmy do końca. Mam nadzieję, że mimo swoich predyspozycji do tłumaczenia tego typu rzeczy, ktoś wyniósł cokolwiek z tego artykułu. Jeśli ktoś widzi jakieś błędy (a na pewno jakieś się znajdą – to już wynika bezpośrednio z praw Murphy’ego, dokładnie z pierwszego, drugiego i szyberdzieści dziabniętego), proszę koniecznie o napisanie tego w komentarzu. Zachęcam też do spojrzenia na lepszą wersję tego algorytmu oświetlenia, czyli “steepowaną” – za co ja się też (mam nadzieję) kiedyś zabiorę, choć artykułu o tym raczej nie napiszę.

Pozdrawiam i dziękuję – SceNtriC.

Ps. Przepraszam za lekką nieestetykę - zauważyłem już od dawna, że metoda Kopiego-Pasta z Notepad++ przy zawijaniu wierszy nie należy do wygodnych.

sobota, 13 grudnia 2008

Studia okiem pierwszoroczniaka

Jakoś tak się złożyło, że całkiem niedawno - pod koniec maja - miałem swój ostatni egzamin dojrzałości. Dziwnym trafem też, dostałem wyniki, bądź co bądź, swojej ciężkiej trzyrocznej pracy na liceum okraszonej wisienką na torcie czyli testem zwanym M.A.T.U.R.A. (Miesięczny Absurdalny Test Umiejętności Raczej Abstrakcyjnych). Niesamowite jest też to, że po późniejszym złożeniu podania na Politechnikę Poznańską dostałem się na pewien kierunek studiów pierwszego stopnia. I paradoksalnie jest to Informatyka na Wydziale Informatyki i Zarządzania. Dziwne - można było się po mnie spodziewać wszystkiego, tylko nie informatyki, prawda?

Cóż, w ten sposób stałem się dzielnym adeptem trudnej sztuki rozumienia, czego od nas chce ta maszyneria zwana komputerem i dlaczego znowu pieniędzy na rozbudowę. Na szczęście dla mnie, nielubiącego się ze sprzętem i elektroniką człowieka (istotnie), duży nacisk na WIiZ-ie położony jest na coś, co nazywam Nowoczesnym Podejściem do Informatyki - czyli nie będę raczej niewysokim i przestraszonym okularnikiem, którego jedynym zajęciem jest chodzenie od domu do domu i w magiczny sposób reperowanie wszystkiego związanego z tymi wrednymi maszynami, ale raczej projektantem, programistą, który wie co robić w danej dziedzinie, a w pozostałych zna podstawy. Przynajmniej taką mam nadzieję - dużo lepiej czuję się we wszelkich matematycznych i projektowych rzeczach, a kompletnie nie widzę siebie jako na przykład sieciowca. Cóż, zobaczymy... Tak czy inaczej - to właśnie z tego kierunku pochodzą zwycięzcy Imagine Cup i innych światowych konkursów.

Sama organizacja uczelni jest dosyć porządna - w końcu drugi człon nazwy wydziału do czegoś zobowiązuje. Ciekawe jest podejście do indeksów - jeszcze ich nie dostaliśmy. I nie dostaniemy na razie, dopóki nie zacznie się S.E.S.J.A. (System Eliminacji Studentów Jest Aktywny) i zostaną nam ponownie odebrane po zakończeniu trudnego okresu. Na roku jest nas około stu czterdziestu - dosyć mało, w porównaniu chociażby z Budownictwem, gdzie jest około czterystu lub coś koło tego, jak mi wiadomo. Co do sal, mamy praktycznie wszystko w jednym miejscu - Centrum Wykładowe prezentuje się całkiem przyzwoicie i ma bardzo wygodne ławeczki w salach wykładowych. Tylko poduszek brak, więc niektórzy posunęli się do zabrania własnych.

A teraz o tym, co przeciętnego studenta interesuje - czyli jakich rzeczy będzie się uczył. Przynajmniej na początku.

Narzędzia Informatyki - przedmiot związany z - jak sama nazwa mówi - programami narzędziowymi stosowanymi w informatyce. Excel, Matlab, LaTeX i te sprawy. Trochę tu też jest pewnego rodzaju rzeczy związanych z typografią, czyli sztuką ładnego redagowania stron. Przedmiot stworzony dla DTP-owców, ludzi poszukujących wielkiego i certyfikowanego kalkulatora oraz szukaczy dziur w całym Excelu. Na szczęście, przez 75% czasu trwania semestru (pół semestru wykłady, cały semestr laboratoria). Do tego, ostatnio w wyniku awarii serwera, nie dość, iż nie było co robić (gdyż Matlab potrzebuje informacji o certyfikacie z Internetu), to jeszcze nastąpiła ewakuacja wszystkich ludzi Centrum. Oczywiście, nikt nie panikował (w końcu to w większości informatycy), wszyscy wzięli kurtki z szatni i stanęli koło wejścia, rozglądając się, gdzie znajduje się źródło ewakuacji. Nie znaleziono, dlatego zajęliśmy się grą w Worms.

Wprowadzenie do Informatyki - przedmiot ogólnorozwojowy i chyba najciekawszy w semestrze. Wykłady prowadzone przez dziekana prowadzą poprzez wszystkie oblicza informatyki, co jest potem dogłębnie sprawdzane na laboratoriach. I to dosłownie - przyjęło się, że zamiast dużego kolokwium na koniec, istnieją 20-minutowe wejściówki co lekcje. Nawet to lepsze rozwiązanie - mniej stresu, mniej materiału. A tłumaczenie wszystkiego bardzo ludzkie i przystępne - jeśli jakość przedmiotu mierzy się ilością osób na wykładzie mimo prezentacji dostępnych w Internecie, to WDI jest pierwsze - przed nią są tylko wykłady, podczas których następuje żmudne pisanie na tablicy i mazanie, czyli Analiza oraz Algebra. Przedmiot egzaminacyjny.

Podstawy Programowania - czyli programowanie w języku Delphi Pascal okraszonego laboratoriami z programami na zaliczenie oraz pewną Żółtą Biblią. Normalnie mrok - trzy programy, możliwość niechodzenia po oddaniu aplikacji przed terminem. Jedynie to Delphi - coś już wcześniej o tym pisałem. Kolejny przedmiot egzaminacyjny.

Matematyka Dyskretna - przedmiot matematyczny, poświęcony obiektom dyskretnym - czyli operujących na zbiorze liczb całkowitych dodatnich. Wszelkiego rodzaju umiejętności kombinatoryczne, rekurencyjne, indukcyjne, teoriomnogościowe i inne są tu mile widziane. Podczas testów następuje jedno z najciekawszych rozwiązań liczenia punktów - jeśli ilość poprawnych odpowiedzi w pytaniu wynosi n, to każda dobrze zakreślona odpowiedź daje +1/n punktów, a kolejny zły wybór to -1/n. Przy czym ilość uzyskanych punktów za pytanie nie spadnie poniżej zera.

Analiza Matematyczna - jeden z ważniejszych przedmiotów, do tego bez prezentacji w Internecie (oficjalnych). Zagadnienia granic, szeregów, pochodnych, całek przedstawione w pozytywnego formie przez doktora będącego autorem największej ilości cytatów znanych na całej uczelni. Ćwiczenia również są prowadzone w przystępnej formie. Chyba najbliższy mnie matematyczny przedmiot, jeśli chodzi o sympatię do tematyki. Przedmiot naturalnie egzaminacyjny.

Algebra Liniowa - raz na dwa tygodnie wypada tydzień, kiedy trzeba zająć się piekielnymi rzeczami w rodzaju algebry abstrakcyjnej. A wcześniej było tak przyjemnie - liczby urojone, macierze, układy równań liniowych. Cóż - trzeba to przeboleć.

Logika Obliczeniowa - mamy trzy przedmioty matematyczne i trzy informatyczne. Pozostaje siódmy przedmiot, który można rzec, iż znajduje się na samym środku. Żelazna logika połączona z reprezentacją komputerową i ostrzeżeniem, że później będziemy programować w Prologu. Także zajmuje 50% czasu semestru (pół semestru wykłady, laboratoria co dwa tygodnie).

Oczywiście, oprócz tego dochodzi Wychowanie Fizyczne trwające dwa semestry (dla tenisistów stołowych mała uwaga - przygotować się na ciężkie rozgrzewki) oraz Język Obcy, który odbywamy przez cztery półrocza.

Co jeszcze? Każdy student może założyć konto studenckie wraz z którym dostaje pocztę elektroniczną, 100 MB na serwerze oraz dostęp do MSDNAA, a co za tym idzie - możliwość ściągnięcia sobie legalnie Okienek, Visual Studio i innych Office'ów. Oczywiście, w ograniczonej formie, ale nie będę się tutaj wypowiadał, dopóki sam nie spróbuję.

Czy polecać ten kierunek? Trudno dokładnie powiedzieć po ledwie dwóch i pół miesiąca studiowania, aczkolwiek chyba jednak mogę polecić. Przynajmniej jeśli mieszkasz w okolicach Wielkopolski, bo w tej części kraju raczej nie znajdziesz nic lepszego związanego z kształceniem się na informatyka. Poza tym, na terenie uczelni bardzo aktywne działają koła naukowe, m.in. Booboo i Grupa .NET - ludzie, którzy mieli z nimi styczność wypowiadali się bardzo pozytywne.

Wszelkie komentarze, jak zawsze, mile widziane. Przy okazji pozdrawiam wszystkich, których miałem w domyśle - czyli wykładowców, ćwiczeniowców i studentów.

I obiecuję, że następna notka będzie już o właściwszych rzeczach, czyli o ostatnio przywołanych moich bojach z shaderami.

Pozdrawiam i dziękuję - SceNtriC.

środa, 10 grudnia 2008

Wrappowanie i "SSGUI"

Kolejna walka z kodem za mną - tak naprawdę opisane niżej "ficzerki" są dziełem już minionego tygodnia, a czekały na objawienie do momentu napisania kolejnej cechy frameworka. Zdaje się jednak, że to, na co czekały, napisze się w czasie dużo dużo dużo dłuższym niż sądziłem. Szlag mnie też trafia, że nie rozumiem do końca tego, co sam mam w shaderach - będzie chyba trzeba się za to wziąć.

Tak czy inaczej - dzisiaj kolejne dwa aspekty Yesty. Tym razem krótko.

Wrapper OpenGL

Wrapper to najogólniej mówiąc kawałek kodu odpowiadający za przepisanie "starych" funkcji w nowy sposób i w zasadzie polega na zrobieniu odpowiednich makr define. Czego dotyczył ten punkt frameworka? Otóż, zniesienia konieczności pisania przedrostków gl, glu i GL_ przed nazwami funkcji i stałych. Fakt, że troszkę to kodu zajmuje - ponad tysiąc. Na razie przepisałem tylko parędziesiąt funkcji, które najczęściej używam w plikach main.cpp - aczkolwiek coś mi się wydaje, że dla zaspokojenia własnej satysfakcji będę musiał przepisać całą resztę. A po co to w ogóle? Wzorem frameworków oraz (zwłaszcza) silników, postanowiłem, żeby prawie nie było widać, że piszemy w OpenGL - taki mały myk i sposób na zabicie czasu. Niekoniecznie efektywne, ale przynajmniej czuję, że Yesta to jakaś wyodrębniona całość, osobna biblioteka. Choć oczywiście co to za framework będący zlepkiem paru tutoriali... W tym miejscu muszę przestrzec młodych adeptów pisania wyrobów silnikopodobnych - nie róbcie tego co ja, czyli nie piszcie kodu, którego nie rozumiecie. Nie chodzi tutaj nawet o brak profesjonalizmu, choć to także. Po prostu po pewnym czasie, gdy minie ekscytacja z dodania kolejnego efektu, zaczniecie się zastanawiać "a jak ja to zrobiłem?", "jak to działa?", "nie umiem tego nawet wytłumaczyć swojej dziewczynie/przyjaciółce/koleżance/koledze/psu/kotu". Będzie mnie zatem czekała trudna próba zrozumienia pewnych aspektów... Wybaczcie.

Szczątkowy System GUI (SSGUI)

Za to teraz coś, co napisałem praktycznie samemu - jedynie w przypadku fontów posiłkowałem się przykładem z UGP. Co potrafi system GUI? Generalnie rzecz biorąc - niewiele. Może wyświetlać tekst, a także obsługiwać twory zwane boxami, czyli po prostu czworokąty z nałożoną teksturą, które można kliknąć lub na nie najechać i wykonują wcześniej zaplanowaną akcję. Do tego właśnie były mi potrzebne dynamicznie alokowane tablice wskaźników na funkcje z poprzedniej notki. Działa to tak, że na początku podajemy ilość obsługiwanych boxów i czcionek. Następnie każdy taki pojedyczny element musimy zainicjalizować i przypisać do niej pełno różnych dziwnych atrybutów jak widoczność (np. po wciśnięciu [H] pokaże się box z menu, a po kolejnym naciśnięciu - zniknie), szerokość, wysokość, pozycja na ekranie, przypisanie wskaźników na funkcje - i voila. Przy pierwszym teście GUI oczywiście nie wyświetliło się prawidłowo. Właściwie, to nie wyświetliło się w ogóle - swoją obecność potwierdziły prawa Murphy'ego. Oczywiście, problem leżał w odpowiednim ustaleniu macierzy projekcji, a właściwie w przełączeniu się z 3D na 2D i powrocie do trójwymiaru. Moment zawahania trwał około doby, zanim nie wpadłem na genialny pomysł, aby ściągać ze stosu również poprzednią macierz modelowania i ją aktywować. Prawo mówiące o tym, że najciemniej pod latarnią znalazło swoje zastosowanie... W każdym razie, z metodą prób i błędów udało mi się doprowadzić kod do końca.

W następnej notce postaram się napisać coś tym razem niezwiązanego z samym programowaniem jako takim. Ktoś ma ochotę poczytać o tym, jak przeciętny student I roku widzi otaczające go życie uczelniane? Bodajże Noir zgłaszał tu taką "chętkę". W takim razie postaram się coś wykombinować, a potem zająć się rzeczonymi shaderem i o tym, jak powinien działać efekt, który miał dziś być opisany, a nie jest.

Jeszcze jedno - odkryłem niedawno potęgę zapisywania sobie notek "co mam dzisiaj zrobić" w kalendarzu "książeczkowym". Nigdy tego nie robiłem, ale to świetna sprawa - pozwala się skupić na tym, co naprawdę chcemy i musimy zrobić. Także pomysły, które przychodzą do głowy około drugiej w nocy i nie pozwalają zasnąć tam trafiają. A potem powstają dziwne wpisy w stylu "dać glsl i check proj". Uroki życia programisty...

Pozdrawiam i dziękuję - SceNtriC.

czwartek, 4 grudnia 2008

Dynamiczne wskaźniki na funkcje

Czasami bywa tak, że trzeba korzystać z pewnych technik, które są wprawdzie niezbyt wygodne dla programisty, ale otwierają przed nim całkiem sporo możliwości, a przy tym uczą. Uczą, jak:

- ...nie dostać szybko wścieklizny
- ...panować nad sobą
- ...[no, dobrze] wykorzystywać pewne zawiłości języka

Generalnie mam na myśli mechanizm zwany wskaźnikiem na funkcje. Być może niektórzy pamiętają, że użyłem tego w klasie CCoreAPI (a inspiracją do tego był mechanizm działania GLUT-a). Ostatnio spotkałem się jednak z nieco bardziej zawiłą sytuacją związaną z tym elementem języka.

Jak być może większość wie, deklaracja takiego tworu wygląda tak:

[typ_zwracany] (*[nazwa_funkcji])([lista_argumentów]);

Czyli na przykład:

void (*PtrToFunc)();

Przypisanie do niego funkcji też jest całkiem proste (choć wygląda skomplikowanie):

void Attach (void (*Param)())
{
PtrToFunc = &(*Param);
}

A następnie wywołanie:

(*PtrToFunc)();

Schody zaczynają się w momencie, kiedy potrzebujemy tablicę takich wskaźników. Oczywiście, można to zrobić w ten sposób:

void (*PtrsToFuncs[10])();

Wszystko dobrze, składnia jest fajna, ładna, kochana i inne nananana, ale co należy zrobić, kiedy musimy utworzyć taką tablicę w sposób dynamiczny za pomocą operatorów new i delete? Cóż, muszę przyznać, iż troszkę się z tym pomęczyłem (dokładnie 15 minut), aż (po inspirującym temacie z Warsztatu) wpadłem na okrężną drogę. Ale skuteczną. Choć przyznaję, iż z początku chciałem upchać "new" gdzieś pomiędzy... Ech, nieważne...

Tworzymy sobie obiekt takiej struktury:

struct CPtrs
{
void (*PtrToFunc)();

void Attach (void (*Param)())
{
PtrToFunc = &(*Param);
}

void FuncExecute()
{
(*PtrToFunc);
}
};

Oczywiście, możemy mieć więcej wskaźników na funkcje w takiej strukturze - im więcej, tym lepsze rozwiązanie to się wydaje. Tym razem, nie musimy się już zastanawiać, gdzie wpakować operator new - wystarczy utworzyć tablicę obiektów typu CPtrs. A to przecież nie jest trudne:

CPtrs* Ptrs = new CPtrs[10];

I wtedy odwołujemy się w ten przykładowy sposób:

Ptrs[4].Attach(BardzoDlugaNazwaFunkcjiAbyByloAbsurdalnieZaUwageDziekuje);
Ptrs[4].FuncExecute();

Naturalnie, na koniec zwalniamy pamięć w typowy sposób:

delete[] Ptrs;
Ptrs = NULL;

To tyle. Niedużo, ale w sumie problem jest o tyle ciekawy, iż warto było się nim podzielić. Choć inna sprawa, kto regularnie używa wskaźników na funkcje...

Pozdrawiam i dziękuję - SceNtriC.