15 lipca 2013

OpenGL + Shadery

W tej części kursu dotyczącego malowania w OpenGL , dodamy nieco życia do naszej nudnej sceny a także zapoznamy się z programowalnym potokiem renderowania. Zatem do dzieła. 

Shadery!

Shader to niewielki program uruchamiany na karcie graficznej. W naszym przypadku (staramy się trzymać kompatybilność z OpenGL ES 2.0) rozróżniamy dwa rodzaje shaderów: Vertex i Fragment. 
Vertex shader jest wykonywany dla każdego wierzchołka przekazywanej geometrii. Odpowiada on za liczenie pozycji wierzchołka, najczęściej na podstawie macierzy (projekcji, modelu lub widoku) oraz jego początkowej pozycji. Może on wykonać także dodatkowe operacje, których wyniki mogą być używane w późniejszych fazach potoku renderowania. Zadaniem fragment shadera zaś, jest obliczenie końcowego koloru piksela. To w nim implementowane jest teksturowanie lub oświetlenie per-pixel
Do pisania shaderów będziemy używać języka GLSL. Jest on bardzo podobny do C, pomijając kilka różnic. Przede wszystkim mamy do dyspozycji nowe typy: wektory i macierze wraz z pełną paletą wbudowanych funkcji. Dodatkowo, dodanych zostało kilka modyfikatorów, które zostaną omówione w dalszej części kursu.
Z poziomu GLSL nie mamy dostępu do wskaźników. Pętle muszą być rozwijalne na etapie kompilacji a długość programu jest ograniczona dostępnymi zasobami sprzętowymi. Widać zatem, że mamy do czynienia z językiem bardziej restrykcyjnym i przeznaczonym dla wyspecjalizowanych celów.

Modyfikatory

Nowe typy modyfikatorów to uniform, attribute oraz varying. Uniform jest to globalna (dla pojedynczego shadera) stała, której wartość została przekazana z zewnątrz. Dzięki tego typu konstrukcji, możemy przekazywać do shadera wcześniej wyliczone wartości dotyczące położenia kamery czy transformacji widoku. Atrybut (attribute) bezpośrednio odnosi się do wierzchołka. Jest to przekazywana dla każdego wierzchołka właściwość, np. położenie, kolor, normalna czy pozycja tekstury. Na samym końcu mamy varying. Tym modyfikatorem oznaczamy zmienne, które chcemy przekazać z Vertex shadera do Fragment shadera. 

Hello world 

Czas więc osobiście zapoznać się z naszymi shaderami. Na pierwszy ogień pójdzie Vertex shader:
attribute vec4 attrPosition;
attribute vec4 attrColor;
 
uniform mat4 unfModel;
uniform mat4 unfProjection;
 
varying vec3 vygColor;
 
