Lekcja 12. Listy wyświetlania
Autor: Marcin 'Aklimx' Milewski
Oryginał: Display Lists (Jeff 'NeHe' Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson12.zip

W tym tutorialu nauczę Cię jak używać list wyświetlania (ang. display lists). One nie tylko przyspieszą Twój kod, ale także go skrócą.

Na przykład. Powiedzmy, że tworzysz grę z asteroidami. Każdy poziom zaczyna się z przynajmniej dwoma asteroidami. Więc siadasz z kartką papieru i próbujesz wymyślić jak zrobić asteroid w 3D. Kiedy już wszystko wymyśliłeś, budujesz go w OpenGL używająć wielokątów albo czworokątów. Powiedzmy, że asteroid jest oktogonalny (8 boków). Jeżeli jesteś bystry, to stworzysz pętlę i narysujesz asteroid w pętli. W ten sposób będziesz miał 18 a może nawet więcej linii kodu tworzących asteroid. Tworzenie asteroidu za każdym razem kiedy ma być wyświetlony jest uciąliwe dla Twojego systemu. Jak zaczniesz tworzyć bardziej skomplikowane obiekty, to dowiesz się co mam na myśli.

Rozwiązanie? Listy wyświetlania!!! Używając ich, tworzysz obiekt tylko raz. Możesz na niego nałożyć teksturę, pokolorować go, cokolwiek chcesz. Listę wyświetlania trzeba nazwać. Jako, że jest to asteroid, listę wyświetlania nazwiemy 'asteroid'. Teraz kiedykolwiek będę chciał narysować oteksteruwany/pokolorowany asteroid na ekranie, będę musiał tylko wywyłać glCallList(asteroid). Wcześniej utworzony asteroid pojawi się na ekranie. Ponieważ asteroid został zbudowany jako lista wyświetlania, to OpenGL nie musi się ponownie zastanawiać jak go zbudować. To bardzo odciąża procesor i przyspiesza Twój program.

Jesteś gotowy na naukę? Program nazwiemy Q-Bert Display List demo. Efektem końcowym będzie 15 sześcianów. Każdy z nich będzie zrobiony z wieczka/klapki i pudełka. Wieczko będzie oddzielną listą wyświetlania, więc możemy nadać mu ciemniejszy kolor. Pudełko jest sześcianem bez górnej ściany :)

Ten kod bazuje na lekcji 6.. Przepiszę większość kodu programu, żeby zmiany były lepiej widoczne.

#include    <windows.h>         // nagłówek dla Windows
#include    <stdio.h>         // nagłówek dla standardowego wejście/wyjścia
#include    <gl\gl.h>         // nagłówek dla OpenGL
#include    <gl\glu.h>         // nagłówek dla GLU
#include    <gl\glaux.h>         // nagłówek dla GLAUX
HDC        hDC=NULL;         // kontekst graficzny
HGLRC        hRC=NULL;         // kontekst renderowania
HWND        hWnd=NULL;         // uchwyt okna
HINSTANCE    hInstance;         // instancja aplkacji
bool        keys[256];         // tablica do obsługi klawiatury
bool        active=TRUE;         // flaga zminimalizowania, domyślnie na tre
bool        fullscreen=TRUE;         // tryb pełnoekranowy. domyślnie na true

Ustawiamy zmienne. Najpierw ustawiamy "magazyn" dla tekstur. Następnie tworzymy dwie nowe zmienne na dwie listy wyświetlania. Te zmienne będą występowały jako wskaźniki na miejsce przechowywania list wyświetlania w pamięci ram.

Następnie mamy 2 zmienne, xloop i yloop, które są używane do określania pozycji sześcianów na ekranie oraz 2 zmienne, xrot i yroot, które są używane do obracania sześcianu w układzie współrzędnych.

