W tej lekcji nauczę Cię jak używać trzech osobnych filtrów tekstur, jak poruszać obiektem używając klawiatury oraz w jaki sposób stworzyć proste oświetlenie dla twojej sceny. Wiele rzeczy się tutaj powtarza, więc jeśli poprzednie lekcje wciąż sprawiają Ci problemy, koniecznie do nich wróć. Ważne jest zrozumienie podstaw przed przystąpieniem do omawiania poniższego kodu.
Po raz kolejny będziemy modyfikować kod z poprzednich tutoriali. Jeśli znajdą się jakieś poważniejsze zmiany wypiszę całą sekcję kodu, która się zmieniła. Zaczniemy od zdefiniowania kilku nowych zmiennych.
Poniższe linie są nowe. Dodajemy trzy zmienne typu BOOL. BOOL oznacza, że zmienna tego typu może przybierać tylko dwie wartości ? TRUE lub FALSE. Definiujemy zmienną light do przechowywania informacji o tym, czy oświetlenie ma być włączone. Zmienne lp i lf przechowywać będą stan klawiszy 'L' i 'F' (czy są wciśnięte lub nie). Później wyjaśnię dlaczego ich potrzebujemy, póki co, wiedz, że są ważne.
Teraz stworzymy parę zmiennych, które kontrolować będą kąt obrotu naszej skrzynki według osi X (xrot), kąt obrotu według osi Y (yrot), prędkość obracania się skrzynki względem osi X (xspeed) oraz prędkość obracania skrzynki względem osi Y (yspeed). Zdefiniujemy również zmienną o nazwie z, która kontrolować będzie jak głęboko w ekranie (na osi z) znajduje się skrzynka.
Następnie definiujemy kilka tablic, które posłużą nam do stworzenia światła. Będziemy używać jego dwóch typów. Pierwszym typem światła jest światło otaczające (ang. ambient light). Światło otaczające, to takie, które nie pochodzi z żadnego konkretnego kierunku. Wszystkie obiekty na twojej scenie będą przez nie lekko oświetlone. Drugim typem jest światło rozproszone (ang. diffuse light), które tworzone jest na podstawie określonego źródła światła i rozjaśnia powierzchnie, na które które pada. Jego zastosowanie stworzy nieźle wyglądający efekt cieniujący na bokach naszej skrzynki.
Światło tworzymy tak samo, jak kolor - na podstawie trzech wartości. Jeśli pierwsza równa jest 1.0f, a dwie następne 0.0f to otrzymamy jasne czerwone światło. Jeśli trzecia jest równa 1.0f, a dwie pierwsze 0.0f to otrzymamy jasne niebieskie światło. Dochodzi do tego jeszcze czwarta wartość alpha, ale póki co będziemy ustawiać ją na 1.0f.
W linii poniżej zapisujemy wartości dla białego otaczającego światła o połowie intensywności. Tworzymy je, ponieważ bez światła otaczającego miejsca do których nie docierałoby światło rozproszone pozostałyby bardzo ciemne.
Teraz z kolei zapiszemy wartości dla bardzo jasnego, w pełni intensywnego światła rozproszonego. Wszystkie wartości równe są 1.0f, co oznacza, że światło jest maksymalnie jasne. Oświetli ono ładnie przód naszej skrzynki.
Ostatecznie zapisujemy pozycję światła. Pierwsze trzy liczby mają takie samo znaczenie, jak trzy parametry funkcji glTranslatef. Pierwsza określa pozycję na osi X, co pozwala poruszać obiektem w prawo i w lewo, druga, pozycję na osi Y, co pozwala poruszać obiektem w górę i w dół, trzecia z nich pozwala poruszać obiektem w głąb i na zewnątrz ekranu według osi Z. Ponieważ chcemy by nasze światło padało prosto na przód naszej skrzynki, ustawiamy pierwszą wartość na 0.0f (nie poruszamy światła po osi X). Nie chcemy również by światło było powyżej lub poniżej skrzynki, więc drugą wartość ustawiamy również na 0.0f. Chcemy mieć jednak zawsze pewność, że światło znajdować się będzie przed skrzynką, więc ustawiamy je ?poza ekranem? podając jako trzecią liczbę 2.0f. Powiedzmy, że powierzchnia twojego monitora to 0.0f na osi Z. Gdybyś mógł zobaczyć nasze światło, lewitowałoby właśnie przed twoim monitorem. Ustawiając w ten sposób jego pozycję, jedyna możliwość by znajdowało się ono za skrzynką polega na tym, że skrzynka również znajdowałaby się przed monitorem. Oczywiście, jeśli zaszłaby taka sytuacja przestałbyś ją widzieć, więc w tym wypadku nie ma znaczenia gdzie światło się znajduje. Ma to sens?
Naprawdę trudno wyjaśnić znaczenie tej trzeciej liczby, określającej pozycję na osi Z. Powinieneś jednak wiedzieć, że obiekt, którego pozycja to -2.0f na osi Z będzie znajdował się bliżej ciebie, niż o wartości Z -5.0f, czy -100.0f. Gdy ustawisz wartość Z na 0.0f, a obiekt, który wyświetlasz jest duży, może zasłonić on cały ekran. Gdy zaczniesz jednak wprowadzać dodatnie wartości, nie zobaczysz już dłużej tego obiektu, ponieważ "minie on ekran". Właśnie to mam na myśli mówiąc "poza ekranem". Obiekt ciągle tam jest, tyle że go nie widać.
Zmienna filter, którą definiujemy poniżej przechowuje informację, którą teksturę należy aktualnie wyświetlić. Tablica texture przechowywać będzie trzy tekstury różniące się zastosowanym na nich filtrem. Pierwsza z nich (texture[0]) nie będzie filtrowana (GL_NEAREST). Druga (texture[1]) będzie używać filtrowania liniowego (GL_LINEAR), co trochę ją wygładzi. Trzecią z kolei (texture[2]) stworzymy w oparciu o tekstury mipmappowych, co sprawi, że będzie ona świetnie wyglądać.
Teraz zajmiemy się ładowaniem bitmapy i tworzeniem z niej tekstur. Użyjemy do tego celu biblioteki glaux, więc upewnij się, że masz ją załączoną w swoim projekcie przed kompilowaniem go. Z tego co wiem, Delphi i Visual C++ posiadają tą bibliotekę, nie jestem jednak pewny, co do innych języków.
[T
UWAGA: Najnowsza wersja Microsoft Visual C++, czyli 2008 (9.0) nie posiada w swoim zbiorze biblioteki glaux. Istnieją dwie metody rozwiązania tego problemu:
1. Ściągnięcie z internetu plików glaux.h oraz glaux.lib oraz wklejenie ich do odpowiednich folderów:
a) glaux.h do C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\gl\
b) glaux.lib do C:\Program Files\Microsoft SDKs\Windows\v6.0A\Lib
2. Skorzystanie z innych funkcji w celu załadowania bitmap
T]Zaraz po powyższym kodzie oraz przed definicją funkcji ReSizeGLScene(), dodamy następującą sekcję. To ten sam kod, którego użyliśmy w lekcji szóstej. Nic się nie zmieniło. Jeśli nie jesteś pewien co robią poniższe linie, przeczytaj szósty tutorial. Wyjaśnia on szczegółowo ładowanie bitmap i tworzenie z nich tekstur.
Poniższa funkcja załaduje bitmapę (wywołując kod powyżej) i stworzy z niej trzy tekstury. Zmienna Status używana jest by zapisać stan wykonywanych operacji (czy się powiodły lub nie).
Polecenie TextureImage[0]=LoadBMP("Data/Crate.bmp") wywoła dla nas przed chwilą zdefiniowaną funkcję LoadBMP() i zapisze rezultat jej wykonania w TextureImage[0]. W tym wypadku załadowana zostanie mapa bitowa o nazwie Crate.bmp, znajdująca się w folderze Data.
Teraz kiedy już mamy załadowaną bitmapę, użyjemy jej do stworzenia tekstur. Linia poniżej mówi OpenGL, że chcemy wygenerować trzy tekstury i przechować je w tablicy texture.
W szóstym tutorialu używaliśmy tekstur liniowo filtrowanych (GL_LINEAR). Wymagają nieco więcej mocy obliczeniowej, ale nie najgorzej wyglądają. Pierwsza tekstura, którą stworzymy będzie używać parametru GL_NEAREST, co oznacza brak filtrowania. Pobiera to bardzo niewiele mocy obliczeniowej, ale wygląda fatalnie. Jedyną korzyścią tego typu tekstur jest to, że aplikacje je wykorzystujące będą działać szybko nawet na słabych komputerach.
Zauważ, że ustawiamy GL_NEAREST (brak filtrowania) zarówno dla sytuacji gdy wyświetlana tekstura jest większa niż obraz, z którego została stworzona (GL_TEXTURE_MAG_FILTER) jak i dla sytuacji odwrotnej, gdy wyświetlana tekstura jest mniejsza od oryginalnego obrazka (GL_TEXTURE_MIN_FILTER). Możesz również mieszać sposoby filtrowania, dzięki czemu tekstury będą wyglądać trochę lepiej. Nas jednak interesuje szybkość działania, więc ustawiamy GL_NEAREST dla obu przypadków.
Następna tekstura, którą stworzymy jest tego samego typu, co tekstury używane przez nas w kursie szóstym - filtrowana liniowo. Jedyną rzeczą, która się zmieniła, jest to, że zapisujemy teksturę w texture[1] zamiast w texture[0], ponieważ jest to druga tekstura, która tworzymy. Gdybyśmy zapisali ją w texture[0], jak powyżej, nadpisałoby to teksturę używającą GL_NEAREST (tą pierwszą).
Teraz czas na nowy sposób tworzenia tekstur. Mipmapping! Być może zauważyłeś, że gdy wyświetlasz obrazek w pomniejszeniu, wiele szczegółów zanika. Wzory, które miały wyglądać naprawdę dobrze zaczynają wyglądać naprawdę źle. Gdy powiesz OpenGL, by stworzył mipmappową tekturę, spróbuje on stworzyć wiele tekstur wysokiej jakości w różnych rozmiarach. Gdy zechcesz wyświetlisz mipmappową teksturę na ekranie, OpenGL wybierze tą najlepiej wyglądającą z tych, które stworzył (o największej liczbie szczegółów) i wyświetli ją na ekranie nie skalując oryginalnego obrazu, co powoduj utratę jakości.
Ponieważ jest to trzecia z kolei tekstura, zapiszemy ją w texture[2].
Powiedziałem w szóstej lekcji, że istnieje limit rozmiaru obrazka z jakiego tekstura zostanie stworzona. Chodziło o to, że szerokość i wysokość obrazka muszą być potęgami dwójki (64, 128, 256 itd.). Powiedziałem również, że istnieje sposób na ominięcie tego problemu. Jest nim funkcja glBuild2DMipmaps. Możesz użyć każdej bitmapy jakiej chcesz (o dowolnym rozmiarze), gdy tworzysz mipmappowe tekstury.
Poniższa linia tworzy właśnie taką teksturę. Mówimy OpenGL, że jest to tekstura 2D używająca trzech wartości kolorów. TextureImage[0]->sizeX jest szerokością obrazka, TextureImage[1]->sizeY ? jego wysokością. GL_RGB oznacza, że używamy koloru czerwonego, zielonego i niebieskiego. GL_UNSIGNED_BYTE oznacza, że dane, które tworzą obraz są typu unsigned byte (bajt bez znaku). TextureImage[0]->data z kolei to dane mapy bitowej.
Teraz zwalniamy pamięć. Sprawdzamy czy dane bitmapy zostały zapisane w TextureImage[0]. Jeśli tak, zwalniamy je.
Ostatecznie zwracamy zmienną Status. Jeśli wszystko poszło dobrze Status będzie równy TRUE, jeśli coś się nie udało przybierze wartość FALSE.
Teraz załadujemy tekstury i zainicjalizujemy OpenGL. Pierwsza linia funkcji InitGL ładuje tekstury używając kodu, który zdefiniowaliśmy przed chwilą. Następnie włączamy mechanizm tekstur używając glEnable(GL_TEXTURE_2D). Tryb cieni ustawiony jest na miękkie cieniowanie, kolor tła ustawiony jest na czarny, aktywujemy testowanie głębi, a następnie obliczamy najlepszą perspektywę.
Teraz inicjalizujemy oświetlenie. Linia poniżej ustawi światło otaczające jako światło numer jeden (GL_LIGHT1). Na początku tego kursu zapisaliśmy jego parametry w AmbientLight.
Następnie ustawiamy światło rozproszone ponownie jako światło numer jeden, jego parametry znajdują się w tablicy DiffuseLight.
Teraz ustalimy pozycję światła. Zapisaliśmy ją w LighPosition (prosto przed przednią ścianą, 0.0f na osi x, 0.0f na osi y, dwie jednostki w kierunku widza na osi z, czyli 2.0f).
Ostatecznie aktywujemy nasze światło. Nie zobaczysz jednak jeszcze żadnego oświetlenia, ponieważ nie wywołaliśmy funkcji glEnable z parametrem GL_LIGHTING. Światło jest uaktywnione oraz ustawione na odpowiedniej pozycji, ale bez włączonego GL_LIGHTING, nie będzie ono działać.
W następnej sekcji kodu wyświetlimy naszą oteksturowaną skrzynię. W razie wątpliwości wróć do szóstego tutoriala.
Następne trzy linie ustawiają i obracają naszą skrzynię. glTranslatef umieści ją na odpowiedniej pozycji, a za pomocą GlRotatef obrócimy ją najpierw względem osi X, następnie względem osi Y. Używamy do tego wcześnie zdefiniowanych zmiennych xrot oraz yrot.
Następna linia nie zmieniła się za bardzo w porównaniu do szóstego kursu. Teraz jednak nie wybieramy texture[0], a texture[filter]. Za każdym razem kiedy naciśniemy klawisz 'F', wartość zmiennej filter zwiększy się, co pozwoli na wybranie jednej z trzech stworzonych przez nas tekstur. Jeśli zmienna ta będzie większa niż dwa, ustawimy ją na zero.
glNormal3f jest nową funkcją w moich lekcjach. Normalna jest linią wyznaczającą prosto ze środka figury kąt 90 stopni. Mówi ona OpenGL, w którą stronę zwrócony jest wielokąt, która strona jest górą itd. Gdy używasz oświetlenia musisz ją określić. Jeśli tego nie zrobisz, możesz zobaczyć dziwne rezultaty.
Patrząc na kod przedniej ściany zauważysz, że jej wartość normalnej na osi Z jest dodatnia. To oznacza, że normalna wskazuje na widza, czyli dokładnie tam gdzie chcemy. Dla tylnej ściany wartość normalnej na osi Z jest ujemna, co oznacza, że normalna wskazuje na przestrzeń przed widzem, znowu tak, jak chcemy. Jeśli nasza skrzynka obróci się o 180 stopni względem osi X lub Y przednia ściana będzie skierowana w kierunku ekranu, a tylna ściana w kierunku widza. Nie ważne która ściana skierowana jest na widza, normalna tej ściany będzie wskazywała również na niego. Ponieważ światło znajduje się blisko widza, kiedy normalna na niego wskazuje, wskazuje również na światło. Kiedy to się stanie, ściana zostanie oświetlona. Im dokładniej normalna będzie na nie wskazywała, tym jaśniej ściana zostanie oświetlona. Jeśli jednak ?wejdziesz? do środka skrzyni zauważysz, że panuje tam ciemność. Pamiętaj - Normalna wskazuje na zewnątrz, nie do wewnątrz obiektu, więc w środku skrzynki nie będzie światła, dokładnie tak jak powinno być.
Następne dwie linie zwiększają zmienne xrot i yrot o wartości zapisane w xspeed oraz yspeed. Im te wartości będą większe, tym szybciej nasze zmienne będą się zwiększały, co skutkować będzie szybszym obracaniem się skrzyni.
Teraz wchodzimy do funkcji WinMain(). Dodamy kod włączający/wyłączający światło, obracający skrzynkę, zmieniający filtry tekstur oraz poruszający skrzynkę w głąb i na zewnątrz ekranu. Gdzieś na jej końcu zobaczysz funkcję SwapBuffers(hDC). Zaraz po tej linii, dodaj następujący kod.
Jeśli lp było równe FALSE, oznacza to, że klawisz 'L' nie został naciśnięty lub został zwolniony. Takie sprawdzanie zmusza osobę do puszczenia klawisza zanim poniższy kod zostanie wywołany. Gdybyśmy tego nie sprawdzali, światło ciągle by migotało, ponieważ program myślałby, że bez przerwy naciskamy klawisz i za każdym razem przechodziłby do poniższego kodu.
Gdy lp przybierze wartość TRUE, oznacza to, że klawisz został wciśnięty, więc włączamy lub wyłączamy światło. Zmienna light może przybierać tylko dwie wartości - TRUE lub FALSE, więc jeśli wydajemy polecenie light=!light, mówimy komputerowi, że ma przestawić wartość zmiennej light na przeciwną. Oznacza to, że jeśli wartością zmiennej light było TRUE, zmienna zostanie teraz ustawiona na FALSE. Analogicznie w odwrotnym przypadku.
Teraz sprawdzamy wartość zmiennej light i w zależności od niej wykonujemy odpowiednie czynności. Jeśli zmienna ta jest równa jest FALSE, wyłączamy światło. W odwrotnym przypadku światło zostaje włączone.
Następne linie sprawdzają czy przestaliśmy naciskać klawisz 'L'. Jeśli tak, ustawiamy zmienną lp na FALSE, co oznacza, że klawisz nie jest wciśnięty. Gdybyśmy nie sprawdzali czy klawisz jest zwolniony, bylibyśmy w stanie włączyć światło raz, lecz komputer wciąż myślałby, że 'L' jest ciągle wciśnięte i nie pozwoliłby nam wyłączyć światła z powrotem.
Teraz dodamy obsługę klawisza 'F'. Jeśli został on naciśnięty i nie jest przytrzymywany, zmienna fp przybierze wartość TRUE, co oznaczać będzie, że klawisz 'F' jest teraz wciśnięty. To zwiększy o jeden zmienną zwaną filter. Jeśli zmienna filter będzie większa od 2, zresetujemy ją, nadając jej z powrotem wartość zero.
Następne linie sprawdzają czy nacisnęliśmy klawisz 'Page Up'. Jeśli tak , zmniejszamy wartość zmiennej z, przez co przesuwamy skrzynię w głąb ekranu. Dzieje się tak, ponieważ wywołujemy glTranslatef(0.0f,0.0f,z) w funkcji DrawGLScene.
Te cztery linie sprawdzają stan klawisza 'Page Down'. Jeśli jest on wciśnięty zwiększamy wartość zmiennej z, poruszając tym samym skrzynię w kierunku widza.
Teraz sprawdzamy wciśnięcia strzałek na klawiaturze. Naciskając strzałkę w lewo lub w prawo, zmienna xspeed jest zwiększa lub zmniejszana. Naciskając strzałkę w górę lub w dół, zmniejszana/zwiększana jest zmienna yspeed. Powiedziałem wcześniej, że im większa jest jedna z tych zmiennych, tym szybciej skrzynia będzie obracać się wokół określonej osi.
Jak we wszystkich poprzednich tutorialach, upewnij się, że tytuł okna jest właściwy.
Po zakończeniu lektury tego kursu powinieneś umieć tworzyć i integrować z wysokiej jakości, oteksturowanymi obiektami, rozumieć korzyści z używania trzech odmiennych filtrów tekstur, posiadać umiejętność korzystania z klawiatury oraz potrafić stworzyć proste oświetlenie dla twojej sceny, tworząc ją bardziej realistyczną.