void main()
{
 gl_Position = unfProjection * unfModel * attrPosition;
 vygColor = attrColor.rgb;
}
Jak więc widać, jest to niewielki program, wykonujący podstawowe opracje. Pierwsza rzaczą, która z pewnością rzuca się w oczy to obecność funkcji main. Jej przeznaczenie jest oczywiste - punkt wejściowy shadera. Kolejnym interesującym zjawiskiem jest gl_Position. Jak widać, jest to zmienna. Nie jest jednak nigdzie zadeklarowana a mimo wszystko jest używana. GLSL posiada zestaw wbudowanych zmiennych, do których można zapisywać dane będące użyte w kolejnych etapach renderowania. Jedną z nich jest właśnie gl_Position. Mamy do niej dostęp tylko z poziomu Vertex shadera.  
Uniformami są tutaj macierze 4x4 (mat4unfModel raz unfProjection. Wartości ich zostały wyliczone w programie głównym, zaś tu zostały tylko przekazane i wykorzystane do wyliczenia końcowej pozycji wierzchołka. Warto tutaj zaznaczyć, że nie możemy tworzyć uniformów zaczynających się prefiksem gl_. 
Jako, że na nasz wierzchołek składa się tylko pozycja i kolor, takie też atrybuty przekazujemy do shadera (attrPosition oraz attrColor). Tutaj jednak pojawia się problem. Dostęp do atrybutów mamy tylko z poziomu  vertex shadera, zaś w nim nie możemy pomalować piksela, przekazanym w atrybucie kolorem. Musimy zatem przekazać kolor do fragment shadera, w którym to zostanie on odpowiednio wykorzystany. Z pomocą przychodzi tu zmienna vygColor, która została oznaczona wcześniej wspomnianym modyfikatorem varying
Na sam koniec zostało wyliczenie końcowej pozycji wierzchołka na ekranie. Jak widać jest to iloczyn macierzy projekcji, modelu (macierz będąca wynikiem wszystkich podstawowych transformacji na obiekcie) oraz początkowego położenia wierzchołka. Jak pamiętamy z podstawówki, lub co ambitniejszych przedszkoli, mnożenie macierzy nie jest przemienne, więc musimy zachować prezentowaną tu kolejność. 
Fragment shader jest znacznie krótszy:
varying vec3 vygColor;
 
void main()
{
 gl_FragColor = vec4(vygColor.r, vygColor.g, vygColor.b, 1.0);
}
Mamy tu tylko przekazany z poprzedniego shadera kolor wierzchołka oraz przypisanie go do wyjściowej zmiennej gl_FragColor.

Tymczasem w C++...

Mając gotowe shadery, możemy zająć się ich kompilacją. Ciekawostką jest to, że będziemy to robić z poziomu naszego programu głównego, w trakcie inizjalizacji. Wykorzystamy do tego niewielką funkcję: 
bool compileShader(GLuint *pOutShader, const char *szSource, ShaderType type)
{
 *pOutShader = glCreateShader(type);
 
 glShaderSource(*pOutShader, 1, &szSource, NULL);
 glCompileShader(*pOutShader);
 
 GLint iCompileStatus = GL_FALSE;
 glGetShaderiv(*pOutShader, GL_COMPILE_STATUS, &iCompileStatus);
 return iCompileStatus == GL_TRUE; 
}
Skupimy się tu przede wszystkim na API OpenGL. Na początek tworzymy obiekt shadera, za pomocą funkcji glCreateShaderPrzyjmuje ona typ shadera: GL_VERTEX_SHADER lub GL_FRAGMENT_SHADER. Następnie kojarzymy dany obiekt z konkretnym kodem źródłowym za pomocą glShaderSource. Funkcja ta przyjmuje tablicę cstringów, której rozmiar określa drugi parametr (w naszym przypadku 1) . Jeśli każdy przekazany tekst kończy się zerem (czyli jest standardowym cstringiem), możemy ustawić ostatni parametr na NULL.W przeciwnym razie, musimy przekazać długość każdego z podczepianych źródeł. Jesteśmy zatem gotowi do kompilacji, która następuje po wywołaniu glCompileShader. Status kompilacji możemy pobrać korzystając z funkcji glGetShaderiv do której przekazać trzeba obiekt właśnie kompilowanego shadera (*pOutShader), oraz wskaźnik na zmienną która przechowa sam status (iCompileStatus GL_TRUE dla sukcesu oraz GL_FALSE dla porażki).
Mając skompilowane shadery, możemy je zlinkować w jeden program:
bool linkProgram(GLuint *pOutProgram, GLuint uVertexShader, GLuint uFragmentShader)
{
 *pOutProgram = glCreateProgram();
 glAttachShader(*pOutProgram, uVertexShader);
 glAttachShader(*pOutProgram, uFragmentShader);
 
 glLinkProgram(*pOutProgram);
 
 GLint iLinkStatus = GL_FALSE;
 glGetProgramiv(*pOutProgram, GL_LINK_STATUS, &iLinkStatus);
 
 return iLinkStatus == GL_TRUE;
}

Po stworzeniu obiektu programu za pomocą glCreateProgram dołączamy za pomocą glAttachShader wcześniej skompilowane shadery. Teraz zostaje nam już tylko zlinkować program (glLinkProgram) oraz sprawdzić za pomocą glGetProgramiv wynik owej operacji. 
Jesteśmy już o krok od namalowania naszego upragnionego kwadracika. Zostało nam jeszcze tylko pobranie informacji o położeniu atrybutów oraz uniformów. Zrobi to dla nas poniższy kod:
glBindAttribLocation(uProgram, 0, "attrPosition");
glBindAttribLocation(uProgram, 1, "attrColor");
 
iProjectionUniform = glGetUniformLocation(uProgram, "unfProjection");
iModelUniform = glGetUniformLocation(uProgram, "unfModel");
Warto zwrócić uwagę, że atrybutowi przypisujemy ID po jego nazwie w shaderze (glBindAttribLocation), zaś ID uniformu jest pobierane (glGetUniformLocation), także na podstawie nazwy, którą nadaliśmy mu w kodzie shadera. Tym akcentem kończymy incjalizację aplikacji. Możemy zająć się pętlą główną.

Malowanie

W pętli głównej możemy zobaczyć taki oto kawałek kodu:
glUseProgram(uProgram);
glUniformMatrix4fv(iProjectionUniform, 1, GL_FALSE, (float*)&(projection[0]));
glUniformMatrix4fv(iModelUniform, 1, GL_FALSE, (float*)&(model[0]));
 
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
 
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), vertices);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (float*)(vertices) + 4);
 
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, faces);
 
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
 