GLuint    texture[1];         // miejsce na jedną teksturę
GLuint    box;         // miejsce na listę wyświetlania - pudełko
GLuint    top;         // miejsce na drugą listę wyświetlania - wieczko
GLuint    xloop;         // pętla na osi x
GLuint    yloop;         // pętla na osi y
GLfloat    xrot;         // obraca sześcian wokół osi x
GLfloat    yrot;         // obraca sześcian wokół osi y

Teraz tworzymy dwie tablice z kolorami. Pierwsza, boxcol, przechowuje wartości składowych dla kolorów: czerwonego, pomarańczowego, żółtego, zielonego i niebieskiego. Każda z trzech wartości w nawiasach klamrowych określa składowe koloru - odpowiednio czerwoną, zieloną i niebieską.

Druga tablica z kolorami przechowuje ciemny czerwony, ciemny pomarańczowy, ciemny żółty, ciemny zielony i ciemny niebieski. Ciemne kolory użyjemy do narysowania wieczek naszych pudełek.

static GLfloat boxcol[5][3]=         // tablica na kolory pudełka
{
        // janse: czerwony, pomarańczowy, żółtym zielony, niebieski
    {1.0f,0.0f,0.0f},{1.0f,0.5f,0.0f},{1.0f,1.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f}
};
static GLfloat topcol[5][3]=         // tablica na kolory wieczka
{
        // ciemne: czerwony, pomarańczowy, żółty, zielony, niebieski
    {.5f,0.0f,0.0f},{0.5f,0.25f,0.0f},{0.5f,0.5f,0.0f},{0.0f,0.5f,0.0f},{0.0f,0.5f,0.5f}
};
LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // deklaracja WndProc

Teraz budujemy listę wyświetlania. Jeśli się przyjrzysz, to zauważysz, że kod budujący pudełko jest w pierwszej liście wyświetlania, a kod budujący wieczko w drugiej. Postaram się wyjaśnić tę część kodu nieco bardziej szczegółowo.

GLvoid BuildLists()         // Stwórz listy wyświetlania
{

Zaszynamy od powiedzenie OpenGL, że chcemy dwie listy. glGenLists(2) tworzy miejsce na dwie listy i zwraca wskaźnik na pierwszą. 'box' będzie przechowywało położenie pierwszej listy. Jeżeli odpalimy listę 'box', to zostanie narysowana pierwsza lista.

    box=glGenLists(2);         // przydziel miejsce na dwie listy wyświetlania

Teraz zbudujemy naszą pierwszą listę. Wszystko co musimy zrobić, to powiedzieć OpenGL gdzie będzie nasza lista przechowywana i jaki rodzaj listy sobie życzymy.

glNewList() załatwi sprawę. Na pewno zauważyłeś 'box' jako pierwszy argument. Mówi on, żeby OpenGL przechowywał listę w miejscu, na które wskazuje 'box'. Drugi parametr, GL_COMPILE mówi, że chcemy zbudować listę i przechowywać ją w pamięci, co pozwoli na nietworzenie obiekty za każdym razem, kiedy chcemy go narysować.

Użycie GL_COMPILE jest podobne do programowania. Kiedy piszesz program, to musisz skompilować go za każdym razem kiedy chcesz go uruchomić. Jeżeli jest już skompilowany to exeka, wystarczy go odpalić i działa. Bez potrzeby ponownej kompilacji. Kiedy OpenGL skompiluje listę wyświetlania, ta jest gotowa do użycia i nie ma potrzeby znów jej kompilować. I tu właśnie jest profit z korzystania z list wyświetlania.

    glNewList(box,GL_COMPILE);         // nowa lista wyświetlania. pudełko, skompilowana

Następna partia kodu rysuje pudełko bez wieczka. Nie pojawi się ono na ekranie. Będzie jedynie przechowywane w na liście wyświetlania.

Między glNewList() i glEndList() możesz używać jakich komend chcesz. Możesz zmieniać klory, tekstury, itp. Jedyne czego nie możesz umieścić to kod, który zmieniałby listę w locie. Kiedy lista zostaje skompilowana, to NIE można jej zmienić.

Dodając linię glColor3ub(rand()%255,rand()%255,rand()%255) do tego blok, mógłbyś sądzić, że przy każdym rysowaniu kolor obiektu się zmieni. Niestety, lista została raz utworzona i kolor ustawiony poraz pierwszy nie ulegnie zmianie.

Jeżeli chcesz zmienić kolor, musisz to zrobić zanim lista zostanie wyświetlona na ekran. Więcej powiem później.

        glBegin(GL_QUADS);         // rysuj czworokątami
        // dolna ściana
            glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
            glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
            glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
            glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
        // przednia ściana
            glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
            glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
            glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
            glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
        // tylna ściana
            glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
            glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
            glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
            glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
        // prawa ściana
            glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
            glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
            glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
            glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
        // lewa ściana
            glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
            glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
            glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
            glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
        glEnd();         // zakończ rysowanie czworokątów

Przez glEndList() mówisz OpenGL, że skończyliśmy rysowanie. Wszystko co jest między glNewList() i glEndList jest częścią listy wyświetlania, wszystko przed i po nich - nie.

    glEndList();         // zakończ budowanie listy wyświetlania pudełka

Czas na drugą listę. Żeby znaleźć miejsce przechowywania listy w pamięci, bierzemy poprzednią listę i dodajemy 1.

    top=box+1;         // lista wyświetlania 'top' jest za listą wyświetlania 'box'

Kiedy już wiemy gdzie będzie nasza lista, możemy ją zbudować. Robimy to analogicznie do przypadku pierwszej listy.

    glNewList(top,GL_COMPILE);         // twórz nową listę wyświetlania 'top'

Poniższy kod rysuje wieczko. To zwykły kwadrat narysowany na planie Z.

        glBegin(GL_QUADS);         // rozpocznij rysowanie czworokątami
        // Top Face
            glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
            glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
            glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
            glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
        glEnd();         // zakończ rysowanie czworokątami

Ponownie mówimy OpenGL, że skończyliśmy budowanie używając glEndList(). To jest to!. Zakończyliśmy tworzenie dwóch list wyśweitlania.

    glEndList();         // zakończ tworzenie listy wyświetlania
}

