Lekce 27 - Stíny

Představuje se vám velmi komplexní tutoriál na vrhání stínů. Efekt je doslova neuvěřitelný. Stíny se roztahují, ohýbají a zahalují i ostatní objekty ve scéně. Realisticky se pokroutí na stěnách nebo podlaze. Se vším lze pomocí klávesnice pohybovat ve 3D prostoru. Pokud ještě nejste se stencil bufferem a matematikou jako jedna rodina, nemáte nejmenší šanci.

Tento tutoriál má trochu jiný přístup - sumarizuje všechny vaše znalosti o OpenGL a přidává spoustu dalších. Každopádně byste měli stoprocentně chápat nastavování a práci se stencil bufferem. Pokud máte pocit, že v něčem existují mezery, zkuste se vrátit ke čtení dřívějších lekcí. Mimo jiné byste také měli mít alespoň malé znalosti o analytické geometrii (vektory, rovnice přímek a rovin, násobení matic...) - určitě mějte po ruce nějakou knihu. Já osobně používám zápisky z matematiky prvního semestru na univerzitě. Vždy jsem věděl, že se někdy budou hodit.

Nyní už ale ke kódu. Aby byl program přehledný, definujeme několik struktur. První z nich, sPoint, vyjadřuje bod nebo vektor v prostoru. Ukládá jeho x, y, z souřadnice.

struct sPoint// Souřadnice bodu nebo vektoru

{

float x, y, z;

};

Struktura sPlaneEq ukládá hodnoty a, b, c, d obecné rovnice roviny, která je definována vzorcem ax + by + cz + d = 0.

struct sPlaneEq// Rovnice roviny

{

float a, b, c, d;// Ve tvaru ax + by + cz + d = 0

};

Struktura sPlane obsahuje všechny informace potřebné k popsání trojúhelníku, který vrhá stín. Instance těchto struktur budou reprezentovat facy (čelo, stěna - nebudu překládat, protože je tento termín hodně používaný i v češtině) trojúhelníků. Facem se rozumí stěna trojúhelníku, která je přivrácená nebo odvrácená od pozorovatele. Jeden trojúhelník má vždy dva facy.

Pole p[3] definuje tři indexy v poli vertexů objektu, které dohromady tvoří tento trojúhelník. Druhé trojrozměrné pole, normals[3], zastupuje normálový vektor každého rohu. Třetí pole specifikuje indexy sousedních faců. PlaneEq určuje rovnici roviny, ve které leží tento face a parametr visible oznamuje, jestli je face přivrácený (viditelný) ke zdroji světla nebo ne.

struct sPlane// Popisuje jeden face objektu

{

unsigned int p[3];// Indexy 3 vertexů v objektu, které vytvářejí tento face

sPoint normals[3];// Normálové vektory každého vertexu

unsigned int neigh[3];// Indexy sousedních faců

sPlaneEq PlaneEq;// Rovnice roviny facu

bool visible;// Je face viditelný (přivrácený ke světlu)?

};

Poslední struktura, glObject, je mezi právě definovanými strukturami na nejvyšší úrovni. Proměnné nPoints a nPlanes určují počet prvků, které používáme v polích points a planes.

struct glObject// Struktura objektu

{

GLuint nPoints;// Počet vertexů

sPoint points[100];// Pole vertexů

GLuint nPlanes;// Počet faců

sPlane planes[200];// Pole faců

};

GLvector4f a GLmatrix16f jsou pomocné datové typy, které definujeme pro snadnější předávání parametrů funkci VMatMult(). Více později.

typedef float GLvector4f[4];// Nový datový typ

typedef float GLmatrix16f[16];// Nový datový typ

Nadefinujeme proměnné. Obj je objektem, který vrhá stín. Pole ObjPos[] definuje jeho polohu, roty jsou úhlem natočení na osách x, y a speedy jsou rychlosti otáčení.

glObject obj;// Objekt, který vrhá stín

float ObjPos[] = { -2.0f, -2.0f, -5.0f };// Pozice objektu

GLfloat xrot = 0, xspeed = 0;// X rotace a x rychlost rotace objektu

GLfloat yrot = 0, yspeed = 0;// Y rotace a y rychlost rotace objektu

Následující čtyři pole definují světlo a další čtyři pole materiál. Použijeme je především v InitGL() při inicializaci scény.

float LightPos[] = { 0.0f, 5.0f,-4.0f, 1.0f };// Pozice světla

float LightAmb[] = { 0.2f, 0.2f, 0.2f, 1.0f };// Ambient světlo

float LightDif[] = { 0.6f, 0.6f, 0.6f, 1.0f };// Diffuse světlo

float LightSpc[] = { -0.2f, -0.2f, -0.2f, 1.0f };// Specular světlo

float MatAmb[] = { 0.4f, 0.4f, 0.4f, 1.0f };// Materiál - Ambient hodnoty (prostředí, atmosféra)

float MatDif[] = { 0.2f, 0.6f, 0.9f, 1.0f };// Materiál - Diffuse hodnoty (rozptylování světla)

float MatSpc[] = { 0.0f, 0.0f, 0.0f, 1.0f };// Materiál - Specular hodnoty (zrcadlivost)

float MatShn[] = { 0.0f };// Materiál - Shininess hodnoty (lesk)

Poslední dvě proměnné jsou pro kouli, na kterou dopadá stín objektu.

GLUquadricObj *q;// Quadratic pro kreslení koule

