Lekcja 6. Teksturowanie
Autor: Marcin 'Aklimx' Milewski
Oryginał: Texture Mapping (Jeff 'NeHe' Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson06.zip

Wiedza o nakładaniu tekstur (ang. texture mapping) przynosi wiele korzyści. Powiedzmy, że chcesz, aby rakieta leciała przez ekran. Przed tą lekcją, zapewne stworzyłbyś rakietę z wielokątów i pokolorował ją. Używając teksturowania możesz wziąć obrazek rakiety i przesuwać go po ekranie. Co wygląda lepiej? Fotografia czy obiekt z trójkątów? Korzystając z tekstur nie tylko poprawisz wygląd, ale także przyspieszysz swój program. Oteksturowana rakieta będzie tylko jednym czworokątem poruszającym się po ekranie. Rakieta stworzona z wielokątów może ich zawierać setki tysięcy. Pojedynczy, oteksturowany czworokąt oznacza mniejsze zapotrzebowanie na moc.

Zacznijmy od dodania paru linijek na początku kodu. Pierwsza linia to #include <stdio.h>. Dodając ten nagłówek możemy pracować z plikami. Następnie dodajemy trzy zmienne zmiennoprzecinkowe (ang. float)... xrot, yrot, zrot. Posłużą nam one do obracania sześcianu wokół osi. Ostatnia linia GLuint texture[1] daje nam miejsce na jedną teksturę. Jeżeli chcesz mieć więcej tekstur, zmień liczbę w nawiasie.

#include <windows.h>         // Nagłówek dla Windows
#include <stdio.h>         // Standardowe wejście/wyjście ( NOWE )
#include <gl\gl.h>         // Nagłówek dla OpenGL
#include <gl\glu.h>         // Nagłówek dla GLu32
#include <gl\glaux.h>         // Nagłówek dla GLaux

HDC hDC=NULL;         // Kontext urządzenia rysującego
HGLRC hRC=NULL;         // Permanentny kontekst rysowania
HWND hWnd=NULL;         // Uchwyt naszego okna
HINSTANCE hInstance;         // Instancja naszej aplikacji

bool keys[256];         // Tablica stanów klawiszy
bool active=TRUE;         // Czy okno jest aktywne?
bool fullscreen=TRUE;         // Czy pełny ekran?

GLfloat xrot;         // Obrót na osi X ( NOWE )
GLfloat yrot;         // Obrót na osi Y ( NOWE )
GLfloat zrot;         // Obrót na osi Z ( NOWE )

GLuint texture[1];         // Miejsce na jedną teksturę ( NOWE )

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // Deklaracja WndProc - procedura okienkowa

Teraz zaraz za powyższym kodem, a przed ReSizeGLScene(), dodajemy poniższą sekcję. Zajmie się ona załadowaniem bitmapy z pliku. Jeżeli takowa nie istnieje dostaniemy NULL, co znaczy, że nie udało się jej załadować. Zanim zaczne, jedna BARDZO ważna rzecz. Szerokość i wysokość obrazka, którego chcesz użyć MUSZĄ być potęgą dwójki. Muszą mieć przynajmniej 64 pixele i raczej nie powinny mieć więcej niż 256 pixeli. Są spodoby na ominięcie tego ograniczenia, ale teraz zastosujmy się do tych wymogów.

Najpierw tworzymy uchwyt do pliku. Uchwyt to liczba określająca zasób tak, że nasz program może z niego skorzystać. Początkowo ustawiamy uchwyt na NULL.

AUX_RGBImageRec *LoadBMP(char *Filename)         // Ładuje bitmapę z pliku
{
    FILE *File=NULL;         // Uchwyt pliku

Musimy się upewnić, że nazwa pliku została podana. Ktoś mógł użyć LoadBMP() bez podania parametru, więc musimy to sprawdzić. Nie chcemy próbować ładować niczego :)

    if (!Filename)         // Upewnij się że podano nazwę pliku
    {
        return NULL;         // Jeżeli nie to zwróć NULL
    }

Jeżeli podano nazwę pliku, sprawdzamy czy istnieje. Ta linia próbuje go otworzyć

    File=fopen(Filename,"r");         // Sprawdź czy plik istnieje

Jeżeli możemy go otworzyć to znaczy, że istnieje. Zamykamy plik. Następnie odczytujemy dane funkcją auxDIBImageLoad(Filename).

    if (File)         // Czy plik istnieje
    {
        fclose(File);         // Zamknij plik
        return auxDIBImageLoad(Filename);         // Załaduj bitmapę i zwróć wskaźnik
    }

Jeżeli nie możemy otworzyć pliku zwracamy NULL. Znaczy to że plik nie mógł zostać załadowany. Później w programie sprawdzimy czy plik udało się załadować, jeżeli nie, to pokażemy komunikat błędu i zamkniemy program.

    return NULL;         // Jeżeli nie udało się załadować pliku, zwróć NULL
}

