Grafika 3D
 
  Zarejestruj się
::  Newsy  ::  Pliki  ::  Twoje Konto  ::  Forum  ::
Menu
· Strona główna
· Forum
· Linki
· Lista u?ytkowników
· O nas...
· Pliki
· Statystyki
· Twoje Konto
Tutoriale
· API
· Matematyka
· Teoria
· Direct3D
· OpenGL
· Techniki
Kto Jest Online
Aktualnie jest 45 gość(ci) i 0 użytkownik(ów) online.

Jesteś anonimowym użytkownikiem. Możesz się zarejestrować za darmo klikając tutaj
Tutoriale - OpenGL - Tekstury i tablice wierzchołków

Hallo people :). Jak tam po świętach? Najedzeni :D ? To dobrze, bo będzie wam dzisiaj potrzebne dużo energii, aby zrozumieć (a chociaż przeczytać - w końcu to sporo przewijania :)))) dzisiejszy temat czyli teksturowanie. Jest to bardzo ważny temat, bo wszystko co nastąpi później będzie w mniejszym lub większym stopniu związane z teksturami (a będą to zaiste trudne zagadnienia ;). Jest też obiecana ostatnio niespodziewajka, która jeszcze bardziej przyspieszy renderowanie :).

Tym razem zaczniemy trochę nietypowo, bo nie od kodu, ale od teorii. Muszę przecież wyjaśnić, co to są tekstury i do czego służą. Sądzę, że każdy(-a) z was oglądał(-a) kiedyś zdjęcia na komputerze np. pliki *.bmp, czy *.jpg. Takie pliki to nic innego jak dwuwymiarowa tablica RGBA, na których zastosowano mniej (*.bmp) lub bardziej wydajne techniki kompresji (*.jpg). No więc tekstury to obrazy, zdjęcia itp. nakładane na obiekt w celu imitacji jakiegoś materiału, czy nawet całego przedmiotu. Są rodzajem skóry, która pokrywa obiekt. Najlepiej to chyba zobaczycie na przykładzie. Otóż wyobraźcie sobie, że chcecie zrobić scenę na której będzie np. wasz wymarzony dom z ogrodem. Projektujecie go bardzo dokładnie. Macie już bardzo ładny dom i chcecie jakoś zaznaczyć zasięg ogrodu, więc robicie ogrodzenie. Powiedzmy, że ogrodzeniem jest zwykłą siatką drucianą (każdy wie jak coś takiego wygląda, mam nadzieję?:). Umiemy już dodawać materiały i oświetlenie do naszej sceny, więc nie ma problemu, żeby siatka lśniła metalicznie w słońcu i wszystko wyglądało pięknie. Jednak, aby zbudować jedno oczko takiej siatki potrzeba powiedzmy 200 trójkątów. Jeśli przyjmiemy, że jedno pole siatki (fragment pomiędzy dwoma słupkami :) składa się z 400 takich oczek, a aby ogrodzić ogród musimy mieć powiedzmy 30 pól, to jak łatwo policzyć musielibyście wyrenderować 2,400,000 trójkątów. Jest to zaiste potworna liczba, szczególnie dla waszych kart graficznych, a jest to tylko ogrodzenie. Trójkątów potrzebnych na dom i drzewa w ogrodzie nawet nie liczę, ale można by przyjąć, że na całą scenę potrzebujemy dziesięć razy tyle. Nie mówię, że coś takiego nie jest możliwe - oczywiście że jest. Tyle tylko, że w programie do grafiki 3D jak np. 3D Studio MAX, LightWave itp. Nie ma najmniejszych szans na zrobienie tego w czasie rzeczywistym jeśli nie macie pod ręką jakiejś porządnej stacji graficznej, albo trochę drobnych, żeby takową zakupić :))))). Jest jednak bardzo prosta metoda ograniczenia ilości trójkątów potrzebnych na ogrodzenie z 2,400,000 do raptem 8 (zakładam, że na jedną stronę ogrodzenia potrzeba 2 trójkątów - ogród jest prostokątem, więc ma cztery strony prawda? :). Możemy to zrobić właśnie za pomocą tekstur. Jak? Otóż robimy sobie zdjęcie takiego pojedynczego pola i nakładamy go na nasz prostokąt (2 trójkąty jakby ktoś nie wiedział :))) robiący za ogrodzenie. W ten sposób otrzymujemy ogrodzenie, które co prawda nie jest takie dokładne i z bliska od razu widać że jest to tylko tekstura, ale jednak działa duuuuuuuuużo szybciej. Co więcej, jeśli chcemy, żeby w naszym ogrodzie była trawa, to również używamy tekstur, bo imitacja trawy za pomocą materiałów byłaby okropnie czasochłonna dla komputera. Jeśli użyjemy tekstur w wysokiej rozdzielczości i odpowiedniego ich filtrowania, jak również kilku ciekawych technik takich jak bumpmapping, to możemy osiągnąć naprawdę oszałamiający efekt. Zresztą popatrzcie na jakąkolwiek grę. Wszędzie są tekstury - na ścianach, podłodze, na twarzach i ciałach postaci, botów itd. Nie ma gry 3D, która nie korzystałaby z tekstur (no może poza starymi dobrymi symulatorami lotu typu LHX, czy F19 - tam była tylko grafika wektorowa, więc nie było też tekstur. Jak ktoś miał kiedyś 386 to wie o czym mówię :)))))).