Kod do tworzenie tekstur jest taki sam jak w poprzednich lekcjach. Teraz chcemy mieć teksturę do nałożenia na wszystkie ściany sześcianu. Zdecydowałem się użyć mipmappingu, żeby tekstura wyglądała gładko. Nie nawidzę pikselozy! Tekstura do załadowania jest w pliku 'cube.bmp'. Znajdź LoadBMP() i zmień odpowiednią linię tak, aby wyglądała jak ta poniżej.

    if (TextureImage[0]=LoadBMP("Data/Cube.bmp"))         // ładuj bitmapkę

Kod do zmiany rozmiaru nie uległ zmianie. Patrz lekcja 6..

Kod do inicjalizacji ma tylko drobne zmiany. Dodałem linijkę BuildList(). Wykona ona skok to sekcji kodu budujądej listy wyświetlania. Zauważ, że BuildList() jest po LoadGLTextures(). Zapamiętaj tę kolejność! Najpierw budujemy teksturę, a potem listy wyświetlania, które będą z niej korzystały.

int InitGL(GLvoid)         // ustawienia OpenGL
{
    if (!LoadGLTextures())         // ładuj tekstury
    {
        return FALSE;         // jeżeli się nie powiodło to zwróć false
    }
    BuildLists();         // stwórz listy wyświetlania
    glEnable(GL_TEXTURE_2D);         // włącz mapowanie tekstur
    glShadeModel(GL_SMOOTH);         // włącz gładkie cieniowanie
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);         // czarne tło
    glClearDepth(1.0f);         // ustaw bufor głębi
    glEnable(GL_DEPTH_TEST);         // włącz testowanie głębi
    glDepthFunc(GL_LEQUAL);         // ustaw sposób testowanie w buforze głębi