glUseProgram(0);

Pierwszą funkcją, na którą powinniśmy zwrócić uwagę to glUseProgram. Jako parametr podajemy jej obiekt wcześniej zlinkowanego programu. Od tej chwili wszystkie operacje, dotyczące shadera, będą dokonywały się właśnie na owym programie. 
Mając ustawiony program, możemy przekazać shaderowi macierze projekcji oraz modelu. Robimy to za pomocą funkcji glUniformMatrix4fv. Warto zaznaczyć, że jest to tylko jeden z wielu członków rodziny funkcji glUniform*. W zależności od typu przyjmowanych parametrów, przesyłają one do shadera różne typy danych. W naszym przypadku, dla uniformu o ID iProjectionUniform, przekazujemy jedną (parametr drugi określa ilość), nietransponowaną (GL_FALSE) macierz 4x4 - projection.  Analogicznie wygląda to dla macierzy modelu. 
Następnie informujemy OpenGL z jakich atrybutów wierzchołka będziemy korzystać (glEnableVertexAttribArray). Będzie to oczywiście pozycja (0) oraz kolor (1), których odpowiednie ID zostały przypisane za pomocą glBindAttribLocation.
Przyjrzyjmy się jeszcze, jak wygląda nasz wierzchołek:
struct ColorRGB
{
 float r, g, b;
};
 
struct Vertex
{
 vec4 position;
 ColorRGB color;
};
Jak widać składa się on z pozycji, która jest czteroelementowym wektorem oraz koloru, który jest wyrażonymi składowymi R, G i B. Podstawowy typ danych to float. Informacje te, są niezbędne w następnym kroku:
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), vertices);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (float*)(vertices) + 4);

Przekazujemy tutaj informacje o dokładnym położeniu każdego atrybutu w naszej strukturze wierzchołka. Idąc od lewej pierwszy parametr to ID atrybutu. Drugi określa liczbę komponentów, z jakich składa się atrybut. Pozycja jest przedstawiana za pomocą współrzędnych X, Y, Z oraz W (4), zaś kolor ma składowe R, G, B (3). Kolejny parametr to typ danych - w naszym przypadku to float. Przekazywane dane nie są znormalizowane, więc czwarty parametr ustawiany jest na wartość GL_FALSE. Jako, że wierzchołki wraz z atrybutami będą składowane w tablicy, musimy określić odległość (w bajtach) od odpowiadających sobie atrybutów - będzie to po prostu rozmiar struktury wierzchołka. Ostatni parametr to wskaźnik na pierwszy atrybut. W przypadku pozycji, będzie to po prostu adres pierwszego elementu tablicy, zaś pozycja koloru jest przesunięta o cztery zmienne typu float, ponieważ znajduje się za pozycją. Myślę, że na poniższym rysunku obok jest to przedstawione w bardziej czytelny sposób. Zostało zatem zawołanie funkcji glDrawElements, ale nie różni się ona niczym w porównaniu do poprzedniej części kursu. 
Został nam do omówienia jeszcze jeden aspekt - samo budowanie macierzy widoku i modelu. Wykorzystałem do tego bibliotekę GLM. Jest ona bardzo prosta i przyjemna w użyciu, także myślę, że nie istnieje tutaj potrzeba dokładnego omawiania kilku operacji które zostały wykonane za jej pomocą. 
Źródła można pobrać tu