No dobra. Starczy już tego gadania - zabierzmy się do roboty. Nie liczcie jednak na to, że omówię cały temat teksturowania, bo jest to zagadnienie tak obszerne, jak cała grafika - to z teksturami jest właśnie związana największa liczba sztuczek. Nie będę omawiał kodu po kolei, ale poskaczę sobie trochę po nim. Po pierwsze musimy wiedzieć jak przechowywane są tekstury w OpenGL. Otóż przechowujemy je w tablicy GLuint. U nas jest to taka tablica:
GLuint      texture[1];
Po drugie musimy jakoś wczytać teksturę i przekształcić ją do formatu używanego w grafice. Robimy to za pomocą dwóch funkcji globalnych. Jeśli komuś z was zdaje się, jakoby już gdzieś ten kod widział, to ma rację - jest to funkcja z NeHe, ale ponieważ dość ciężko wymyślić w tej kwestii coś nowego, a poniższy kod działa całkiem nieźle, to go tu umieszczam.
AUX_RGBImageRec *LoadBMP(char* filename)
{
    FILE* file=NULL;

    if (!filename)
    {
        return NULL;
    }

    file=fopen( filename, "r" );
    if( file )
    {
        fclose( file );
        return auxDIBImageLoad( filename );
    }

    return NULL;
}
Otóż i pierwsza z nich. Jej zadaniem jest wczytanie tekstury w formacie *.bmp. Aby to zrobić sprawdzamy czy istnieje plik o podanej nazwie, a następnie, jeśli istnieje, odczytujemy go za pomocą funkcji auxDIBImageLoad() z pomocniczej biblioteki OpenGL o nazwie AUX (od Auxiliary czyli pomocnicza - na pewno zauważyliście, że dodałem jeden plik nagłówkowy i jedną bibliotekę). W bibliotece tej zdefiniowana jest struktura i nazwie AUX_RGBImageRec, do której wskaźnik zwraca nasza funkcja. Zadaniem tej struktury jest przechowywanie danych tekstury. Muszę jeszcze wspomnieć o jednej bardzo ważnej rzeczy - szerokość i wysokość tekstur, których używamy ZAWSZE! są potęgą 2. Tzn. tekstura musi być w rozdzielczości np. 64x64, 2048x2048, czy też 4096x4096 (to już jest ogromna tekstura, która zajmuje kupę pamięci). Co prawda, my użyjemy takich funkcji, które pozwolą na wczytanie tekstur o dowolnych rozdzielczościach, ale i tak OpenGL przekształci je do potęgi 2.
int LoadGLTextures(void)
{
    int status=FALSE;
    AUX_RGBImageRec *TextureImage[1];

    memset(TextureImage,0,sizeof(void *)*1);
    if (TextureImage[0]=LoadBMP("Data/Crate.bmp"))
    {
        status=TRUE;
        glGenTextures(1, &texture[0]);
        glBindTexture(GL_TEXTURE_2D, texture[0]);
        gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 
                          GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri( GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    }

    if (TextureImage[0])
    {
        if (TextureImage[0]->data)
        {
            free(TextureImage[0]->data);
        }
        free(TextureImage[0]);
    }

    return status;
}
Ta funkcja używa poprzedniej, aby wczytać tekstury dla sceny, przekształcić je do formatu używanego przez OpenGL i ustalenia ich parametrów.
AUX_RGBImageRec *TextureImage[1];
memset(TextureImage,0,sizeof(void *)*1);
Tego chyba nie muszę omawiać? No dobra. Dla tych, którzy nie wiedzą co się dzieje napiszę. Robimy sobie wskaźnik (a właściwie tablice wskaźników) do naszej struktury, która przechowuje bitmapę i zerujemy ją za pomocą funkcji memset (czyli ustawiamy jej elementy na NULL).
glGenTextures(1, &texture[0]);
Zadaniem tej funkcji jest poinformowanie OpenGL'a, że chcemy zrobić jedną teksturę i umieścić ją w tablicy pod indeksem 0. Czyli po prostu, że będziemy teraz robić nową teksturę, która będzie znajdowała się pod indeksem 0 w naszej tablicy tekstur.
glBindTexture(GL_TEXTURE_2D, texture[0]);
Ta funkcja mówi OpenGL, że tekstura texture[0] jest teksturą dwuwymiarową, czyli właśnie tablicą RGB (istnieją jeszcze tekstury jedno- i trójwymiarowe). Jednocześnie ta funkcja ustala, że obecną teksturą jest tekstura texture[0].
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 
                  GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
Ta funkcja buduje naszą teksturę. Co więcej ustala mipmapping tekstury. Co to są mipmapy wyjaśnię później, a na razie przypatrzmy się parametrom z jakimi nasza funkcja jest wywoływana. Pierwszym z nich jest typ tekstury. Do tej wersji funkcji musi to być GL_TEXTURE_2D (bo jest gluBuild2DMipmaps). Jako drugi parametr podajemy z ilu komponentów składa się nasz obraz. U nas 3, bo teksturę mamy w formacie RGB. Jako trzeci parametr podajemy szerokość, a jako czwarty wysokość naszej tekstury. U nas parametry te przechowywane są w strukturze AUX_RGBImageRec. Piąty parametr określa jaki format ma nasza tekstura. To od niego zależy ilość komponentów jakie podamy. Możliwe wartości podane są w poniższej tabeli.

Format tekstury
Ilość komponentów
GL_COLOR_INDEX
1
GL_RED
1
GL_GREEN
1
GL_BLUE
1
GL_ALPHA
1
GL_RGB
3
GL_RGBA
4
GL_BGR_EXT
3
GL_BGRA_EXT
4
GL_LUMINANCE
1
GL_LUMINANCE_ALPHA
2

Kilka z podanych wartości wymaga wyjaśnienia. Po pierwsze GL_COLOR_INDEX. Jest to tryb, w którym musimy używać palety kolorów, a poszczególne kolory zdefiniowane są jako indeksy tej tablicy. Nie zajmujemy się nim, ponieważ jest to bez sensu - nasze karty graficzne potrafią świetnie radzić sobie z trybem RGB. GL_BGR_EXT i GL_BGRA_EXT. Są to tryby należące do rozszerzeń OpenGL. Te dwa rozszerzenia umożliwiają odczyt obrazów o kolorach zapisanych w odwrotnej kolejności. O rozszerzeniach będziemy jeszcze mówić, ale jest to temat bardziej zaawansowany, pozwalający na osiągnięcie wielu ciekawych efektów. Na razie powiem tylko, że są one zależne od karty graficznej jaką posiadacie. Możecie spróbować i zamiast GL_RGB podać GL_BGR_EXT. Jeśli zadziała, to znaczy, że posiadacie odpowiednie rozszerzenie (ale pamiętajcie, że będziecie mieli odwrócone kolory :). GL_LUMINANCE - kolor traktowany jest jako skala szarości. Wartość 1 to biały, a 0 to czarny. Wartości pośrednie oznaczają właśnie szary (ciemniejszy lub jaśniejszy :). GL_LUMINANCE_ALPHA to to samo, tyle że dodatkowo z kanałem alfa (do czego jest on nam potrzebny dowiemy się w następnym artykule).

Szóstym parametrem jest typ, w jakim zapisane są składowe. Jest kilka możliwości. Jeśli chcecie poznać wszystkie odsyłam do dokumentacji. Ostatnim parametrem jest wskaźnik do tablicy, w której przechowywana jest tekstura (u nas jako zbiór wartości RGB).

Skoro już wiemy, jak wywołuje się naszą funkcję, to musimy jeszcze wiedzieć co to są te mipmapy. Otóż jest to technika pozwalająca na zmniejszenie rozdzielczości i ilości detali tekstury w zależności od odległości (inaczej Level-of-Detail - LOD). Działa to w taki sposób, że z naszej tekstury buduje się jeszcze kilka (kilkanaście), tyle że o mniejszej rozdzielczości. Jak to wygląda w praktyce? Ano mniej więcej tak jak na poniższym rysunku:


Jak widać tekstura początkowa (0) jest regularnie zmniejszana. Warto nadmienić, że jej rozmiar maleje dwukrotnie (czyli każdy następny rozmiar jest dwa razy mniejszy od poprzedniego :)). Po co stosuje się taką operację? Po pierwsze dlatego, że bez tej operacji tekstura widziana z większej odległości strasznie się zniekształca przy stosowaniu tradycyjnych filtrów. Po drugie nie ma sensu, żeby stosować obliczenia dla każdego piksela dużej tekstury, jeśli jest ona widziana z bardzo dużej odległości. Oto przykład jak wygląda scena z mipmappingiem i bez niego.


