Lekce 32 - Picking, alfa blending, alfa testing, sorting

V tomto tutoriálu se pokusím zodpovědět několik otázek, na které jsem denně dotazován. Chcete vědět, jak při kliknutí tlačítkem myši identifikovat OpenGL objekt nacházející se pod kurzorem (picking). Dále byste se chtěli dozvědět, jak vykreslit objekt bez zobrazení určité barvy (alfa blending, alfa testing). Třetí věcí, se kterou si nevíte rady, je, jak řadit objekty, aby se při blendingu správně zobrazily (sorting). Naprogramujeme hru, na které si vše vysvětlíme.

Vítejte do 32. lekce. Je asi nejdelší, jakou jsem kdy napsal - přes 1000 řádků kódu a více než 1500 řádků HTML. Také je prvním, který používá nový NeHeGL základní kód. Tutoriál zabral hodně času, ale myslím si, že stojí za to. Probírá se v něm především: alfa blending, alfa testing, čtení zpráv myši, současné používání perspektivní i pravoúhlé projekce, zobrazování kurzoru myši pomocí OpenGL, ruční řazení objektů podle hloubky, snímky animace z jedné textury a to nejdůležitější: naučíte se vše o pickingu.

V první verzi program zobrazoval tři polygony, které po kliknutí měnily barvu. Jak vzrušující! Tak, jako vždycky, chci zapůsobit super cool tutoriálem. Nejen, že jsou v něm zahrnuty všechny informace k probíranému tématu, ale samozřejmě musí být také hezký na pohled. Dokonce i tehdy, pokud neprogramujete, vás může zaujmout - kompletní hra. Objekty se sestřelují tak dlouho, dokud vám neochabne ruka držící myš, takže už nejste schopni stisknout tlačítko.

Poznámka ohledně kódu: Budu vysvětlovat pouze lesson33.cpp. V NeHeGL jsou změny především v podpoře myši ve funkci WindowProc(). Také už nebudu vysvětlovat loading textur, vytváření display listů fontu a výstup textu. Vše bylo vysvětleno v minulých tutoriálech.

Textury, používané v tomto programu, byly nakresleny v Adobe Photoshopu. Každý z .TGA obrázků má barevnou hloubku 32 bitů na pixel, obsahuje tedy alfa kanál. Pokud si nejste jistí, jak ho přidat, kupte si nějakou knihu, prozkoumejte internet nebo zkuste help. Postup je podobný vytváření masky v tutoriálu o maskingu, nahrajte svůj obrázek do Adobe Photoshopu nebo jakéhokoli grafického editoru s podporou alfa kanálu. Proveďte výběr barvy, abyste označili oblast okolo objektu, zkopírujte výběr a vložte ho do nového obrázku. Negujte obrázek, takže oblast, kde by měl být, bude černá. Změňte okolí na bílé, vyberte celý obrázek a zkopírujte ho. Vraťte se na originál a vytvořte alfa kanál, do kterého vložte masku. Uložte obrázek jako 32 bitový .TGA soubor. Ujistěte se, že je zaškrtnuto Uchovat průhlednost a ukládejte bez komprese.

Zjistíme, jestli je definovaná symbolická konstanta CDS_FULLSCREEN a pokud ne, nadefinujeme ji na hodnotu 4. Pro ty z vás, kteří se úplně ztratili... některé kompilátory nepřiřazují CDS_FULLSCREEN hodnotu. Pokud ji pak v programu použijeme, kompilace skončí s chybovou zprávou. Abychom tomuto předešli, tak ji v případě potřeby nadefinujeme ručně.

#ifndef CDS_FULLSCREEN// Některé kompilátory nedefinují CDS_FULLSCREEN

#define CDS_FULLSCREEN 4// Ruční nadefinování

#endif

Deklarujeme funkci DrawTargets(), potom proměnnou okna a kláves.

void DrawTargets();// Deklarace funkce

GL_Window* g_window;// Okno

Keys* g_keys;// Klávesy

Každý program potřebuje proměnné. Base ukládá display listy fontu, roll slouží k pohybu země a rolování mraků. Jako ve všech hrách i my začínáme prvním levelem. Miss vede záznam, do kolika objektů se v daném levelu střelec nestrefil, kill je jeho pravý opak. Score zahrnuje součty zasažených objektů z jednotlivých levelů. Game signalizuje konec hry.

GLuint base;// Display listy fontu

GLfloat roll;// Rolování mraků

GLint level = 1;// Aktuální level

GLint miss;// Počet nesestřelených objektů

GLint kills;// Počet sestřelených objektů v daném levelu

GLint score;// Aktuální skóre

bool game;// Konec hry?

Nadefinujeme nový datový typ, díky kterému budeme moci předat struktury porovnávací funkci. Qsort() totiž očekává v posledním parametru ukazatel na funkci s parametry (const* void, const* void).

typedef int (*compfn)(const void*, const void*);// Ukazatel na porovnávací funkci

Struktura objects bude ukládat všechny informace popisující sestřelovaný objekt. Rychlý průzkum proměnných: rot určuje směr rotace na ose z. Pokud ještě nebyl objekt sestřelen, hit bude obsahovat false. Frame definuje snímek animace při explozi, dir určuje směr pohybu. Texid je indexem do pole textur, nabývá hodnot nula až čtyři, z čehož plyne, že máme celkem pět druhů objektů. X a y definuje aktuální pozici, spin úhel rotace na ose z. Distance je hodně důležitá proměnná, určuje hloubku ve scéně. Právě podle ní budeme při blendingu řadit objekty, aby se nejdříve vykreslovali vzdálenější a až po nich bližší.

struct objects// Struktura objektu

{

GLuint rot;// Rotace (0 - žádná, 1 - po směru hodinových ručiček, 2 - proti směru)

bool hit;// Byl objekt zasažen?

GLuint frame;// Aktuální snímek exploze

GLuint dir;// Směr pohybu (0 - vlevo, 1 - vpravo, 2 - nahoru, 3 - dolů)

GLuint texid;// Index do pole textur

GLfloat x;// X pozice

GLfloat y;// Y pozice

GLfloat spin;// Směr rotace na ose z

GLfloat distance;// Hloubka ve scéně

};

Následující pole vedou záznamy o deseti texturách a třiceti objektech.

TextureImage textures[10];// Deset textur

objects object[30];// 30 Objektů

Nebudeme limitovat velikost objektů. Váza by měla být vyšší než plechovka coly a kýbl naopak širší než váza. Abychom si ulehčili život, vytvoříme strukturu obsahující výšku a šířku. Definujeme a ihned inicializujeme pole těchto struktur o pěti prvcích. Na každém indexu se nachází jeden z pěti typů objektů.