12 lipca 2013

Macalna sciana

Dziś czas na kolejną, małą prezentację. Jak zwykle będzie ona związana z technologiami około Kinectowymi, które wypełniają aktualnie cały mój czas. Tym razem jednak, nie będzie ona dotyczyła typowego rozwiązania, opartego na gestach i machaniu rękami. Będzie to coś bardziej tradycyjnego - powierzchnia dotykowa, a dokładniej rzecz ujmując: zwykła ściana. 
Jak zatem zmienić ścianę w obiekt reagujący na macanie? Wystarczy w zasadzie sensor głębi (zalecany jest tu jednak Asus Xtion Pro) oraz odrobinę softu. Samo stworzenie tej wersji oprogramowania zajęło miesiąc i jest to jego dość wczesna wersja, co doskonale widać na poniższym filmie. Po zapewnieniu odpowiednich warunków, możemy delektować się jednak nieograniczoną radością z zabijania świń wypuszczanymi z procy ptakami, na każdej ścianie przed którą możemy postawić Kinecta. 


22 maja 2013

Nowy Kinect

Jest! W końcu! Po trzech latach od prezentacji pierwszej generacji czujnika głębi sygnowanego logiem Microsoftu, wczoraj mieliśmy okazję zobaczyć jego następcę. Podczas trwania konferencji nie usłyszeliśmy o nim zbyt wiele, pomijając niewielki fragment podczas którego podziwialiśmy czułość na gesty oraz możliwości rozpoznawania mowy. O ile ta ostatnia funkcjonalność nie robi już dziś dużego wrażenia (mamy przecież Google Now czy Siri od Apple) to rozpoznawanie ruchów ręki czy dłoni, ciągle wydaje się możliwością zakrawającą of Science Fiction. 
Dziś w sieci ukazał się krótki film, na którym mogliśmy trochę bliżej poznać nowy kontroler do nadchodzącej konsoli Xbox. Zostało ujawnionych znacznie więcej szczegółów dotyczących działania urządzenia. Przede wszystkim została zmieniona technologia "widzenia" głębi. W aktualnej generacji wykorzystywany jest promiennik podczerwieni rzucający siatkę punktów, która po odpowiedniej analizie daje nam stosunkowo dokładne informacje o położeniu w przestrzeni trójwymiarowej. Jak już wspominałem we wcześniejszej notce zostało to opracowane przez PrimeSense. Tym razem Microsoft postanowił uniezależnić się od zewnętrznych instytucji i... przejął dwie mniejsze firmy zajmujące się tworzeniem sensorów 3D. W tym wypadku postawiono na technologię o nazwie Time of Light. W wielkim skrócie i uproszczeniu, mamy tutaj do czynienia z kamerą która potrafi mierzyć prędkość odbitego światła od poszczególnych obiektów sceny. Brzmi niesamowicie, ale jest to stosunkowo stara metoda wykorzystywana w dalmierzach laserowych. Tutaj jednak równocześnie dokonujemy pomiaru dla całej macierzy punktów. Warto zaznaczyć, że pierwsze tego typu kamery pojawiły na rynku się już w 2006 roku. Gdzie jest zatem postęp, którym tak bardzo chwali się Microsoft? Rozdzielczość. Nie ujawniono jeszcze dokładnej ilości odczytywanych pikseli, jednak już na pierwszej prezentacji można zauważyć, że poczyniono w tym zakresie znaczny progres. Przedstawiciel korporacji mówił o rozdzielczości HD, jednak myślę, że podobnie jak w pierwszej wersji sensora, siatka punktów została reskalowana by dopasować się do kamery RGB. Dodatkowo dzięki tej technologii kamera znacznie lepiej radzi sobie z obiektami położonymi blisko - nie ma już efektu "oślepienia" czujnika podczerwieni. 
Rendering głębi
Z interesujących rzeczy, to w zasadzie tyle. Pewnie wiele osób zauważyło, że zostało zaprezentowanych jeszcze kilka innych możliwości: badanie obciążenia poszczególnych kości szkieletu czy mierzenie siły uderzenia. Przyznam jednak, że (o ile nie dostaliśmy tego w zestawie z API) jesteśmy w stanie to osiągnąć korzystając z obecnej generacji sprzętu. Jedynym usprawnieniem na jakie zwróciłem uwagę to zwiększona ilość kości, dla wykrywanego szkieletu. Dalej jendak nie ma tu rewolucji - ręka zyskała drugi palec, zaś barki dodatkowe jointy. Obsługa gestów także nie została specjalnie usprawniona. "Pinch to zoom" w wersji dla XBoksa jest wykonalny (dość łatwo) już dziś, zaś "Swipe left / right" działa tak samo, jak dla aktualnej generacji (swoją drogą, reimplementując obsługę tego gestu można uzyskać znacznie lepsze wyniki, czego prototyp pokazałem tu). Zaprezentowane na koniec wykrywanie tętna gracza wydaje mi się mocno naciągane, jednak będę musiał dokładniej poznać temat by wyrobić sobie o nim zdanie. Czy to koniec narzekania? Jeszcze nie. Rzeczą która najbardziej mnie zawiodła, to ciągle słaba dokładność śledzenia szkieletu. Nie uczyniono tutaj zbyt dużego postępu, zaś demko pokazywane przez PrimeSense ponad dwa lata temu, prezentuje się w tej materii znacznie lepiej (swoja drogą nigdy nie udało mi się uzyskać tak rewelacyjnych wyników jak na owym filmiku). 
Czy jest zatem na co czekać? Moim zdaniem tak. Myślę, że do końca roku Microsoft dopracuje API oraz sterownik i część przedstawionych tu bolączek zostanie zlikwidowana. Liczę także, że konkurencja pokaże nowe wersje swoich rozwiązań. O ile PrimeSense dość mocno interesuje się rynkiem mobilnym (kto nie chciałby Kinecta w telefonie?) to zwiększenie dokładności stacjonarnych wersji urządzeń, też powinno leżeć w ich interesie. Zagadką pozostaje także dostępność nowego Kinecta w wersji dla PC. Na kompatybilność z OpenNI, chyba nie mamy co liczyć. 