Prawda, że po prawej stronie efekt jest lepszy? A dla niedowiarków w funkcji renderującej umieściłem dwie funkcje w komentarzu. Jeśli chcecie zobaczyć jak będzie wyglądała nasza tekstura bez mipmapping'u, to usuńcie komentarze i weźcie w nie poprzednio używane funkcje. A ja już spieszę z wyjaśnieniami jak działa ta druga budująca tekstury, która jest bardzo podobna.
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, TextureImage[0]->sizeX, TextureImage[0]->sizeY,
             0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
Otóż jej działanie jest prawie identyczne jak poprzedniej, oprócz tego, że nie robi mipmap. Pojawiły się tylko trzy nowe parametry, więc omówię tylko je - reszta działa tak jak poprzednio. Pierwszy nowy (pierwsze zero :) to poziom mipmapy. Tak wiem, mówiłem, że ta funkcja nie generuje mipmap. No bo nie generuje ich automatycznie. Możemy jej do tego użyć, ale wtedy wszystkie mipmapy musimy zrobić sami. Drugi nowy parametr to GL_RGB8. W dokumentacji możemy się dowiedzieć, że jest to tzw. InternalFormat, czyli po polskiemu format wewnętrzny OpenGL. Jest to format koloru tekstury (u nas po osiem bitów na każdą składową RGB, czyli w sumie 24 bity). Nie będę wypisywał tutaj wszystkich możliwości, bo jest ich ok. 30. Zainteresowanych odsyłam do dokumentacji. Trzeci nowy parametr (drugie 0) określa, czy tekstura ma mieć ramkę (wartość 1), czy nie (wartość 0). A do czego wykorzystuje się ramkę powiem później.
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
To kolejna funkcja w naszym kodzie. Jej zadaniem jest ustalenie parametrów tekstury. Pierwszym parametrem jest GL_TEXTURE_ENV i jest to jedyna dostępna możliwość. Drugi parametr może przyjmować dwie wartości :
  • GL_TEXTURE_ENV_COLOR - definiuje kolor, który jest potem łączony z teksturą
  • GL_TEXTURE_ENV_MODE - definiuje tryb teksturowania.