float SpherePos[] = { -4.0f, -5.0f, -6.0f };// Pozice koule

Struktura datového souboru, který používáme pro definici objektu, není až tak složitá, jak na první pohled vypadá. Soubor se dělí do dvou částí: jedna část pro vertexy a druhá pro facy. První číslo první části určuje počet vertexů a po něm následují jejich definice. Druhá část začíná specifikací počtu faců. Na každém dalším řádku je celkem dvanáct čísel. První tři představují indexy do pole vertexů (každý face má tři vrcholy) a zbylých devět hodnot určuje tři normálové vektory (pro každý vrchol jeden). To je vše. Abych nezapomněl v adresáři Data můžete najít ještě tři podobné soubory.

24

-2 0.2 -0.2

2 0.2 -0.2

2 0.2 0.2

-2 0.2 0.2

-2 -0.2 -0.2

2 -0.2 -0.2

2 -0.2 0.2

-2 -0.2 0.2

-0.2 2 -0.2

0.2 2 -0.2

0.2 2 0.2

0.2 2 0.2

-0.2 -2 -0.2

0.2 -2 -0.2

0.2 -2 0.2

-0.2 -2 0.2

-0.2 0.2 -2

0.2 0.2 -2

0.2 0.2 2

-0.2 0.2 2

-0.2 -0.2 -2

0.2 -0.2 -2

0.2 -0.2 2

-0.2 -0.2 2

36

1 3 2 0 1 0 0 1 0 0 1 0

1 4 3 0 1 0 0 1 0 0 1 0

5 6 7 0 -1 0 0 -1 0 0 -1 0

5 7 8 0 -1 0 0 -1 0 0 -1 0

5 4 1 -1 0 0 -1 0 0 -1 0 0

5 8 4 -1 0 0 -1 0 0 -1 0 0

3 6 2 1 0 0 1 0 0 1 0 0

3 7 6 1 0 0 1 0 0 1 0 0

5 1 2 0 0 -1 0 0 -1 0 0 -1

5 2 6 0 0 -1 0 0 -1 0 0 -1

3 4 8 0 0 1 0 0 1 0 0 1

3 8 7 0 0 1 0 0 1 0 0 1

9 11 10 0 1 0 0 1 0 0 1 0

9 12 11 0 1 0 0 1 0 0 1 0

13 14 15 0 -1 0 0 -1 0 0 -1 0

13 15 16 0 -1 0 0 -1 0 0 -1 0

13 12 9 -1 0 0 -1 0 0 -1 0 0

13 16 12 -1 0 0 -1 0 0 -1 0 0

11 14 10 1 0 0 1 0 0 1 0 0

11 15 14 1 0 0 1 0 0 1 0 0

13 9 10 0 0 -1 0 0 -1 0 0 -1

13 10 14 0 0 -1 0 0 -1 0 0 -1

11 12 16 0 0 1 0 0 1 0 0 1

11 16 15 0 0 1 0 0 1 0 0 1

17 19 18 0 1 0 0 1 0 0 1 0

17 20 19 0 1 0 0 1 0 0 1 0

21 22 23 0 -1 0 0 -1 0 0 -1 0

21 23 24 0 -1 0 0 -1 0 0 -1 0

21 20 17 -1 0 0 -1 0 0 -1 0 0

21 24 20 -1 0 0 -1 0 0 -1 0 0

19 22 18 1 0 0 1 0 0 1 0 0

19 23 22 1 0 0 1 0 0 1 0 0

21 17 18 0 0 -1 0 0 -1 0 0 -1

21 18 22 0 0 -1 0 0 -1 0 0 -1

19 20 24 0 0 1 0 0 1 0 0 1

19 24 23 0 0 1 0 0 1 0 0 1

Právě představený soubor nahrává funkce ReadObject(). Pro pochopení podstaty by měly stačit komentáře.

inline int ReadObject(char *st, glObject *o)// Nahraje objekt

{

FILE *file;// Handle souboru

unsigned int i;// Řídící proměnná cyklů

file = fopen(st, "r");// Otevře soubor pro čtení

if (!file)// Podařilo se ho otevřít?

return FALSE;// Pokud ne - konec funkce

fscanf(file, "%d", &(o->nPoints));// Načtení počtu vertexů

for (i = 1; i <= o->nPoints; i++)// Načítá vertexy

{

fscanf(file, "%f", &(o->points[i].x));// Jednotlivé x, y, z složky

fscanf(file, "%f", &(o->points[i].y));

fscanf(file, "%f", &(o->points[i].z));

}

fscanf(file, "%d", &(o->nPlanes));// Načtení počtu faců

for (i = 0; i < o->nPlanes; i++)// Načítá facy

{

fscanf(file, "%d", &(o->planes[i].p[0]));// Načtení indexů vertexů

fscanf(file, "%d", &(o->planes[i].p[1]));

fscanf(file, "%d", &(o->planes[i].p[2]));

fscanf(file, "%f", &(o->planes[i].normals[0].x));// Normálové vektory prvního vertexu

fscanf(file, "%f", &(o->planes[i].normals[0].y));

fscanf(file, "%f", &(o->planes[i].normals[0].z));

fscanf(file, "%f", &(o->planes[i].normals[1].x));// Normálové vektory druhého vertexu

fscanf(file, "%f", &(o->planes[i].normals[1].y));

fscanf(file, "%f", &(o->planes[i].normals[1].z));

fscanf(file, "%f", &(o->planes[i].normals[2].x));// Normálové vektory třetího vertexu

fscanf(file, "%f", &(o->planes[i].normals[2].y));

fscanf(file, "%f", &(o->planes[i].normals[2].z));

}

return TRUE;// Vše v pořádku

}

