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ů.
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
}
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>