Trzeci parametr zależy od poprzedniego. Dla GL_TEXTURE_ENV_COLOR jest to tablica wartości RGBA. Dla GL_TEXTURE_ENV_MODE jest to jeden z trzech parametrów :
  • GL_DECAL - tekstura jest nakładana na wielokąt bez żadnych modyfikacji koloru
  • GL_MODULATE - przed nałożeniem na wielokąt obraz tekstury jest mnożony przez istniejący już na wielokącie kolor. Czyli innymi słowy kolor każdego z pikseli tekstury jest łączony z kolorem piksela będącym na wielokącie.
  • GL_BLEND - przed nałożeniem ma wielokąt piksele tekstury są mieszane z kolorem ustalonym przez GL_TEXTURE_ENV_COLOR.
No i przechodzimy do filtrowania tekstur :
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
Funkcja glTexParameteri() przyjmuje trzy parametry. Pierwszym jest rodzaj tekstury (GL_TEXTURE_1D, GL_TEXTURE_2D, GL_TEXTURE_3D_EXT). Drugi parametr może przyjmować jedną z kilku wartości :
  • GL_TEXTURE_MIN_FILTER - określa, że chcemy ustalić filtr używany przy zmniejszaniu tekstury
  • GL_TEXTURE_MAG_FILTER - określa, że chcemy ustalić filtr używany przy powiększeniu tekstury (czyli filtr dla tekstury o oryginalnej wielkości)
  • GL_TEXTURE_WRAP_S - określa, że chcemy ustalić sposób w jaki OpenGL traktuje współrzędne S tekstury poza zakresem [0, 1].
  • GL_TEXTURE_WRAP_T - określa, że chcemy ustalić sposób w jaki OpenGL traktuje współrzędne T tekstury poza zakresem [0, 1].
  • GL_BORDER_COLOR - określa, że chcemy ustalić kolor używany jako ramka tekstury, jeśli ona sama nie ma zdefiniowanej ramki.
