03 lutego 2013

OpenGL, część druga

Dziś będziemy kontynuować naszą przygodę z malowaniem, tudzież rysowaniem. Po ostatnich wywodach pewnie każdy wie jak poprawnie trzymać pędzel w ręku. Dziś w końcu będziemy mogli  uwolnić nasz talent i stworzyć nasze pierwsze, wielkie dzieło - "Hello world".

Wierzchołki i indeksy

W świecie OpenGL'a podstawowym budulcem wyświetlanych obiektów są wierzchołki. Za ich pomocą możemy namalować pojedynczy punkt, linię oraz trójkąt. W naszym przypadku nawet koło będzie składać się ze skończonej liczby elementów, choć w grafice dwuwymiarowej niekoniecznie musi to być prawdą. Jak było wspomniane w poprzedniej notce, wierzchołek składa się  z atrybutów. Podstawowym atrybutem jaki musi mieć każdy wierzchołek to jego pozycja, w lokalnym układzie współrzędnych modelu. Zazwyczaj jest to wektor czterowymiarowy (x, y, z, w). Ostatnia współrzędna nie jest jawnie używana. Potrzebna jest ona do przekształceń macierzowych i koniec końców, ustawiamy jej wartość na jeden. Dodatkowo wierzchołek może mieć kolor (np. r, g, b, a) lub informacje o położeniu tekstury - texcoord, przekazywane jako wektor dwuwymiarowy. 
Wszystko co będziemy renderować, będzie stworzone z trójkątów - jednego z trzech wyżej wymienionych prymitywów. Każdy model, we współczesnych grach trójwymiarowych, jest obiektem stworzonym z  dużej liczby trójboków. Dzieje się tak dlatego, że dostępne dziś na rynku układy graficzne, zoptymalizowane są właśnie do pracy z trójkątami. Nawet gdy będziemy operować na zwykłych sprite'ach, znanych choćby z biblioteki SDL, to dalej dla nas będą to wieloboki - choć znacznie prostsze.  
Kolejność przeciwna do ruchu wskazówek
 zegara (counterclockwise direction; CCW)
Tutaj przechodzimy do kilku podstawowych kwestii technicznych. Przede wszystkim kolejność ułożenia wierzchołków: zgodna lub przeciwna z ruchem wskazówek zegara. Dlaczego ma to znaczenie? W grę wchodzą zagadnienia optymalizacyjne. Mając konkretnie zdefiniowaną kolejność, możemy określić przednią oraz tylną stronę wielokąta i renderować tylko jedną z nich (zazwyczaj przednią). Domyślnym trybem jest ten  pokazany na rysunku obok - przeciwny do ruchów wskazówek zegara. Oczywiście możemy go zmienić.

Indeksowanie wierzchołków
Pozostaje jeszcze jedna wątpliwość - sam proces budowania prymitywów z wierzchołków. Rozważmy prosty przykład - prostokąt. Jest to wielokąt składający się z czterech wierzchołków. Aby go wyświetlić jednak potrzebujemy dwóch trójkątów, czyli o dwa wierzchołki więcej. Dla małych modeli taka nadmiarowość ma niewielkie znaczenie, jednak dla wirtualnych środowisk składających się z milionów wielokątów, może być to poważny narzut danych. Jak sobie z tym poradzić? Stosując indeksację. Wystarczy, że do funkcji renderującej przekażemy dwie informacje: samą listę wierzchołków oraz trójki indeksów wierzchołków, budujących trójkąty. Brzmi lekko skomplikowanie, ale w rzeczywistości jest to bardzo proste. Popatrzmy na rysunek obok. Mamy tam przedstawiony kwadrat składający się  z dwóch trójkątów i czterech wierzchołków. Wierzchołki dwa i cztery są wspólne dla obu prymitywów. Pierwszy trójkąt zatem będzie się składał z wierzchołków o numerach: zero, jeden i dwa, zaś drugi budują wierzchołki dwa, trzy i zero. To w zasadzie wszystko. Mając te informacje, możemy budować geometrię dla naszych lokacji. 

Tablice wierzchołków i indeksów 

Pokodujmy więc chwilę. Na początek zdefiniujmy wszystkie niezbędne typy:
struct Vec4d
{
    float x, y, z, w;
};

struct ColorRGB
{
    float r, g, b;
};

struct Vertex
{
    Vec4d position;
    ColorRGB color;
};