Następne linie włączają szybkie i jednocześnie brzydkie oświetlenia. Light0 jest predefiniowane na większości kart, więc nie musimy się martwić o jego ustawianie. Jeżeli oświetlenie nie działa na Twojej karcie (widzisz ciemność) to je wyłącz.

Ostatnia linia GL_COLOR_MATERIAL, pozwala nam dodać kolor do tekstury. Jeżeli nie włączymy mapowania kolorów, to zmiana koloru (glColor3f(r,g,b)) nie da żadnego efektu. Pamiętaj, aby to włączyć.

    glEnable(GL_LIGHT0);         // szybkie i brzydkie oświetlenie
    glEnable(GL_LIGHTING);         // włącz oświetlenie
    glEnable(GL_COLOR_MATERIAL);         // włącz kolorowanie materiału

Poniższa linia ustawia korektę perspektywy tak, aby ta ładnie wyglądała. Potem zwracamy TRUE, ponieważ inicjalizacja zakończyła się sukcesem.

    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);         // ładne poprawianie perspektywy
    return TRUE;         // inicjalizacji powiodła się

A teraz rysowanie. Jak zwykle mam bzika na punkcie matematyki. Tym razem nie ma trygonometrii, ale wciąż wygląda nieco dziwnie :) Jak zwykle zaczynamy od wyczyszczenia ekranu i bufora głębi.

Podpinamy teksturę do sześcianu. Mogłem dodać tę linię do listy wyświetlania, ale nie zrobiłem tego. Dzięki temu mogę zmieniać teksturę gdzie tylko chcę.

int DrawGLScene(GLvoid)         // rysowanie/rendering
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // wyczyść ekran i bufor głębi
    glBindTexture(GL_TEXTURE_2D, texture[0]);         // wybierz teksturę

A teraz coś zabawnego - pętelki :D Pierwsza pętla ustala pozycję na osi Y (góra/dół). Chcemy mieć 5 rzędów, więc robimy pętlę od 1 do mniej niż 6 (czyli 5).

    for (yloop=1;yloop<6;yloop++)         // pętla po y'kach
    {

Kolejna pętla. Ustawia sześciany na osi x (lewo/prawo). Liczba sześcianów zależy od tego, w którym jesteśmy rzędzie. Jeżeli jesteśmy na górze, to xloop będzie między 0 i 1 (tylko jeden sześcian). W kolejnych rzędach będzie o jeden sześcian więcej.

        for (xloop=0;xloop<yloop;xloop++)         // pętla po x'ach
        {

Resetujemy macierz widoku.

            glLoadIdentity();         // zresetuj macierz widoku

Następna linie przesuwa nas w odpowiednie miejsce na ekranie. Wygląda na skomplikowane, ale takie wcale nie jest.

Oto co się dzieje na osi X: przesuwamy się w prawo 1.4 jednostki, więc piramida jest wyśrodkowana. Mnożymy xloop przez 2.8 i dodajemy 1.4. (mnożymy przez 2.8, dzięki czemu sześciany nie nachodzą na siebie. 2.8 jest szerokością sześcianu, kiedy obrócimy go o 45 stopni). Ok. Odejmujemy yloop*1.4. To przesuwa sześciany w lewo w zależności od rzędu, w którym jesteśmy. Gdybyśmy tego nie zrobili to piramida była by wyrównana do lewej krawędzi (i nie wyglądałaby jak piramida :/ ).

A na osi Y? Odejmujemy yloop od 6 - w przeciwnym razie piramida powstałaby do góry nogami. Mnożymy wynik przez 2.4. W przeciwnym razie sześciany nachodziłyby na siebie. Odejmujemy 7, więc piramida zaczyna się na dole i jest budowana do góry.

W końcu, na osi Z przesuwamy się "w ekran" 20 jednostek. W ten sposób piramida ładnie pasuje do ekranu.

        // pozycja sześcianu na ekranie
            glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f),((6.0f-float(yloop))*2.4f)-7.0f,-20.0f);