Ta sekcja ładuje bitmapę i przekształca ją w texturę

int LoadGLTextures()         // Ładuj bitmapę i przekształć ją w teksturę
{

Tworzymy zmienną Status - posłuży nam do kontrolowania procesu tworzenia textury z bitmapy. FALSE (pol. fałsz) oznacza, że nie załadowano lub nie zbudowano textury. Jest to wartość domyślna

    int Status=FALSE;         // Status tworzenia tekstury

Teraz tworzymy strukturę, w której będziemy mogli trzymać naszą bitmapkę. Rekord ten zawiera wysokość, szerokość oraz dane bitmapy.

    AUX_RGBImageRec *TextureImage[1];         // Rekord dla naszego obrazka

Czyścimy obrazek, żeby upewnić się, że jest pusty.

    memset(TextureImage,0,sizeof(void *)*1);         // Wyzeruj wskaźnik

Następnie ładujemy bitmapę i przekształcamy ją w teksturę. TextureImage[0]=LoadBMP("Data/NeHe.bmp") skoczy do procedury LoadBMP(). Plik NeHe.bmp, który jest w katalogu Data zostanie załadowany. Jeżeli wszystko pójdzie dobrze to dane obrazka znajdą się w TextureImage[0]. Ustawiamy Status na TRUE (pol. prawda) i zaczynamy budować teksturę.

        // Załaduj obrazek
    if (TextureImage[0]=LoadBMP("Data/NeHe.bmp"))
    {
        Status=TRUE;         // Ustaw Status na true (powodzenie)

Teraz, kiedy załadowaliśmy obrazek do TextureImage[0], zbudujemy z niego texturę. Pierwsza linia glGenTextures(1, &texture[0]) mówi OpenGL, że chcemy stworzyć jedną teksturę (zwiększ liczbę, jeżeli chcesz mieć ich więcej). Pamiętasz pewnie jak na początku stworzyliśmy miejsce na jedną teksturę pisząc GLuint texture[1]. Pierwsza tekstura jest przechowywana w &texture[0], a nie w &texture[1], jak mogłeś pomyśleć. Gdybyśmy chcieli mieć dwie textury napisalibyśmy GLuint texture[2] i druga tekstura byłaby wtedy w texture[1].

Druga linia glBindTexture(GL_TEXTURE_2D, texture[0]) każe OpenGL powiązać nazwę texture[0] z obiektem tekstury. Tekstury 2D mają wysokość (oś Y) i szerokość (oś X). Głównym zadaniem glBindTexture() jest przypisanie nazwy tekstury do jej danych. W tym wypadku mówimy OpenGL, że pamięć jest dostępna w &texture[0]. Możemy już stworzyć teksturę, będzie ona przechowywana w pamięci, na którą wskazuje referencja &texture[0].

        glGenTextures(1, &texture[0]);         // Stwórz obiekt tekstury
        // Powiązanie z obiektem tekstury
        glBindTexture(GL_TEXTURE_2D, texture[0]);

Następnie tworzymy rzeczywistą teksturę. Poniższe linie mówia, żę tekstura będzie dwuwymiarowa (GL_TEXTURE_2D). Zero oznacza poziom szczegółowości - z reguły równe zero. Trzy jest ilością składników obrazu. Ponieważ obraz jest zrobiony z czerwonego, zielonego i niebieskiego, wpisujemy 3. TextureImage[0]->sizeX oznacza szerokość tekstury. Jeśli znasz szerokość możesz ją tu wpisać, ale pewniej jest jak komputer zrobi to za Ciebie. TextureImage[0]->sizeY to wysokość tekstury. Zero znasza ramkę - najczęściej ustawione na zero. GL_RGB mówi OpenGL, że obraz składa się z czerownego, zielonego i niebieskiego - w tej kolejności. GL_UNSIGNED_BYTE oznacza, że dane, które tworzą obraz są typu unsigned byte, i w końcu...TextureImage[0]->data mówi OpenGL skąd wziąć dane o obrazie.

        // Stwórz teksturę
        glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);

Następne dwie linie mówią OpenGL jakiego typu filtrowania użyć, kiedy obrazek jest większy (GL_TEXTURE_MAG_FILTER) albo rozciągnięty na ekranie, a jakiego kiedy mniejszy (GL_TEXTURE_MIN_FILTER) na ekranie niż w texturze. Bardzo często używam GL_LINEAR dla obu. Tekstura wygląda wtedy gładko z daleka i z bliska. Używanie GL_LINEAR wymaga większej pracy procesora karty graficznej, więc jeśli Twój system jest wolny, skorzystaj z GL_NEAREST. Tekstura będzie wyglądała gorzej, ale przyspieszysz program. Możesz też spróbować wymieszać sposoby filtrowania. Lepiej filtrując obiekty, które są blisko, a gorzej te w oddali.

        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);         // Filtrowanie liniowe
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);         // Filtrowanie liniowe
    }

