Lekce 36 - Radial Blur, renderování do textury

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>

Zdrojové kódy

Lekce 36

<<< Lekce 35 | Lekce 37 >>>