struct Face
{
    unsigned short int v0, v1, v2;
};
Jak widać, nasz wierzchołek posiada dwa atrybuty: czterowymiarową pozycję i kolor. Dodatkowym typem jest Face. Struktura ta będzie odpowiedzialna za przechowywanie indeksów wielokąta (jak to bywa w C, indeksujemy od zera). Możemy zatem zająć się tworzeniem i ładowaniem geometrii. Przykładem niech będzie wspomniany wcześniej kwadrat. Tablica wierzchołków i indeksów, może być dla niego zdefiniowana następująco: 

Vertex vertices[] =
{
    /* x, y, z, w,             r, g, b */
    {0.5f, 0.5f, 1.0f, 1.0f,   0.0f, 0.0f, 1.0f},
    {-0.5f, 0.5f, 1.0f, 1.0f,  0.0f, 1.0f, 0.0f},
    {-0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f},
    {0.5f, -0.5f, 1.0f, 1.0f,  0.0f, 0.0f, 0.0f}
};

Face faces[] =
{
    {0, 1, 2},
    {2, 3, 0}
};

Myślę, że przedstawiony wyżej kod jest w miarę oczywisty. Współrzędne wierzchołków podajemy w lokalnym dla modelu, układzie współrzędnych.

Malujemy!

Czas na główny gwóźdź programu, czyli renderowanie. Malować będziemy w pętli głównej przed podmianą buforów. Jak to zwykle bywa, wyświetlanie składać się będzie z kilku krótkich kroków. 
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

glVertexPointer(4, GL_FLOAT, sizeof(Vertex), vertices);
glColorPointer(3, GL_FLOAT,  sizeof(Vertex), (float*)(vertices) + 4);

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, faces);

glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
Wynik działania programu
Funkcja glEnableClientState informuje maszynę stanów OpenGL, z jakiego typu tablic będziemy korzystać. Stała GL_VERTEX_ARRAY może być nieco myląca, bo nie odnosi się do wierzchołka jako takiego, lecz tylko do jego pozycji. Przekazując GL_COLOR_ARRAY odblokowujemy możliwość renderowania kolorowych vertexów. Następne dwie funkcje (glVertexPointer oraz glColorPointer) pokazują lokalizacje tablic z pozycją i kolorem wierzchołka. Dlaczego mówię tu o tablicach? Bo dla OpenGL'a są to w zasadzie dwie tablice. Nic nie stoi na przeszkodzie, by także z naszego punktu widzenia były to dwie oddzielne zmienne, jednak z powodów edukacyjnych zostaniemy przy tym rozwiązaniu. Także w przyszłości, gdy poznamy już bufory wierzchołków i indeksów, będziemy stosować takie podejście. Parametry przyjmowane przez obie funkcje są takie same. Na początek podajemy liczbę komponentów składających się na pozycję (x, y, z, w - 4) i kolor (r, g, b - 3). Następnie przekazujemy typ użyty do ich reprezentacji. W naszym przypadku jest to float (stała GL_FLOAT). Inne możliwe to: GL_SHORT, GL_INT oraz GL_DOUBLE. Kolejny parametr pokazuje jak daleko (w bajtach) oddalone są od siebie analogiczne parametry, w określonej tablicy. Jeśli dla pozycji i kolorów chcielibyśmy użyć dwóch oddzielnych tablic, parametr ten miałby wartość zero. Dane byłby wówczas upakowane ściśle obok siebie. Gdy jednak, tablica zawiera więcej informacji, przekazujemy wielkość wierzchołka w bajtach. Ostatni parametr to wskaźnik na zerowy element danej tablicy. 
Doszliśmy w końcu do malowania - funkcja glDrawElements. Wyrenderuje ona dla nas indeksowaną geometrię. Na wstępie przekazujemy typ rysowanych prymitywów - trójkąty. Jeśli chcielibyśmy zobaczyć punkty przekazalibyśmy GL_POINTS, zaś dla linii GL_LINES. Kolejny parametr to liczba wierzchołków do wyświetlenia, zaś GL_UNSIGNED_SHORT to typ danych użytych jako nasze indeksy wierzchołków (unsigned char - GL_UNSIGNED_BYTE, unsigned int - GL_UNSIGNED_INT). Ostatni parametr to wskaźnik na indeksy. Jeszcze tylko blokujemy (glDisableClientState) możliwość używania tablic pozycji i koloru wierzchołka. 
I to tyle. Naszym oczom powinien ukazać się piękny, wielokolorowy kwadrat (a w zasadzie prostokąt, ale o tym następnym razem).  
Solucja do pobrania tu.

Brak komentarzy:

Prześlij komentarz