Díky funkci SetConnectivity() začínají být věci zajímavé :-) Hledáme v ní ke každému facu tři sousední facy, se kterými má společnou hranu. Protože je zdrojový kód, abych tak řekl, trochu hůře pochopitelný, přidávám i pseudo kód, který by mohl situaci maličko objasnit.

Začátek funkce

{

Postupně se prochází každý face (A) v objektu

{

V každém průchodu se znovu prochází všechny facy (B) objektu (zjišťuje se sousedství A s B)

{

Dále se projdou všechny hrany facu A

{

Pokud aktuální hrana ještě nemá přiřazeného souseda

{

Projdou se všechny hrany facu B

{

Provedou se výpočty, kterými se zjistí, jestli je okraj A stejný jako okraj B

Pokud ano

{

Nastaví se soused v A

Nastaví se soused v B

}

}

}

}

}

}

}

Konec funkce

Už chápete?

inline void SetConnectivity(glObject *o)// Nastavení sousedů jednotlivých faců

{

unsigned int p1i, p2i, p1j, p2j;// Pomocné proměnné

unsigned int P1i, P2i, P1j, P2j;// Pomocné proměnné

unsigned int i, j, ki, kj;// Řídící proměnné cyklů

for(i = 0; i < o->nPlanes-1; i++)// Každý face objektu (A)

{

for(j = i+1; j < o->nPlanes; j++)// Každý face objektu (B)

{

for(ki = 0; ki < 3; ki++)// Každý okraj facu (A)

{

if(!o->planes[i].neigh[ki])// Okraj ještě nemá souseda?

{

for(kj = 0; kj < 3; kj++)// Každý okraj facu (B)

{

Nalezením dvou vertexů, které označují konce hrany a jejich porovnáním můžeme zjistit, jestli mají společný okraj. Část (kj+1) % 3 označuje vertex umístěný vedle toho, o kterém uvažujeme. Ověříme, jestli jsou vertexy stejné. Protože může být jejich pořadí rozdílné musíme testovat obě možnosti.

// Výpočty pro zjištění sousedství

p1i = ki;

p1j = kj;

p2i = (ki+1) % 3;

p2j = (kj+1) % 3;

p1i = o->planes[i].p[p1i];

p2i = o->planes[i].p[p2i];

p1j = o->planes[j].p[p1j];

p2j = o->planes[j].p[p2j];

P1i = ((p1i+p2i) - abs(p1i-p2i)) / 2;

P2i = ((p1i+p2i) + abs(p1i-p2i)) / 2;

P1j = ((p1j+p2j) - abs(p1j-p2j)) / 2;

P2j = ((p1j+p2j) + abs(p1j-p2j)) / 2;

if((P1i == P1j) && (P2i == P2j))// Jsou sousedé?

{

o->planes[i].neigh[ki] = j+1;

o->planes[j].neigh[kj] = i+1;

}

}

}

}

}

}

}

Abychom se mohli alespoň trochu nadechnout :-) vypíši kód funkce DrawGLObject(), který je na první pohled maličko jednodušší. Jak už z názvu vyplývá, vykresluje objekt.

void DrawGLObject(glObject o)// Vykreslení objektu

{

unsigned int i, j;// Řídící proměnné cyklů

glBegin(GL_TRIANGLES);// Kreslení trojúhelníků

for (i = 0; i < o.nPlanes; i++)// Projde všechny facy

{

for (j = 0; j < 3; j++)// Trojúhelník má tři rohy

{

// Normálový vektor a umístění bodu

glNormal3f(o.planes[i].normals[j].x, o.planes[i].normals[j].y, o.planes[i].normals[j].z);

glVertex3f(o.points[o.planes[i].p[j]].x, o.points[o.planes[i].p[j]].y, o.points[o.planes[i].p[j]].z);

}

}

glEnd();

}

Výpočet rovnice roviny vypadá pro ne-matematika sice hodně složitě, ale je to pouze implementace matematického vzorce, který se, když je potřeba, najde v tabulkách nebo knížce.

Překl.: Maličká chybička. Pole v[] má rozsah čtyři prvky, ale používají se jenom tři. Index 0 se nikdy nepoužije.

inline void CalcPlane(glObject o, sPlane *plane)// Rovnice roviny ze tří bodů

{

sPoint v[4];// Pomocné hodnoty

int i;// Řídící proměnná cyklů

for (i = 0; i < 3; i++)// Pro zkrácení zápisu

{

v[i+1].x = o.points[plane->p[i]].x;// Uloží hodnoty do pomocných proměnných

v[i+1].y = o.points[plane->p[i]].y;

v[i+1].z = o.points[plane->p[i]].z;

}

plane->PlaneEq.a = v[1].y*(v[2].z-v[3].z) + v[2].y*(v[3].z-v[1].z) + v[3].y*(v[1].z-v[2].z);

plane->PlaneEq.b = v[1].z*(v[2].x-v[3].x) + v[2].z*(v[3].x-v[1].x) + v[3].z*(v[1].x-v[2].x);

plane->PlaneEq.c = v[1].x*(v[2].y-v[3].y) + v[2].x*(v[3].y-v[1].y) + v[3].x*(v[1].y-v[2].y);

plane->PlaneEq.d = -( v[1].x*(v[2].y*v[3].z - v[3].y*v[2].z) + v[2].x*(v[3].y*v[1].z - v[1].y*v[3].z) + v[3].x*(v[1].y*v[2].z - v[2].y*v[1].z) );

}

