27 stycznia 2013

Pędzle w dłoń!

Dziś będzie o malowaniu. Robić to będziemy w OpenGL'u do spółki z GLSL'em.
Pierwszą rzeczą o jaką musimy zadbać, to zaopatrzyć się w dwie biblioteki:  GLFW i GLEW. Pierwsza z nich będzie odpowiadać za tworzenie okna, kontekstu oraz obsługę wejścia. Robi to ona dla trzech wiodących systemów operacyjnych, więc gdy będziemy chcieli nasz kod opublikować w wersji dla Linuxa czy OS X'a, nie będziemy musieli zbyt wiele grzebać w kodzie (przynajmniej teoretycznie). GLEW zaś, ma za zadanie ułatwić pracę z rozszerzeniami, z którymi jak wiadomo, jest mały problem na platformie Microsoftu.

Rozkładamy sztalugi 

Mając przygotowane wszystkie narzędzia bierzemy się do pracy. Jako, że wspominanie wcześniej biblioteki są niewielkich rozmiarów możemy ich źródła dodać do solucji. Pierwszą rzeczą jaką będziemy musieli wykonać to utworzenie okna, po którym będziemy mogli machać pędzlem. Zrobi to za nas funkcja glfwOpenWindow. Przyjmuje ona kilka parametrów. Pierwsze dwa określają rozmiary okna. Następnie przekazujemy liczbę bitów na każdy kanał koloru (red, green, blue) i przezroczystości (alpha). Kolejnymi parametrami jest liczba bitów jakie chcemy przeznaczyć na bufor głębi oraz szablonu (stencil buffer). Na końcu przekazujemy informacje o trybie pracy okna - pełnoekranowym lub okienkowym. Zanim jednak utworzymy okno, będziemy musieli zainicjować cały subsystem, wołając bezargumentową funkcję glfwInit
Po wykonaniu powyższych operacji możemy przejść do pętli głównej, która jest sercem naszej aplikacji. W niej będzie wykonywało się wyświetlanie, odczytywanie stanu wejścia a także cała logika gry lub symulacji. Jak to zwykle bywa w tego typu aplikacjach, będzie to pętla nieskończona, przerywana na życzenie użytkownika. Tym życzeniem będzie po prostu wyjście z programu.
bool bRun = true;
while(bRun == true)
{
    glfwSwapBuffers();
    bRun = (glfwGetWindowParam(GLFW_OPENED) == GL_TRUE) && (glfwGetKey(GLFW_KEY_ESC) == GLFW_RELEASE);
}
Mamy więc kilka tajemniczych linii kodu. Pierwsza z wywoływanych funkcji (glfwSwapBuffers) odpowiada za podmianę buforów (podwójne buforowanie) i obsługę zdarzeń związanych z działaniem naszego okna. Linijkę niżej jest podejmowana decyzja o dalszym życiu naszej aplikacji. W pierwszej części wyrażenia sprawdzamy czy okno nadal jest widoczne, zaś w drugiej czy klawisz Escape jest puszczony. Jeśli oba te warunki są spełnione, możemy działać dalej. Jeśli zmienna bRun przyjmie wartość false, będzie to oznaczać, że czas życia naszego programu dobiegł końca. Żeby poprawnie zakończyć pracę i pozbierać za sobą wszystkie zabawki musimy wywołać funkcję glfwTerminate. Uprzątnie ona za nas cały bałagan który przed chwilą stworzyliśmy. Całość prezentuje się następująco:
int main()
{
    bool bReady = false;
    bReady = (glfwInit() == GL_TRUE);
 
    if(bReady == true)
    {
        glfwOpenWindow(800, 600, 8, 8, 8, 8, 32, 32, GLFW_WINDOW);
 
        bool bRun = true;
        while(bRun == true)
        {
            glfwSwapBuffers();
            bRun = (glfwGetWindowParam(GLFW_OPENED) == GL_TRUE) && (glfwGetKey(GLFW_KEY_ESC) == GLFW_RELEASE);
        }
 
        glfwTerminate();
    }
    return 0;
}