Teraz zwalniamy pamięć, w której przechowywana była bitmapa. Sprawdzamy, czy była ona w TextureImage[0]. Jeżeli tak to sprawdzamy czy dane o niej były tam przechowywane. Jeżeli tak to usuwamy ją. Wtedy zwalniamy strukturę obrazka upewniając się, że użyta pamięć została zwolniona.

    if (TextureImage[0])         // Jeżeli tekstura istnieje
    {
        if (TextureImage[0]->data)         // Jeżeli obrazek tekstury istnieje
        {
            free(TextureImage[0]->data);         // Zwolnij pamięć obrazka tekstury
        }
        free(TextureImage[0]);         // Zwolnij strukturę obrazka
    }

W końcu zwracamy status. Jeżeli wszystko dobrze, zmienna status będzie miała wartość TRUE. Jeżeli coś zawiodło - FALSE.

    return Status;         // Zwróć status operacji
}

Dodałem parę linijek do kodu InitGL. Przepiszę całą sekcję, żebyś widział co i gdzie dodałem. Pierwsza linia if (!LoadGLTextures()) skacze do procedury wyżej - ładuje bitmapę i robi z niej teksturę. Jeśli LoadGLTexture() zawiedzie, to następna linia zwróci FALSE. Jeżeli wszystko się powiedzie, włączamy mechanizm teksturowania (ang. texture mapping). Jeżeli zapomnisz o tej lini, Twój obiekt będzie prawdopodobnie koloru białego - uważaj na to.

int InitGL(GLvoid)         // Tutaj wszystko ustawiamy
{
    if (!LoadGLTextures())         // Załaduj tekstury ( NOWE )
    {
        return FALSE;         // Jeżeli tekstury nie załadowano zrwóć FALSE ( NOWE )
    }
    glEnable(GL_TEXTURE_2D);         // Włącz nakładanie tekstur ( NOWE )
    glShadeModel(GL_SMOOTH);         // Włącz gładkie cieniowanie
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);         // Czarne tło
    glClearDepth(1.0f);         // Ustawienie bufora głębi
    glEnable(GL_DEPTH_TEST);         // Włącz testowanie głębi
    glDepthFunc(GL_LEQUAL);         // Sposób testowania głębi (mniejsze, równe)
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);         // Dobre obliczenia perspektywy
    return TRUE;         // Zainicjowano pomyślnie
}

Teraz rysujemy sześcian (ang. cube). Możesz zamienić kod DrawGLScene z kodem poniżej, albo tylko dodać nowe linie. Ta sekcja jest obfita w komentarze i datego jej zrozumienie nie powinno sprawić Ci problemów. Pierwsze dwie linie glClear() oraz glLoadIdentity() są takie jak w poprzedniej lekcji. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) wyczyści ekran na kolor wybrany w InitGL(). w naszym przypadku będzie to czarny. Bufor głębi także zostaje wyczyszczony. Widok jest resetowany poleceniem glLoadIdentity().

int DrawGLScene(GLvoid)         // Tu wszystko rysujemy
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Wyczyść ekran
    glLoadIdentity();         // Zresetuj aktualną macierz
    glTranslatef(0.0f,0.0f,-5.0f);         // Przesuń w ekran 5 jednostek

