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

Brak komentarzy:

Prześlij komentarz