Trzeci parametr jest zależny od poprzedniego. Możliwe do ustalenia filtry to :
  • GL_NEAREST - czyli nie jest używany żaden filtr - tekstura jest nakładana na wierzchołek bez żadnych modyfikacji koloru pikseli
  • GL_LINEAR - ustawia filtrację tekstur polegającą na uśrednianiu koloru sąsiadujących ze sobą pikseli (interpolacja liniowa, czyli filtrowanie bilinearne).
  • GL_NEAREST_MIPMAP_NEAREST - to samo co GL_NEAREST tyle, że dodatkowo przeprowadzany jest mipmapping
  • GL_NEAREST_MIPMAP_LINEAR - to samo co GL_LINEAR, ale dodatkowo przeprowadzany jest mipmapping
  • GL_LINEAR_MIPMAP_NEAREST - następuje interpolacja liniowa sąsiadujących ze sobą mipmap, ale sama tekstura nie jest filtrowana liniowo.
  • GL_LINEAR_MIPMAP_LINEAR - czyli "liniowa interpolacja interpolowanych mipmap" (fajnie brzmi nie? :D). Jest to interpolacja sąsiadujących ze sobą mipmap, z tym, że tekstura jest również interpolowana. Jednym słowem trilinear filtering (daje najlepsze efekty, ale wymaga również najwięcej obliczeń).
Użycie ostatnich czterech filtrów ma sens jedynie przy drugim parametrze ustawionym na GL_TEXTURE_MIN_FILTER. Filtry są bardzo potężnym narzędziem. To dzięki nim nasze tekstury wyglądają tak jak powinny. Co by było, gdybyśmy nie stosowali filtrów? Otóż bylibyśmy wciąż w erze DOOM'a, gdzie po podejściu do ściany można było na palcach policzyć widoczne teksele (piksele tekstury). To dzięki filtracji tekstur dzisiejsze gry niejednokrotnie zachwycają swoim wyglądem (pamiętacie co się działo jak wyszedł UNREAL?). Oczywiście same filtry nie wystarczą. Muszą być jeszcze zastosowane do tekstur w odpowiednio wysokiej rozdzielczości. Wtedy całość wygląda naprawdę zabójczo :))).

Dla kolejnych dwóch parametrów (GL_TEXTURE_WRAP_S i GL_TEXTURE_WRAP_T) trzeci parametr może przyjmować dwie wartości. Pierwsza to GL_REPEAT, która powoduje, że tekstura jest powielana na całej długości wielokąta (jeśli podamy wartość większą niż jeden - co to znaczy powiem później). Druga możliwość to GL_CLAMP. Ustawienie tego parametru powoduje, że poza obszarem tekstury wielokąt będzie pokrywany kolorem zdefiniowanym przez GL_BORDER_COLOR (patrz wyżej) lub kolorem ramki samej tekstury. Najlepiej będzie to widać na przykładzie. Oto tekstura nałożona ze zdefiniowanym (czerwonym) kolorem ramki (np. poprzez GL_BORDER_COLOR) i ustaloną wartością GL_CLAMP dla GL_TEXTURE_WRAP_S i GL_TEXTURE_WRAP_T.


Jak widać tekstura jest tylko w rogu naszego kwadratu. Reszta to kolor jaki zdefiniowaliśmy sobie dla ramki. Na drugim obrazku widzimy natomiast jak będzie wyglądał nasz kwadrat, jeśli takiego koloru nie zdefiniujemy. Użyta jest ta sama tekstura, tylko została trochę inaczej pokolorowana :)


Jak widać pomimo, iż nasza tekstura jest znowu w rogu, to jednak na kwadracie pojawiły się pasy. Powstały poprzez rozciągnięcie na resztę kwadratu ostatnich pikseli tekstury. Efekt nie jest na pewno taki, jakiego oczekiwalibyśmy. Jeślibyśmy natomiast jako trzeci parametr podali GL_REPEAT, to kwadrat z czerwonym tłem wyglądałby mniej więcej tak:


Jak widać nasza szachownica została powtórzona na powierzchni całego kwadratu. U mnie co prawda przy górnej i prawej krawędzi tekstura została obcięta (widać, że nie cała kratka jest widoczna), ale to dlatego, że nie podałem współrzędnych całkowitych, tylko ułamkowe. Jeśli byłyby to wartości całkowite, to tekstura zostałaby zmieszczona na kwadracie tyle razy, o ile wartość podana byłaby większa od 1.