Te trzy linie obracają kostkę wokół osi x, y i z. Jak bardzo to robią zależy od zmiennych xrot, yrot, zrot.

    glRotatef(xrot,1.0f,0.0f,0.0f);         // Obróć na osi X
    glRotatef(yrot,0.0f,1.0f,0.0f);         // Obróć na osi Y
    glRotatef(zrot,0.0f,0.0f,1.0f);         // Obróć na osi Z

Następna linia wybiera teksturę, której chcemy użyć. Jeżeli byłoby więcej tekstur, wybierał byś ją w tak: glBindTexture(GL_TEXTURE_2D, texture[numer_tekstury]). Jeżeli chcesz zmienić teksturę, to poprostu bindujesz inną - nic prostszego :) Bardzo ważną rzeczą jest zakaz bindowania tekstury w bloku glBegin() i glEnd() - rób to zawsze przed albo po.

    glBindTexture(GL_TEXTURE_2D, texture[0]);         // Wybierz naszą teksturę

Żeby właściwie nałożyć teksturę na czworokąt, musisz upewnić się, że prawy górny róg textury jest nakładany na prawy górny róg czworokąta - pozostałe analogicznie. Jeżeli narożniki tekstury nie pasują do odpowiednich rogów czworokąta, to obrazek będzie zniekształcony (np. do góry nogami) albo nie będzie go wcale :(

Pierwsz wartość glTexCoord2f to współrzędna X, 0.0f oznacza lewą część tekstury, 0.5f - środek, 1.0f - prawą. Druga wartość to współrzędna Y. 0.0f oznacza dół, 0.5f - środek, 1.0f - górę.

Wiemy, że lewy górny róg textury to (0.0f, 1.0f) a lewy górny wierzchołek czworokąta to (-1.0f, 1.0f). Teraz musisz połączyć pozostałe trzy wierzchołki textury z narożnikami czworokąta.

Spróbuj pobawić się z wartościami w glTexCoord2f(). Zmieniając 1.0f na 0.5f spowodujesz rysowanie tylko jednej połówki textury, od 0.0f(lewo) do 0.5f(środek). Zmiana 0.0f na 0.5f spowoduje narywowanie tylko drugiej połówki tekstury, od 0.5f(środek) do 1.0f(prawo).

    glBegin(GL_QUADS);
        // Przednia ściana
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);         // Lewy dolny wierzchołek tekstury i czworokąta
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);         // Prawy dolny wierzchołek tekstury i czworokąta
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);         // Prawy górny wierzchołek tekstury i czworokąta
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);         // Lewy górny wierzchołek tekstury i czworokąta
        // Tylna ściana
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);         // Prawy dolny
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);         // Prawy górny
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);         // Lewy górny
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);         // Lewy dolny
        // Górna ściana
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);         // Lewy górny
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);         // Lewy dolny
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);         // Prawy doln
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);         // Prawy górny
        // Dolna ściana
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);         // Prawy górny
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);         // Lewy górny
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);         // Lewy dolny
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);         // Prawy dolny
        // Prawa ściana
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);         // Prawy dolny
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);         // Prawy górny
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);         // Lewy górny
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);         // Lewy dolny
        // Lewa ściana
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);         // Lewy dolny
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);         // Prawy dolny
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);         // Prawy górny
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);         // Lewy górny
    glEnd();

Teraz zwiększamy wartości dla xrot, yrot i zrot. Spróbuj zmienić poniższe wartości, to kostka będzie obracać się szybciej lub wolniej. Spróbuj też pozmieniać znaki.

    xrot+=0.3f;         // Obrót na osi X
    yrot+=0.2f;         // Obrót na osi Y
    zrot+=0.4f;         // Obrót na osi Z
    return true;         // Wszystko OK!
}

Teraz powinieneś lepiej rozumieć mechanizm nakładania tekstur (and. texture mapping). Powinieneś być w stanie nakładać tekstury na każdą powierzchnię. Jeśli czujesz, że dobrze rozumiesz temat, to spróbuj nałożyć sześć różnych tekstur na nasz sześcian.

Teksturowanie nie jest trudne, kiedy rozumiesz jak działają współrzędne tekstury. Jeśli masz problem w zrozumieniu, którejś części tej lekcji, to daj znać. Przepiszę wtedy tą sekcję, albo odpowiem na maila. Baw się dobrze tworząc oteksturowane obiekty na scenie :)