24 kwietnia 2013

COTW - Pola i flagi bitowe w C++

Każdy pewnie korzystał z flag bitowych w C/C++. Wygodne i zgrabne narzędzie do przekazywania parametrów, które szczególnie ukochali sobie programiści Microsoftu projektujący WinApi.
Czasem zdarza się jednak, że przekazane kombinacje mogą być bardzo finezyjne, a parametry dla instrukcji warunkowych niekoniecznie muszą mieścić się na jednym ekranie (nazwy flag często są dość długie). W takiej sytuacji idealnym wyjściem byłoby przypisanie wyniku sumy bitowej każdej flagi do osobnej zmiennej typu bool i na niej wykonywać operacje warunkowe. Nie ma jednak nic za darmo. W najbardziej pesymistycznym przypadku będziemy musieli stworzyć 32 lub 64 takie zmienne i dla każdej z nich dokonać przypisania. Narzut jest dość spory. Można to rozwiązać lepiej? Można. Z pomocą przychodzą pola bitowe. 


#include 
 
struct Data
{
 bool a : 1;
 bool b : 1;
 bool c : 1;
};
 
enum Flags
{
 FLAG_1 = 0x1,
 FLAG_2 = 0x2,
 FLAG_3 = 0x4
};
 
int main()
{
 unsigned int uFlags = FLAG_1 | FLAG_3;
 Data f;
 
 f = *((Data*)&uFlags);
 
 printf("%s\n", f.a == true ? "a == true" : "a == false");
 printf("%s\n", f.b == true ? "b == true" : "b == false");
 printf("%s\n", f.c == true ? "c == true" : "c == false");
 
 return 0;
}