struct dimensions// Rozměr objektu

{

GLfloat w;// Šířka

GLfloat h;// Výška

};

// Velikost každého objektu: Modrá tvář, kýbl, terč, Coca-cola, Váza

dimensions size[5] = {{1.0f,1.0f}, {1.0f,1.0f}, {1.0f,1.0f}, {0.5f,1.0f}, {0.75f,1.5f}};

Tento kód bude volán funkcí qsort(). Porovnává hloubku dvou objektů ve scéně a vrací -1, pokud je první objekt dále, bude-li ale vzdálenější druhý objekt vrátí funkce 1. Získáme-li 0, znamená to, že jsou oba ve stejné vzdálenosti od pozorovatele.

// *** Modifikovaný MSDN kód pro tento tutoriál ***

int Compare(struct objects *elem1, struct objects *elem2)// Porovnávací funkce

{

if (elem1->distance < elem2->distance)// První je vzdálenější

{

return -1;

}

else if (elem1->distance > elem2->distance)// První je bližší

{

return 1;

}

else// Vzdálenosti jsou stejné

{

return 0;

}

}

Ve funkci InitObject() nastavujeme objekt na výchozí hodnoty. Přiřadíme mu rotaci po směru hodinových ručiček. Animace exploze samozřejmě začíná na prvním (nultém) snímku. Objekt ještě nebyl zasažen, takže nastavíme hit na false. Randomem zvolíme jednu z pěti dostupných textur.

GLvoid InitObject(int num)// Inicializace objektu

{

object[num].rot = 1;// Rotace po směru hodinových ručiček

object[num].frame = 0;// První snímek exploze

object[num].hit = FALSE;// Ještě nebyl zasažen

object[num].texid = rand() % 5;// Náhodný index textury

Vzdálenost od pozorovatele nastavíme opět náhodně na hodnotu 0.0f až -40.0f (4000/100 = 40). Před renderingem objektu však scénu ještě posouváme do hloubky o dalších deset jednotek, takže se objekt defakto zobrazí v rozmezí od -10.0f do -50.0f. Ani příliš blízko ani příliš daleko.

object[num].distance = -(float(rand() % 4001) / 100.0f);// Náhodná hloubka

Po definování hloubky určíme výšku nad zemí. Nechceme, aby se objekt nacházel níže než -1.5f, protože by byl pod zemí. Také by neměl být výše než 3.0f. Abychom zůstali v tomto rozmezí, výsledek randomu nesmí být vyšší než 4.5f (-1.5f + 4.5f = 3.0f).

object[num].y = -1.5f + (float(rand() % 451) / 100.0f);// Náhodná y pozice

Výpočet počáteční x pozice je maličko složitější. Vezmeme pozici objektu v hloubce a odečteme od ní 15.0f. Výsledek operace vydělíme dvěma a odečteme od něj 5*level. Následuje další odčítání. Tentokrát odečteme náhodné číslo od 0 do 5 násobené aktuálním levelem. Předpokládám, že nechápete :-). Objekty se nyní ve vyšších levelech zobrazují dále od viditelné části scény (vlevo nebo vpravo). Kdybychom toto neudělali, zobrazovaly by se rychle jeden za druhým, takže by bylo velmi obtížné všechny zasáhnout a dostat se tak do dalšího levelu.

Abyste lépe pochopili určování x pozice, uvedu příklad. Řekněme, že se objekt nachází -30.0f jednotek hluboko ve scéně a aktuální level je 1.

object[num].x = ((-30.0f - 15.0f) / 2.0f) - (5*1) - float(rand() % (5*1));

object[num].x = (-45.0f / 2.0f) - 5 - float(rand() % 5);

object[num].x = (-22.5f) - 5 - { řekněme 3.0f };

object[num].x = (-22.5f) - 5 - { 3.0f };

object[num].x = -27.5f - { 3.0f };

object[num].x = -30.5f;

Před renderingem objektu provádíme translaci o deset jednotek do scény na ose z a hloubka v našem příkladu je -30.0f. Celková hloubka ve scéně je tedy -40.0f. Používáním perspektivního kódu z NeHeGL můžeme předpokládat, že levý okraj viditelné scény je -20.0f a pravý okraj se nachází na +20.0f. Před odečítáním randomů se rovná x-ová pozice -22.5f, což je PRÁVĚ okraj viditelné scény. Po těchto operacích to už je ale -30.0f a to znamená, že než se poprvé objeví, musí nejdříve urazit celých 8 jednotek doprava. Už je to jasnější?

// Náhodná x pozice založená na hloubce v obrazovce a s náhodným zpožděním před vstupem na scénu

object[num].x = ((object[num].distance - 15.0f) / 2.0f) - (5*level) - float(rand() % (5*level));

Nakonec zvolíme náhodný směr pohybu: 0 vlevo nebo 1 vpravo.

object[num].dir = (rand() % 2);// Náhodný směr pohybu

Nyní se podíváme, kterým směrem se bude objekt posunovat. Pokud půjde doleva (dir == 0), změníme rotaci na proti směru hodinových ručiček (rot = 2). Pozice na ose x je defaultně záporná. Nicméně, pokud se máme pohybovat vlevo, musíme se na začátku nacházet vpravo. Negujeme tedy hodnotu x.

if (object[num].dir == 0)// Pohybuje se doleva?

{

object[num].rot = 2;// Rotace proti směru hodinových ručiček

object[num].x = -object[num].x;// Výchozí pozice vpravo

}

Zjistíme, který druh objektu počítač vybral. Pokud se index textury rovná nule, zvolil texturu modré tváře a ty se vždy pohybují těsně nad zemí. Ručně nastavíme y pozici na -2.0f.

if (object[num].texid == 0)// Modrá tvář

{

object[num].y = -2.0f;// Vždy těsně nad zemí

}

Práce s objektem kýblu bude složitější. Padají totiž z nebe (dir = 3). Z toho také plyne, že bychom měli nastavit novou x-ovou pozici, protože by nikdy nebyl vidět (objekty jsou na začátku vždy vlevo nebo vpravo od scény). Namísto odečítání 15 z minulého příkladu odečteme pouze 10. Tímto dosáhneme menšího rozmezí hodnot, které udrží objekt viditelně na scéně. Předpokládáme-li, že se hloubka rovná -30.0f, skončíme s náhodnou hodnotou od 0.0f do +40.0f. Horní hodnota je kladná a ne záporná, jak by se mohlo zdát, protože rand() vždy vrací kladné číslo. Získali jsme tedy číslo od 0.0f do 40.0f, k němu přičteme hloubku (záporné číslo) mínus 10.0f a to celé dělené dvěma. Opět příklad: přepokládáme, že vrácená náhodná hodnota je 15 a objekt se nachází ve vzdálenosti -30.0f jednotek.