I to by było tyle, jeśli idzie o wczytywanie tekstur. Teraz zajmiemy się tym jak nakłada się je na wielokąty. Otóż robi się to za pomocą tzw. współrzędnych, czy koordynatów tekstury. Ponieważ nasza tekstura jest dwuwymiarowa, toteż będziemy potrzebować tylko dwóch współrzędnych. Współrzędna S będzie odpowiednikiem X w dwóch wymiarach, a współrzędna T odpowiednikiem Y. Aby nałożyć teksturę na wielokąt należy dodać po dwie dodatkowe wartości (współrzędne) do każdego z jego wierzchołków, a następnie poinformować o tym OpenGL'a i przesłać mu te współrzędne. A jak ustalić współrzędne? Popatrzmy na kwadrat wyżej i załóżmy, że tekstura ma być nałożona na niego tylko raz. Ponieważ jak już powiedziałem nasza tekstura, to dwuwymiarowa tablica, więc przyjmujemy, że jej lewy dolny róg jest jej początkiem (czyli cała tekstura leży w pierwszej ćwiartce układu współrzędnych). Jakie współrzędne ma ten początek? Ano takie jak początek układu współrzędnych, czyli (0,0). Dolny prawy wierzchołek tekstury (pamiętajmy, że jest ona kwadratem lub prostokątem) ma współrzędne (1, 0) - przesuwamy się po szerokości naszej tekstury, więc zwiększamy współrzędną S. Lewy górny wierzchołek tekstury ma współrzędne (0, 1), bo tym razem dodajemy na wysokości czyli współrzędna T rośnie. Chyba już domyślacie się jakie współrzędne ma prawy górny róg? No właśnie zgadliście - oczywiście (1,1), ponieważ dodajemy do współrzędnej S przesuwając się w prawo i do współrzędnej T przesuwając się w górę. Co więc robimy, aby teksturę nałożyć na nasz kwadrat? Po prostu podajemy kolejne współrzędne dla kolejnych wierzchołków kwadratu. Ot i cała polityka :). Zauważcie, że podając jak zdefiniowane są współrzędne tekstury nie mówiłem o wierzchołkach wielokąta, ale o wierzchołkach tekstury, a to dlatego że wcale nie musimy podawać 0 i 1 jako jedynych parametrów. Możemy równie dobrze zamiast 1 podać 10 (oczywiście efekt będzie zależał od tego jaką wartość podaliśmy dla GL_TEXTURE_WRAP_S i GL_TEXTURE_WRAP_T).

Możemy zresztą podawać wartości ułamkowe np. (0, 0.5f) co oznacza środek lewej ściany tekstury. No tak, wiemy już jak nakładać tekstury, nie wiemy jednak jeszcze jak przesłać ich współrzędne do OpenGL'a. Tradycyjną metodą (tak, tak, dzisiaj nie będziemy renderować tradycyjnie :P) zrobilibyśmy to poprzez dodanie funkcji
glTexCoord2f(float S, float T);
przed każdym z wierzchołków. Funkcja ta działa tak jak glVertex2f(), tyle że zamiast przekazywać współrzędne wierzchołka przekazuje współrzędne tekstury dla wierzchołka, który zostanie przesłany za chwilę poprzez np. glVertex3f(). Istnieje oczywiście odpowiednik tej funkcji pracujący z tablicami czyli glTexCoord2fv(float*), którego użycie jest oczywiście szybsze niż poprzedniej funkcji (ale nie za bardzo). Ponadto są jeszcze funkcje glTexCoord[3,4]f[v] i wariacje na temat typów ich parametrów, ale nie będziemy się na razie nimi zajmować, bo nie są nam potrzebne trzy ani tym bardziej cztery współrzędne tekstury (choć niewątpliwie będą w przyszłości :).

No dobra a teraz zajmiemy się samym renderowaniem, czyli tym co dzisiaj zrobimy nietypowo (w porównaniu do poprzednich przykładów oczywiście :). Będziemy bowiem renderować za pomocą tzw. Vertex Arrays. Jest to odpowiednik VertexBuffer'a z D3D, a właściwie to VertexBuffer jest odpowiednikiem Vertex Arrays jako, że te pojawiły się dużo wcześniej (jak zresztą większość rzeczy w OpenGL). Istnieje oczywiście znacząca różnica między tymi dwoma systemami renderowania - ten z D3D jest chyba trudniejszy do zainicjowania (a przynajmniej dłuższy :)) i możliwe, że szybszy (to niestety wina Windows'a - OpenGL ma utrudnione dojście do karty graficznej, bo musi się przebić przez kilka warstw Windows'a). Nazwa może być nieco myląca, bo o ile w D3D VertexBuffer to rzeczywiście bufor wierzchołków, o tyle w OpenGL'u są to tablice nie tylko wierzchołków, ale także koloru, współrzędnych tekstury, normalnych itd. Używa się ich bardzo prosto - wystarczy utworzyć tablice zawierającą współrzędne wierzchołków, koloru, czy normalnych i przesłać ich adres do OpenGL'a jednocześnie informując go, że będziemy ich używać.