Funkce, které jsme právě napsali se volají ve funkci InitGLObjects(). Neexistuje-li požadovaný soubor, vrátíme false. Pokud ale existuje, funkcí ReadObject() ho nahrajeme do paměti, pomocí SetConnectivity() najdeme sousedící facy a potom se v cyklu spočítáme rovnici roviny každého facu.

int InitGLObjects()// Inicializuje objekty

{

if (!ReadObject("Data/Object2.txt", &obj))// Nahraje objekt

{

return FALSE;// Při chybě konec

}

SetConnectivity(&obj);// Pospojuje facy (najde sousedy)

for (unsigned int i = 0; i < obj.nPlanes; i++)// Prochází facy

CalcPlane(obj, &(obj.planes[i]));// Spočítá rovnici roviny facu

return TRUE;// Vše v pořádku

}

Nyní přichází funkce, která renderuje stín. Na začátku nastavíme všechny potřebné parametry OpenGL a poté, ne na obrazovku, ale do stencil bufferu, vyrenderujeme stín. Dále vykreslíme vepředu před scénu velký šedý obdélník. Tam, kde byl stencil buffer modifikován se zobrazí šedé plochy - stín.

void CastShadow(glObject *o, float *lp)// Vržení stínu

{

unsigned int i, j, k, jj;// Pomocné

unsigned int p1, p2;// Dva body okraje vertexu, které vrhají stín

sPoint v1, v2;// Vektor mezi světlem a předchozími body

Nejprve určíme, které povrchy jsou přivrácené ke světlu a to tak, že zjistíme, která strana facu je osvětlená. Provedeme to velice jednoduše: máme rovnici roviny (ax + by + cz + d = 0) i polohu světla, takže dosadíme x, y, z koordináty světla do rovnice. Nezajímá nás hodnota, ale znaménko výsledku. Pokud bude výsledek větší než nula, míří normálový vektor roviny na stranu ke světlu a rovina je osvětlená. Při záporném čísle míří vektor od světla, rovina je od něj odvrácená. Vyšel-li by výsledek nula, bude světlo ležet v rovině facu, ale tím se nebudeme zabývat.

float side;// Pomocná proměnná

for (i = 0; i < o->nPlanes; i++)// Projde všechny facy objektu

{

// Rozhodne jestli je face přivrácený nebo odvrácený od světla

side = o->planes[i].PlaneEq.a * lp[0] + o->planes[i].PlaneEq.b * lp[1] + o->planes[i].PlaneEq.c * lp[2] + o->planes[i].PlaneEq.d * lp[3];

if (side > 0)// Je přivrácený?

{

o->planes[i].visible = TRUE;

}

else// Není

{

o->planes[i].visible = FALSE;

}

}

Nastavíme parametry OpenGL, které jsou nutné pro vržení stínu. Vypneme světla, protože nebudeme renderovat do color bufferu (výstup na obrazovku), ale pouze do stencil bufferu. Ze stejného důvodu zakážeme pomocí glColorMask() vykreslování na obrazovku. Ačkoli je testování hloubky stále zapnuté, nechceme, aby stíny byly v depth bufferu reprezentovány pevnými objekty. Jako prevenci tedy nastavíme masku hloubky na GL_FALSE. Nakonec nastavíme stencil buffer tak, aby na místa v něm označená mohly být vykresleny stíny.

glDisable(GL_LIGHTING);// Vypne světla

glDepthMask(GL_FALSE);// Vypne zápis do depth bufferu

glDepthFunc(GL_LEQUAL);// Funkce depth bufferu

glEnable(GL_STENCIL_TEST);// Zapne stencilové testy

glColorMask(0, 0, 0, 0);// Nekreslit na obrazovky

glStencilFunc(GL_ALWAYS, 1, 0xffffffff);// Funkce stencilu

Protože máme zapnuté ořezávání zadních stran trojúhelníků (viz. InitGL()), specifikujeme, které strany jsou přední. Také nastavíme stencil buffer tak, aby se v něm při kreslení zvětšovaly hodnoty.

glFrontFace(GL_CCW);// Čelní stěna proti směru hodinových ručiček

glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);// Zvyšování hodnoty stencilu

V cyklu projdeme každý face a pokud je označen jako viditelný (přivrácený ke světlu), zkontrolujeme všechny jeho okraje. Pokud vedle něj není žádný sousední face nebo sice má souseda, který ale není viditelný, našli jsme okraj objektu, který vrhá stín. Pokud se nad těmito dvěma podmínkami zamyslíte, zjistíte, že jsou pravdivé. Získali jsme první dvě souřadnice čtyřúhelníku, který je stěnou stínu. V tomto případě si představte stín jako oblast, který je ohraničena na jedné straně objektem bránícím průchodu světelných paprsků, z druhé strany promítací rovinou (stěna místnosti) a na okrajích čtyřúhelníky, které se právě snažíme vykreslit. Už je to trochu jasnější?

for (i = 0; i < o->nPlanes; i++)// Každý face objektu