Teraz obracamy wokół osi x. Przechylamy sześcian w przód o 45 stopni i odejmujemy 2*yloop. Perspektywa pochyla sześciany automatycznie, więc odejmuję, aby zrekompensować przechył. Może nie najlepsze rozwiązanie, ale działa :)

Na koniec dodajemy xrot. Daje nam to kontrolę nad kątem za pomocą klawiatury. (życzę dobrej zabawy).

Jak już skończyliśmy z obrotami na osi x, robimy obród o 45 stopni na wokół osi y i dodajemy yrot, żeby mieć kontrolę nad obrotem za pomocą klawiatury.

            glRotatef(45.0f-(2.0f*yloop)+xrot,1.0f,0.0f,0.0f);         // pochylenie sześcianu góra/dół
            glRotatef(45.0f+yrot,0.0f,1.0f,0.0f);         // obrót sześcianu lewo/prawo

Teraz wybieramy kolor pudełka. Zauważ, że używamy glColor3fv(). Ta funkcja ładuje trzy składowe koloru za jednym razem. 3fv oznacza 3 wartości zmiennoprzecinkowe (ang. floating point), v oznacza wskaźnik na tablice. Kolor, który wybieramy to yloop-1, więc każdy rząd będzie miał inny kolor. Spróbuj użyć xloop-1.

            glColor3fv(boxcol[yloop-1]);         // wybierz kolor pudełka

Kiedy ustawiliśmy kolor, czas na rysowanie. Zamiast pisać cały kod rusyjący, wystarczy, że wywołamy listę wyświetlania. Robimy to przez glCallList(box). box mówi OpenGL, że ma narysować pudełko (bez wieczka).

            glCallList(box);         // narusuj pudełko

Wybieramy kolor wieczka zanim przystąpimy do rysowania. Kolor uzależniamy od numeru rzędu - (yloop-1).

            glColor3fv(topcol[yloop-1]);         // wybierz kolor wieczka

I rysujemy. Banalne!

            glCallList(top);         // narysuj wieczko
        }
    }
    return TRUE;         // wróc
}

Pozostałe zmiany zaszły w WinMain(). Kod został dodany zaraz po linijce z SwapBuffers(hDC). Kod sprawdza czy zostały naciśnięte strzałki i odpowiednio porusza sześcianami.

        SwapBuffers(hDC);         // zamień bufory (podwójne buforowanie)
        if (keys[VK_LEFT])         // strzałka w lewo?
        {
            yrot-=0.2f;         // obróć w lewo
        }
        if (keys[VK_RIGHT])         // strzałka w prawo?
        {
            yrot+=0.2f;         // obróć w prawo
        }
        if (keys[VK_UP])         // strzałka w górę?
        {
            xrot-=0.2f;         // pochyl w górę
        }
        if (keys[VK_DOWN])         // strzałka w dół
        {
            xrot+=0.2f;         // pochyl w dół
        }

Jak w poprzednich lekcjach, upewnijmy się, że napis na belce tytułowej programu jest poprawny.

        if (keys[VK_F1])         // klawisz F1 naciśnięty?
        {
            keys[VK_F1]=FALSE;         // jeżeli tak, to ustaw na false
            KillGLWindow();         // zabij aktualne okno
            fullscreen=!fullscreen;         // zmień tryb pełnoekranowy/okienkowy
        // utwórz ponownie okno
            if (!CreateGLWindow("NeHe's Display List Tutorial",640,480,16,fullscreen))
            {
                return 0;         // wyjdź jeżeli nie udało się utworzyć
            }
        }
    }
}

Teraz powinieneś już dobrze rozumieć jak działają listy wyświetlania, jak je stworzyć i jak je wyświetlić na ekran. Listy wyświetlania są świetne. Nie tylko ze względu na proste tworzenie złożonych projektów, ale także ze względu na przyspieszenie, króre dają, a które jest wymagane do osiągnięcia wysokich FPSów.

Mam nadzieję, że tutorial Ci się podobał. Jeżeli masz jakieś pytania, albo coś nie jest jasne, napisz do mnie maila.