Później wystarczy już tylko wyrenderować obiekt za pomocą odpowiedniej funkcji (także nowość) i koniec :). Istnieją dwie możliwości uruchomienia Vertex Arrays'ów. Pierwsza polega na stworzeniu osobnych tablic dla poszczególnych parametrów (kolor, współrzędne itd.), a druga na zrobieniu jednej, dużej tablicy zawierającej to wszystko. My użyjemy tej drugiej metody, bo jest chyba wygodniejsza. Tak więc zaczynamy :
struct Vertex
{
    float tex[2];
    float xyz[3];
};
Struktura definiująca nasz wierzchołek. Ważna jest kolejność składowych (ta kolejność jest zdefiniowana w OpenGL i nie można jej zmienić) - najpierw współrzędne tekstury, później współrzędne wierzchołka. Następnie tworzymy tablice wierzchołków, które utworzą sześcian:
Vertex cube[] =
{
    // sciana gorna
    { 0.0f, 0.0f,  1.0f, 1.0f,-1.0f },
    { 1.0f, 0.0f, -1.0f, 1.0f,-1.0f },
    { 1.0f, 1.0f, -1.0f, 1.0f, 1.0f },
    { 0.0f, 1.0f,  1.0f, 1.0f, 1.0f },

    // sciana dolna
    { 1.0f, 0.0f,  1.0f,-1.0f, 1.0f },
    { 1.0f, 1.0f, -1.0f,-1.0f, 1.0f },
    { 0.0f, 1.0f, -1.0f,-1.0f,-1.0f },
    { 0.0f, 0.0f,  1.0f,-1.0f,-1.0f },

    // sciana przednia
    { 0.0f, 3.0f,  1.0f, 1.0f, 1.0f },
    { 0.0f, 0.0f, -1.0f, 1.0f, 1.0f },
    { 3.0f, 0.0f, -1.0f,-1.0f, 1.0f },
    { 3.0f, 3.0f,  1.0f,-1.0f, 1.0f },

    // sciana tylna
    { 1.0f, 1.0f,  1.0f,-1.0f,-1.0f },
    { 0.0f, 1.0f, -1.0f,-1.0f,-1.0f },
    { 0.0f, 0.0f, -1.0f, 1.0f,-1.0f },
    { 1.0f, 0.0f,  1.0f, 1.0f,-1.0f },

    // sciana lewa
    { 1.0f, 0.0f, -1.0f, 1.0f, 1.0f },
    { 1.0f, 1.0f, -1.0f, 1.0f,-1.0f },
    { 0.0f, 1.0f, -1.0f,-1.0f,-1.0f },
    { 0.0f, 0.0f, -1.0f,-1.0f, 1.0f },

    // sciana prawa
    { 0.0f, 0.0f,  1.0f, 1.0f,-1.0f },
    { 1.0f, 0.0f,  1.0f, 1.0f, 1.0f },
    { 1.0f, 1.0f,  1.0f,-1.0f, 1.0f },
    { 0.0f, 1.0f,  1.0f,-1.0f,-1.0f }

};
Jak widać podajemy najpierw współrzędne tekstury, a później współrzędne wierzchołka. Tak utworzoną tablice przesyłamy do OpenGL w funkcji inicjującej:
glInterleavedArrays(GL_T2F_V3F, 0, cube);
Pierwszy parametr oznacza typ tablicy (kolejność składowych). U nas w tablicy mają się znajdować dwie współrzędne tekstury typu float (T2F)i trzy współrzędne wierzchołka, też typu float (V3F) - w taj kolejności !!. Istnieje więcej zdefiniowanych możliwości oznaczających typ tablicy - znajdziecie je np. w MSDN-ie. Drugi parametr oznacza co ile elementów tablicy znajdują się dane figury, czyli ile elementów OpenGL ma przeskoczyć po każdym wierzchołku. Możemy przecież zrobić tablicę, w której mamy zapisane jakieś inne informacje o każdym wierzchołku - takie których nie możemy lub nie chcemy przekazywać do OpenGL. U nas wszystko jest "upakowane" równo, więc podajemy 0 jako drugi parametr. Trzecim parametrem jest wskaźnik do tablicy przechowującej dane naszej figury. I to wszystko - po wywołaniu tej jednej funkcji OpenGL włącza nam Vertex Arrays i możemy ich używać. Pozostało nam już tylko dowiedzieć się jak wyrenderować to co właśnie zdefiniowaliśmy. Otóż i metoda :
glDrawArrays(GL_QUADS, 0, 24);
Ta funkcja znajdująca się w funkcji renderującej rysuje zawartość naszej tablicy używając do tego typu wielokątów podanych jako pierwszy parametr (zauważyliście brak glBegin() i glEnd()??) zaczynając od wierzchołka (wierzchołka, z nie indeksu tablicy!!!) podanego jako drugi parametr i kończąc po narysowaniu dwudziestu czterech wierzchołków (parametr trzeci jakby ktoś nie zauważył :))). Jak zwykle nie jest to jedyna metoda, ale nie będę się na razie zajmował innymi - jeśli będą potrzebne, to do nich wrócę. Zastanawiacie się pewnie po co to wszystko? Oczywiście po to co zawsze, czyli dla zwiększenia wydajności. Tym razem osiąga się to poprzez ograniczenie dziesiątek jeśli nie setek wywołań funkcji przesyłających dane. Dodatkowo niektóre kary działają szybciej, jeśli cała geometria zostanie do nich wysłana naraz, a nie po kawałku jak do tej pory. Popatrzcie ile funkcji musielibyśmy wywołać normalnie - 24 dla wierzchołków plus 24 dla współrzędnych tekstury plus dodatkowo glBegin(), glEnd() to razem 50 . A tak wywołujemy tylko jedną. Już na pierwszy rzut oka widać jaki zysk przynosi używanie tablic wierzchołków nawet na tak prostej scenie. A jakby tak było 40,000,000 kwadratów ...