{

if (o->planes[i].visible)// Je přivrácený ke světlu

{

for (j = 0; j < 3; j++)// Každý okraj facu

{

k = o->planes[i].neigh[j];// Index souseda (pomocný)

Nyní zjistíme, jestli je vedle aktuálního okraje face, který buď není viditelný nebo vůbec neexistuje (nemá souseda). Pokud podmínka platí, našli jsme okraj objektu, který vrhá stín.

// Pokud nemá souseda, který je přivrácený ke světlu

if ((!k) || (!o->planes[k-1].visible))

{

Rohy hrany právě ověřovaného trojúhelníku udávají první dva body stínu. Další dva získáme spočítáním směrového vektoru, který vychází ze světla, prochází bodem p1 popř. p2 a díky násobení stem pokračuje ve stejném směru někam do hlubin scény. Násobení stem bychom si mohli představit jako měřítko pro prodloužení vektoru a tudíž i polygonu, aby dosáhl až k promítací rovině a neskončil někde před ní.

Kreslení stínu hrubou silou použité zde, není zrovna nejvhodnější, protože má velmi velké nároky na grafickou kartu. Nekreslíme totiž pouze k promítací rovině, ale až za ni kód této lekce. ( * 100). Pro větší účinnost by bylo vhodné modifikovat tento algoritmus tak, aby se polygony stínu ořezaly objektem, na který dopadá. Tento postup by ovšem byl mnohem náročnější na vymyšlení a asi by byl problematický sám o sobě.

// Našli jsme okraj objektu, který vrhá stín - nakreslíme polygon

p1 = o->planes[i].p[j];// První bod okraje

jj = (j+1) % 3;// Pro získání druhého okraje

p2 = o->planes[i].p[jj];// Druhý bod okraje

// Délka vektoru

v1.x = (o->points[p1].x - lp[0]) * 100;

v1.y = (o->points[p1].y - lp[1]) * 100;

v1.z = (o->points[p1].z - lp[2]) * 100;

v2.x = (o->points[p2].x - lp[0]) * 100;

v2.y = (o->points[p2].y - lp[1]) * 100;

v2.z = (o->points[p2].z - lp[2]) * 100;

Zbytek už je celkem snadný. Máme dva body s délkou a tak vykreslíme čtyřúhelník - jeden z mnoha okrajů stínu.

glBegin(GL_TRIANGLE_STRIP);// Nakreslí okrajový polygon stínu

glVertex3f(o->points[p1].x, o->points[p1].y, o->points[p1].z);

glVertex3f(o->points[p1].x + v1.x, o->points[p1].y + v1.y, o->points[p1].z + v1.z);

glVertex3f(o->points[p2].x, o->points[p2].y, o->points[p2].z);

glVertex3f(o->points[p2].x + v2.x, o->points[p2].y + v2.y, o->points[p2].z + v2.z);

glEnd();

V cyklech zůstaneme tak dlouho, dokud nenajdeme a nevykreslíme všechny okraje stínu.

}

}

}

}

Nejjednodušší a nejpochopitelnější vysvětlení toho, proč vykreslujeme to samé ještě jednou, je obrázek - stíny budou pouze tam, kde být mají. Při vykreslování se nyní budou hodnoty ve stencil bufferu snižovat. Také si všimněte, že funkcí glFrontFace() budeme ořezávat opačné strany trojúhelníků.

Bez druhého kreslení Se druhým kreslením

glFrontFace(GL_CW);// Čelní stěna po směru hodinových ručiček

glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);// Snižování hodnoty stencilu

for (i=0; i < o->nPlanes; i++)// Každý face objektu

{

if (o->planes[i].visible)// Je přivrácený ke světlu

{

for (j = 0; j < 3; j++)// Každý okraj facu

{

k = o->planes[i].neigh[j];// Index souseda (pomocný)

// Pokud nemá souseda, který je přivrácený ke světlu

if ((!k) || (!o->planes[k-1].visible))

{

// Našli jsme okraj objektu, který vrhá stín - nakreslíme polygon

p1 = o->planes[i].p[j];// První bod okraje

jj = (j+1) % 3;// Pro získání druhého okraje

p2 = o->planes[i].p[jj];// Druhý bod okraje

// Délka vektoru

v1.x = (o->points[p1].x - lp[0])*100;

v1.y = (o->points[p1].y - lp[1])*100;

v1.z = (o->points[p1].z - lp[2])*100;

v2.x = (o->points[p2].x - lp[0])*100;

v2.y = (o->points[p2].y - lp[1])*100;

v2.z = (o->points[p2].z - lp[2])*100;

glBegin(GL_TRIANGLE_STRIP);// Nakreslí okrajový polygon stínu

glVertex3f(o->points[p1].x, o->points[p1].y, o->points[p1].z);

glVertex3f(o->points[p1].x + v1.x, o->points[p1].y + v1.y, o->points[p1].z + v1.z);

glVertex3f(o->points[p2].x, o->points[p2].y, o->points[p2].z);

glVertex3f(o->points[p2].x + v2.x, o->points[p2].y + v2.y, o->points[p2].z + v2.z);

glEnd();

}

}

}

}

Až teď opravdu zobrazíme na scénu stíny. Na úrovni roviny obrazovky vykreslíme velký, šedý, poloprůhledný obdélník. Zobrazí se pouze ty pixely, které byly právě označeny ve stencil bufferu (na pozici stínu). Čím bude obdélník tmavší, tím tmavší bude i stín. Můžete zkusit jinou průhlednost nebo dokonce i barvu. Jak by se vám líbil červený, zelený nebo modrý stín? Žádný problém!

