Na początek chciałem powiedzieć, że jestem bardzo dumny z tego tutoriala :) Kiedy Jonathan de Blok podsunął mi pomysł napisania programu obsługującego odtwarzanie plików AVI w OpenGL, nie miałem pojęcia jak otworzyć taki plik, nie mówiąc już o napisaniu odtwarzacza AVI. Pierwsze co zrobiłem było przeszukanie mojej kolekcji książek dotyczących programowania. W żadnej z nich nie było wzmianki o plikach AVI. Potem przeczytałem wszystko co było do przeczytania o formacie AVI w MSDN. Znalazłem tam mnóstwo przydatnych informacji, ale nie dostarczały one całej wiedzy potrzebnej do napisania mojego AVI playera.
Godziny przeszukiwania Internetu w poszukiwaniu kodów źródłowych wykorzystujących format AVI zaowocowały znalezieniem tylko dwóch stron o tej tematyce. Nie chcę powiedzieć, że moje umiejętności szukania informacji są niesamowite, ale do tej pory w 99.9% przypadków nie miałem najmniejszego problemu ze znalezieniem informacji na dowolny temat, który mnie interesował. Byłem bardzo zadziwiony, kiedy zorientowałem się, jak mało informacji o plikach AVI było zamieszczonych w sieci. Na dodatek większość z tego, co znalazłem nie chciała się skompilować... Tylko garstka ze znalezionych kodów źródłowych była napisana porządnie (przynajmniej według mnie). Duża część z nich działała, ale była napisana w VB, Delphi, itd. (nie VC++).
Pierwsza z sensownych stron na jakie się natknąłem zawierała artykuł Jonathana Nix'a zatytyłowany "pliki AVI" [AVI Files]. Możecie na nią zajrzeć pod adresem: http://www.gamedev.net/reference/programming/features/avifile/ . Wielkie brawa dla Jonathana za napisanie tak świetnego dokumentu opisującego format AVI. Jednak zdecydowałem napisać mój programik po swojemu, a jego fragmenty kodu i czytelne komentarze bardzo ułatwiły mi to zadanie. Następna znaleziona strona była zatytułowana "przegląd formatu AVI" [The AVI Overview] autorstwa Johna F. McGowan'a, Ph.D.. Mógłbym długo się rozwodzić nad wartościami merytorycznymi strony John'a, ale o wiele lepiej będzie jeśli po prostu sami się o nich przekonacie! Adres strony: http://www.jmcgowan.com/avi.html . Wyjaśnia ona ogromną większość z zagadnień związanych z formatem AVI! Wielkie dzięki John'owi za udostępnienie tak cennej strony szerszej publice.
Ostatnią rzeczą, o której chciałem wspomnieć jest fakt, że ani jedna linijka kodu nie została zaczerpnięta ani tym bardziej skopiowana z jakichkolwiek źródeł. Pisałem go przez trzy dni szalonego siedzenia przy komputerze, przy użyciu informacji z wyżej wymienionych źródeł. Myślę, że warto przyznać się, że w związku z tym, mój kod może nie być najlepszym sposobem odtwarzania plików AVI. Być może nie jest to nawet prawidłowa metoda odtwarzania tychże plików, ale działa i jest łatwa w użyciu! Jeśli nie podoba ci się mój kod, mój styl pisania, lub jeśli uważasz, że poprzez publikowanie tego tutoriala krzywdzę społeczność programistów, to masz kilka wyjść: 1) poszukaj w Internecie innych źródeł wiedzy na temat plików AVI 2) napisz swój własny odtwarzacz plików AVI lub 3) napisz lepszy tutorial! Każdy, kto wchodzi na tą stronę powinien już wiedzieć, że jestem średnim programistą ze średnimi umiejętnościami (wspominałem o tym już na wielu innych stronach NeHe)! Programuję dla przyjemności! Celem tej strony jest ułatwienie życia ludziom, którzy nie należą do elity programistów, aby łatwiej im było rozpocząć swoją przygodę z OpenGL. Tutoriale na tej stronie opisują po prostu sposób, w jaki udało mi się osiągnąć konkretny efekt... Nic więcej, nic mniej!
Przejdźmy wreszcie do kodu...
Pierwszą rzeczą, jaką zauważysz jest to, że dołączamy (dyrektywa #include) i linkujemy nagłówek i bibliotekę Video dla Windows (Video For Windows). Wielkie podziękowania dla Microsoft'u (nie mogę uwierzyć że właśnie to powiedziałem :). Ta biblioteka sprawia, że otwieranie i odtwarzanie plików AVI to pestka! Ale póki co... Wszystko czego potrzebujesz wiedzieć to to, że MUSISZ dołączyć plik nagłówkowy "vfw.h" i musisz dołączyć bibliotekę vfw32.lib jeśli chcesz, żeby twój kod się kompilował :)
Teraz definiujemy zmienne. Kąt będzie użyty do obracania naszych obiektów dookoła zależnie od czasu jaki upłynął. Dla uproszczenia ten sam kąt zostanie przyjęty dla wszystkich obrotów.
zmienna next jest liczbą całkowitą [integer], która będzie przechowywała wartość czasu, jaki upłynął (w milisekundach). Użyjemy jej do utrzymania ilości FPS [frames per second - ilość klatek na sekundę, framerate] na stałej prędkości. W dalszej części tutoriala wrócimy jeszcze do tego zagadnienia.
frame to oczywiście numer aktualnie wyświetlanej klatki [frame]. Zaczniemy od klatki 0 (naszej pierwszej klatki). Myślę, że można założyć, że jeśli uda nam się otworzyć plik z animacją/filmem, to ma on co najmniej jedną klatkę animacji :)
effect to numer efektu aktualnie wyświetlanego na ekranie (obiekt: sześcian, sfera, cylinder, nic). env to wartość boolean. Jeśli jest ustawiona na 'true' to mapowanie środowiskowe jest włączone, jeśli jest ustawiona na 'false', obiekt NIE będzie mapowany środowiskowo. Jeśli bg jest ustawione na 'true', zobaczysz animację w trybie pełnoekranowym za obiektem. Jeśli jest ustawiona na 'false' zobaczysz tylko obiekt (nie będzie tła).
Zmienne sp, ep i bp pozwolą nam się upewnić, że użytkownik nie trzyma przytrzymanego klawisza.
W dalszej części kodu struktura psi będzie przechowywała informacje o naszym pliku AVI. pavi jest wskaźnikiem na bufor, który otrzymuje nowy uchwyt strumienia w momencie gdy plik AVI został otwarty. pgf to wskaźnik na nasz obiekt GetFrame. bmih będzie przez nas używany do konwersji klatki animacji na format jakiego chcemy użyć przy wyświetlaniu (zawiera info nagłówka bitmapy opisujące format, w jakim została zapisana). lastframe będzie przechowywać numer ostatniej klatki animacji AVI. width i height będą przechowywać wielkości strumienia AVI i w końcu... pdata to wskaźnik na dane obrazu, które otrzymamy poprzez wyłuskanie klatki animacji z pliku AVI! mpf będzie użyty do obliczania przez ile milisekund jest wyświetlana każda klatka. Później napiszę o tym więcej.
W tym tutorialu stworzymy dwie różne kwadryki (sferę [czyli kulę bez środka, sama powłoka] i cylinder) przy użyciu biblioteki GLU. quadratic jest wskaźnikiem na nasz obiekt.
hdd to wskaźnik na kontekst urządzenia DrawDib (po prostu wskaźnik na naszą bitmapę). hdc to uchwyt na kontekst urządzenia.
hBitmap to wskaźnik na zależną od urządzenia bitmapę [od tłumacza - nie jestem pewien czy to wierny przekład, w oryginale - device dependant bitmap] (użytą w procesie konwersji nieco później).
data to wskaźnik, który będzie wskazywał na bufor pixeli naszej przekonwertowanej bitmapy. To wszystko wyjaśni się później w kodzie. Nie przestawajcie czytać :)
Teraz zajmiemy się przez chwilę assemblerem. Nie przejmujcie się jeśli nigdy wcześniej nie używaliście tego języka. Kod może wyglądać groźnie, ale w rzeczywistości jest bardzo prosty!
Pisząc ten tutorial odkryłem coś bardzo dziwnego. Pierwszy plik video jaki odpaliłem przy pomocy mojego programu był odtwarzany prawidłowo, ale miał pomieszane kolory. Wszystko co powinno być czerwone, było niebieskie, a wszystko co powinno być niebieskie było czerwone. Zupełnie nie wiedziałem co się stało! Byłem przekonany, że zrobiłem jakiś błąd w kodzie. Przejrzałem od początku cały kod, ale wciąż nie mogłem znaleźć żadnej pomyłki! Zacząłem od nowa przeszukiwać MSDN. Czemu bajty z kolorem czerwonym i niebieskim mogą być zamienione? W MSDN było wyraźnie napisane, że 24 bitowe bitmapy mają format RGB!!! Po jakimś czasie czytania MSDN zorientowałem się w czym tkwi problem - w Windows! Windows przechowuje dane odwrotnie - format BGR. W OpenGL RGB oznacza po prostu... RGB!
Po kilku mailach od fanów Microsoftu :) zdecydowałem się umieścić w tutorialu małe sprostowanie. Nie narzekam na Microsoft dlatego, że dane w formacie RGB są przechowywane od tyłu. Po prostu to bardzo frustrujące, gdy coś jest nazwane RGB, podczas gdy tak na prawdę w pliku jest to przechowywane jako BGR!
To ma chyba coś wspólnego z "[little endian*]" i "[big endian*]". Intel i układy z nim kompatybilne używają [little endian] - najmniej znaczący bajt [left sygnificant byte - LSB] jest przechowywany jako pierwszy. OpenGL pochodzi od maszyn z Silicon Graphics, które prawdopodobnie używają [big endian] i dlatego OpenGL standardowo wymaga aby bitmapa maiła format big endian. Myślę, że w taki sposób to działa.<br>
[od tłumacza - *: 'little endian' i 'big endian' - nigdy wcześniej nie spotkałem się z tymi pojęciami - zdaje się, że chodzi po prostu o sposób przechowywania informacji - "Intel and Intel compatibles use little endian where the least significant byte (LSB) is stored first" (na logikę wydaje się, że powinno być odwrotnie).]
Świetnie! W takim razie mamy odtwarzacz, który działa jak kompletny szmelc! Pierwszym rozwiązaniem, o jakim pomyślałem była ręczna zamiana bajtów za pomocą zwykłej pętli for. Działało, ale było bardzo wolne. Mając kompletnie dosyć, zmodyfikowałem kod generowania tekstur tak, aby używał GL_BGR_EXT zamiast GL_RGB. Ogromny przyrost prędkości, a kolory wyglądały świetnie! Więc mój problem był rozwiązany... a przynajmniej tak mi się wydawało! Okazało się, że niektóre sterowniki OpenGL miały problemy z obsługą GL_BGR_EXT... Byłem znowu w punkcie wyjścia :(
[od tłumacza - mówiąc szczerze nigdy na żadnej karcie nie spotkałem się z problemami z obsługą GL_BGR_EXT].
Po rozmowie z moim wielkim przyjacielem Maxwell'em Sayles, zdecydowałem się skorzystać z jego rady i zamienić bajty używając kodu assemblera. Kilka minut później otrzymałem od niego przez ICQ kod, który znajduje się poniżej. Być może nie jest on zoptymalizowany, ale jest szybki i działa!
Każda klatka animacji jest przechowywana w buforze. Wielkość obrazka będzie zawsze taka sama: 256 pixeli szerokości, 256 pixeli wysokości i 1 bajt na kolor (3 bajty na każdy pixel). Poniższy kod będzie 'szedł' przez cały bufor zamieniając bajt czerwony z niebieskim. Czerwony jest przechowywany na pozycji ebx+0 a niebieski ebx+2. Będziemy przesuwać w buforze za każdym razem 3 bajty (ponieważ kolor każdego pixela jest przechowany jako 3 bajty). 'Przejdziemy' przez cały bufor zanim wszystkie bajty koloru zostaną zamienione miejscami.
Kilku z was było niezadowolonych z powodu użycia kodu assemblera, więc zdecydowałem się wytłumaczyć dlaczego został on użyty w tym tutorialu. Początkowo planowałem użycie GL_BGR_EXT, które jak już wspomniałem doskonale działało. Ale nie na wszystkich kartach! Później zdecydowałem się użyć metody zamiany z poprzedniego tutoriala (bardzo schludny kod zamiany oparty na XOR). Ten sposób rozwiązania problemu działa na każdym komputerze, ale nie jest wystarczająco szybkie. To prawda, że w poprzednim tutorialu sprawowało się doskonale, ale tym razem mamy do czynienia z filmikiem, który będzie wyświetlany w czasie rzeczywistym. Jeśli dążymy do szybkiej zamiany bajtów, możemy ją uzyskać. Po rozważeniu wszystkich możliwych opcji doszedłem od wniosku, że użycie assemblera jest najlepszą możliwością! Jeśli masz lepszy pomysł na kod, który działałby w ten sam sposób powinieneś go użyć. Nie mówię ci, w jaki sposób MUSISZ robić określone rzeczy, po prostu pokazuję ci jak ja sobie z nimi poradziłem. Ponadto opisuje ze szczegółami jak działa mój kod. Dlatego jeśli chcesz zastąpić mój kod twoim, który działa lepiej, a wiesz dokładnie co robi mój kod zamieszczony poniżej, po prostu znajdź alternatywne rozwiązanie problemu, i jeśli chcesz napisz to po swojemu!
Poniższy kod otwiera plik AVI w trybie odczytu. szFile to nazwa pliku, który chcemy otworzyć. title[100] będzie używany do zmieniania tytułu okienka (żeby pokazć informacje o pliku AVI).
Pierwszą rzeczą, jaką musimy zrobić jest wywołanie funkcji AVIFileInit(). Służy ona do inicjalizacji biblioteki pliku AVI.
Są różne metody otwierania pliku AVI. Ja zdecydowałem się użyć AVIStreamOpenFromFile(...). Ta funkcja otwiera pojedynczy strumień z pliku AVI (pliki AVI mogą zawierać wiele strumieni).
Parametry funkcji są następujące: pavi to wskaźnik na bufor, który przyjmuje nowy uchwyt strumienia. szFile to oczywiście nazwa pliku, który chcemy otworzyć (razem ze ścieżką). Trzeci parametr to typ strumienia, jaki chcemy otworzyć. W naszym projekcie interesuje nas tylko strumień VIDEO (streamtypeVIDEO) [typstrumieniaVIDEO]. Wartość czwartego parametru to 0. To oznacza, że chcemy otworzyć pierwszy zapisany w pliku strumień VIDEO (plik AVI może zawierać kilka strumieni video... w takim wypadku chcemy otworzyć pierwszy z nich). OF_READ oznacza, że chcemy otworzyć plik tylko do odczytu. Ostatnim parametrem jest wskaźnik na identyfikator klasy handler'a [od tłumacza - od handle - uchwyt]. Mówiąc szczerze, nie mam pojęcia do czego to służy. Pozwalam windows'owi wybrać to za mnie podając NULL jako parametr!
[od tłumacza - dokładny opis parametrów funkcji AVIStreamOpenFromFile znajduje się oczywiście w MSDN].
Jeśli przy otwieraniu pliku wystąpiły jakiekolwiek błędy, wyświetlony zostaje message box, który informuje nas, że strumień nie mógł zostać otworzony. Jeśli wystąpił jakiś błąd, to nigdzie nie wysyłam informacji o nim, więc program będzie starał się działać dalej. Dodanie sprawdzanie błędów nie powinno kosztować cię wiele wysiłku, ja byłem za leniwy :)
Skoro do tej pory program się nie wysypał, możemy założyć, że plik oraz strumień zostały otwarte! Pora dowiedzieć się czegoś o pliku AVI przy użyciu funkcji AVIStreamInfo(...).
Wcześniej utworzyliśmy obiekt struktury AVISTREAMINFO, który nazwaliśmy psi. Teraz uzupełnimy tą strukturę danymi o pliku AVI (pierwsza linijka kodu). Wszystkie informacje począwszy od szerokości strumienia (w pixelach), aż po ilość klatek na sekundę animacji jest przechowywana w strukturze psi. Dla tych z was, którym zależy na dokładnych [prawidłowych] prędkościach odtwarzania, zapamiętajcie to o czym właśnie powiedziałem. Aby dowiedzieć się więcej o AVIStreamInfo, zajrzyjcie do MSDN.
Możemy obliczyć szerokość klatki poprzez odjęcie od pozycji prawej krawędzi pozycję lewej. Wynikiem powinna być dokładna szerokość wyrażona w pixelach. Aby uzyskać szerokość, odejmujemy pozycję górnej krawędzi od pozycji dolnej.
Chcemy uzyskać numer ostatniej klatki pliku AVI - używamy do tego AVIStreamLength(...). Funkcja zwraca numer klatek zapisanych w pliku z animacją. Wynik jest przechowywany w zmiennej lastframe.
Obliczenie ilości klatek na sekundę jest dość proste. Ilość klatek na sekundę = psi.dwRate / psi.dwScale. Wynik powinien zgadzać się z informacją o ilości klatek na sekundę otrzymaną poprzez kliknięcie prawym przyciskiem na plik AVI aby sprawdzić jego właściwości. Spytacie: dobrze, ale co to ma wspólnego z mpf? Kiedy pierwszy raz pisałem kod animacji, spróbowałem użyć wartości FPS do wybrania konkretnej klatki animacji. Pojawił się wtedy problem... Wszystkie filmiki były odtwarzane za szybko! Sprawdziłem właściwości pliku video. Plik face2.avi miał 3.36 sekund długości. Ilość FPS'ów wynosiła 29.974 klatek na sekundę. Plik miał 91 klatek animacji. Jeśli pomnożycie 3.36 przez 29.974 wyjdzie wam 100 klatek animacji. Bardzo dziwne!
Dlatego właśnie zdecydowałem się napisać całość w inny sposób. Zamiast liczyć FPS'y, wyliczam jak długo powinna być wyświetlana każda klatka. AVIStreamSampleToTime() konwertuje aktualną pozycję animacji na ilość milisekund potrzebną od otworzenia pliku do 'dojścia' do aktualnie odtwarzanej klatki. Tak więc liczymy ile czasu (w milisekundach) trwa odtwarzanie video poprzez uzyskanie czasu ostatniej klatki animacji (lastframe). Wówczas dzielimy wynik przez liczbę wszystkich klatek animacji. To nam daje czas, przez jaki wyświetlana jest każda klatka (w milisekundach). Umieszczamy rezultat w mpf (milisekundy na klatkę). Możesz również policzyć milisekundy na klatkę poprzez uzyskanie czasu odtwarzania tylko jednej klatki animacji za pomocą następującego kodu: AVIStreamSampleToTime(pavi,1). Ta metoda również powinna działać poprawnie! Wielkie podziękowania dla Alberta Chaulk'a za pomysł!
Powód dla którego używam terminu milisekundy na klatkę: mpf jest liczbą całkowitą, więc każda wartość ułamkowa zostanie przybliżona.
Ponieważ OpenGL wymaga, aby wysokość i szerokość tekstur były potęgami dwójki, a większość filmików ma wymiary 160x120, 320x240 albo jeszcze inne, potrzebujemy szybkiej metody, aby przeskalować video "w locie" na format, którego będziemy mogli użyć jako tekstury. Aby tego dokonać wykorzystamy specyficzne funkcje Windowsowej bitmapy Dib.
Pierwszą rzeczą, jaką należałoby opisać są parametru obrazu, jaki chcemy otrzymać. Aby to zrobić wypełniamy nagłówek bitmapy [BitmapInfoHeader] - strukturę bmih z parametrami, jakich żądamy. Zaczniemy od ustawienia rozmiaru naszej struktury. Ustawiamy 'bitplanes' na 1. Trzy bajty danych oznaczają 24 bity (RGB). Chcemy, aby nasz filmik miał wymiary 256x256 pixeli. Nie chcemy kompresji obrazu - ustawiamy zatem pole biCompression na BI_RGB.
CreateDIBSection tworzy bitmapę niezależną od platformy [od tłumacza - dib - device independent bitmap], do którego możemy bezpośrednio zapisywać dane. Jeśli nie napotkaliśmy na jakiś błąd, hBitmap będzie wskazywać na obszar przechowujący bajty kolorów poszczególnych pixeli naszej bitmapy. hdc jest uchwytem na kontekst urządzenia (DC). Drugim parametrem jest wskaźnik na strukturę BitmapInfo. Zawiera ona wyżej wymienione informacje o pliku bitmapy. Trzeci parametr (DIB_RGB_COLORS) określa, że bufor pixeli zawiera dane w formacie RGB. data to wskaźnik na zmienną, której będzie przypisany adres bufora pixeli naszej bitmapy. Ustawiamy piąty parametr na NULL, aby system zaalokował pamięć na naszą bitmapę. Ostatnim parametr może zostać pominięty (lub tak jak w kodzie poniżej ustawiony na NULL).
Cytat z MSDN: Funkcja SelectObject przypisuje obiekt konkretnemu kontekstowi urządzenia (DC).
Zatem stworzyliśmy bitmapę, po której możemy bezpośrednio rysować. Cudownie :)
Jest jeszcze kilka rzeczy do zrobienia zanim będziemy mogli odczytywać poszczególne klatki z pliku AVI. Powinniśmy teraz przygotować nasz program do dekompresowania klatek video z pliku AVI. Użyjemy do tego funkcji AVIStreamGetFrameOpen(...).
Możesz przesłać strukturę podobną do tej powyżej jako drugi parametr tej funkcji, aby uzyskać konkretny format video. Niestety, jedyną rzeczą jaką możesz zmienić jest szerokość i wysokość zwracanego obrazu. W MSDN znajduje się również informacja, że możesz podać AVIGETFRAMEF_BESTDISPLAYFMT aby uzyskać najlepszy format wyświetlania. Co dziwne mój kompilator nie definiował tej stałej [od tłumacza - ...więc teoretycznie nie można było jej używać - problem nieaktualnych plików nagłówkowych. Pamiętaj, że im nowszy kompilator, tym nowsze pliki nagłówkowe].
Jeśli wszystko pójdzie zgodnie z planem, obiekt GETFRAME jest zwracany (będziemy z niego czytać dane każdej klatki filmu). Jeśli wystąpiły jakieś błędy, zostanie wyświetlony MessageBox z informacją o błędzie.
Poniższy kod wypisuje w pasku tytułowym okna szerokość, wysokość i numer ostatniej klatki. Wyświetlamy tytuł okienka funkcją SetWindowText(...). Otwórz program w trybie okienkowym, aby zobaczyć co robi poniższy kod.
Teraz zaczyna się zabawa... pobieramy ilość klatek pliku AVI i konwertujemy ją na image size / color depth. lpbi będzie przechowywać informacje z nagłówka bitmapy dla klatki animacji. Zrobiliśmy w ten sposób w drugiej linijce kodu kilka rzeczy na raz. Po pierwsze uzyskaliśmy numer klatki animacji... jest ona teraz zapisana w zmiennej frame. To uzupełni dane klatki animacji i przypisze lpbi informacje nagłówkowe dla tej klatki.
Dalsza część zabawy :) potrzebujemy wskazać dane pixeli obrazka. Aby tego dokonać musimy pominąć informacje nagłówkowe (lpbi->biSize). Zanim zacząłem pisać ten tutorial nie zdawałem sobie również sprawy z tego, że musimy pominąć również informacje o kolorze. W tym celu również przesuwamy nasz wskaźnik do przodu o ilość użytych kolorów razy rozmiar struktury RGBQUAD (biClrUsed * sizeof(RGBQUAD)). Po tym wszystkim zostajemy ze wskaźnikiem do danych obrazka (pdata).
Teraz musimy przekonwertować klatkę animacji do rozmiaru tekstury, a także zamienić dane video na dane w formacie RGB [pamiętasz nasz kod assemblera :]. Użyjemy do tego funkcji DrawDibDraw(...).
Krótkie wyjaśnienie. Możemy rysować bezpośrednio na naszej bitmapce, to właśnie robi DrawDibDraw(...). Pierwszym parametrem jest uchwyt na kontekst urządzenia naszej bitmapy. Drugi parametr to uchwyt na kontekst urządzenia. Potem podajemy współrzędne 2D górnego lewego rogu (0,0) obrazka docelowego i dolnego prawego rogu obrazka docelowego.
lpbi to owskaźnik na bitmapinfoheader (nagłówek bitmapy) dla klatki, którą właśnie wczytaliśmy. pdata to wskaźnik na bufor pixeli klatki, którą wczytaliśmy.
Potem podajemy współrzędne 2D górnego lewego rogu (0,0) obrazka źródłowego (klatki, którą właśnie wczytaliśmy) i prawego dolnego rogu klatki (szerokość klatki, wysokość klatki). Ostatnim parametrem powinno być 0.
To spowoduje przekonwertowanie obrazka o dowolnych rozmiarach i głębi kolorów na obrazek 256x256x24 bity.
Mamy już naszą klatkę animacji, ale bajty przechowujące informacje o kolorze czerwonym i niebieskim są ze sobą zamienione miejscami. Aby rozwiązać ten problem, przeskakujemy do naszego szybkiego kodu funkcji flipIt(...). Pamiętaj, że data to wskaźnik na zmienną, która przechowuje wskaźnik na pozycję wartości pixeli bitmapy w pamięci. To znaczy, że po wywołaniu DrawDibDraw, data będzie wskazywać na przeskalowane (256x256)/ zmodyfikowane (24bity) dane pixeli.
Początkowo aktualizowałem teksturę poprzez tworzenie jej od nowa w każdej klatce animacji. Otrzymałem kilka maili, których autorzy radzili mi użyć funkcji glTexSubImage2D(). Przeglądając "OpenGL: Księga Eksperta" ["OpenGL: The Red Book"] natknąłem się na następujący tekst: "Tworzenie tekstury może wymagać więcej obliczeń niż modyfikowanie już istniejącej. W OpenGL w wersji 1.1 [i wyższych] jest kilka nowych metod, aby zastąpić całą lub część tekstury nowymi informacjami. To może być pomocne w niektórych typach aplikacji, takich jak te które w czasie rzeczywistym przechwytują obrazy video jako tekstury. Dla takich aplikacji warto jest stworzyć jedną teksturę i przy użyciu glTexSubImage2D() nieustannie zamieniać dane tekstury na te utrzymane z nowej klatki video".
Osobiście nie zauważyłem większego wzrostu wydajności, ale na wolniejszych kartach różnica może być widoczna! Parametry glTexSubInage2D() są następujące: nasz cel, jakim jest tekstura dwuwymiarowa (GL_TEXTURE_2D). Poziom szczegółowości (0), używany do mipmappingu. Współrzędne x (0) i y (0), które mówią OpenGL od którego punktu zaczynać zapisywać nowe dane na texturze (0,0 to lewy dolny wierzchołek tekstury). Potem mamy szerokość obrazka, który chcemy przekopiować i który ma 256 pixeli szerokości i 256 wysokości. GL_RGB jest formatem, w jakim są zapisane nasze dane. Kopiujemy unsigned bytes [od tłumacza - bajty bez znaku - nieujemne, typ danych]. W końcu... wskaźnik na nasze dane nowej tekstury. Bardzo proste!
Kevin Rogers pisze: Chciałem zwrócić uwagę na inny, ważny powód użycia glTexSubImage2D(). Nie tylko jest to szybszy sposób dla wielu programów OpenGL, ale także wielkość obrazka nie musi być potęgami dwójki. To jest szczególnie ważne dla odtwarzania filmów w ich domyślnych wymiarach, ponieważ rozmiar klatek jest dość rzadko potęgami dwójki (najczęściej jest to coś w rodzaju 320x200). To daje ci możliwość odtwarzania strumienia video w jego oryginalnych wymiarach.
[od tłumacza - no nie bardzo - tekstury jako potęgi dwójki nie są ograniczeniem OpenGL (zauważmy że jest to również jeden z wymogów DirectX'a) a raczej karty graficznej. Zazwyczaj po podaniu tekstury o wymiarach 320x200 jest ona i tak przeskalowana przez kartę do najbliższych rozmiarów będących potęgami dwójki (w tym wypadku byłoby to 256x256), przy czym zaokrąglenie odbywa się zawsze w dół. Tak więc możemy wyświetlić teksturę na obszarze 320x200, ale będzie ona rozmazana, ponieważ jej oryginalne wymiary będą dużo mniejsze. Pierwsza karta, która potrafi obsługiwać tekstury nie będące potęgami dwójki opuściła taśmę produkcyjną w 2005 roku, więc wciąż nie powinno się używać 'niewymiarowych' tekstur (piszę te słowa 09.2005). Z drugiej strony w jakiś sposób odtwarzacze filmów radzą sobie z wyświetlaniem obrazu w niestandardowych wymiarach - pytanie - w jaki sposób zostało to osiągnięte?]
Należy również podkreślić, że NIE MOŻESZ modyfikować textury jeśli jej najpierw nie stworzyłeś! Tworzymy textury wewnątrz funkcji Initialize()!
Chciałem również wspomnieć... Jeśli planowałeś użyć więcej niż jednej tekstury w twoim projekcie, upewnij się, że powiązałeś teksturę, którą chcesz zmodyfikować [glBindTexture(...)]. Jeśli tego nie zrobiłeś może się skończyć modyfikowaniem tekstur, których wcale nie chcesz modyfikować!
Następna funkcja jest wywoływana, w czasie zakończenia pracy programu. Zamykamy nasz DrawDib DC, oraz zwalniamy zaalokowane zasoby. Potem zwalniamy zasoby AVIGetFrame. Na końcu zamykamy strumień i plik.
Inicjalizacja jest dość oczywista. Ustawiamy początkowy kąt na 0. Potem otwieramy bibliotekę DrawDib (która zwraca DC). Jeśli wszystko pójdzie prawidłowo, hdd staje się uchwytem na świeżo utworzony kontekst urządzenia.
Nasz kolor czyszczenia ekranu to czarny, testowanie głębi jest włączone, itd.
Teraz tworzymy nową kwadrykę. quadratic to wskaźnik na nasz nowo utworzony obiekt. Ustawiamy wektory normalne tak aby obiekt wyglądał na gładki, włączamy generację współrzędnych tekstur dla naszej kwadryki.
W następnej części kodu włączamy mapowanie dla tekstur 2D, ustawiamy filtrowanie tekstur na GL_NEAREST (szybkie, ale brzydko wygląda) i inicjalizujemy mapowanie sferyczne (aby stworzyć efekt mapowania środowiskowego). Pobaw się filtrami. Jeśli masz dobry sprzęt spróbuj użyć GL_LINEAR dla ładniejszego efektu.
Po ustawieniu naszej tekstury i mapowania sferycznego, otwieramy plik .AVI. Plik, który mamy zamiar otworzyć nosi nazwę face2.avi i jest umieszczony w folderze data.
Ostatnią rzeczą jaką musimy zrobić jest stworzenie naszej początkowej tekstury. Musimy to zrobić, aby móc korzystać z glTexSubImage2D() aby umożliwić sobie modyfikowanie tekstury w funkcji GrabAVIFrame().
Przy zamykaniu programu, wywołujemy funkcję CloseAVI(). Jej celem jest prawidłowe zamknięcie pliku AVI i zwolnienie wszystkich użytych zasobów.
Tutaj sprawdzamy czy nie został naciśnięty jakiś przycisk na klawiaturze oraz aktualizujemy nasz obrót (kąt) na podstawie czasu jaki upłynął. Po tylu tutorialach nie powinienem wyjaśniać szczegółowo poniższego kodu. Sprawdzamy, czy spacja została naciśnięta. Jeśli tak, zwiększamy zmienną effect. Mamy trzy efekty (sześcian, sfera, cylinder), więc jeśli wybrano czwarty efekt nic nie jest rysowane... tylko tło! Jeśli jesteśmy na czwartym efekcie i naciśnięto spację, wracamy z powrotem do pierwszego efektu (effect = 0). Tak, wiem że powinienem nazwać tą zmienną OBJEKT :)
Sprawdzamy, czy klawisz 'B' jest naciśnięty, jeśli tak, to przełączamy efekt tła (bg) z włączonego na wyłączony lub z wyłączonego na włączony.
Mapowanie środowiskowe jest włączane w ten sam sposób. Sprawdzamy, czy klawisz 'E' jest naciśnięty. Jeśli tak, przełączamy env z TRUE na FALSE lub z FALSE na TRUE.
Kąt jest zwiększany o malutką wartość każdorazowo gdy wywołana zostanie funkcja Update(). Podzieliłem zwiększany czas przez 60.0f aby zmniejszyć szybkość animacji.
W pierwszej wersji tego tutoriala wszystkie pliki AVI były odtwarzane z taką samą prędkością. Od tamtego czasu tutorial został przepisany, a kod poprawiony, tak aby odtwarzał pliki video z właściwą prędkością. next jest zwiększane o ilość milisekund od momentu ostatniego wywołania funkcji. We wcześniejszym fragmencie tutoriala liczyliśmy jak długo powinna być na ekranie wyświetlana każda klatka animacji (w milisekundach) - zmienna mpf. Aby obliczyć aktualną klatkę, dzielimy czas jaki upłynął (next) przez czas wyświetlania każdej klatki (mpf).
Następnie sprawdzamy, czy nie doszliśmy do końca ostatniej klatki filmu. Jeśli tak frame oraz zegar animacji (next) są ustawiane na 0 i cały film jest odtwarzany od początku.
Kod poniżej powoduje, że jeśli twój komputer jest zbyt wolny lub inny program wykorzystuje intensywnie procesor, to część klatek będzie pomijanych. Jeśli chcesz, aby każda klatka filmiku była wyświetlana niezależnie od szybkości komputera użytkownika, możesz sprawdzać czy next jest większe niż mpf, jeśli tak, ustawiasz next na 0 i zwiększasz frame o jeden. Takie rozwiązanie również będzie działać, jednak poniższy kod jest lepszy dla szybszych komputerów.
Jeśli czujesz się na siłach, spróbuj dodać przewijanie do tyłu i do przodu, pauzę albo odtwarzanie od tyłu!
Teraz trochę kodu rysowania na ekranie :) Czyścimy bufor ekranu i głębokości. Potem pobieramy klatkę animacji. Przesyłasz klatkę animacji do funkcji GrabAVIFrame(). Całkiem proste! Oczywiście, jeśli chcesz odtwarzać wiele plików AVI będziesz musiał w tej funkcji podać ID tekstury.
Kod umieszczony poniżej sprawdza czy chcemy wyrenderować obrazek tła. Jeśli bg ma wartość TRUE to resetujemy macierz widoku modelu [modelview matrix] i rysujemy pojedynczy quad wielkości całego ekranu pokryty teksturą (naszą klatką animacji). Quad jest rysowany 10 jednostek w głąb ekranu, więc jest rysowany za naszym obiektem.
Poniższy kod dodałem na ostatnią chwilę. Powoduje on obrót wokół osi x i y (na podstawie wartości zmiennej angle) a potem dokonuje translacji od dwie jednostki na osi z. To przesuwa nas dalej od centrum ekranu. Jeśli usuniesz trzy linijki z kodu poniżej, obiekt będzie wirował na środku ekranu. W kodzie poniżej obiekty zarówno się obracają, jak i dokonywane są na nich małe przesunięcia.
Jeśli nie rozumiesz obrotów i translacji... nie powinieneś czytać tego tutoriala :)
Następny fragment kodu sprawdza który efekt (obiekt) chcemy wyświetlić. Jeśli wartość efektu wynosi 0, robimy kilka obrotów i renderujemy sześcian. Obroty powodują obrót sześcianu wokół osi x, y, z. Do tej pory powinieneś mieć kod tworzący sześcian wypalony w twoim umyśle :)
Oto fragment kodu, w którym rysujemy sferę. Zaczynamy określając kilka obrotów wokół osi x,y,z. Kiedy narysujemy sferę, będzie ona miała promień 1.3f, z 10 pasami pionowymi i 20 poziomymi. Zdecydowałem się na dwadzieścia w poziomie i w pionie, ponieważ nie chciałem mieć idealnie gładkiej sfery. Użycie mniejszej ilości pasów trójkątów nada sferze bardziej kanciasty wygląd (mniej gładki), pokazując dokładnie, że sfera się obraca, kiedy mapowanie sferyczne zostanie włączone. Spróbuj pobawić się wartościami! Ważne jest także aby pamiętać, że im więcej pasów trójkątów, tym więcej klatka animacji wymaga obliczeń procesora do renderingu!
Teraz następuje kod rysowania cylindra. Zaczynamy ponownie poprzez określenie obrotów wokół wszystkich trzech osi. Nasz cylinder ma dolny i górny promień 1.0f jednostki. Jest wysoki na 3.0f jednostek oraz złożony z 32 ścian i 32 pasów. Jeśli zmniejszysz liczbę ścianek cylindra, będzie on złożony z mniejszej ilości poligonów i będzie wyglądał na mniej zaokrąglony.
Zanim narysujemy cylinder, dokonujemy translacji o -1.5f jednostek na osi z aby cylinder obracał się wokół punktu będącego środkiem bryły. Główną zasadą centrowania brył jest podzielenie ich wysokości przez 2 i dokonanie translacji przez wynik w przeciwnym kierunku niż znak wyniku na osi z. Jeśli nie nasz pojęcia o czym teraz mówię, wykomentuj linijkę translatef(...). Cylinder będzie się obracał wokół punktu leżącego na jego podstawie, zamiast środka bryły.
Teraz sprawdzimy czy env ma wartość TRUE. Jeśli tak, wyłączamy mapowanie sferyczne. Wywołujemy glFlush(), aby umieścić wszystko co jeszcze nie zostało wyrenderowane na ekranie (aby mieć pewność, że wszystko się wyrenderowało, zanim zaczniemy renderować następną klatkę).
Mam nadzieję, że podobał wam się ten tutorial. Jest już 2 w nocy... [heh - u mnie 3:07 w nocy :P - tłumacz]. Pracowałem nad tym tutorialem przez ostatnie 6 godzin. Brzmi na szaleństwo, ale sensowne opisywanie powyższych partii kodu nie jest łatwym zadaniem. Czytałem tutorial od początku trzy razy i wciąż staram się wprowadzać poprawki ułatwiające zrozumienie kodu. Wierzcie lub nie, dla mnie to ważne żeby wiedzieć w jaki sposób kod działa i dlaczego działa. To dlatego bez przerwy gadam, wprowadzam oczywiste komentarze, itd.
Tak czy inaczej... Chciałbym usłyszeć jakieś opinie o tym tutorialu. Jeśli znajdziesz jakieś błędy lub chciałbyś przyczynić się, aby ten tutorial stał się lepszy, skontaktuj się ze mną. Tak jak powiedziałem, to moja pierwsza próba odtwarzania plików AVI. W normalnych okolicznościach nie pisałbym tutoriala mówiącego o czymś, czego się właśnie nauczyłem, ale moje podekscytowanie zrobiło swoje. Znaczenie miał również fakt, że denerwował mnie kompletny brak informacji w Internecie na ten temat. Mam nadzieję, że ten tutorial spowoduje natłok świetnej jakości demek AVI i przykładowych kodów! Może się tak zdarzyć... a może nie. Tak czy inaczej zamieszczony tutaj kod jest dla ciebie i zrób z niego użytek jaki tylko chcesz!
Wielkie podziękowania dla Fredster'a dla plik face AVI. Face była jedną z 6-ciu animacji które mi przesłał na użytek mojego tutoriala. Bez zadawania pytań, bez stawiania warunków. Wysłałem mu e-mail z prośbą, a on zdecydował się bezinteresownie mi pomóc... Pełen szacunek!
Wielkie podziękowania kieruję również do Jonathan'a de Blok. Gdyby nie on, ten tutorial nigdy by nie powstał. To on zainteresował mnie formatem AVI przez przesłanie mi fragmentów kodu z jego własnego odtwarzacza AVI. Ponadto odpowiadał na każde moje pytanie dotyczące wątpliwości, jakie miałem na temat działania jego kodu. Warto zaznaczyć, że ani linijka kodu mojego odtwarzacza nie została pożyczona lub przekopiowana z jego kodu, posłużył mi on tylko do zrozumienia, w jaki sposób działa odtwarzacz AVI. Mój odtwarzacz otwiera, dekoduje i odtwarza pliki AVI przy użyciu zupełnie innego kodu!
Wielkie dzięki dla każdego za wsparcie. Ta strona byłaby niczym, gdyby nie odwiedzający!!!