Přehrávání AVI videa v OpenGL? Na pozadí, povrchu krychle, koule, či válce, ve fullscreenu nebo v obyčejném okně. Co víc si přát...
Na začátku bych chtěl poděkovat Fredsterovi za AVI animaci, Maxwellu Saylesovi za rady při programování, Jonathanu Nixovi a Johnu F. MCGowanovi, Ph. D. za skvělé články/dokumenty o AVI formátu. Moc jste mi pomohli.
Musím říci, že jsem na tento tutoriál opravdu pyšný. když mě Jonathan F. Blok přivedl na nápad AVI přehrávače v OpenGL, neměl jsem nejmenší potuchu, jak AVI otevřít, natož jak by mohl video přehrávač fungovat. Začal jsem listováním ve svých knihách o programování - vůbec nic. Poté jsem zkusil MSDN. Našel jsem spoustu užitečných informací, ale bylo jich potřeba mnohem, mnohem více. Po hodinách prolézání internetu jsem měl poznamenány pouze dva weby. Nemohu říct, že moje vyhledávací postupy jsou úplně nejlepší, ale v cca. 99,9% případů jsem nikdy neměl nejmenší problémy. Byl jsem absolutně šokován, když jsem zjistil, jak málo příkladů na přehrávání videa tam bylo. Většina z nich navíc nešla zkompilovat, některé byly komplexní (alespoň pro mě) a plnily svůj účel, nicméně byly programovány ve VB, Delphi nebo podobně (ne VC++).
První z užitečných stránek, které jsem našel, byl článek od Janathana Nixe nadepsaný AVI soubory. Jonathan má u mě obrovský respekt za tak extrémně brilantní dokument. Ačkoli jsem se rozhodl jít jinou cestou než on, vnesl mě do problematiky. Druhý web, tentokrát od Johna F. MCGowana, Ph. D., má titulek The AVI Overview. Mohl bych teď začít popisovat, jak úžasné jsou Johnovi stránky, ale snadnější bude, když se sami podíváte. Soustředil na nich snad vše, co je o AVI známo.
Poslední věcí, na kterou chci upozornit, je, že žádná část z celého kódu NEBYLA vypůjčena a nic nebylo okopírováno. Kódování mi zabralo plné tři dny, používal jsem pouze informace z výše uvedených zdrojů. Zároveň cítím, že by bylo vhodné poznamenat, že můj kód nemusí být nejlepším způsobem pro přehrávání AVI souborů. Dokonce nemusí být ani vhodnou cestou, ale funguje a snadno se používá. Nicméně pokud se vám můj styl a kód nelíbí, nebo cítíte-li, že uvolněním tohoto tutoriálu dokonce zraňuji programátorskou komunitu, máte několik možností: 1) zkuste si na internetu najít jiné zdroje, 2) napište si svůj vlastní AVI přehrávač nebo 3) napište lepší tutoriál. Každý, kdo navštíví tento web, by měl vědět, že kóduji pro zábavu. Hlavním účelem těchto stránek je ulehčit život ne-elitním programátorům, kteří začínají s OpenGL. Tutoriály ukazují, jak jsem !já! dokázal vytvořit specifický efekt... nic více, nic méně.
Pojďme ale ke kódu. Jako první věc vložíme a přilinkujeme knihovnu Video For Windows. Obrovské díky Microsoft®u (Nikdy bych nevěřil, že to řeknu). Pomocí této knihovny bude otevírání a přehrávání AVI pouhou banalitou.
#include <vfw.h>// Hlavičkový soubor knihovny Video pro Windows
#pragma comment(lib, "vfw32.lib")// Přilinkování VFW32.lib
Deklarujeme proměnné. Angle je úhel natočení zobrazovaného objektu. Next představuje celé číslo, které použijeme pro spočítání množství uplynulého času (v milisekundách), abychom mohli udržet framerate na správné hodnotě. Více o tomto dále. Frame bude samozřejmě obsahovat číslo aktuálně zobrazovaného snímku animace. Effect představuje druh objektu na obrazovce (krychle, koule, válec, žádný). Bude-li env rovno true, budou se automaticky generovat texturové souřadnice. Bg představuje flag, který definuje, jestli se má pozadí zobrazovat nebo ne. Sp, ep a bp slouží pro ošetření delšího stisku kláves.
float angle;// Úhel rotace objektu
int next;// Pro animaci
int frame = 0;// Aktuální snímek videa
int effect;// Zobrazený objekt
bool env = TRUE;// Automaticky generovat texturové koordináty?
bool bg = TRUE;// Zobrazovat pozadí?
bool sp;// Stisknut mezerník?
bool ep;// Stisknuto E?
bool bp;// Stisknuto B?
Struktura psi bude udržovat informace o AVI souboru. Pavi představuje ukazatel na buffer, do kterého po otevření AVI obdržíme handle nového proudu. Pgf, pointer na objekt GetFrame, použijeme pro získávání jednotlivých snímků, které pomocí bmih zkonvertujeme do formátu, který potřebujeme pro vytvoření textury. Lastframe ukládá číslo posledního snímku animace. Width a height definují rozměry AVI proudu, pdata je ukazatel na data obrázku vrácené po požadavku na snímek. Mpf (Miliseconds Per Frame) použijeme pro výpočet doby zobrazení snímku. Předpokládám, že nemáte nejmenší ponětí, k čemu všechny tyto proměnné vlastně slouží... vše byste měli pochopit dále.
AVISTREAMINFO psi;// Informace o datovém proudu videa
PAVISTREAM pavi;// Handle proudu
PGETFRAME pgf;// Ukazatel na objekt GetFrame
BITMAPINFOHEADER bmih;// Hlavička pro DrawDibDraw dekódování
long lastframe;// Poslední snímek proudu
int width;// Šířka videa
int height;// Výška videa
char* pdata;// Ukazatel na data textury
int mpf;// Doba zobrazení jednoho snímku (Milliseconds Per Frame)
Pomocí knihovny GLU budeme moci vykreslit dva quadratic útvary, kouli a válec. Hdd je handle na DIB (Device Independent Bitmap) a hdc je handle na kontext zařízení. HBitmap představuje handle na bitmapu závislou na zařízení (DDB - Device Dependent Bitmap), použijeme ji dále při konverzích. Data je pointer, který bude ukazovat na data obrázku použitelná pro vytvoření textury. Opět - více pochopíte dále.
GLUquadricObj *quadratic;// Objekt quadraticu
HDRAWDIB hdd;// Handle DIBu
HBITMAP hBitmap;// Handle bitmapy závislé na zařízení
HDC hdc = CreateCompatibleDC(0);// Kontext zařízení
unsigned char* data = 0;// Ukazatel na bitmapu o změněné velikosti
Nyní malý úvod do jazyka Assembler (ASM). Pokud jste ho ještě nikdy dříve nepoužili, nelekejte se. Může vypadat složitě, ale vše je velmi jednoduché. Při programování tohoto tutoriálu jsem se dostal před velký problém. Aplikace běžela v pořádku, ale barvy byly divné. Vše, co mělo být červené bylo modré, a vše co mělo být modré bylo červené - klasické prohození R a B složky pixelů. Byl jsem absolutně šokovaný. Myslel jsem si, že jsem v kódu udělal nějakou šílenou chybu typu "čárka sem, znaménko tam...". Po pečlivém prostudování všeho, co jsem do té doby napsal, jsem nebyl schopen bug najít. Začal jsem znovu pročítat MSDN. Proč byla červená a modrá složka barvy prohozená?! V MSDN bylo přece jasně napsáno, že 24 bitové bitmapy jsou ve formátu RGB!!! Po spoustě dalšího čtení jsem problém objevil. Ve Windows se RGB data ukládají pozpátku a RGB uložené pozpátku je přeci BGR! Takže si jednou pro vždy zapamatujte, že v OpenGL RGB znamená RGB a ve Windows RGB znamená BGR - jak jednoduché.
Po stížnostech od fanoušků Microsoft®u (Překl.: Ono něco takového existuje?!): Rozhodl jsem se přidat krátké vysvětlení... Nepomlouvám Microsoft kvůli tomu, že označil BGR formát barvy za RGB. Jestli se mu převrácená zkratka líbí více, ať si ji používá. Nicméně nalezení chyby může být pro cizího programátora velice frustrující (zvlášť když žádná neexistuje).
Blue přidal: Má to co dělat s konvencemi little endian a big endian. Intel a Intel kompatibilní systémy používají little endian, u kterého se méně významné byty ukládají dříve než více významné. Specifikaci OpenGL vytvořila firma SGI (Silicon Graphic), jejíž systémy pravděpodobně používají big endian, a tudíž OpenGL standardně vyžadují bitmapy ve formátu big endian.
Skvělý! Takže jsem vytvořil přehrávač, který je absolutně k ničemu (Překl.: v originále absolute crap - zkuste si toto slovo najít ve slovníku, já chci být slušný :-). Prvním řešením, které mě napadlo, bylo prohodit byty manuálně pomocí cyklu for. Pracovalo to v pořádku, ale strašně pomalu. Měl jsem všeho po krk. Zkusil jsem modifikoval generování textury na GL_BGR_EXT místo GL_RGB. Obrovský nárůst rychlosti a barvy vypadají skvěle! Takže jsem problém konečně vyřešil... alespoň jsem si to myslel. Některé OpenGL ovladače mají s GL_BGR_EXT problémy :-( Maxwell Sayles mi doporučil prohození bytů pomocí ASM. O minutku později mi ICQ-oval kód uvedený níže, který je rychlý a plní dokonale svou funkci.
Každý snímek animace se ukládá do bufferu, obrázek má vždy čtvercovou velikost 256 pixelů a 3 barevné složky ve formátu BGR (speciálně pro Billa Gatese: RGB). Funkce flipIt() prochází tento buffer po tří bytových krocích a zaměňuje červenou složku za modrou. R má být uloženo na pozici abx+0 a B na abx+2. Cyklus se opakuje tak dlouho, dokud nejsou všechny pixely ve formátu RGB.
Předpokládám, že většina z vás není z ASM moc nadšená. Jak už jsem psal, původně jsem plánoval použít GL_BGR_EXT. Funguje, ale ne na všech kartách. Potom jsem se rozhodl jít cestou minulých tutoriálů a swapovat byty pomocí bitových operací XOR, které pracují na všech počítačích, ale ne extrémně rychle. Dokud jsme nepracovali s real-time videem, stačily, ale tentokrát potřebujeme co možná nejrychlejší metodu. Zvážíme-li všechny možnosti, je ASM podle mého názoru nejlepší volbou. Pokud máte ještě lepší způsob, prosím... POUŽIJTE HO! Neříkám vám, jak co MÁTE dělat, já pouze ukazuji, jak jsem problémy vyřešil já. Vše proto také vysvětluji do detailů, abyste můj kód, pokud znáte lepší, mohli nahradit.
void flipIt(void* buffer)// Prohodí červenou a modrou složku pixelů v obrázku
{
void* b = buffer;// Ukazatel na buffer
__asm // ASM kód
{
mov ecx, 256*256 // Řídící "proměnná" cyklu
mov ebx, b // Ebx ukazuje na data
label: // Návěští pro cyklus
mov al, [ebx+0] // Přesune B složku do al
mov ah, [ebx+2] // Přesune R složku do ah
mov [ebx+2], al // Vloží B na správnou pozici
mov [ebx+0], ah // Vloží R na správnou pozici
add ebx, 3 // Přesun na další tři byty
dec ecx // Dekrementuje čítač
jnz label // Pokud se čítač nerovná nule skok na návěští
}
}
Jak už z názvu funkce OpenAVI() vyplývá, otevírá AVI soubor. Parametr szFile je řetězec s diskovou cestou k souboru. Řetězec title použijeme pro zobrazení informací o AVI do titulku okna.
void OpenAVI(LPCSTR szFile)// Otevře AVI soubor
{
TCHAR title[100];// Pro vypsání textu do titulku okna
Abychom inicializovali knihovnu AVI file, zavoláme AVIFileInit(). Existuje mnoho způsobů, jak otevřít video soubor. Rozhodl jsem se použít AVIStreamOpenFromFile(), která otevře jeden datový proud. Pavi představuje ukazatel na buffer, kam funkce vrací handle nového proudu, szFile označuje diskovou cestu k souboru. Třetí parametr určuje typ proudu, který si přejeme otevřít. V tomto projektu nás zajímá pouze video. Nula, další parametr, oznamuje, že se má použít první výskyt proudu streamtypeVIDEO - v AVI jich může být více. OF_READ definuje, že nám stačí otevření pouze pro čtení a NULL na konci je ukazatel na třídní identifikátor handleru (Překl.: class identifier of the handler). Abych byl upřímný nemám nejmenší představu, co to znamená, proto pomocí NULL nechávám knihovnu, aby vybrala za mě.
Nastanou-li při otevírání jakékoli problémy, zobrazí se uživateli informační okno, nicméně ukončení programu není implementováno. Přidání nějakého druhu chybových testů by pro vás nemělo být moc těžké, já jsem byl příliš líný.
AVIFileInit();// Připraví knihovnu AVIFile na použití
if (AVIStreamOpenFromFile(&pavi, szFile, streamtypeVIDEO, 0, OF_READ, NULL) != 0)// Otevře AVI proud
{
// Chybová zpráva
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Stream", "Error", MB_OK | MB_ICONEXCLAMATION);
}
Pokud jsme se dostali až sem, můžeme předpokládat, že se soubor otevřel v pořádku a video proud byl lokalizován. U deklarace proměnných jsme vytvořili objekt struktury AVISTREAMINFO a nazvali ho psi. Voláním funkce AVIStreamInfo() do něj nagrabujeme různé informace o AVI, s jejichž pomocí spočítáme šířku a výšku snímku v pixelech. Potom funkcí AVIStreamLength() získáme číslo posledního snímku videa, které zároveň označuje celkový počet všech snímků.
Výpočet framerate je snadný. Počet snímků za sekundu se rovná psi.dwRate děleno psi.dwScale. Tato hodnota by měla odpovídat číslu, které lze získat kliknutím na AVI soubor a zvolením vlastností. Ptáte se, co to má co společného s mpf (čas zobrazení jednoho snímku)? Když jsem poprvé psal kód pro animaci, zkoušel jsem pro zvolení správného snímku animace použít FPS. Dostal jsem se do problémů... všechna videa se přehrávala příliš rychle. Proto jsem nahlédl do vlastností video souboru face2.avi. Je dlouhé 3,36 sekund, framerate činí 29,974 FPS a má celkem 91 snímků. Pokud vynásobíme 3,36 krát 29,976 dostaneme 100 snímků - velmi nepřesné.
Proto jsem se rozhodl dělat věci trochu jinak. Namísto počtu snímků za sekundu spočítáme, jak dlouho by měl být snímek zobrazen. Funkce AVIStreamSampleToTime() zkonvertuje pozici v animaci na čas v milisekundách, než se video dostane do této pozice. Získáme tedy čas posledního snímku, vydělíme ho jeho pozicí (=počtem všech snímků) a výsledek vložíme do proměnné mpf. Stejné hodnoty byste dosáhli nagrabováním množství času potřebného pro jeden snímek. Příkaz by vypadal takto: AVIStreamSampleToTime(pavi, 1). Oba způsoby jsou možné. Děkuji Albertu Chaulkovi za nápad.
AVIStreamInfo(pavi, &psi, sizeof(psi));// Načte informace o proudu
width = psi.rcFrame.right - psi.rcFrame.left;// Výpočet šířky
height = psi.rcFrame.bottom - psi.rcFrame.top;// Výpočet výšky
lastframe = AVIStreamLength(pavi);// Poslední snímek proudu
mpf = AVIStreamSampleToTime(pavi, lastframe) / lastframe;// Počet milisekund na jeden snímek
OpenGL požaduje, aby rozměry textury byly mocninou čísla 2, ale většina videí mívá velikost 160x120, 320x240 nebo jiné nevhodné hodnoty. Pro konverzi na potřebné rozměry použijeme Windows funkce pro práci s DIB obrázky. Jako první věc specifikujeme hlavičku bitmapy a to tak, že vyplníme BITMAPINFOHEADER proměnnou bmih. Nastavíme velikost struktury a biPlanes. Barevnou hloubku určíme na 24 bitů (RGB), obrázek bude mít rozměry 256x256 pixelů a nebude komprimovaný.
bmih.biSize = sizeof(BITMAPINFOHEADER);// Velikost struktury
bmih.biPlanes = 1;// BiPlanes
bmih.biBitCount = 24;// Počet bitů na pixel
bmih.biWidth = 256;// Šířka bitmapy
bmih.biHeight = 256;// Výška bitmapy
bmih.biCompression = BI_RGB;// RGB mód
Funkce CreateDibSection() vytvoří obrázek DIB, do kterého budeme moci přímo zapisovat. Pokud vše proběhne v pořádku měl by hBitmap obsahovat nově vytvořený obrázek. Hdc představuje handle kontextu zařízení, druhý parametr je ukazatel na strukturu, kterou jsme právě inicializovali. Třetí parametr specifikuje RGB typ dat. Do proměnné data se uloží ukazatel na data vytvořeného obrázku. Nastavíme-li předposlední parametr na NULL, funkce za nás sama alokuje paměť. Poslední parametr budeme jednoduše ignorovat. Příkaz SelectObject() zvolí obrázek do kontextu zařízení.
hBitmap = CreateDIBSection(hdc, (BITMAPINFO*)(&bmih), DIB_RGB_COLORS, (void**)(&data), NULL, NULL);
SelectObject(hdc, hBitmap);// Zvolí bitmapu do kontextu zařízení
Předtím než budeme moci načítat jednotlivé snímky, musíme připravit program na dekomprimaci videa. Zavoláme funkci AVIStreamGetFrameOpen() a předáme jí ukazatel na datový proud videa. Za druhý parametr se může předat struktura podobná té výše, pomocí které lze specifikovat vrácený video formát. Bohužel jedinou věcí, kterou lze ovlivnit je šířka a výška obrázku. V MSDN se také uvádí, že se může předat AVIGETFRAMEF_BESTDISPLAYFMT, který automaticky zvolí nejlepší formát zobrazení. Nicméně můj kompilátor nemá pro tuto symbolickou konstantu žádnou definici. Dopadne-li vše dobře, získáme GETFRAME objekt potřebný pro čtení dat jednotlivých snímků. Při problémech se zobrazí chybové okno.
pgf = AVIStreamGetFrameOpen(pavi, NULL);// Vytvoří PGETFRAME použitím požadovaného módu
if (pgf == NULL)// Neúspěch?
{
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Frame", "Error", MB_OK | MB_ICONEXCLAMATION);
}
Jako třešničku na dortu zobrazíme do titulku okna šířku, výšku a počet snímků videa.
// Informace o videu (šířka, výška, počet snímků)
wsprintf (title, "NeHe's AVI Player: Width: %d, Height: %d, Frames: %d", width, height, lastframe);
SetWindowText(g_window->hWnd, title);// Modifikace titulku okna
}
Otevírání AVI proběhlo bez problémů, následující funkce nagrabuje jeho jeden snímek, zkonvertuje ho do použitelné formy (velikost, barevná hloubka RGB) a vytvoří z něj texturu. Proměnná lpbi bude ukládat informace o hlavičce bitmapy snímku. Příkaz na dalším řádku plní hned několik funkcí. Nagrabuje snímek specifikovaný pomocí frame a vyplní lpbi informacemi o hlavičce snímku. Přeskočením hlavičky (lpbi->biSize) a informací o barvách (lpbi->biClrUsed * sizeof(RGBQUAD)) získáme ukazatel na opravdová data obrázku.
void GrabAVIFrame(int frame)// Grabuje požadovaný snímek z proudu
{
LPBITMAPINFOHEADER lpbi;// Hlavička bitmapy
lpbi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, frame);// Grabuje data z AVI proudu
pdata = (char *)lpbi + lpbi->biSize + lpbi->biClrUsed * sizeof(RGBQUAD);// Ukazatel na data
Kvůli textuře musíme zkonvertovat právě získaný obrázek na použitelnou velikost a barevnou hloubku. Pomocí funkce DrawDibDraw() můžeme kreslit přímo do našeho DIBu. Její první parametr je DrawDib DC, další parametr představuje handle na kontext zařízení. Nuly definují levý horní a 256 pravý dolní roh výsledného obdélníku. Lpbi je ukazatel na hlavičku snímku, který jsme právě načetli, a pdata ukazuje na data obrázku. Následuje levý horní a pravý dolní roh zdrojového obrázku (čili šířka a výška snímku). Poslední parametr necháme na nule. Touto cestou můžeme zkonvertovat obrázek o jakékoli šířce, výšce a barevné hloubce na obrázek 256x256x24.
// Konvertování obrázku na požadovaný formát
DrawDibDraw(hdd, hdc, 0, 0, 256, 256, lpbi, pdata, 0, 0, width, height, 0);
V současné chvíli už v rukách držíme data, ze kterých lze vygenerovat texturu. Nicméně její R a B složky jsou prohozeny. Proto zavoláme naši ASM funkce, která jednotlivé byty umístí na korektní pozice v obrázku.
flipIt(data);// Prohodí R a B složku pixelů
Původně jsem texturu aktualizoval jejím smazáním a znovuvytvořením. Několik lidí mi nezávisle na sobě poradilo, abych zkusil použít glTexSubImage2D(). Uvádím citaci z OpenGL Red Book: "Vytvoření textury může být mnohem náročnější než modifikace už existující. V OpenGL Release 1.1 přibyly nové rutiny pro nahrazení všech částí textury za nové informace. Toto může být užitečné pro programy, které např. v real-timu snímají obrázky videa a vytvářejí z nich textury. Aplikace pak za běhu vytvoří pouze jednu texturu a pomocí glTexSubImage2D() bude postupně nahrazovat její data za nové snímky videa."
Osobně jsem nezaznamenal větší nárůst rychlosti, ale na pomalejších kartách může být vše jinak. Parametry funkce jsou následující: typ výstupu, úroveň detailů pro mipmapping, x a y offset počátku kopírované oblasti (0, 0 - levý dolní roh), šířka a výška oblasti, RGB formát pixelů, typ dat a ukazatel na data.
Kevin Rogers přidal: Chtěl bych poukázat na další důležitou vlastnost glTexSubImage2d(). Nejen, že je rychlejší na mnoha OpenGL implementacích, ale cílová oblast obrázku nemusí být nutně mocninou čísla 2. Toto je především užitečné pro přehrávání videa, jehož rozlišení bývá mocninou dvojky opravdu zřídka (většinou 320x200). Dostáváme tak flexibilní možnost přehrávat video v jeho originální velikosti než jej složitě měnit, někdy i dvakrát (do textury, zpět na obrazovku).
Není možné aktualizovat texturu, pokud jste ji ještě nevytvořili! My ji vytváříme v kódu funkce Initialize(). Druhá důležitá věc spočívá v tom, že pokud váš projekt obsahuje více než jednu texturu, musíte před aktualizací zvolit jako aktivní (glBindTexture()) tu správnou, protože byste mohli přepsat texturu, kterou nechcete.
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, 256, GL_RGB, GL_UNSIGNED_BYTE, data);// Aktualizace textury
}
Následující funkce je volána při ukončování programu. Má za úkol smazat DrawDib DC a uvolnit alokované zdroje. Zavírá také GetFrame zdroj, odstraňuje souborový proud a ukončuje práci s AVI souborem.
void CloseAVI(void)// Zavření AVI souboru
{
DeleteObject(hBitmap);// Smaže bitmapu
DrawDibClose(hdd);// Zavře DIB
AVIStreamGetFrameClose(pgf);// Dealokace GetFrame zdroje
AVIStreamRelease(pavi);// Uvolnění proudu
AVIFileExit();// Uvolnění souboru
}
Inicializace je hezky přímočará. nastavíme počáteční úhel na nulu a pomocí knihovny DrawDib nagrabujeme DC. Pokud se vše zdaří, tak by se mělo hdd stát handlem na nově vytvořený kontext zařízení. Dále určíme černé pozadí, zapneme hloubkové testování atd.
BOOL Initialize (GL_Window* window, Keys* keys)// Inicializace
{
g_window = window;
g_keys = keys;
angle = 0.0f;// Na počátku nulový úhel
hdd = DrawDibOpen();// Kontext zařízení DIBu
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);// Černé pozadí
glClearDepth(1.0f);// Nastavení hloubkového bufferu
glDepthFunc(GL_LEQUAL);// Typ testů hloubky
glEnable(GL_DEPTH_TEST);// Zapne testování hloubky
glShadeModel(GL_SMOOTH);// Jemné stínování
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Perspektivní korekce
V další části kódu zapneme mapování 2D textur, nastavíme filtr GL_NEAREST a definujeme kulové mapování, které umožní automatické generování texturových koordinátů. Pokud máte výkonný systém, zkuste použít lineární filtrování, bude vypadat lépe.
quadratic = gluNewQuadric();// Vytvoří objekt quadraticu
gluQuadricNormals(quadratic, GLU_SMOOTH);// Normály
gluQuadricTexture(quadratic, GL_TRUE);// Texturové koordináty
glEnable(GL_TEXTURE_2D);// Zapne texturování
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);// Filtry textur
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);// Automatické generování koordinátů
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
Po obvyklé inicializaci otevřeme AVI soubor. Jistě jste si všimli, že jsem se snažil udržet rozhraní v co nejjednodušší formě, takže stačí předat pouze řetězec se jménem souboru. Na konci vytvoříme texturu a ukončíme funkci.
OpenAVI("data/face2.avi");// Otevření AVI souboru
// Vytvoření textury
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
return TRUE;// Vše OK
}
Při deinicializaci zavoláme CloseAVI(), čímž kompletně ukončíme práci s videem.
void Deinitialize(void)// Deinicializace
{
CloseAVI();// Zavře AVI
}
Ve funkci Update() zjišťujeme případné stisky kláves a v závislosti na uplynulém čase aktualizujeme poměry ve scéně. Jako vždy ESC ukončuje program a F1 přepíná mód fullscreen/okno. Mezerníkem inkrementujeme proměnnou efekt, jejíž hodnota určuje, jestli se ve scéně zobrazuje krychle, koule, válec, popř. nic (pouze pozadí).
void Update(DWORD milliseconds)// Aktualizace scény
{
if (g_keys->keyDown[VK_ESCAPE] == TRUE)// ESC
{
TerminateApplication (g_window);// Konec programu
}
if (g_keys->keyDown[VK_F1] == TRUE)// F1
{
ToggleFullscreen (g_window);// Zamění mód fullscreen/okno
}
if ((g_keys->keyDown[' ']) && !sp)// Mezerník
{
sp = TRUE;
effect++;// Následující objekt v řadě
if (effect > 3)// Přetečení?
{
effect = 0;
}
}
if (!g_keys->keyDown[' '])// Uvolnění mezerníku
{
sp = FALSE;
}
Pomocí klávesy B zapínáme/vypínáme pozadí. Generování texturových koordinátů určuje flag env, který negujeme po stisku klávesy E.
if ((g_keys->keyDown['B']) && !bp)// Klávesa B
{
bp = TRUE;
bg = !bg;// Nastaví flag pro zobrazování pozadí
}
if (!g_keys->keyDown['B'])// Uvolnění B
{
bp = FALSE;
}
if ((g_keys->keyDown['E']) && !ep)// Klávesa E
{
ep = TRUE;
env = !env;// Nastaví flag pro automatické generování texturových koordinátů
}
if (!g_keys->keyDown['E'])// Uvolnění E
{
ep = FALSE;
}
V závislosti na uplynulém čase zvětšíme úhel natočení objektu.
angle += (float)(milliseconds) / 60.0f;// Aktualizace úhlu natočení
V originální verzi tutoriálu byla všechna videa přehrávána vždy stejnou rychlostí a to nebylo příliš vhodné. Proto jsem kód přepsal tak, aby jeho rychlost byla vždy korektní. Obsah proměnné next zvětšíme o počet uplynulých milisekund od milého volání. Jistě si pamatujete, že mpf obsahuje čas, jak dlouho má být každý snímek zobrazen. Vydělíme-li tedy číslo next hodnotou mpf, získáme správný snímek. Nakonec se ujistíme, že nově vypočtený snímek nepřetekl přes maximální hodnotu. V takovém případě začneme video přehrávat znovu od začátku.
Asi vás nepřekvapí, že pokud je počítač příliš pomalý, některé snímky se automaticky přeskakují. Pokud chcete, aby byl každý snímek zobrazen, přičemž nezávisí na tom, jak pomalu program běží, můžete otestovat, jestli je next vyšší než mpf a pokud ano, inkrementujte snímek o jedničku a resetujte next zpět na nulu. Oba způsoby pracují, ale pro rychlé počítače je vhodnější uvedený kód.
Cítíte-li se plni síly a energie, zkuste implementovat obvyklé funkce video přehrávačů - např. rychlé převíjení, pauzu nebo zpětný chod.
next += milliseconds;// Zvětšení next o uplynulý čas
frame = next / mpf;// Výpočet aktuálního snímku
if (frame >= lastframe)// Přetečení snímků?
{
frame = 0;// Přetočí video na začátek
next = 0;// Nulování času
}
}
Už máme téměř vše, zbývá pouze vykreslování scény. Jako vždy na začátku smažeme obrazovku a hloubkový buffer. Potom nagrabujeme požadovaný snímek animace. Pokud byste chtěli současně používat více videí, museli byste přidat i ID textury - další práce pro vás.
void Draw(void)// Vykreslování
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže buffery
GrabAVIFrame(frame);// Nagrabuje požadovaný snímek videa
Chceme-li kreslit pozadí, resetujeme modelview matici a na obyčejný obdélník namapujeme daný snímek videa. Aby se objevil až za všemi objekty, umístíme ho dvacet jednotek do scény a samozřejmě ho roztáhneme na požadovanou velikost.
if (bg)// Zobrazuje se pozadí?
{
glLoadIdentity();// Reset matice
glBegin(GL_QUADS);// Vykreslování obdélníků
glTexCoord2f(1.0f, 1.0f); glVertex3f( 11.0f, 8.3f,-20.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-11.0f, 8.3f,-20.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-11.0f,-8.3f,-20.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 11.0f,-8.3f,-20.0f);
glEnd();
}
Resetujeme matici a přesuneme se deset jednotek do scény. Pokud se env rovná TRUE, zapneme automatické generování texturových koordinátů.
glLoadIdentity();// Reset matice
glTranslatef(0.0f, 0.0f, -10.0f);// Posun do scény
if (env)// Zapnuto generování souřadnic textur?
{
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
}
Na poslední chvíli jsem přidal i rotaci objektu na osách x, y a následné přiblížení na ose z. Objekt se bude pohybovat po scéně. Bez těchto tří řádků by pouze rotoval na jednom místě uprostřed obrazovky.
glRotatef(angle*2.3f, 1.0f, 0.0f, 0.0f);// Rotace
glRotatef(angle*1.8f, 0.0f, 1.0f, 0.0f);
glTranslatef(0.0f, 0.0f, 2.0f);// Přesun na novou pozici
Pomocí větvení do více směrů vykreslíme objekt, který je právě aktivní. Jako první možnost máme krychli.
switch (effect)// Větvení podle efektu
{
case 0:// Krychle
glRotatef(angle*1.3f, 1.0f, 0.0f, 0.0f);// Rotace
glRotatef(angle*1.1f, 0.0f, 1.0f, 0.0f);
glRotatef(angle*1.2f, 0.0f, 0.0f, 1.0f);
glBegin(GL_QUADS);// Kreslení obdélníků
// Čelní stěna
glNormal3f(0.0f, 0.0f, 0.5f);
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);
// Zadní stěna
glNormal3f(0.0f, 0.0f,-0.5f);
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);
// Horní stěna
glNormal3f(0.0f, 0.5f, 0.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);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// Spodní stěna
glNormal3f(0.0f,-0.5f, 0.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);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// Pravá stěna
glNormal3f(0.5f, 0.0f, 0.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);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// Levá stěna
glNormal3f(-0.5f, 0.0f, 0.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);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
glEnd();
break;
Jak vykreslit kouli, už jistě dávno víte, nicméně pro jistotu přidávám krátký komentář. Její poloměr činí 1.3f jednotek, skládá se z dvaceti poledníků a dvaceti rovnoběžek. Používám číslo 20, protože chci, aby nebyla perfektně hladká, ale trochu segmentovaná - bude vidět náznak její rotace.
case 1:// Koule
glRotatef(angle*1.3f, 1.0f, 0.0f, 0.0f);// Rotace
glRotatef(angle*1.1f, 0.0f, 1.0f, 0.0f);
glRotatef(angle*1.2f, 0.0f, 0.0f, 1.0f);
gluSphere(quadratic, 1.3f, 20, 20);// Vykreslení koule
break;
Válec vykreslíme pomocí funkce gluCylinder(). Bude mít průměr 1.0f a jeho výška bude činit tři jednotky.
case 2:// Válec
glRotatef(angle*1.3f, 1.0f, 0.0f, 0.0f);// Rotace
glRotatef(angle*1.1f, 0.0f, 1.0f, 0.0f);
glRotatef(angle*1.2f, 0.0f, 0.0f, 1.0f);
glTranslatef(0.0f,0.0f,-1.5f);// Vycentrování
gluCylinder(quadratic, 1.0f, 1.0f, 3.0f, 32, 32);// Vykreslení válce
break;
}
Pokud je env v jedničce, vypneme generování texturových koordinátů.
if (env)// Zapnuto generování souřadnic textur?
{
glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
}
glFlush();// Vyprázdní OpenGL pipeline
}
Doufám, že jste si, stejně jako já, užili tento tutoriál. Za chvíli budou 2 hodiny ráno... už na něm pracuji přes šest hodin. Zní to šíleně, ale psaní textu, aby dával smysl, není lehký úkol. Vše jsem třikrát přečetl a snažil se objasnit věci co nejlépe. Věřte nebo ne, pro mě je důležité, abyste pochopili, jak věci pracují a proč vůbec pracují. Bez čtenářů bych brzy skončil.
Jak už jsem napsal, toto je můj první pokus o přehrávání videa. Normálně nepíši o předmětu, který jsem se právě naučil, ale myslím, že mi to pro jednou odpustíte. Faktem je, že jsem si od cizích lidí půjčil opravdu absolutní minimum kódu, vše je původní. Doufám, že se mi podařilo otevřít dveře povodni přehrávání AVI ve vašich kvalitních demech. Možná se tak stane, možná ne. Každopádně ukázkový tutoriál už máte.
Obrovské díky patří Fredsterovi, který vytvořil ukázkové video tváře. Byla to jedna z celkem šesti animací, které mi poslal. Žádné dotazy, žádné požadavky. Poslal jsem mu email s prosbou a on mi pomohl. Obrovský respekt.
Největší dík však patří Jonathanu de Blok. Nebýt jeho, tento tutoriál by nevznikl. Právě on ve mně vzbudil zájem o AVI formát. Poslal mi totiž část kódu z jeho přehrávače. Trpělivě odpovídal na všechny otázky ohledně jeho kódu. Nic jsem si však nepůjčil, můj kód pracuje na úplně jiném základu.
napsal: Jeff Molofee - NeHe <nehe (zavináč) connect.ab.ca>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>