glFrontFace(GL_CCW);// Čelní stěna proti směru hodinových ručiček

glColorMask(1, 1, 1, 1);// Vykreslovat na obrazovku

// Vykreslení obdélníku přes celou scénu

glColor4f(0.0f, 0.0f, 0.0f, 0.4f);// Černá, 40% průhledná

glEnable(GL_BLEND);// Zapne blending

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);// Typ blendingu

glStencilFunc(GL_NOTEQUAL, 0, 0xffffffff);// Nastavení stencilu

glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);// Neměnit hodnotu stencilu

glPushMatrix();// Uloží matici

glLoadIdentity();// Reset matice

glBegin(GL_TRIANGLE_STRIP);// Černý obdélník

glVertex3f(-0.1f, 0.1f,-0.10f);

glVertex3f(-0.1f,-0.1f,-0.10f);

glVertex3f( 0.1f, 0.1f,-0.10f);

glVertex3f( 0.1f,-0.1f,-0.10f);

glEnd();

glPopMatrix();// Obnoví matici

Nakonec obnovíme změněné parametry OpenGL na výchozí hodnoty.

// Obnoví změněné parametry OpenGL

glDisable(GL_BLEND);

glDepthFunc(GL_LEQUAL);

glDepthMask(GL_TRUE);

glEnable(GL_LIGHTING);

glDisable(GL_STENCIL_TEST);

glShadeModel(GL_SMOOTH);

}

DrawGLScene(), ostatně jako vždycky, zajišťuje všechno vykreslování. Proměnná Minv bude reprezentovat OpenGL matici, wlp budou lokální koordináty a lp pomocná pozice světla.

int DrawGLScene(GLvoid)// Hlavní vykreslovací funkce

{

GLmatrix16f Minv;// OpenGL matice

GLvector4f wlp, lp;// Relativní pozice světla

Smažeme obrazovkový, hloubkový i stencil buffer. Resetujeme matici a přesuneme se o dvacet jednotek do obrazovky. Umístíme světlo, provedeme translaci na pozici koule a pomocí quadraticu ji vykreslíme.

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

glLoadIdentity();// Reset matice

glTranslatef(0.0f, 0.0f, -20.0f);// Přesun 20 jednotek do hloubky

glLightfv(GL_LIGHT1, GL_POSITION, LightPos);// Umístění světla

glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);// Umístění koule

gluSphere(q, 1.5f, 32, 16);// Vykreslení koule

Spočítáme relativní pozici světla vzhledem k lokálnímu souřadnicovému systému objektu, který vrhá stín. Do proměnné Min uložíme transformační matici objektu, ale obrácenou (vše se zápornými čísly a zadávané opačným pořadím), takže se stane invertovanou transformační maticí. Z lp vytvoříme kopii pozice světla a poté ho vynásobíme právě získanou OpenGL maticí. Jednoduše řečeno: na konci bude lp pozicí světla v souřadnicovém systému objektu.

glLoadIdentity();// Reset matice

glRotatef(-yrot, 0.0f, 1.0f, 0.0f);// Rotace na ose y

glRotatef(-xrot, 1.0f, 0.0f, 0.0f);// Rotace na ose x

glGetFloatv(GL_MODELVIEW_MATRIX, Minv);// Uložení ModelView matice do Minv

lp[0] = LightPos[0];// Uložení pozice světla

lp[1] = LightPos[1];

lp[2] = LightPos[2];

lp[3] = LightPos[3];

VMatMult(Minv, lp);// Vynásobení pozice světla OpenGL maticí

glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);// Posun záporně o pozici objektu

glGetFloatv(GL_MODELVIEW_MATRIX, Minv);// Uložení ModelView matice do Minv

wlp[0] = 0.0f;// Globální koordináty na nulu

wlp[1] = 0.0f;

wlp[2] = 0.0f;

wlp[3] = 1.0f;

VMatMult(Minv, wlp);// Originální globální souřadnicový systém relativně k lokálnímu

lp[0] += wlp[0];// Pozice světla je relativní k lokálnímu souřadnicovému systému objektu

lp[1] += wlp[1];

lp[2] += wlp[2];

Vykreslíme místnost s objektem a potom zavoláme funkci CastShadow(), která vykreslí stín objektu. Předáváme jí referenci na objekt spolu s pozicí světla, která je nyní ve stejném souřadnicovém systému jako objekt.

glLoadIdentity();// Reset matice

glTranslatef(0.0f, 0.0f, -20.0f);// Přesun 20 jednotek do hloubky

DrawGLRoom();// Vykreslení místnosti

glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);// Umístění objektu

glRotatef(xrot, 1.0f, 0.0f, 0.0f);// Rotace na ose x

glRotatef(yrot, 0.0f, 1.0f, 0.0f);// Rotace na ose y

DrawGLObject(obj);// Vykreslení objektu

CastShadow(&obj, lp);// Vržení stínu založené na siluetě

Abychom po spuštění dema viděli, kde se právě nachází světlo, vykreslíme na jeho pozici malý oranžový kruh (respektive kouli).

glColor4f(0.7f, 0.4f, 0.0f, 1.0f);// Oranžová barva

glDisable(GL_LIGHTING);// Vypne světlo

glDepthMask(GL_FALSE);// Vypne masku hloubky

glTranslatef(lp[0], lp[1], lp[2]);// Translace na pozici světla