Do zagadnienia tablic wierzchołków będziemy jeszcze niejednokrotnie wracać tym bardziej, że od dzisiaj będziemy ich używać zawsze w naszym kodzie (no chyba, że coś nie wypali i będę musiał chwilowo wrócić do starej wersji ;). Jeszcze tylko jedna sprawa związana z tablicami wierzchołków. Po pierwsze nie mogą to być tablice statyczne (czyli takie definiowane ze słówkiem static na początku), a po drugie nie możemy umieszczać ich w listach wyświetlania (istnieje do tego specjalne rozszerzenie - compiled_vertex_arrays - ale do tego jeszcze wrócimy kiedyś). Na tym kończę dzisiejszy artykuł, bo jeszcze trochę i książkę bym napisał :D. Od następnego arta będzie się robić coraz ciekawiej, bo w końcu skończymy podstawy i zabierzemy się za troszkę bardziej zaawansowane tematy - blending, mgla, multitexturing, mapowanie sferyczne i sześcienne, bumpapping. Chętnie zrobiłbym odpowiedniki Vertex i Pixel Shadera z D3D, ale niestety nie wiem ilu(-e) z was posiada odpowiednie karty graficzne. Pierwszy, czyli vertex_program wymaga przynajmniej GF3 (chociaż może i pod GF2 ruszy), ale drugi czyli fragment_program nie chce ruszyć nawet na moim GF4 TI 4600. Do niego potrzebny jest "minimum" ATI Radeon 9500 ;((((((. Pożyjemy zobaczymy, może wcześniej wyjdzie specyfikacja OpenGL 2.0 i coś się zmieni na lepsze (na przykład wspomniane rozszerzenia staną się standardem i będzie można je uruchomić na tzw. software vertex processing jak w D3D), a wtedy ... Aż miło pomarzyć :))))))

Taki efekt powinniście uzyskać uruchamiając kod przykładowego programu. A kod źródłowy znajdziecie nowym zwyczajem :) tam gdzie całą resztę czyli w plikach.



Kod źródłowy

©Copyright by Domino   



Tutoriale - OpenGL
Nasze newsy s� w RSS: backend.php
PHP-Nuke Copyright © 2005 by Francisco Burzi. This is free software, and you may redistribute it under the GPL. PHP-Nuke comes with absolutely no warranty, for details, see the license.
Tworzenie strony: 0.06 sekund

:: Layout strony został stworzony przez www.nukemods.com w oparciu o styl phpbb2 Helius, którego autorem jest Cyberalien ::