Struktura Data przechowuje pola bitowe. Każde pole zajmuje jeden bit. Ich kolejność jest zgodna z flagami enuma Flags. Dzięki przypisaniu zmiennej przechowującej flagi (uFlags) do owej struktury typu Data (f = *((Data*)&uFlags);), możemy w szybki sposób zacząć pracować z flagami bitowymi, tak samo jak ze zwykłymi zmiennymi typu logicznego. 


28 lutego 2013

Kinect dev diary - part 2

Tak jak obiecałem na facebooku, dziś kolejne dev diary, tym razem Kinectowe. Od pewnego czasu pracujemy nad rozwiązaniami dotyczącymi interfejsów użytkownika, dla urządzeń użytku publicznego. Podejść było bardzo dużo. Głównie chodziło o intuicyjność oraz stabilność śledzenia. Niżej możemy zobaczyć końcowy efekt. Jest jeszcze kilka rzeczy do poprawienia, jednak całość zachowuje się już dość stabilnie.
Warto zaznaczyć, że warstwa prezentacji jest stworzona we Flashu. Grafiki są jak najbardziej testowe - chodzi tylko o pogląd na ogólne rozwiązanie. Gest machnięcia w prawo (swipe right) został zaimplementowany przez nas. NITE udostępnia jego rozpoznawanie, jednak działało to dość losowo.  
Klienta Flashowego stworzył Michał Prażuch. 


25 lutego 2013

C++ COTW - Placement new jako memcpy


#include <stdio.h>
#include <new>
 
struct Vec3d
{
    float x,y,z;
 
    Vec3d()
    {
        x = y = z = 0.0f;
    }
 
    Vec3d(float x, float y, float z)
    {
        this->x = x;
        this->y = y;
        this->z = z;
    }
 
};
 
int main()
{
 
    Vec3d v1(1.0f, 2.0f, 3.0);
    Vec3d v2;
 
    v2 = (*new(&v2)Vec3d(v1));
 
    printf("%f, %f, %f\n", v2.x, v2.y, v2.z);
}

Kolejna część z serii Coś Of The Week, dla C++. Tym razem został pokazany dość oryginalny sposób kopiowania danych, beż użycia bibliotecznej funkcji memcpy. Wykorzystać możemy do tego operator placement new i konstruktor kopiujący obiektu. Czy jest to wydajniejsze? Mówiąc szczerze, pomiarów nie dokonywałem, ale na pierwszy rzut oka, raczej nie. Poza tym, kod jest bardziej zagmatwany. Przydaje się to jednak wtedy, gdy nie mamy dostępu lub nie chcemy załączać nagłówków gdzie znajdują się funkcje operujące na pamięci. 
.

21 lutego 2013

Obsługa bramki SMS Orange w.. C++

Ostatnio postanowiłem się pobawić nieco socketami i obsługą protokołu HTTP. Przeglądarki własnej pisać nie będę (przynajmniej nie w przeciągu najbliższych kilku dni), chciałem jednak nieco podglądnąć jak wygląda komunikacja serwerem a klientem z wykorzystaniem HTTP. Tak oto powstał ten niewielki programik. Możemy za jego pomocą wysłać SMSa na dowolny numer w Orange (jeśli jego właściciel aktywował możliwość otrzymywania wiadomości z internetu). Nie jest tu potrzebna znajomość obsługi web-service'ów czy innych baz danych. Po prostu, ściągamy stronkę gdzie znajduje się obrazek CAPTCHA, wpisujemy jego zawartość i wysyłamy odpowiednio spreparowane zapytanie metodą POST. Nie odbyło się bez zabawy w "reverse engineering" - na początek trzeba podglądnąć jakie informacje wysyła przeglądarka internetowa. 
Program miał być miał i lekki, dlatego GUI pisane w czystym WinApi (o zgrozo!). Cała część "internetowa" znajduje się w plikach HTTPDriver oraz HTMLParser. Cały chaos związany z interfejsem użytkownika, znajduje się w pliku main.cpp.
Oczywiście brakuje kilka podstawowych ficzerów - mieszkanie w tray'u oraz książka adresowa. To może jednak, innym razem. 
Wszelakie uwagi mile widziane.