Vítejte u druhého dílu seriálu o programování v knihovně OpenGL. Dnes se podíváme na to, jak vykreslovat základní objekty a jak s nimi pohybovat.
Stavový automat
Nejdříve jenom krátká poznámka k tzv. stavovému automatu. OpenGL funguje jako stavový automat, což v praxi znamená, že vždy nějakým příkazem něco pustíte a ono se to děje, dokud to zase nevypnete nebo nezměníte. Je to hrozně jednoduché a pokud si to člověk uvědomí hned zkraje, ušetří mu to notnou dávku frustrace. Například příkaz glEnable(GL_LIGHT0) pouští světlo s označením 0 a to svítí, dokud nezavoláme glDisable(GL_LIGHT0). Funkce glEnable() a glDisable() jsou ostatně dost časté a fungují na spoustu věcí, ale k tomu se postupně dostaneme až později. Dalším příkladem může být funkce glMatrixMode(), kterou nastavujeme, s jakou maticí (vysvětlím později) chceme momentálně manipulovat. Nebo třeba glBindTexture(1) zase říká, že veškeré operace s texturou se dějí pro texturu 1, atd… To je zatím jenom tak pro informaci, určitě se k tomu ještě budu vracet.
Trocha teorie
Velice rád bych začal s klasickým „Hello world!“, ale u OGL je to s vypisováním textů docela složité (resp. OGL sama o sobě to neumožňuje, ale existují jiné způsoby, např. pomocí samotné knihovny GLUT). Ale nezoufejte, nakreslíme si čtvereček a potom i krychličku.
Funkce pro vykreslení bodu je následující:
glVertex3f(float x, float y, float z)
Její syntaxe není tak složitá, glVertex je jasné, 3 tady znamená, že se jedná o bod, který má 3 souřadnice, další možnosti by byly 2 pro 2D bod nebo 4 v případě, že by zde byla ještě 4. souřadnice w. f říká, že jako parametr budou předávány hodnoty typu float. Pak zde ještě může být parametr v (př. GlVertex3fv()) který značí, že je předáváno pole hodnot a ne jednotlivé hodnoty. [celý popis f-ce glVertex]
Abychom mohli nakreslit nějaký tvar, musíme funkci glVertex uzavřít do jakéhosi bloku, v jehož začátku parametrem specifikujeme, co konkrétně budeme vykreslovat. Nejzákladnější variantou je:
glBegin(GL_POINTS); //glBegin otevírá blok a parametr říká, co se bude kreslit (v tomto případě body)
glVertex3f(0.0,0.0,0.0);
glVertex3f(0.2,0.3,0.4);
glEnd(); //uzavírá blok (tentokrát není parametr potřeba)
Parametr f-ce glBegin() může nabývat různých hodnot podle toho, co chceme kreslit. My se tady zaměříme na několik základních. [plný výčet]
- GL_POINTS
- Vykresluje jednotlivé body. V bloku mezi glBegin a glEnd je jenom jedna f-ce glVertex.
- GL_LINES
- Vykresluje čáru mezi 2 body, ekvivalentem k němu je parametr GL_LINE_STRIP, který vykresluje čáru probíhající libovolným počtem bodů (Zkuste si to, nakreslete třeba pěticípou hvězdu jedním tahem.).
- GL_TRIANGLES
- Vykresluje trojúhelník definovaný třemi body. Toto se používá asi nejčastěji (resp. my to budeme používat, až se budeme učit loadovat modely vytvořené v 3Ds Max), jelikož trojúhelník, tedy face, je základem každého modelu. Stejně jako u čáry je zde GL_TRIANGLE_STRIP, ten opět vytváří jakýsi pruh trojúhelníků ze zadaných bodů.
- GL_QUADS
- Vykresluje čtverec ze čtyř zadaných bodů (opět má ekvivalent GL_QUAD_STRIP.
- GL_POLYGON
- Vykresluje libovolný n-úhelník z bodů ležících v jedné rovině.
Funkce glVertex není jediná, která se do bloku mezi glBegin a glEnd vkládá. Ještě je důležité specifikovat normálu plochy resp. normály bodů. Podle ní se totiž v OpenGL počítá světlo. K tomu slouži funkce glNormal3f(x,y,z). Toto opět není její jediný tvar, ale my ji budeme požívat jenom takhle. [další tvary]. Pro naši momentální potřebu stačí specifikovat normálu jenom jednou na začátku bloku, jelikož je pro všechny body stejná. Pokud se ale jedná o složitější povrch, kde nechceme mít vidět jednotlivé placky, je potřeba ji specifikovat pro každý bod. Světlo se pak počítá pro každý bod zvlášť a interpoluje se po ploše, to tvoří dojem plynulého povrchu.
Než se vrhneme na praktický případ, zmíním ještě základní transformace, které můžeme provádět, tj. rotaci a posun. Zatím jenom s použitím 2 základních f-cí – glTranslate a glRotate. Nebudeme se v tom teď moc babrat a způsob, jak fungují, popíšu příště,až se budeme věnovat transformačním maticím (mimochodem, zašátral bych být vámi po tom, co si pamatujete z analytické geometrie, jelikož bez ní se v OGL neobejdete, zopakujte si vektory a operace s nimi a také matice).
F-ce glTranslatef(float x, float y, float z) (jakoby) posune objekt do zadaných souřadnic x, y, z. Respektive posune všechny objekty, které jsou vykresleny po jejím zavolání, o vektor x, y, z. My ale dnes budeme řešit příklad, kde máme jeden objekt, a to v počátečních souřadnicích 0,0,0, takže je to v zásadě stejné. F-ce glTranslate může mít ještě podobu glTranslated(double x, double y, double z).
F-ce glRotatef(float angle, float x, float y, float z) rotuje objekty (resp. násobí momentální matici rotační maticí). První parametr je úhel rotace ve stupních, další tři pak specifikují vektor, okolo kterého se točíme (tedy osu rotace).
Praxe
Teď už nám tedy nic nebrání v tom, abychom si kreslili. Otevřete šablonu, kterou jste minule vytvořili a jdeme na to.
Pracovat budeme v zásadě jenom s funkcí onDisplay, kde se odehrává veškeré vykreslování. Vypadat bude následovně.
void onDisplay(void){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();
glTranslatef(0.0, 0.0, -2.0);
glRotatef(20,1.0,1.0,1.0);
glBegin(GL_QUADS);
glNormal3f(0.0,0.0,1.0); //normála míří směrem k nám po ose z
glVertex3f(-0.1,0.1,0.0);
glVertex3f(0.1,0.1,0.0);
glVertex3f(0.1,-0.1,0.0);
glVertex3f(-0.1,-0.1,0.0);
glEnd();
glutSwapBuffers();
}
První příkaz je jasný, jím se vymaže framebuffer z minulého framu tak, aby se do něj dalo znova kreslit. Funkce glLoadIdentity() zruší všechny transformace provedené v minulém průchodu, takže se jede zase od znova. Jinak by se transformace za každý frame sčítaly. V řeči matic to znamená, že modelview matici nastavíme znovu na jednotkovou.
Pak se provede posun a rotace. Zkuste si ty funkce schválně prohodit a uvidíte, co se stane. Jde o to, že v prvním případě se nejprve posune celý souřadný systém o vektor x,y,z a pak se otočí okolo svého posunutého středu, takže nám to připadá, jako bychom posunuli objekt a otočili s ním. Je důležité si uvědomit, že to tak není, že f-ce glTranslate a glRotate hýbou celým souřadným systémem. Pokud je prohodíme, nejprve se otočí celý souřadný systém a v něm pak dojde k posunu. Tzn. osa z už nemíří do středu obrazovky, ale je vůči kameře pootočená. Toho se dá využít, budeme-li chápat předchozí transformace jako pohyb kamerou.
Možná to bude jasnější (možná komplikovanější) na jiném příkladu.
glRotatef(10,0.0,1.0,0.0);
glTranslatef(0.0, 0.0, -2.0);
glRotatef(45,0.0,0.0,1.0);
První dvě transformace jakoby posunou kameru dozadu směrem od sledovaného objektu a otočí s ní o 10 stupňů. Ta třetí už pak točí se samotným objektem. Lépe se to chápe, když si člověk zvykne pracovat s maticemi, v příštím díle se to pokusím nějak srozumitelně vysvětlit.
Takže jsme zatím nakreslili čtverec. Teď přijde krychle, což je v podstatě to samé, jenom je tam víc stran. Vytvoříme si funkci, která kreslí krychli se stranou a.
void krychle(float a) {
a/=2.0;
//pření stěna
glBegin(GL_QUADS);
glNormal3f(0.0,0.0,1.0);
glVertex3f(-a,a,a);
glVertex3f(a,a,a);
glVertex3f(a,-a,a);
glVertex3f(-a,-a,a);
glEnd();
//pření stěna
glBegin(GL_QUADS);
glNormal3f(0.0,0.0,1.0);
glVertex3f(-a,a,a);
glVertex3f(-a,-a,a);
glVertex3f(a,-a,a);
glVertex3f(a,a,a);
glEnd();
//zadní stěna
glBegin(GL_QUADS);
glNormal3f(0.0,0.0,-1.0);
glVertex3f(-a,a,-a);
glVertex3f(a,a,-a);
glVertex3f(a,-a,-a);
glVertex3f(-a,-a,-a);
glEnd();
//vrchní stěna
glBegin(GL_QUADS);
glNormal3f(0.0,1.0,0.0);
glVertex3f(-a,a,-a);
glVertex3f(-a,a,a);
glVertex3f(a,a,a);
glVertex3f(a,a,-a);
glEnd();
//spodní stěna
glBegin(GL_QUADS);
glNormal3f(0.0,-1.0,0.0);
glVertex3f(-a,-a,-a);
glVertex3f(a,-a,-a);
glVertex3f(a,-a,a);
glVertex3f(-a,-a,a);
glEnd();
//levá stěna
glBegin(GL_QUADS);
glNormal3f(-1.0,0.0,0.0);
glVertex3f(-a,a,a);
glVertex3f(-a,a,-a);
glVertex3f(-a,-a,-a);
glVertex3f(-a,-a,a);
glEnd();
//pravá stěna
glBegin(GL_QUADS);
glNormal3f(1.0,0.0,0.0);
glVertex3f(a,a,a);
glVertex3f(a,-a,a);
glVertex3f(a,-a,-a);
glVertex3f(a,a,-a);
glEnd();
}
Všiměte si způsobu, jak jsou seřazeny jednotlivé body. Jdou vždy proti směru hodinových ručiček (pokud se na danou plochu díváme z pohledu jakoby proti normále). To je důležité dodržovat kvůli ořezáváni stran (viz níže).
Funkce onDisplay() pak bude vypadat takhle.
void onDisplay(void){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();
glTranslatef(0.0, 0.0, -2.0);
glRotatef(20,1.0,1.0,1.0);
krychle(0.5);
glutSwapBuffers();
}
Teď se ještě vrhneme na základní způsob, jak to rozhýbat. Nejjednodušší by bylo jenom dosadit např. za úhel rotace nějakou proměnou a k té pak každý frame přičítat nějakou pevnou hodnotu. Problém je ale v tom, že by pak rychlost animace byla závislá na počtu snímků za vteřinu (FPS), což není dobré. Jak tedy na to? Nejdřív musíme zjistit počet snímků za vteřinu. Pomůže nám k tomu následující funkce, kterou vložíme na začátek funkce onDisplay.
int fps,timeSinceStart,frames,lastTime; // zaregistrujeme proměné, se kterými se pracuje
void timer() {
frames++; // proměná se zvetší pri každém průchodu f-ce onDisplay()
timeSinceStart=glutGet(GLUT_ELAPSED_TIME); // čas od startu aplikace
if ((timeSinceStart-lastTime)>1000) { // pokud uplynula vteřina od předchozího zápisu
lastTime=timeSinceStart;
fps=frames; // zapíše se, kolik proběhlo snímků za poslední vteřinu
frames=0; // počítadlo se vynuluje
}
}
Tato funkce vždy po dobu jedné sekundy přičítá framy. Po uplynutí jedné sekundy se počet framů uloží do proměnné fps a jede se odznovu. Čas od začátku běhu aplikace zde zjišťujeme pomocí funkce glutGet() s parametrem GLUT_ELAPSED_TIME. GlutGet() umožňuje získávat spoustu informací o běhu programu. [celkový seznam jejích parametrů].
Zavedeme si ještě jednu proměnou, a to float speed, ta nám bude udávat rychlost rotace ve stupních za vteřinu. Jediné, co pak musíme udělat, je zvětšit úhel otočení o speed/fps. Je třeba dát pozor ještě na jednu věc a tou je dělení nulou. To lze naštěstí ošetřit jednoduchou podmínkou. Celá f-ce onDisplay pak bude vypadat takto.
float uhel; //úhel rotace
float speed=20.0; // rychlost rotace
void onDisplay(void){
timer(); //fce. měřící fps
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();
if (fps>0.0) { //ošetříme dělení nulou
uhel+=speed/fps;
}
glTranslatef(0.0, 0.0, -2.0);
glRotatef(uhel,1.0,1.0,1.0);
krychle(0.5);
glutSwapBuffers();
}
Zkuste si s tím pohrát, udělejte třeba krychli, která se bude nejenom točit, ale bude i měnit svojí velikost (pulzovat za pomoci nějaké cos funkce), nebo si zkuste pohrát s nějakou animací s čárami.
Dodatky
Points a lines
Teď ještě dodatek k čarám a bodům.
void glPointSize( GLfloat size )
Tato funkce nastavuje, jak veliká bude reprezentace bodu na obrazovce. Je možno ji zavolat jednou při inicializaci, případně během vykreslování a opět tak měnit velikost dynamicky.
void glLineWidth( GLfloat width )
Obdobně nastavuje šířku čáry.
Pokud se vám budou čáry zdát moc kostrbaté nebo body příliš hranaté, můžete povolit jejich vyhlazování.
glEnable(GL_POINT_SMOOTH)
glEnable(GL_LINE_SMOOTH)
Funkce glDisable pak vyhlazování vypíná.
Polygony
Co se týče vykreslování polygonů, je třeba se ještě zmínit o ořezávání (tzv. Culling). Lze totiž nastavit, aby se jedna ze stran polygonu (přední nebo zadní) nevykreslovala. Zadní stěnu polygonů tvořících uzavřené objekty například vůbec nevidíme, proto je netřeba ji zobrazovat.
Funkce glEnable s parametrem GL_CULL_FACE zapíná ořezávání. F-ce glCullFace() pak specifikuje, jakou stranu chceme ořezávat, může mít za parametr GL_FRONT (přední), nebo GL_BACK (zadní). Zkuste si vložit do funkce init() následující kód.
glCullFace(GL_FRONT);
glEnable(GL_CULL_FACE);
Zdá se, jako by se nic nestalo, jelikož se ořezává něco, co stejně nevidíme. Zkuste si ale místo krychle vykreslit jenom rotující čtverec a uvidíte, že když se obrátí svou zadní stranou směrem k vám, tak nebude vidět (pokud použijete parametr GL_BACK, tak to bude opačně). To, která strana je přední nebo zadní, se určuje podle pořadí jednotlivých bodů tvořících polygon. Defaultně je nastaveno, že přední je strana, kde jsou strany seřazeny proti směru hodinových ručiček, toto chování se dá ale měnit následujícími příkazy.
glFrontFace(GL_CW); // přední strana je po směru hodinových ručiček
glFrontFace(GL_CWW); // … proti …
Polygony je také možno vykreslit jenom pomocí čar, které reprezentují jejich okraje, případně pomocí bodů. To se dělá následující funkcí.
void glPolygonMode( GLenum face, GLenum mode )
Má dva parametry, prvním říkáme, jestli ji chceme aplikovat na přední face-y (GL_FRONT), zadní facy (GL_BACK), nebo na obojí (GL_FRONT_AND_BACK). Druhý parametr nastavuje způsob vykreslování. Defaultně je nastaveno GL_FILL (vykresluje se výplň polygonu), další možnosti jsou GL_LINE (vykreslují se pouze hrany) a GL_POINT (pouze jednotlivé body). Obojí možná vypadá zbytečně, ale může se to hodit, když pracujeme s nějakým složitějším tvarem a potřebujeme se ujistit, že všechny čáry vedou tam, kam mají, atd.
Příště
To by bylo tedy pro tento díl vše. Na příště se pokusím nějak shrnout práci s maticemi, bude to spíš teoretičtější téma, ale podle mě je důležité pochopit ho hned ze začátku.