// Pořád jsme v lokálním souřadnicovém systému objektu

gluSphere(q, 0.2f, 16, 8);// Vykreslení malé koule (reprezentuje světlo)

glEnable(GL_LIGHTING);// Zapne světlo

glDepthMask(GL_TRUE);// Zapne masku hloubky

Aktualizujeme rotaci objektu a ukončíme funkci.

xrot += xspeed;// Zvětšení úhlu rotace objektu

yrot += yspeed;

glFlush();

return TRUE;// Všechno v pořádku

}

Dále napíšeme speciální funkci DrawGLRoom(), která vykreslí místnost. Je jí obyčejná krychle.

void DrawGLRoom()// Vykreslí místnost (krychli)

{

glBegin(GL_QUADS);// Začátek kreslení obdélníků

// Podlaha

glNormal3f(0.0f, 1.0f, 0.0f);// Normála směřuje nahoru

glVertex3f(-10.0f,-10.0f,-20.0f);// Levý zadní

glVertex3f(-10.0f,-10.0f, 20.0f);// Levý přední

glVertex3f( 10.0f,-10.0f, 20.0f);// Pravý přední

glVertex3f( 10.0f,-10.0f,-20.0f);// Pravý zadní

// Strop

glNormal3f(0.0f,-1.0f, 0.0f);// Normála směřuje dolů

glVertex3f(-10.0f, 10.0f, 20.0f);// Levý přední

glVertex3f(-10.0f, 10.0f,-20.0f);// Levý zadní

glVertex3f( 10.0f, 10.0f,-20.0f);// Pravý zadní

glVertex3f( 10.0f, 10.0f, 20.0f);// Pravý přední

// Čelní stěna

glNormal3f(0.0f, 0.0f, 1.0f);// Normála směřuje do hloubky

glVertex3f(-10.0f, 10.0f,-20.0f);// Levý horní

glVertex3f(-10.0f,-10.0f,-20.0f);// Levý dolní

glVertex3f( 10.0f,-10.0f,-20.0f);// Pravý dolní

glVertex3f( 10.0f, 10.0f,-20.0f);// Pravý horní

// Zadní stěna

glNormal3f(0.0f, 0.0f,-1.0f);// Normála směřuje k obrazovce

glVertex3f( 10.0f, 10.0f, 20.0f);// Pravý horní

glVertex3f( 10.0f,-10.0f, 20.0f);// Pravý spodní

glVertex3f(-10.0f,-10.0f, 20.0f);// Levý spodní

glVertex3f(-10.0f, 10.0f, 20.0f);// Levý zadní

// Levá stěna

glNormal3f(1.0f, 0.0f, 0.0f);// Normála směřuje doprava

glVertex3f(-10.0f, 10.0f, 20.0f);// Přední horní

glVertex3f(-10.0f,-10.0f, 20.0f);// Přední dolní

glVertex3f(-10.0f,-10.0f,-20.0f);// Zadní dolní

glVertex3f(-10.0f, 10.0f,-20.0f);// Zadní horní

// Pravá stěna

glNormal3f(-1.0f, 0.0f, 0.0f);// Normála směřuje doleva

glVertex3f( 10.0f, 10.0f,-20.0f);// Zadní horní

glVertex3f( 10.0f,-10.0f,-20.0f);// Zadní dolní

glVertex3f( 10.0f,-10.0f, 20.0f);// Přední dolní

glVertex3f( 10.0f, 10.0f, 20.0f);// Přední horní

glEnd();// Konec kreslení

}

Předtím než zapomenu... v DrawGLScene() jsme použili funkci VMatMult(), která násobí vektor maticí. Opět se jedná o implementaci vzorce z knížky o matematice.

void VMatMult(GLmatrix16f M, GLvector4f v)

{

GLfloat res[4];// Ukládá výsledky

res[0] = M[ 0]*v[0] + M[ 4]*v[1] + M[ 8]*v[2] + M[12]*v[3];

res[1] = M[ 1]*v[0] + M[ 5]*v[1] + M[ 9]*v[2] + M[13]*v[3];

res[2] = M[ 2]*v[0] + M[ 6]*v[1] + M[10]*v[2] + M[14]*v[3];

res[3] = M[ 3]*v[0] + M[ 7]*v[1] + M[11]*v[2] + M[15]*v[3];

v[0] = res[0];// Výsledek uloží zpět do v

v[1] = res[1];

v[2] = res[2];

v[3] = res[3];// Homogenní souřadnice

}

V Inicializaci OpenGL nejsou téměř žádné novinky. Na začátku nahrajeme a inicializujeme objekt, který vrhá stín, potom nastavíme obvyklé parametry a světla.

int InitGL(GLvoid)// Nastavení OpenGL

{

if (!InitGLObjects())// Nahraje objekt

return FALSE;

glShadeModel(GL_SMOOTH);// Jemné stínování

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

glClearDepth(1.0f);// Nastavení hloubkového bufferu

glClearStencil(0);// Nastavení stencil bufferu

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

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

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Perspektivní korekce

glLightfv(GL_LIGHT1, GL_POSITION, LightPos);// Pozice světla

glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmb);// Ambient světlo

glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDif);// Diffuse světlo

glLightfv(GL_LIGHT1, GL_SPECULAR, LightSpc);// Specular světlo

glEnable(GL_LIGHT1);// Zapne světlo 1

glEnable(GL_LIGHTING);// Zapne světla

Materiály, které určují jak vypadají polygony při dopadu světla, jsou, myslím, novinkou. Nemusíme vepisovat žádné hodnoty, protože předávané pole jsou definovány na začátku tutoriálu. Materiály mimo jiné určují i barvu povrchu, takže při zapnutém světle nebude mít změna barvy pomocí glColor() žádný vliv (Překl. To jsem zjistil úplně náhodou. Nevím, jestli je to pravda obecně, ale minimálně v tomto demu ano.).

glMaterialfv(GL_FRONT, GL_AMBIENT, MatAmb);// Prostředí, atmosféra

glMaterialfv(GL_FRONT, GL_DIFFUSE, MatDif);// Rozptylování světla

glMaterialfv(GL_FRONT, GL_SPECULAR, MatSpc);// Zrcadlivost

glMaterialfv(GL_FRONT, GL_SHININESS, MatShn);// Lesk

Abychom alespoň trochu zrychlili vykreslování, zapneme culling, takže se zadní strany trojúhelníků nebudou vykreslovat. Která strana je odvrácená se určí podle pořadí zadávání vrcholů polygonů (po/proti směru hodinových ručiček).

glCullFace(GL_BACK);// Ořezávání zadních stran

glEnable(GL_CULL_FACE);// Zapne ořezávání

Budeme vykreslovat i nějaké koule, takže vytvoříme a inicializujeme quadratic.

q = gluNewQuadric();// Nový quadratic

gluQuadricNormals(q, GL_SMOOTH);// Generování normálových vektorů pro světlo

gluQuadricTexture(q, GL_FALSE);// Nepotřebujeme texturovací koordináty

return TRUE;// V pořádku

}

Poslední funkcí tohoto tutoriálu je ProcessKeyboard(). Stejně jako vykreslování, tak i ona, se volá v každém průchodu hlavní smyčky programu. Ošetřuje uživatelské příkazy při stisku kláves. Jak se program zachová, popisují komentáře.

void ProcessKeyboard()// Ošetření klávesnice

{

// Rotace objektu

if (keys[VK_LEFT]) yspeed -= 0.1f;// Šipka vlevo - snižuje y rychlost

if (keys[VK_RIGHT]) yspeed += 0.1f;// Šipka vpravo - zvyšuje y rychlost

if (keys[VK_UP]) xspeed -= 0.1f;// Šipka nahoru - snižuje x rychlost

if (keys[VK_DOWN]) xspeed += 0.1f;// Šipka dolů - zvyšuje x rychlost

// Pozice objektu

if (keys[VK_NUMPAD6]) ObjPos[0] += 0.05f;// '6' - pohybuje objektem doprava

if (keys[VK_NUMPAD4]) ObjPos[0] -= 0.05f;// '4' - pohybuje objektem doleva

if (keys[VK_NUMPAD8]) ObjPos[1] += 0.05f;// '8' - pohybuje objektem nahoru

if (keys[VK_NUMPAD5]) ObjPos[1] -= 0.05f;// '5' - pohybuje objektem dolů

if (keys[VK_NUMPAD9]) ObjPos[2] += 0.05f;// '9' - přibližuje objekt

if (keys[VK_NUMPAD7]) ObjPos[2] -= 0.05f;// '7' oddaluje objekt

// Pozice světla

if (keys['L']) LightPos[0] += 0.05f;// 'L' - pohybuje světlem doprava

if (keys['J']) LightPos[0] -= 0.05f;// 'J' - pohybuje světlem doleva

if (keys['I']) LightPos[1] += 0.05f;// 'I' - pohybuje světlem nahoru

if (keys['K']) LightPos[1] -= 0.05f;// 'K' - pohybuje světlem dolů

if (keys['O']) LightPos[2] += 0.05f;// 'O' - přibližuje světlo

if (keys['U']) LightPos[2] -= 0.05f;// 'U' - oddaluje světlo

// Pozice koule

if (keys['D']) SpherePos[0] += 0.05f;// 'D' - pohybuje koulí doprava

if (keys['A']) SpherePos[0] -= 0.05f;// 'A' - pohybuje koulí doleva

if (keys['W']) SpherePos[1] += 0.05f;// 'W' - pohybuje koulí nahoru

if (keys['S']) SpherePos[1] -= 0.05f;// 'S'- pohybuje koulí dolů

if (keys['E']) SpherePos[2] += 0.05f;// 'E' - přibližuje kouli

if (keys['Q']) SpherePos[2] -= 0.05f;// 'Q' - oddaluje kouli

}

Několik poznámek ohledně tutoriálu

Na první pohled vypadá demo hyperefektně :-), ale má také své mouchy. Tak například koule nezastavuje projekci stínu na stěnu. V reálném prostředí by také vrhala stín, takže by se nic moc nestalo. Nicméně je zde pouze na ukázku toho, co se se stínem stane na zakřiveném povrchu.

Pokud program běží extrémně pomalu, zkuste přepnout do fullscreenu nebo změnit barevnou hloubku na 32 bitů. Arseny L. napsal: "Pokud máte problémy s TNT2 v okenním módu, ujistěte se, že nemáte nastavenu 16bitovou barevnou hloubku. V tomto barevném módu je stencil buffer emulovaný, což ve výsledku znamená malý výkon. V 32bitovém módu je vše bez problémů."

napsal: Banu Cosmin - Choko & Brett Porter <brettporter (zavináč) yahoo.com>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 27

<<< Lekce 26 | Lekce 28 >>>