object[num].x = float(rand() % int(-30.0f - 10.0f)) + ((-30.0f - 10.0f) / 2.0f);

object[num].x = float(rand() % int(-40.0f) + (-40.0f) / 2.0f);

object[num].x = { předpokládejme 15 } + (-20.0f);

object[num].x = 15.0f - 20.0f;

object[num].x = -5.0f;

Nakonec určíme umístění na ose y. Chceme, aby padal z oblohy, ale nevystupoval z mraků. Číslo 4.5f odpovídá pozici maličko níže pod mraky.

if (object[num].texid == 1)// Kýbl

{

object[num].dir = 3;// Padá dolů

object[num].x = float(rand() % int(object[num].distance - 10.0f)) + ((object[num].distance - 10.0f) / 2.0f);

object[num].y = 4.5f;// Těsně pod mraky

}

Objekt terče by měl vystoupit nahoru ze země (dir = 2). Pro umístění na ose x použijeme stejný postup jako před chvílí. Nechceme, aby jeho počáteční poloha začínala nad zemí, takže nastavíme y na -3.0f (pod zemí). Od něj odečteme náhodné číslo od nuly do 5*level, aby se neobjevil hned, ale se zpožděním až po chvíli. Čím vyšší level, tím déle trvá, než se objeví. To dává hráči trochu času na vzpamatování se - bez této operace by terče vyskakovaly rychle jeden za druhým.

if (object[num].texid == 2)// Terč

{

object[num].dir = 2;// Vyletí vzhůru

object[num].x = float(rand() % int(object[num].distance - 10.0f)) + ((object[num].distance - 10.0f) / 2.0f);

object[num].y = -3.0f - float(rand() % (5*level));// Pod zemí

}

Všechny ostatní objekty se pohybují zleva doprava, a proto není nutné, abychom jejich nastavení nějakým způsobem měnili.

Mohli bychom už skončit, ale zbývá ještě udělat jednu velice důležitou věc. Aby alfa blending pracoval správně, musí být průhledné polygony vykreslovány od nejvzdálenějších po nejbližší a nesmí se protínat. Z buffer totiž vyřazuje vzdálenější polygony, jsou-li již nějaké před nimi. Kdyby ty přední nebyly průhledné, ničemu by to nevadilo a navíc by se rendering urychlil, nicméně, když jsou objekty vepředu průhledné, tak by objekty za nimi měly být vidět. Nyní se buď nezobrazí nebo je kolem předních vykreslen čtvercový tvar, reprezentující původní polygon bez průhlednosti... nic hezkého.

Známe hloubku všech objektů, takže není žádný problém, abychom je po inicializaci nového seřadili, jak potřebujeme. Použijeme standardní funkci qsort() (quick sort - rychlé řazení). Při následném renderingu vezmeme první prvek pole a vykreslíme ho. Nebudeme se muset o nic starat, protože víme, že je ve scéně nejhlouběji.

Tento kód jsem nalezl v MSDN, ale úspěchu předcházelo dlouhé hledání na internetu. Funkce qsort() pracuje dobře a dovoluje řadit celé struktury. Předáváme jí čtyři parametry. První ukazuje na pole objektů, které mají být seřazeny, druhý určuje jejich počet (odpovídá aktuálnímu levelu). Třetí parametr definuje velikost jedné struktury a čtvrtý je ukazatelem na porovnávací funkci Compare(). S největší pravděpodobností existuje nějaká lepší metoda pro řazení struktur, ale qsort() vyhovuje. Je rychlá a snadno se používá.

Důležitá poznámka: Pokud používáte glAlphaFunc() a glEnable(GL_ALPHA_TEST) namísto "klasického" blendingu, není řazení nutné. Používáním alpha funkcí jste ale omezeni na úplnou průhlednost nebo úplnou neprůhlednost, nic mezi tím. Používání BlendFunc() a řazení objektů stojí sice trochu práce navíc, ale dovoluje mít objekty poloprůhledné.

// *** Modifikovaný MSDN kód pro tento tutoriál ***

qsort((void *) &object, level, sizeof(struct objects), (compfn)Compare);// Řazení objektů podle hloubky

}

První dva příkazy v inicializačním kódu nagrabují informace o okně a indikátoru stisknutých kláves. Funkcí srand() inicializujeme generátor náhodných čísel, potom loadujeme textury a vytvoříme display listy fontu.

BOOL Initialize (GL_Window* window, Keys* keys)// Inicializace OpenGL

{

g_window = window;

g_keys = keys;

srand((unsigned)time(NULL));// Inicializace generátoru náhodných čísel

if ((!LoadTGA(&textures[0],"Data/BlueFace.tga")) ||// Modrá tvář

(!LoadTGA(&textures[1],"Data/Bucket.tga")) ||// Kbelík

(!LoadTGA(&textures[2],"Data/Target.tga")) ||// Terč

(!LoadTGA(&textures[3],"Data/Coke.tga")) ||// Coca-Cola

(!LoadTGA(&textures[4],"Data/Vase.tga")) ||// Váza

(!LoadTGA(&textures[5],"Data/Explode.tga")) ||// Exploze

(!LoadTGA(&textures[6],"Data/Ground.tga")) ||// Země

(!LoadTGA(&textures[7],"Data/Sky.tga")) ||// Obloha

(!LoadTGA(&textures[8],"Data/Crosshair.tga")) ||// Kurzor

(!LoadTGA(&textures[9],"Data/Font.tga")))// Font

{

return FALSE;// Inicializace se nezdařila

}

BuildFont();// Vytvoří display listy fontu

Nastavíme černé pozadí. Depth bufferem testujeme na méně nebo rovno (GL_LEQUAL).

glClearColor(0.0f, 0.0f, 0.0f, 0.0f);// Černé pozadí

glClearDepth(1.0f);// Nastavení depth bufferu

glDepthFunc(GL_LEQUAL);// Typ testování hloubky

glEnable(GL_DEPTH_TEST);// Zapne testování hloubky

Příkaz glBlendFunc() je VELMI důležitý. Parametry GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA oznamují OpenGL, aby při renderingu používalo alfa hodnoty uložené v textuře. Aby se blending mohl projevit, musíme ho zapnout. Dále zapínáme i mapování 2D textur a ořezávání zadních stran polygonů. Při kreslení zadáváme souřadnice polygonů proti směru hodinových ručiček, takže odstranění zadních stran polygonů ničemu nevadí. Navíc se program urychlí, protože má s kreslením pouze polovinu práce.

Výše v tutoriálu jsem psal o použití glAlphaFunc() namísto blendingu. Pokud chcete používat raději alfa funkci, zakomentářujte dva řádky důležité pro blending a odkomentářujte dva řádky alfy. Zakomentářovat můžete také řazení objektů pomocí qsort() a vše s ním spojené. Při alfa testingu není pořadí renderingu důležité.

Program půjde v pořádku, ale obloha se nezobrazí. Příčinou je její textura, která má alfa hodnotu 0.5f. Alfa, narozdíl od blendingu, však může být buď nula nebo jedna, nic mezi. Problém lze vyřešit modifikací alfa kanálu textury. Obě metody přinášejí velmi dobré výsledky.

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);// Nastavení alfa blendingu

glEnable(GL_BLEND);// Zapne alfa blending

// glAlphaFunc(GL_GREATER, 0.1f);// Nastavení alfa testingu

// glEnable(GL_ALPHA_TEST);// Zapne alfa testing

glEnable(GL_TEXTURE_2D);// Zapne mapování textur

glEnable(GL_CULL_FACE);// Ořezávání zadních stran polygonů

Na tomto místě inicializujeme všechny objekty, které program používá a potom ukončíme funkci.

for (int loop = 0; loop < 30; loop++)// Prochází všechny objekty

{

InitObject(loop);// Inicializace každého z nich

}

return TRUE;// Inicializace úspěšná

}

Naprogramujeme detekci zásahů do objektů. Ze všeho nejdříve deklarujeme buffer, který použijeme k uložení informací o vybraných objektech. Proměnná hits slouží k počítání zásahů.

void Selection(void)// Detekce zasažení objektů

{

GLuint buffer[512];// Deklarace selection bufferu

GLint hits;// Počet zasažených objektů

Skončila-li hra, není žádný důvod, abychom hledali, který objekt byl zasažen, a proto ukončíme funkci. Pokud je hráč stále ve hře, přehrajeme zvuk výstřelu. Tato funkce je volána pouze tehdy, když hráč stiskl tlačítko myši. A pokud stiskl tlačítko myši, znamená to, že chtěl vystřelit. Nezáleží, jestli zasáhl nebo ne, zvuk výstřelu je slyšet vždy. Přehrajeme ho v asynchroním módu (SND_ASYNC), aby běžel na pozadí a program nemusel čekat až skončí.

if (game)// Konec hry?

{

return;// Není důvod testovat na zásah

}

PlaySound("data/shot.wav", NULL, SND_ASYNC);// Přehraje zvuk výstřelu

Nastavíme pole viewport tak, aby obsahovalo pozici x, y se šířkou a výškou aktuálního viewportu (OpenGL okna). Voláním funkce glSelectBuffer() nařídíme OpenGL, aby použilo naše pole buffer pro svůj selection buffer.

GLint viewport[4];// Velikost viewportu. [0] = x, [1] = y, [2] = výška, [3] = šířka

glGetIntegerv(GL_VIEWPORT, viewport);// Nastaví pole podle velikosti a lokace scény relativně k oknu

glSelectBuffer(512, buffer);// Přikáže OpenGL, aby pro selekci objektů použilo pole buffer

Všechen kód níže je velmi důležitý. Nejdříve převedeme OpenGL do selection módu. Nic, co se vykresluje, se nezobrazí, ale namísto toho se informace o renderovaných objektech uloží do selection bufferu. Potom voláním glInitNames() a glPushName(0) inicializujeme name stack (stack jmen). Kdyby OpenGL nebylo v selection módu, glPushName() by bylo ignorováno.

(void) glRenderMode(GL_SELECT);// Převedení OpenGL do selection módu

glInitNames();// Inicializace name stacku

glPushName(0);// Vloží 0 (nejméně jedna položka) na stack

Po přípravě name stacku musíme omezit kreslení na oblast pod kurzorem. Zvolíme projekční matici, pushneme ji na stack a resetujeme ji voláním glLoadIdentity().

glMatrixMode(GL_PROJECTION);// Zvolí projekční matici

glPushMatrix();// Uložení projekční matice

glLoadIdentity();// Reset matice

Oblast kreslení omezíme příkazem gluPickMatrix(). První parametr určuje pozice myši na ose x, druhý je na ose y. Jedničky představují šířku a výšku picking regionu. Posledním parametrem je pole viewport, které určuje aktuální okraje viewportu. Mouse_x a mouse_y budou středem picking regionu.

// Vytvoření matice, která zvětší malou část obrazovky okolo kurzoru myši

gluPickMatrix((GLdouble) mouse_x, (GLdouble) (viewport[3] - mouse_y), 1.0f, 1.0f, viewport);

Voláním gluPerspective vynásobíme perspektivní matici pick maticí, která omezuje vykreslování na oblast vyžádanou od gluPickMatrix(). Potom přepneme na matici modelview a vykreslíme sestřelované objekty. Kreslíme je funkcí DrawTargets() a ne Draw(), protože chceme určit zásahy do objektů a ne do oblohy, země nebo kurzoru. Po vykreslení objektů přepneme zpět na projekční matici a popneme ji ze stacku. Nakonec se znovu vrátíme k matici modelview. Posledním příkazem přepneme OpenGL zpět do renderovacího módu, takže se opět budou vykreslované objekty zobrazovat na scénu. Proměnná hits bude po přiřazení obsahovat počet objektů, které byly vykresleny na oblast specifikovanou gluPickMatrix(). Tedy tam, kde se nacházel kurzor myši při výstřelu.

// Aplikování perspektivní matice

gluPerspective(45.0f, (GLfloat) (viewport[2] - viewport[0]) / (GLfloat) (viewport[3] - viewport[1]), 0.1f, 100.0f);

glMatrixMode(GL_MODELVIEW);// Modelview matice

DrawTargets();// Renderuje objekty do selection bufferu

glMatrixMode(GL_PROJECTION);// Projekční matice

glPopMatrix();// Obnovení projekční matice

glMatrixMode(GL_MODELVIEW);// Modelview matice

hits = glRenderMode(GL_RENDER);// Přepnutí do renderovacího módu, uložení počtu objektů pod kurzorem

Zjistíme, jestli bylo zaznamenáno více než nula zásahů. Pokud ano, přiřadíme proměnné choose jméno prvního objektu, který byl vykreslen do picking oblasti. Depth ukládá, jak hluboko ve scéně se tento objekt nachází. Každý zásah zabírá v bufferu čtyři položky. První je počtem jmen v name stacku, když se zásah udál. Druhá položka představuje minimální z hodnotu (hloubku) ze všech vertexů, které protínaly zobrazenou oblast v čase zásahu. Třetí naopak obsahuje maximální z hodnotu a poslední položka je obsahem name stacku v čase zásahu, nebo-li jméno objektu. V tomto programu nás zajímá minimální z hodnota a jméno objektu.

if (hits > 0)// Bylo více než nula zásahů?

{

int choose = buffer[3];// Uloží jméno prvního objektu

int depth = buffer[1];// Uloží jeho hloubku

Založíme cyklus skrz všechny zásahy, abychom se ujistili, že žádný z objektů není blíže než ten první. Jinými slovy potřebujeme najít nejbližší objekt ke střelci. Kdybychom ho nehledali a střelec zasáhl dva překrývající se objekty najednou, mohl by ten v pořadí pole první být vzdálenější od pozorovatele. kliknutí myší by sestřelilo špatný objekt. Je jasné, že pokud je několik terčů za sebou, tak se při výstřelu zasáhne vždy ten nejbližší.

Každý objekt má v poli buffer čtyři položky, takže násobíme aktuální průběh čtyřmi. Abychom získali hloubku objektu (druhá položka), přičítáme jedničku. pokud je právě testovaná hloubka menší než aktuálně nejnižší, připíšeme informace o jménu objektu a jeho hloubce. Po všech průchodech cyklem, bude choose obsahovat jméno ke střelci nejbližšího zasaženého objektu a depth jeho hloubku.

for (int loop = 1; loop < hits; loop++)// Prochází všechny detekované zásahy

{

if (buffer[loop*4 + 1] < GLuint(depth))// Je tento objekt blíže než některý z předchozích?

{

choose = buffer[loop*4 + 3];// Uloží jméno bližšího objektu

depth = buffer[loop*4 + 1];// Uloží jeho hloubku

}

}

Našli jsme zasažený objekt. Přiřazením TRUE do hit ho označíme, aby nemohl být zasažen po druhé nebo zničen automaticky po opuštění scény. Přičteme k hráčovu score jedničku a také inkrementujeme počet zásahů v daném levelu.

if (!object[choose].hit)// Nebyl ještě objekt zasažen?

{

object[choose].hit = TRUE;// Označí ho jako zasažený

score += 1;// Zvýší celkové skóre

kills += 1;// Zvýší počet zásahů v levelu

Chceme, aby v každém následujícím levelu musel hráč sestřelit větší počet objektů. Tím se znesnadňuje postup mezi levely. Zkontrolujeme, jestli je kills větší než aktuální level násobený pěti. V levelu jedna stačí pro postup sestřelit pouze pět objektů (1*5). V druhém levelu už je to deset (2*5), atd. Hra začíná být těžší a těžší.

Nastal-li čas pro přesun do následujího levelu, nastavíme počet nezasažených objektů na nulu, aby jím měl hráč větší šanci úspěšně projít. Ale aby vše nebylo zase tak jednoduché, vynulujeme i počet zasažených objektů. Nakonec nesmíme zapomenout inkrementovat level a otestovat, jestli už nebyl poslední. Důvod proč máme zrovna třicet levelů je velice jednoduchý. Třicátý level je už šíleně obtížný, myslím, že nikdo nemá šanci ho dosáhnout. Druhým důvodem je maximální počet objektů - je jich právě třicet. Chcete-li jich více poupravujte program.

Na scéně můžete mít ale MAXIMÁLNĚ 64 objektů (0 až 63). Pokud jich zkusíte renderovat 65 a více, PICKING PŘESTANE PRACOVAT SPRÁVNĚ a začnou se dít podivné věci. Všechno od náhodně vybuchujících objektů až k celému vašemu počítači se kompletně zhroutí. 64 objektů je fyzikální limit OpenGL, stejně jako například 8 světel ve scéně.

Pokud jste nějakou šťastnou náhodou bohem :-) a dostanete se až k třicátému levelu, výše už bohužel nepostoupíte. Nicméně celkové skóre se bude stále zvyšovat a počet zasažených i nezasažených objektů se vždy na tomto místě resetuje.

if (kills > level*5)// Čas pro další level?

{

miss = 0;// Nulování nezasažených objektů

kills = 0;// Nulování zasažených objektů v tomto levelu

level += 1;// Posun na další level

if (level > 30)// Poslední level?

{

level = 30;// Nastavení levelu na poslední

}

}

}

}

}

Ve funkci Update() testujeme stisk kláves a aktualizujeme umístění objektů ve scéně. Jednou z příjemných věcí je předávaný parametr miliseconds, který definuje uplynulý čas od předchozího volání. Na jeho bázi posuneme objekt o danou vzdálenost. A výsledek? Hra půjde stejně rychle na libovolném procesoru. ALE je zde jeden nedostatek. Řekněme, že máme objekt pohybující se pět jednotek za deset sekund. Rychlý počítač posune objektem o půl jednotky za sekundu. Na pomalém systému může trvat 2 sekundy, než se funkce znovu zavolá. Tím vznikají různá zpoždění a trhání, zkrátka animace už není plynulá. Lepší řešení však neexistuje. Pomalý počítač nezrychlíte, leda koupit nový...

Ale zpátky ke kódu. První podmínka zjišťuje stisk klávesy ESC, který ukončuje aplikaci.

void Update(DWORD milliseconds)// Aktualizace pohybů ve scéně a stisk kláves

{

if (g_keys->keyDown[VK_ESCAPE])// Klávesa ESC?

{

TerminateApplication(g_window);// Ukončení programu

}

Klávesa F1 přepíná mód okna mezi systémem a fullscreenem.

if (g_keys->keyDown[VK_F1])// Klávesa F1?

{

ToggleFullscreen(g_window);// Přepnutí fullscreen/okno

}

Stisk mezerníku po skončení hry založí novou. Inicializujeme všech třicet objektů, nastavíme konec hry na false, skóre na nulu, první level a zasažené i nezasažené objekty v tomto levelu také na nulu. Nic nepochopitelného.

if (g_keys->keyDown[' '] && game)// Mezerník na konci hry?

{

for (int loop = 0; loop < 30; loop++)// Prochází všechny objekty

{

InitObject(loop);// Jejich inicializace

}

game = FALSE;// Ještě není konec hry

score = 0;// Nulové skóre

level = 1;// První level

kills = 0;// Nula zasažených objektů

miss = 0;// Nula nezasažených objektů

}

K vytvoření iluze plujících mraků a pohybující se země, odečteme od roll číslo 0.00005f násobené počtem milisekund od minulého renderingu. Princip časování jsme si vysvětlili výše.

roll -= milliseconds * 0.00005f;// Mraky plují a země se pohybuje

Založíme cyklus, který prochází všechny objekty ve scéně a aktualizuje je. Jejich počet je roven aktuálnímu levelu.

for (int loop = 0; loop < level; loop++)// Aktualizace všech viditelných objektů

{

Potřebujeme zjistit, kterým směrem se který objekt otáčí. Podle směru rotace upravíme aktuální úhel natočení o 0.2 stupňů vynásobených řídící proměnnou cyklu sečtenou s milisekundami. Přičítáním loop získáme rozdílnou rotaci pro každý objekt. Druhý objekt se nyní otáčí rychleji než první a třetí objekt ještě rychleji než druhý.

if (object[loop].rot == 1)// Rotace po směru hodinových ručiček?

object[loop].spin -= 0.2f * (float(loop + milliseconds));

if (object[loop].rot == 2)// Rotace proti směru hodinových ručiček?

object[loop].spin += 0.2f * (float(loop + milliseconds));

Přesuneme se ke kódu zajišťujícímu pohyby. Pokud se objekt pohybuje doprava (dir == 1), přičteme k x pozici 0.0012f. Podobným způsobem ošetříme posun doleva (dir == 0). Při směru nahoru (dir == 2) zvětšíme y hodnotu, protože kladná část osy y leží nahoře. Směr dolů (dir == 3) je úplně stejný jako předchozí. Odečítáme však menší číslo, aby byl pád pomalejší.

if (object[loop].dir == 1)// Pohyb doprava?

object[loop].x += 0.012f * float(milliseconds);

if (object[loop].dir == 0)// Pohyb doleva?

object[loop].x -= 0.012f * float(milliseconds);

if (object[loop].dir == 2)// Pohyb nahoru?

object[loop].y += 0.012f * float(milliseconds);

if (object[loop].dir == 3)// Pohyb dolů?

object[loop].y -= 0.0025f * float(milliseconds);

Posunuli jsme objektem a nyní potřebujeme otestovat, jestli je na scéně ještě vidět. Můžeme to zjistit podle hloubky ve scéně mínus 15.0f (malá tolerance navíc) a dělením dvěma. Pro ty z vás, kteří od inicializace objektů už zapomněli... Pokud jste dvacet jednotek ve scéně, máte z každé strany zhruba deset jednotek viditelné scény (záleží na nastavení perspektivy). Takže -20.0f (hloubka) -15.0f (extra okraj) = -35.0f. Vydělíme 2.0f a získáme -17.5f, což je přibližně 7.5 jednotek vlevo od viditelné scény. Objekt tedy už určitě není vidět.

Musí také platit podmínka, že se objekt pohybuje doleva (dir == 0). Pokud ne, nestaráme se o něj. Poslední část logického výrazu představuje test zásahu. Shrneme to: pokud objekt vyletěl vlevo ze scény, pohybuje se doleva a nebyl zasažen, uživatel ho už nemá šanci zasáhnout. Zvýšíme počet nezasažených objektů a označíme objekt jako zasažený, aby se o něj program už příště nestaral. Touto cestou (hit = true) také zajistíme autodestrukci, což nám po nějaké době umožní jeho automatickou reinicializaci - nová textura, směr pohybu, rotace ap.

// Objekt vyletěl vlevo ze scény, pohybuje se vlevo a ještě nebyl zasažen

if ((object[loop].x < (object[loop].distance - 15.0f) / 2.0f) && (object[loop].dir == 0) && !object[loop].hit)

{

miss += 1;// Zvýšení počtu nezasažených objektů

object[loop].hit = TRUE;// Odstranění objektu (zajišťuje animaci exploze a reinicializaci)

}

Analogicky ošetříme opuštění scény vpravo a náraz do země.

// Objekt vyletěl vpravo ze scény, pohybuje se vpravo a ještě nebyl zasažen

if ((object[loop].x > -(object[loop].distance - 15.0f) / 2.0f) && (object[loop].dir == 1) && !object[loop].hit)

{

miss += 1;// Zvýšení počtu nezasažených objektů

object[loop].hit = TRUE;// Odstranění objektu (zajišťuje animaci exploze a reinicializaci)

}

// Objekt narazil do země, pohybuje se dolů a ještě nebyl zasažen

if ((object[loop].y < -2.0f) && (object[loop].dir == 3) && !object[loop].hit)

{

miss += 1;// Zvýšení počtu nezasažených objektů

object[loop].hit = TRUE;// Odstranění objektu (zajišťuje animaci exploze a reinicializaci)

}

Narozdíl od předchozích testů při letu vzhůru uděláme menší změnu. Pokud se objekt dostane na ose y výše než 4.5f jednotek (těsně pod mraky), nezničíme ho, ale pouze změníme jeho směr, aby se pohyboval dolů. Destrukci zajistí předchozí kód pro naražení do země.

if ((object[loop].y > 4.5f) && (object[loop].dir == 2))// Objekt je pod mraky a směřuje vzhůru

object[loop].dir = 3;// Změna směru na pád

}

}

Do mapy zpráv ve funkci WindowProc() přidáme dvě větve, které obsluhují události myši. Při stisknutí levého tlačítka uložíme pozici kliknutí v okně a ve funkci Selection() zjistíme, jestli se hráč strefil do některého z objektů nebo ne. Protože vykreslujeme vlastní OpenGL kurzor, potřebujeme při renderingu znát jeho pozici. O to se stará WM_MOUSEMOVE.

// Funkce WindowProc

case WM_LBUTTONDOWN:// Stisknutí levého tlačítka myši

mouse_x = LOWORD(lParam);

mouse_y = HIWORD(lParam);

Selection();

break;

case WM_MOUSEMOVE:// Pohyb myši

mouse_x = LOWORD(lParam);

mouse_y = HIWORD(lParam);

break;

Přistoupíme k vykreslení objektu. Funkci se předávají celkem tři parametry, které ho dostatečně popisují - šířka, výška a textura. Obdélník renderujeme zadáváním bodů proti směru hodinových ručiček, abychom mohli použít culling.

void Object(float width, float height, GLuint texid)// Vykreslí objekt

{

glBindTexture(GL_TEXTURE_2D, textures[texid].texID);// Zvolí správnou texturu

glBegin(GL_QUADS);// Kreslení obdélníků

glTexCoord2f(0.0f, 0.0f); glVertex3f(-width,-height, 0.0f);// Levý dolní

glTexCoord2f(1.0f, 0.0f); glVertex3f( width,-height, 0.0f);// Pravý dolní

glTexCoord2f(1.0f, 1.0f); glVertex3f( width, height, 0.0f);// Pravý horní

glTexCoord2f(0.0f, 1.0f); glVertex3f(-width, height, 0.0f);// Levý horní

glEnd();// Konec kreslení

}

Kód pro renderování exploze dostává pouze jeden parametr - identifikátor objektu. Potřebujeme nagrabovat souřadnice oblasti na textuře exploze. Uděláme to podobnou cestou, jako když jsme získávali jednotlivé znaky z textury fontu. Ex a ey představují sloupec a řádek závislý na pořadí snímku animace (framu).

Textura snímků exploze

Pozici na ose x získáme dělením aktuálního snímku čtyřmi. Protože máme 64 snímků a pouze 16 obrázků, potřebujeme animaci zpomalit. Zbytek po dělení upraví číslo na hodnoty 0 až 3 a aby texturové koordináty byly v rozmezí 0.0f a 1.0f, dělíme čtyřmi. Získali jsme sloupec, nyní ještě řádek. První dělení opět zmenšuje číslo, druhé dělení eliminuje celý řádek a posledním dělením získáme vertikální souřadnici na textuře.

Pokud je aktuální snímek 16, ey = 16/4/4/4 = 4/4/4 = 0,25. Jeden řádek dolů. je-li snímek 60, ey = 60/4/4/4 = 15/4/4 = 3/4 = 0,75. Matematici nevěří vlastním očím... Důvod proč se 15/4 nerovná 3,75 je to, že do posledního dělení pracujeme s celými čísly. Počítáme-li se zaokrouhlováním dojdeme k závěru, že výsledkem jsou vždy čísla 0.0f, 0.25f, 0.50f nebo 0.75f. Doufám, že to dává smysl. Je to jednoduché, ale matematika zastrašuje.

void Explosion(int num)// Animace exploze objektu

{

float ex = (float)((object[num].frame/4)%4)/4.0f;// Výpočet x snímku exploze (0.0f - 0.75f)

float ey = (float)((object[num].frame/4)/4)/4.0f;// Výpočet y snímku exploze (0.0f - 0.75f)

Získali jsme texturovací koordináty, zbývá vykreslit obdélník. Vertexy jsou fixovány na -1.0f a 1.0f. U textur odečítáme ey od 1.0f. Pokud bychom to neudělali animace by probíhala v opačném pořadí. Počátek texturovacích souřadnic je vlevo dole.

glBindTexture(GL_TEXTURE_2D, textures[5].texID);// Textura exploze

glBegin(GL_QUADS);// Kreslení obdélníků

glTexCoord2f(ex, 1.0f - (ey));

glVertex3f(-1.0f, -1.0f, 0.0f);// Levý dolní

glTexCoord2f(ex + 0.25f, 1.0f - (ey));

glVertex3f( 1.0f, -1.0f, 0.0f);// Pravý dolní

glTexCoord2f(ex + 0.25f, 1.0f - (ey + 0.25f));

glVertex3f( 1.0f, 1.0f, 0.0f);// Pravý horní

glTexCoord2f(ex, 1.0f - (ey + 0.25f));

glVertex3f(-1.0f, 1.0f, 0.0f);// Levý horní

glEnd();// Konec kreslení

Jak je vysvětleno výše, snímek nesmí být vyšší než 63, jinak by animace začala nanovo. Při přesáhnutí tohoto čísla reinicializujeme objekt.

object[num].frame += 1;// Zvýší snímek exploze

if (object[num].frame > 63)// Poslední snímek?

{

InitObject(num);// Reinicializace objektu

}

}

Následující sekce kódu vykresluje objekty. Začneme resetováním matice a přesunem o deset jednotek do hloubky.

void DrawTargets(void)// Vykreslí objekty

{

glLoadIdentity();// Reset matice

glTranslatef(0.0f, 0.0f, -10.0f);// Posun do hloubky

Založíme cyklus procházející všechny aktivní objekty. Funkcí glLoadName() skrytě označíme individuální objekty - každému se určí jméno (číslo), které odpovídá indexu v poli. Prvnímu se přiřadí nula, druhému jednička atd. Podle tohoto jména můžeme zjistit, který objekt byl zasažen. Pokud program není v selection módu glLoadName() je ignorováno. Po přiřazení jména uložíme matici.

for (int loop = 0; loop < level; loop++)// Prochází aktivní objekty

{

glLoadName(loop);// Přiřadí objektu jméno (pro detekci zásahů)

glPushMatrix();// Uložení matice

Přesuneme se na pozici objektu, kde má být vykreslen.

glTranslatef(object[loop].x, object[loop].y, object[loop].distance);// Umístění objektu

Před renderingem testujeme, jestli byl zasažen nebo ne. Pokud podmínka platí, vykreslíme místo objektu snímek animace exploze, jinak otočíme objektem na ose z o jeho úhel spin a až potom ho vykreslíme. Pro určení rozměrů použijeme pole size, které jsme vytvořili na začátku programu. Texid reprezentuje typ objektu (texturu).

if (object[loop].hit)// Byl objekt zasažen?

{

Explosion(loop);// Vykreslí snímek exploze

}

else// Objekt nebyl zasažen

{

glRotatef(object[loop].spin,0.0f,0.0f,1.0f);// Natočení na ose z

Object(size[object[loop].texid].w, size[object[loop].texid].h, object[loop].texid);// Vykreslení

}

Po renderingu popneme matici, abychom zrušili posun a natočení.

glPopMatrix();// Obnoví matici

}

}

Draw() je hlavní vykreslovací funkcí. Jako obvykle smažeme buffery a resetujeme matici, kterou následně pushneme.

void Draw(void)// Vykreslení scény

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže buffery

glLoadIdentity();// Reset matice

glPushMatrix();// Uloží matici

Zvolíme texturu (v pořadí sedmá) a pokusíme se vykreslit oblohu. Je složena ze čtyř otexturovaných obdélníků. První představuje oblohu od země přímo vzhůru. Textura na něm roluje docela pomalu. Druhý obdélník je vykreslen na stejném místě, ale jeho textura roluje rychleji. Obě textury se blendingem spojí dohromady a vytvoří tak hezký vícevrstvý efekt.

glBegin(GL_QUADS);// Kreslení obdélníků

glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f);// Pravý horní

glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f);// Levý horní

glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f);// Levý dolní

glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f);// Pravý dolní

glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f);// Pravý horní

glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f);// Levý horní

glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f);// Levý dolní

glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f);// Pravý dolní

Abychom přidali iluzi, že mraky plují směrem k pozorovateli, třetí obdélník směřuje z hloubky dopředu. Další obdélník je opět na stejné místě, ale textura roluje rychleji. Výsledkem čtyř obyčejných obdélníků je obloha, která se jeví, jako by stoupala od země vzhůru a přibližovala se k pozorovateli. Mohl jsem použít otexturovanou polokouli, ale byl jsem příliš líný. Efekt s obdélníky vypadá celkem slušně.

glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,0.0f);// Pravý horní

glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,0.0f);// Levý horní

glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f);// Levý dolní

glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f);// Bottom Right

glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,0.0f);// Pravý horní

glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,0.0f);// Levý horní

glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f);// Levý dolní

glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f);// Pravý dolní

glEnd();// Konec kreslení

Nyní vykreslíme zemi. Začíná tam, kde se nachází nejnižší bod oblohy a směřuje směrem k pozorovateli. Roluje stejně rychle jako mraky. Abychom přidali trochu více detailů a zamezili tak nepříjemnému kostičkování při velkém zvětšení, namapujeme texturu sedmkrát na ose x a čtyřikrát na ose y.

glBindTexture(GL_TEXTURE_2D, textures[6].texID);// Textura země

glBegin(GL_QUADS);// Kreslení obdélníků

glTexCoord2f(7.0f,4.0f-roll); glVertex3f( 27.0f,-3.0f,-50.0f);// Pravý horní

glTexCoord2f(0.0f,4.0f-roll); glVertex3f(-27.0f,-3.0f,-50.0f);// Levý horní

glTexCoord2f(0.0f,0.0f-roll); glVertex3f(-27.0f,-3.0f,0.0f);// Levý dolní

glTexCoord2f(7.0f,0.0f-roll); glVertex3f( 27.0f,-3.0f,0.0f);// Pravý dolní

glEnd();// Konec kreslení

Pozadí je vykresleno, přistoupíme k sestřelovaným objektům. Napsali jsme pro ně speciální funkci. Potom obnovíme matici.

DrawTargets();// Sestřelované objekty

glPopMatrix();// Obnovení matice

Vykreslíme kurzor myši. Nagrabované rozměry okna uložíme do struktury obdélníku window. Zvolíme projekční matici a pushneme ji, resetujeme ji a převedeme scénu z perspektivního módu do pravoúhlé projekce. Souřadnice 0, 0 se nacházejí vlevo dole.

Ve funkci glOrtho() prohodíme třetí a čtvrtý parametr, aby byl kurzor renderován proti směru hodinových ručiček a culling pracoval tak, jak chceme. Kdyby byl počátek souřadnic nahoře, zadávání bodů by probíhalo v opačném směru a kurzor s textem by se nezobrazil.

RECT window;// Proměnná obdélníku

GetClientRect (g_window->hWnd,&window);// Grabování rozměrů okna

glMatrixMode(GL_PROJECTION);// Projekční matice

glPushMatrix();// Uloží projekční matici

glLoadIdentity();// Reset projekční matice

glOrtho(0, window.right, 0, window.bottom, -1, 1);// Nastavení pravoúhlé scény

Po nastavení kolmé projekce zvolíme modelview matici a umístíme kurzor. Problém je v tom, že počátek scény (0, 0) je vlevo dole, ale okno (systém) ho má vlevo nahoře. Kdybychom pozici kurzoru neinvertovali, tak by se při posunutí dolů, pohyboval nahoru. Od spodního okraje okna odečteme mouse_y. Namísto předávání velikosti v OpenGL jednotkách, specifikujeme šířku a výšku v pixelech.

Rozhodl jsem se použít vlastní a ne systémový kurzor ze dvou důvodů. První a více důležitý je, že vypadá lépe a může být modifikován v jakémkoli grafickém editoru, který podporuje alfa kanál. Druhým důvodem je, že některé grafické karty kurzor ve fullscreenu nezobrazují. Hrát hru podobného typu bez kurzoru není vůbec snadné :-).

glMatrixMode(GL_MODELVIEW);// Zvolí matici modelview

glTranslated(mouse_x, window.bottom-mouse_y, 0.0f);// Posun na pozici kurzoru

Object(16, 16, 8);// Vykreslí kurzor myši

Vypíšeme logo NeHe Productions zarovnané na střed horní části okna, dále zobrazíme aktuální level a skóre.

glPrint(240, 450, "NeHe Productions");// Logo

glPrint(10, 10, "Level: %i", level);// Level

glPrint(250, 10, "Score: %i", score);// Skóre

Otestujeme, jestli hráč nestrefil více než devět objektů. Pokud ano, nastavíme game na true, čímž indikujeme konec hry.

if (miss > 9)// Nestrefil hráč více než devět objektů?

{

miss = 9;// Limit je devět

game = TRUE;// Konec hry

}

Po skončení hry vypisujeme text GAME OVER. Je-li hráč ještě ve hře, vypíšeme kolik objektů mu může ještě uniknout. Text je ve formátu např. '6/10' - může ještě nezasáhnout šest objektů z deseti.

if (game)// Konec hry?

{

glPrint(490, 10, "GAME OVER");// Vypíše konec hry

}

else

{

glPrint(490, 10, "Morale: %i/10", 10-miss);// Vypíše počet objektů, které nemusí sestřelit

}

Zbývá obnovit původní nastavení. Zvolíme projekční matici, obnovíme ji, zvolíme modelview matici a vyprázdníme buffer, abychom se ujistili, že všechny objekty byly v pořádku zobrazeny.

glMatrixMode(GL_PROJECTION);// Projekční matice

glPopMatrix();// Obnovení projekční matice

glMatrixMode(GL_MODELVIEW);// Modelview matice

glFlush();// Vyprázdní OpenGL renderovací pipeline

}

Tento tutoriál je výsledkem mnoha probdělých nocí při kódování a psaní HTML. Nyní byste měli rozumět pickingu, alfa testingu a řazení podle hloubky při alfa blendingu. Picking umožňuje vytvořit interaktivní software, který se ovládá myší. Všechno od her až po nádherné GUI. Největší výhodou pickingu je, že si nemusíme vést složitý záznam, kde se objekty nacházejí, o translacích a rotacích ani nemluvě. Objektu stačí přiřadit jméno a počkat na výsledek. S alfa blendingem a testingem můžete vykreslit objekt kompletně neprůhledný a/nebo plný otvorů. Výsledek je úžasný, nemusíte se starat o prosvítání textur.

Mohl jsem strávit spoustu času přidáváním pohybů podle fyzikálních zákonů, grafiky, zvuků a podobně. Nicméně jsem vysvětlil OpenGL techniky bez dalších zbytečností. Doufám, že se po čase objeví nějaké skvělé modifikace kódu, které už ale nechám na vás.

napsal: Jeff Molofee - NeHe <nehe (zavináč) connect.ab.ca>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 32

<<< Lekce 31 | Lekce 33 >>>