Dobieramy farby

Mając gotową obsługę okna, możemy zająć się malowaniem. Tutaj warto zatrzymać się nad kilkoma aspektami technicznymi. Przede wszystkim nie będą opisane tu przestarzałe funkcje z rodziny fixed pipeline.  Po drugie, wersja OpenGL'a, którą będziemy używać to 2.0. Dlaczego nie nowsza? Końcowy plan jest taki, by kod renderujący, który tutaj stworzymy, przenieść na platformę mobilną (jak będę bogaty będzie to iOS, jeśli spłacę długi będzie to Android, a jeśli żadna z tych rzeczy się nie wydarzy - bada), gdzie niepodzielnie rządzi OpenGL ES 2.0, który jest kompatybilny (pomijając pewne niuanse) z omawianą tu wersją desktopową.
Skoro mamy to już wyjaśnione, porozmawiajmy trochę o shaderach. Ten magiczny stwór, to nic innego jak program uruchamiany na karcie graficznej. Jego nazwa (program cieniujący) może być nieco myląca, ponieważ nie odpowiada on tylko za cieniowanie i oświetlenie. Język GLSL jest na tyle elastyczny, że możemy w nim stworzyć praktycznie dowolny efekt.
Źródło
Podstawowym kryterium podziału shaderów jest ich przeznaczenie - wierzchołki (vertex shaders) i fragmenty (fragment shaders).  Fragmenty, to nic innego jak piksele. Możemy zatem wykonywać operacje na wierzchołkach jak i na pojedynczych pikselach. Kolejność wykonywania shaderów, wraz z resztą funkcji potoku renderowania zastała przedstawiona na obrazku obok.
Zobaczmy zatem jak wygląda GLSL. Jest on bardzo zbliżony do C. Posiada trochę więcej typów oraz kilka wbudowanych zmiennych (prefiks gl_).
Jak na porządny język programowania przystało, całość zaczyna się od funkcji main. Jest bezargumentowa i nie zwraca nic. Wynik działania shadera zapisywany jest w zmiennych wyjściowych. W naszym przypadku dla wierzchołków ich obliczoną pozycję będziemy przekazywać do gl_Position, zaś ostateczny kolor piksela zapisywać w gl_FragColor. Obie zmienne są typu vec4 (x,y,z,w). Komunikacja na linii aplikacja - shader odbywa się za pomocą uniformów. Są to specjalne zmienne, których adres możemy pobrać za pomocą funkcji udostępnianych przez API OpenGL'a, a następnie ustawić je na konkretne wartości. Dodatkowo do dyspozycji mamy także atrybuty wierzchołków. Są to dane, które opisują wierzchołek (kolor, pozycja w układzie współrzędnych modelu, texcoord) i nie zmieniają się zbyt często. Możliwa jest także rozmowa pomiędzy vertex i fragment shaderem. Wystarczy, że w obu źródłach stworzymy zmienną, o tej samej nazwie oraz typie i dodamy jej modyfikator varying. Wówczas wartość przypisana do tej zmiennej w shaderze wierzchołków, będzie widoczna w shaderze fragmentów.
Pozostała jeszcze ostatnia rzecz - kompilacja i linkowanie. Każdy program, nawet ten uruchamiany na GPU, musi zostać skompilowany. Będziemy to robić w czasie działania aplikacji, za pomocą odpowiednich funkcji OpenGL. Vertex oraz fragment shadery kompilowane są osobno, jednak ostatecznie linkowane są w jeden program.
To tyle na dziś. Znów było teoretyczne przynudzanie. Następnym razem przedstawię bufory wierzchołków, wspólnie popełnimy pierwszy shader i wspomnę coś o matmie.
Tradycyjnie, solucja do pobrania tu

1 komentarz: