Společnými silami vytvoříme extrémně působivý efekt radial blur, který nevyžaduje žádná OpenGL rozšíření a funguje na jakémkoli hardwaru. Naučíte se také, jak lze na pozadí aplikace vyrenderovat scénu do textury, aby pozorovatel nic neviděl.
Ahoj, jmenuji se Dario Corno, ale jsem také z nám jako rIo ze Spinning Kids. První ze všeho vysvětlím, proč jsem se rozhodl napsat tento tutoriál. Roku 1989 jsem se stal "scénařem". Chtěl bych po vás, abyste si stáhli nějaká dema. Pochopíte, co to demo je a v čem spočívají demo efekty.
Dema vytvářejí opravdoví kodeři na ukázku hardcore a často i brutálních kódovacích technik. Svým způsobem jsou druhem umění, ve kterém se spojuje vše od hudby (hudba na pozadí, zvuky) a malířství (grafika, design, modely) přes matematiku a fyziku (vše funguje na nějakých principech) až po programování a detailní znalost počítače na úrovni hardwaru. Obrovské kolekce dem můžete najít na http://www.pouet.net/ a http://ftp.scene.org/, v Čechách pak http://www.scene.cz/. Ale abyste se hned na začátku nevylekali... toto není pravý smrtící tutoriál, i když musím uznat, že výsledek stojí za to.
Překl.: Se svým prvním demem jsem se setkal ve druháku na střední, kdy nám spolubydlící na intru Lukáš Duzsty Hoger ukazoval na 486 notebooku jeden prográmek, který zabíral kolem 2 kB. Na začátku byla vidět ruka, jak kreslí na plátno dům, strom a postavy, scéna se vyboulila do 3D a musím říct, že na 256 barev a DOSovou grafiku vše vypadalo úchvatně - kam se programátoři využívající pohodlných služeb OpenGL vůbec hrabou :-). Proti tomu koderovi fakt batolata. Asi nejlepší demo, které jsem kdy viděl byla 64 kB animace "reálného" 3D prostředí ve video kvalitě, která trvala něco přes čtvrt hodiny. Jenom texty v kreditu na konci musely zabírat polovinu místa. Zkuste si pro zajímavost zkompilovat prázdnou MFC aplikaci vygenerovanou APP Wizzardem, která navíc tahá většinu potřebných funkcí z DLL knihoven - nedostanete se pod 30 kB.
Tolik tedy k úvodu... Co se ale dozvíte v tomto tutoriálu? Vysvětlím vám, jak vytvořit perfektní efekt (používaný v demech), který vypadá jako radial blur (radiální rozmazání). Někdy je také označován jako volumetrická světla, ale nevěřte, je to pouze obyčejný radial blur.
Radial blur bývá obyčejně vytvářen (pouze při softwarovém renderingu) rozmazáváním pixelů originálního obrázku v opačném směru než se nachází střed rozmazávání. S dnešním hardwarem je docela obtížné provádět ruční blurring (rozmazávání) za použití color bufferu (alespoň v případě, že je podporován všemi grafickými kartami), takže potřebujeme využít malého triku, abychom dosáhli alespoň podobného efektu. Jako bonus se také dozvíte, jak je snadné renderovat do textury.
Objekt, který jsem se pro tento tutoriál rozhodl použít, je spirála, protože vypadá hodně dobře. Navíc jsem už celkem unavený z krychliček :-] Musím ještě poznamenat, že vysvětluji hlavně vytváření výsledného efektu, naopak pomocný kód už méně detailněji. Měli byste ho mít už dávno zažitý.
// Uživatelské proměnné
float angle;// Úhel rotace spirály
float vertexes[4][3];// Čtyři body o třech souřadnicích
float normal[3];// Data normálového vektoru
GLuint BlurTexture;// Textura
Tak tedy začneme... Funkce EmptyTexture() generuje prázdnou texturu a vrací číslo jejího identifikátoru. Na začátku alokujeme paměť obrázku o velikosti 128*128*4. Tato čísla označují šířku, výšku a barevnou hloubku (RGBA) obrázku. Po alokaci paměť vynulujeme. Protože budeme texturu roztahovat, použijeme pro ni lineární filtrování, GL_NEAREST v našem případě nevypadá zrovna nejlépe.
GLuint EmptyTexture()// Vytvoří prázdnou texturu
{
GLuint txtnumber;// ID textury
unsigned int* data;// Ukazatel na data obrázku
data = (unsigned int*) new GLuint[((128 * 128) * 4 * sizeof(unsigned int))];// Alokace paměti
ZeroMemory(data,((128 * 128)* 4 * sizeof(unsigned int)));// Nulování paměti
glGenTextures(1, &txtnumber);// Jedna textura
glBindTexture(GL_TEXTURE_2D, txtnumber);// Zvolí texturu
glTexImage2D(GL_TEXTURE_2D, 0, 4, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);// Vytvoření textury
// Lineární filtrování pro zmenšení i zvětšení
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
delete [] data;// Uvolnění paměti
return txtnumber;// Vrátí ID textury
}
Následující funkce normalizuje vektor, který je předán v parametru jako pole tří floatů. Spočítáme jeho délku a s její pomocí vydělíme všechny tři složky.
void ReduceToUnit(float vector[3])// Výpočet normalizovaného vektoru (jednotková délka)
{
float length;// Délka vektoru
// Výpočet současné délky vektoru
length = (float)sqrt((vector[0]*vector[0]) + (vector[1]*vector[1]) + (vector[2]*vector[2]));
if(length == 0.0f)// Prevence dělení nulou
{
length = 1.0f;
}
vector[0] /= length;// Vydělení jednotlivých složek délkou
vector[1] /= length;
vector[2] /= length;
// Výsledný vektor je předán zpět v parametru funkce
}
Pomocí funkce calcNormal() lze vypočítat vektor, který je kolmý ke třem bodům tvořícím rovinu. Dostali jsme dva parametry: v[3][3] představuje tři body (o třech složkách x,y,z) a do out[3] uložíme výsledek. Na začátku deklarujeme dva pomocné vektory a tři konstanty, které vystupují jako indexy do pole.
void calcNormal(float v[3][3], float out[3])// Výpočet normálového vektoru polygonu
{
float v1[3], v2[3];// Vektor 1 a vektor 2 (x,y,z)
static const int x = 0;// Pomocné indexy do pole
static const int y = 1;
static const int z = 2;
Ze třech bodů předaných funkci vytvoříme dva vektory a spočítáme třetí vektor, který je k nim kolmý.
v1[x] = v[0][x] - v[1][x];// Výpočet vektoru z 1. bodu do 0. bodu
v1[y] = v[0][y] - v[1][y];
v1[z] = v[0][z] - v[1][z];
v2[x] = v[1][x] - v[2][x];// Výpočet vektoru z 2. bodu do 1. bodu
v2[y] = v[1][y] - v[2][y];
v2[z] = v[1][z] - v[2][z];
// Výsledkem vektorového součinu dvou vektorů je třetí vektor, který je k nim kolmý
out[x] = v1[y]*v2[z] - v1[z]*v2[y];
out[y] = v1[z]*v2[x] - v1[x]*v2[z];
out[z] = v1[x]*v2[y] - v1[y]*v2[x];
Aby vše bylo dokonalé, tak výsledný vektor normalizujeme na jednotkovou délku.
ReduceToUnit(out);// Normalizace výsledného vektoru
// Výsledný vektor je předán zpět v parametru funkce
}
Následující rutina vykresluje spirálu. Po deklaraci proměnných nastavíme pomocí gluLookAt() výhled do scény. Díváme se z bodu 0, 5, 50 do bodu 0, 0, 0. UP vektor míří vzhůru ve směru osy y.
void ProcessHelix()// Vykreslí spirálu
{
GLfloat x;// Souřadnice x, y, z
GLfloat y;
GLfloat z;
GLfloat phi;// Úhly
GLfloat theta;
GLfloat u;
GLfloat v;
GLfloat r;// Poloměr závitu
int twists = 5;// Pět závitů
GLfloat glfMaterialColor[] = { 0.4f, 0.2f, 0.8f, 1.0f};// Barva materiálu
GLfloat specular[] = { 1.0f, 1.0f, 1.0f, 1.0f};// Specular světlo
glLoadIdentity();// Reset matice
gluLookAt(0,5,50, 0,0,0, 0,1,0);// Pozice očí (0,5,50), střed scény (0,0,0), UP vektor na ose y
Uložíme matici a přesuneme se o padesát jednotek do scény. V závislosti na úhlu angle (globální proměnná) se spirálou rotujeme. Také nastavíme materiály.
glPushMatrix();// Uložení matice
glTranslatef(0, 0, -50);// Padesát jednotek do scény
glRotatef(angle/2.0f, 1, 0, 0);// Rotace na ose x
glRotatef(angle/3.0f, 0, 1, 0);// Rotace na ose y
// Nastavení materiálů
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, glfMaterialColor);
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR,specular);
Pokud ovládáte goniometrické funkce, je výpočet jednotlivých bodů spirály relativně jednoduchý, ale nebudu to zde vysvětlovat (Překl.: díky bohu... :-), protože spirála není hlavní náplní tohoto tutoriálu. Navíc jsem si kód půjčil od kamarádů z Listen Software. Půjdeme jednodušší, ale ne nejrychlejší cestou. S vertex arrays by bylo vše mnohem rychlejší.
r = 1.5f;// Poloměr
glBegin(GL_QUADS);// Kreslení obdélníků
for(phi = 0; phi <= 360; phi += 20.0)// 360 stupňů v kroku po 20 stupních
{
for(theta = 0; theta <= 360*twists; theta += 20.0)// 360 stupňů* počet závitů po 20 stupních
{
v = (phi / 180.0f * 3.142f);// Úhel prvního bodu (0)
u = (theta / 180.0f * 3.142f);// Úhel prvního bodu (0)
x = float(cos(u) * (2.0f + cos(v))) * r;// Pozice x, y, z prvního bodu
y = float(sin(u) * (2.0f + cos(v))) * r;
z = float(((u - (2.0f * 3.142f)) + sin(v)) * r);
vertexes[0][0] = x;// Kopírování prvního bodu do pole
vertexes[0][1] = y;
vertexes[0][2] = z;
v = (phi / 180.0f * 3.142f);// Úhel druhého bodu (0)
u = ((theta + 20) / 180.0f * 3.142f);// Úhel druhého bodu (20)
x = float(cos(u) * (2.0f + cos(v))) * r;// Pozice x, y, z druhého bodu
y = float(sin(u) * (2.0f + cos(v))) * r;
z = float(((u - (2.0f * 3.142f)) + sin(v)) * r);
vertexes[1][0] = x;// Kopírování druhého bodu do pole
vertexes[1][1] = y;
vertexes[1][2] = z;
v=((phi + 20) / 180.0f * 3.142f);// Úhel třetího bodu (20)
u=((theta + 20) / 180.0f * 3.142f);// Úhel třetího bodu (20)
x = float(cos(u) * (2.0f + cos(v))) * r;// Pozice x, y, z třetího bodu
y = float(sin(u) * (2.0f + cos(v))) * r;
z = float(((u - (2.0f * 3.142f)) + sin(v)) * r);
vertexes[2][0] = x;// Kopírování třetího bodu do pole
vertexes[2][1] = y;
vertexes[2][2] = z;
v = ((phi + 20) / 180.0f * 3.142f);// Úhel čtvrtého bodu (20)
u = ((theta) / 180.0f * 3.142f);// Úhel čtvrtého bodu (0)
x = float(cos(u) * (2.0f + cos(v))) * r;// Pozice x, y, z čtvrtého bodu
y = float(sin(u) * (2.0f + cos(v))) * r;
z = float(((u - (2.0f * 3.142f)) + sin(v)) * r);
vertexes[3][0] = x;// Kopírování čtvrtého bodu do pole
vertexes[3][1] = y;
vertexes[3][2] = z;
calcNormal(vertexes, normal);// Výpočet normály obdélníku
glNormal3f(normal[0], normal[1], normal[2]);// Poslání normály OpenGL
// Rendering obdélníku
glVertex3f(vertexes[0][0], vertexes[0][1], vertexes[0][2]);
glVertex3f(vertexes[1][0], vertexes[1][1], vertexes[1][2]);
glVertex3f(vertexes[2][0], vertexes[2][1], vertexes[2][2]);
glVertex3f(vertexes[3][0], vertexes[3][1], vertexes[3][2]);
}
}
glEnd();// Konec kreslení
glPopMatrix();// Obnovení matice
}
Funkce ViewOrtho() slouží k přepnutí z perspektivní projekce do pravoúhlé a ViewPerspective() k návratu zpět. Vše už bylo popsáno například v tutoriálech o fontech, ale i jinde, takže to zde nebudu znovu probírat.
void ViewOrtho()// Nastavuje pravoúhlou projekci
{
glMatrixMode(GL_PROJECTION);// Projekční matice
glPushMatrix();// Uložení matice
glLoadIdentity();// Reset matice
glOrtho(0, 640 , 480 , 0, -1, 1);// Nastavení pravoúhlé projekce
glMatrixMode(GL_MODELVIEW);// Modelview matice
glPushMatrix();// Uložení matice
glLoadIdentity();// Reset matice
}
void ViewPerspective()// Obnovení perspektivního módu
{
glMatrixMode(GL_PROJECTION);// Projekční matice
glPopMatrix();// Obnovení matice
glMatrixMode(GL_MODELVIEW);// Modelview matice
glPopMatrix();// Obnovení matice
}
Pojďme si vysvětlit, jak pracuje naše imitace efektu radial blur. Potřebujeme vykreslit scénu tak, aby se jevila jakoby rozmazaná od středu do všech směrů. Nemůžeme číst ani zapisovat pixely a pokud chceme zachovat kompatibilitu s různým grafickými kartami, neměli bychom používat ani OpenGL rozšíření ani jiné příkazy specifické pro určitý hardware. Řešení je docela snadné, OpenGL nám dává možnost blurnout (rozmazat) textury. OK... ne opravdový blurring. Pokud za použití lineárního filtrování roztáhneme textury, výsledek bude, s trochou představivosti, vypadat podobně jako gausovo rozmazávání (gaussian blur). Takže, co se stane, pokud přilepíme spoustu roztáhnutých textur vyobrazujících 3D objekt na scénu přesně před něj? Odpověď je celkem snadná - radial blur!
Potřebujeme však vyřešit dva související problémy: jak v realtimu vytvářet tuto texturu a jak ji zobrazit přesně před objekt. Řešení prvního je mnohem snazší než si asi myslíte. Co takhle renderovat přímo do textury? Pokud aplikace používá double buffering, je přední buffer zobrazen na obrazovce a do zadního se kreslí. Dokud nezavoláme příkaz SwapBuffers(), změny se navenek neprojeví. Renderování do textury spočívá v renderingu do zadního bufferu (tedy klasicky, jak jsme zvyklí) a v zkopírování jeho obsahu do textury pomocí funkce glCopyTexImage2D().
Problém dva: vycentrování textury přesně před 3D objekt. Víme, že pokud změníme viewport bez nastavení správné perspektivy, získáme deformovanou scénu. Například, nastavíme-li ho opravdu široký bude scéna roztáhnutá vertikálně.
Nejdříve nastavíme viewport tak, aby byl čtvercový a měl stejné rozměry jako textura (128x128). Po renderování objektu, nakopírujeme color buffer do textury a smažeme ho. Obnovíme původní rozměry a vykreslíme objekt podruhé, tentokrát při správném rozlišení. Poté, co texturu namapujeme na obdélník o velikosti scény, roztáhne se zpět na původní velikost a bude umístěná přesně před 3D objekt. Doufám, že to dává smysl. Představte si 640x480 screenshot zmenšený na bitmapu o velikosti 128x128 pixelů. Tuto bitmapu můžeme v grafickém editoru roztáhnout na původní rozměry 640x480 pixelů. Kvalita bude o mnoho horší, ale obrázku si budou odpovídat.
Pojďme se podívat na kód. Funkce RenderToTexture() je opravdu jednoduchá, ale představuje kvalitní "designový trik". Nastavíme viewport na rozměry textury a zavoláme rutinu pro vykreslení spirály. Potom zvolíme blur texturu jako aktivní a z viewportu do ní nakopírujeme color buffer. První parametr funkce glCopyTexImage2D() indikuje, že používáme 2D texturu, nula označuje úroveň mip mapy (mip map level), defaultně se zadává nula. GL_LUMINANCE představuje formát dat. Používáme právě tuto část bufferu, protože výsledek vypadá přesvědčivěji, než kdybychom zadali např. GL_ALPHA, GL_RGB, GL_INTENSITY nebo jiné. Další dva parametry říkají, kde začít (0, 0), dvakrát 128 představuje výšku a šířku. Poslední parametr bychom změnili, kdybychom požadovali okraj (rámeček), ale teď ho nechceme. V tuto chvíli máme v textuře uloženu kopii color bufferu. Smažeme ho a nastavíme viewport zpět na správné rozměry.
DŮLEŽITÉ: Tento postup může být použit pouze s double bufferingem. Důvodem je, že všechny potřebné operace se musí provádět na pozadí (v zadním bufferu), aby je uživatel neviděl.
void RenderToTexture()// Rendering do textury
{
glViewport(0, 0, 128, 128);// Nastavení viewportu (odpovídá velikosti textury)
ProcessHelix();// Rendering spirály
glBindTexture(GL_TEXTURE_2D, BlurTexture);// Zvolí texturu
// Zkopíruje viewport do textury (od 0, 0 do 128, 128, bez okraje)
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, 128, 128, 0);
glClearColor(0.0f, 0.0f, 0.5f, 0.5);// Středně modrá barva pozadí
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže obrazovku a hloubkový buffer
glViewport(0, 0, 640, 480);// Obnovení viewportu
}
Funkce DrawBlur() vykresluje před scénu několik průhledných otexturovaných obdélníků. Pohrajeme-li si trochu s alfou dostaneme imitaci efektu radial blur. Nejprve vypneme automatické generování texturových koordinátů a potom zapneme 2D textury. Vypneme depth testy, nastavíme blending, zapneme ho a zvolíme texturu. Abychom mohli snadno kreslit obdélníky přesně přes celou scénu, přepneme do pravoúhlé projekce.
void DrawBlur(int times, float inc)// Vykreslí rozmazaný obrázek
{
float spost = 0.0f;// Počáteční offset souřadnic na textuře
float alphainc = 0.9f / times;// Rychlost blednutí pro alfa blending
float alpha = 0.2f;// Počáteční hodnota alfy
glDisable(GL_TEXTURE_GEN_S);// Vypne automatické generování texturových koordinátů
glDisable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_2D);// Zapne mapování textur
glDisable(GL_DEPTH_TEST);// Vypne testování hloubky
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Mód blendingu
glEnable(GL_BLEND);// Zapne blending
glBindTexture(GL_TEXTURE_2D, BlurTexture);// Zvolí texturu
ViewOrtho();// Přepne do pravoúhlé projekce
V cyklu vykreslíme texturu tolikrát, abychom vytvořili radial blur. Souřadnice vertexů zůstávají pořád stejné, ale zvětšujeme koordináty u textur a také snižujeme alfu. Takto vykreslíme celkem 25 quadů, jejichž textura se roztahuje pokaždé o 0.015f.
alphainc = alpha / times;// Hodnota změny alfy při jednom kroku
glBegin(GL_QUADS);// Kreslení obdélníků
for (int num = 0; num < times; num++)// Počet kroků renderování skvrn
{
glColor4f(1.0f, 1.0f, 1.0f, alpha);// Nastavení hodnoty alfy
glTexCoord2f(0 + spost, 1 - spost);// Texturové koordináty (0, 1)
glVertex2f(0, 0);// První vertex (0, 0)
glTexCoord2f(0 + spost, 0 + spost);// Texturové koordináty (0, 0)
glVertex2f(0, 480);// Druhý vertex (0, 480)
glTexCoord2f(1 - spost, 0 + spost);// Texturové koordináty (1, 0)
glVertex2f(640, 480);// Třetí vertex (640, 480)
glTexCoord2f(1 - spost, 1 - spost);// Texturové koordináty (1, 1)
glVertex2f(640, 0);// Čtvrtý vertex (640, 0)
spost += inc;// Postupné zvyšování skvrn (zoomování do středu textury)
alpha = alpha - alphainc;// Postupné snižování alfy (blednutí obrázku)
}
glEnd();// Konec kreslení
Zbývá obnovit původní parametry.
ViewPerspective();// Obnovení perspektivy
glEnable(GL_DEPTH_TEST);// Zapne testování hloubky
glDisable(GL_TEXTURE_2D);// Vypne mapování textur
glDisable(GL_BLEND);// Vypne blending
glBindTexture(GL_TEXTURE_2D, 0);// Zrušení vybrané textury
}
Draw() je tentokrát opravdu krátká. Nastavíme černé pozadí, smažeme obrazovku i hloubku a resetujeme matici. Vyrenderujeme spirálu do textury, potom i na obrazovku a nakonec vykreslíme blur efekt.
void Draw(void)// Vykreslení scény
{
glClearColor(0.0f, 0.0f, 0.0f, 0.5);// Černé pozadí
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže obrazovku a hloubku
glLoadIdentity();// Reset matice
RenderToTexture();// Rendering do textury
ProcessHelix();// Rendering spirály
DrawBlur(25, 0.02f);// Rendering blur efektu
glFlush();// Vyprázdnění OpenGL pipeline
}
Doufám, že se vám tento tutoriál líbil. Nenaučili jste se sice nic víc než rendering do textury, ale výsledný efekt vypadá opravdu skvěle.
Máte svobodu v používání tohoto kódu ve svých programech jakkoli chcete, ale před tím, než tak učiníte, podívejte se na něj a pochopte ho - jediná podmínka! Abych nezapomněl, uveďte mě prosím do kreditů.
Tady vám nechávám seznam úloh, které si můžete zkusit vyřešit:
Tak to už bylo opravdu všechno. Zkuste navštívit mé webové stránky http://www.spinningkids.org/rio, naleznete tam několik dalších tutoriálů...
napsal: Dario Corno - rIo <rio (zavináč) spinningkids.org>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>