Witaj w kompletnej lekcji o rzucaniu cieni. Efekt demka, które napiszemy jest niewiarygodny. Cienie, które rozciągają się, wyginają, obejmują inne obiekty i padają na ścianę. Wszystkim co jest na scenie możemy poruszać używając klawiatury.
Ten tutorial ma zupełnie inne podejście - zakłada, że dużo wiesz o OpenGL. Powinieneś rozumieć czym jest bufor powielania (ang. stencil buffer) oraz znać podstawowe ustawienia OpenGL. Jeśli czegoś nie wiesz, zajrzyj do poprzednich lekcji. Funkcje takie jak CreateGLWindow czy WinMain nie będę omawiane w tym tutorialu. Dodatkowo, powinieneś mieć podstawy matematyki 3D.
Najpierw mamy definicję INFINITY (pl. nieskończoność), która reprezentuje jak dalego ma sięgać cień (wyjaśnię to trochę później). Jeżeli używasz innych koordynatów w swoim systemie, dopasuj tę wartość. [T Chodzi o ostatni parametr w gluPerspective(). T]
#define INFINITY 100
Następna jest definicja struktury Point3f, która przechowuje koordynaty (inaczej współrzędne) w przestrzeni 3D (ang. 3D space). Może zostać wykorzytana do wierzchołków lub wektorów.
struct Point3f
{
GLfloat x, y, z;
};
Struktura Plane zawiera 4 wartości, które opisują równanie płaszczyzny (ang. plane). Te płaszczyzny będą reprezentowały ściany (ang. faces) obiektu.
struct Plane
{
GLfloat a, b, c, d;
};
Struktura Face zawiera wszystkie informacje niezbędne do rzucenia cienia.
- Indices oznacza indeksy wierzchołków w tablicy wierzchołków obiektu.
- Wektory normalne są wykorzystywane do obliczania orientacji ściany (ang. face) w przestrzeni, więc możesz określić, które są zwrócone w stronę żródła światła kiedy rzucasz cień.
- Równanie płaszczyzny opisuje płaczszyznę na której leży dany trójkąt
- neighbourIndices to indeksy w tablicy ścian obiektu. Pozwala to określić, które ściany łączą się z daną ścianą na każdej z krawędzi trójkąta.
- Parametr visible określa czy ściana jest widoczna dla źródła światła, które rzuca cień.
struct Face
{
int vertexIndices[3];
Point3f normals[3];
Plane planeEquation;
int neighbourIndices[3];
bool visible;
};
W końcu, struktura ShadowedObject zawierająca wierzchołki (ang. vertices) i ściany (ang. faces) obiektu. Pamięć jest przydzielana dynamicznie podczas ładowania.
struct ShadowedObject
{
int nVertices;
Point3f *pVertices;
int nFaces;
Face *pFaces;
};
Funkcja readObject() mówi sama za siebie. Wypełni ona podany obiekt wartościami z pliku alokując pamięć na wierzchołki i ściany. Indeksy sąsiadów ustawia na -1, co znaczy, że jeszcze ich nie ma. Policzymy to później.
bool readObject( const char *filename, ShadowedObject& object )
{
FILE *pInputFile;
int ic;
pInputFile = fopen( filename, "r" );
if ( pInputFile == NULL )
{
cerr << "Nieudalo sie otworzyc pliku: " << filename << endl;
return false;
}
fscanf( pInputFile, "%d", &object.nVertices );
object.pVertices = new Point3f[object.nVertices];
for ( ic = 0; ic < object.nVertices; ic++ )
{
fscanf( pInputFile, "%f", &object.pVertices[ic].x );
fscanf( pInputFile, "%f", &object.pVertices[ic].y );
fscanf( pInputFile, "%f", &object.pVertices[ic].z );
}
fscanf( pInputFile, "%d", &object.nFaces );
object.pFaces = new Face[object.nFaces];
for ( ic = 0; ic < object.nFaces; ic++ )
{
int j;
Face *pFace = &object.pFaces[ic];
for ( j = 0; j < 3; j++ )
pFace->neighbourIndices[j] = -1;
for ( j = 0; j < 3; j++ )
{
fscanf( pInputFile, "%d", &pFace->vertexIndices[j] );
pFace->vertexIndices[j]--;
}
for ( j = 0; j < 3; j++ )
{
fscanf( pInputFile, "%f", &pFace->normals[j].x );
fscanf( pInputFile, "%f", &pFace->normals[j].y );
fscanf( pInputFile, "%f", &pFace->normals[j].z );
}
}
return true;
}
Podobnie nazwa funkcji killObject() wyjaśnia co robi. Zwalnia dynamicznie przydzieloną pamięć dla tablic w obiekcie. Zauważ, że linia została dodana do KillGLWindow, żeby zapytać czy wywołać tę funkcję.
void killObject( ShadowedObject& object )
{
delete[] object.pFaces;
object.pFaces = NULL;
object.nFaces = 0;
delete[] object.pVertices;
object.pVertices = NULL;
object.nVertices = 0;
}
Teraz (poczynając od setConnectivity ) zaczynają się rzeczy ciekawe. Ta funkcja wyszukuje sąsiadów. Oto pseudokod:
for each face (A) in the object
for each edge in A
if we don't know this edges neighbour yet
for each face (B) in the object (except A)
for each edge in B
if A's edge is the same as B's edge, then they are neighbouring each other on that edge
set the neighbour property for each face A and B, then move onto next edge in A
Ostatnie dwie linie są wykonane przez poniższy kod. Znajdując dwa wierzchołki, które oznaczają koniec krawędzi i porównując je, możesz dowiedzieć się czy to ta sama krawędź. Część (edgeA+1)%3 pobiera następny wierzchołek względem aktualnie rozpatrywanego. Wtedy sprawdzasz czy wierzchołki pasują (kolejność może być różma, stąd drugi case w if'ie).
int vertA1 = pFaceA->vertexIndices[edgeA];
int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];
int vertB1 = pFaceB->vertexIndices[edgeB];
int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3];
if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
{
pFaceA->neighbourIndices[edgeA] = faceB;
pFaceB->neighbourIndices[edgeB] = faceA;
edgeFound = true;
break;
}
Na szczęście inna prosta funkcja, żeby złapać oddech. drawObject renderuje (wyświetla) po kolei wszystkie ściany (ang. faces).
void drawObject( const ShadowedObject& object )
{
glBegin( GL_TRIANGLES );
for ( int ic = 0; ic < object.nFaces; ic++ )
{
const Face& face = object.pFaces[ic];
for ( int j = 0; j < 3; j++ )
{
const Point3f& vertex = object.pVertices[face.vertexIndices[j]];
glNormal3f( face.normals[j].x, face.normals[j].y, face.normals[j].z );
glVertex3f( vertex.x, vertex.y, vertex.z );
}
}
glEnd();
}
Obliczanie równania płaszczyzny wygląda okropnie, ale jest to prosta, matematyczna formuła, która bierze się z książki kiedy jest potrzebna.
void calculatePlane( const ShadowedObject& object, Face& face )
{
const Point3f& v1 = object.pVertices[face.vertexIndices[0]];
const Point3f& v2 = object.pVertices[face.vertexIndices[1]];
const Point3f& v3 = object.pVertices[face.vertexIndices[2]];
face.planeEquation.a = v1.y*(v2.z-v3.z) + v2.y*(v3.z-v1.z) + v3.y*(v1.z-v2.z);
face.planeEquation.b = v1.z*(v2.x-v3.x) + v2.z*(v3.x-v1.x) + v3.z*(v1.x-v2.x);
face.planeEquation.c = v1.x*(v2.y-v3.y) + v2.x*(v3.y-v1.y) + v3.x*(v1.y-v2.y);
face.planeEquation.d = -( v1.x*( v2.y*v3.z - v3.y*v2.z ) +
v2.x*(v3.y*v1.z - v1.y*v3.z) +
v3.x*(v1.y*v2.z - v2.y*v1.z) );
}
I jak, złapałeś już oddech? Dobrze, bo teraz przechodzimy do senda sprawy - rzucania cieni! Funkcja castShadow ustawia odpowiednio maszynę stanów i przekazuje działanie (właściwy rendering) do doShadowPass która wyrenderuje cień w dwóch przebiegach.
Po pierwsze, ustalamy czy ściana jest zwrócona do światła. Robimy to sprawdzając po której stronie płaszczyzny jest światło (źródło światła), a konkretniej podstawiamy pozycję światła do równania płaszczyzny. Jeżeli wynik jest większy od 0, to jest po tej samej stronie co normalna do płaszczyzny i widziana przez światło. W przeciwnym razie nie jest widziana przez światło. (Po więcej wyjaśnień zajrzyj do książki od matematyki).
void castShadow( ShadowedObject& object, GLfloat *lightPosition )
{
for ( int ic = 0; ic < object.nFaces; ic++ )
{
const Plane& plane = object.pFaces[ic].planeEquation;
GLfloat side =
plane.a*lightPosition[0]+
plane.b*lightPosition[1]+
plane.c*lightPosition[2]+
plane.d;
if ( side > 0 )
object.pFaces[ic].visible = true;
else
object.pFaces[ic].visible = false;
}
Następna sekcja ustawia potrzebne stany w OpenGL, zeby wyrendereować cień.
Najpierw, udkładamy wszystkie atrybuty na stos. Dzięki temu przywrócimy je potem z łatwością.
Oświetlenie jest wyłączone, ponieważ nie będziemy rysować do wyjściowego bufora (bufora koloru), tylko do bufora powielania (ang. stencil buffer). Z pewnych względów, maska koloru wyłącza wszystkie składowe kolorów (więc rysowanie wielokątów nie będzie się odbywało do wyjściowego bufora).
Chociaż testowanie głębi (ang. depth testing) jest wciąż włączone, to nie chcemy, żeby cienie pojawiały się jako wypełnione obiekty w buforze głębi (ang. depth buffer). Maska głębi (ang. depth mask) załatwia sprawę.
Bufor powielania jest włączony, i to w nim odbędzie się rysowanie cienia.
glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT );
glDisable( GL_LIGHTING );
glDepthMask( GL_FALSE );
glDepthFunc( GL_LEQUAL );
glEnable( GL_STENCIL_TEST );
glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );
Ok, teraz cienie są faktycznie renderowane. Za moment do tego wrócimy, a teraz zobaczmy na funkcję doShadowPass. Rysowanie odbywa się w dwóch przebiegach. Pierwszy zwiększa bufor głębi o przednie ściany (rzucanie cienia), a drugi zmniejsza o tylne ściany (wyłączając cień między obiektem a dowolną inną powierzchnią).
glprzedniFace( GL_CCW );
glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
doShadowPass( object, lightPosition );
glprzedniFace( GL_CW );
glStencilOp( GL_KEEP, GL_KEEP, GL_DECR );
doShadowPass( object, lightPosition );
Żeby zrozumieć jak działa drugi przebieg najlepiej włącz jeszcze raz tutorial. Żeby zaoszczędzić Ci kłopotu, już to zrobiłem:
Ostatnia sekcja tej funkcji rysuje przenikający prostokąt na całym ekranie, żeby rzucać cień. Im ciemniejszy będzie ten prostokąt tym ciemniejszy cień zostanie rzucony. Więc, aby zmienić kolor cienia, zmień argumenty w glColor4f(). Wyższa składowa ALPHA (ostatni argument) przyciemni cień. Możesz też ustawić kolor cienia na czerwień, zieleń, czy fiolet...!
glprzedniFace( GL_CCW );
glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );
glColor4f( 0.0f, 0.0f, 0.0f, 0.4f );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glStencilFunc( GL_NOTEQUAL, 0, 0xFFFFFFFFL );
glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );
glPushMatrix();
glLoadIdentity();
glBegin( GL_TRIANGLE_STRIP );
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();
glPopAttrib();
}
Następna partia rysuje czworokąty (ang. quads) z cieniem. Jak działa? Przechodzimy przez każdą ze ścian i jeżeli jest widoczna to sprawdzasz każdą z jej krawędzi (ang. edge). Jeżeli jesteś na krawędzi, która nie ma sąsiadów, albo sąsiad jest niewidoczny, to rzucasz cień. Jeżeli zastanowisz się dokładnie, to zauważysz, że jest to prawdą. Rysując czworkobok (ang. quadrilateral) (jako dwa trójkąty) złożony z punktów krawędzi i krawędzi przedstawionych od tyłu przez scenę otrzymujesz rzut cienia.
Przez użyte tutaj podejście typu brute force po prostu rysujesz do nieskończoności, a cień wielokąta (ang. polygon) jest obcinany przez napotkane wielokąty. To powoduje dziurawienie, które zestressuje urzadzenia video :P Żeby więcej wycisnąć z tego algorytmu, powienieneś obciąć wielokąt do obiektu za nim. To szczwane i jest problemem samym w sobie, ale jeżeli to to czego Ci potrzeba, to zajrzyj pod ten link this Gamasutra article
Kod, który się tym wszystkim zajmuje nie jest tak szczwany jak wygląda. Tu jest kawałek, który przegląda obiekty. Na końcu mamy krawędź, j oraz sąsiadujące ściany okreslone przez neighbourIndex.
void doShadowPass( ShadowedObject& object, GLfloat *lightPosition )
{
for ( int ic = 0; ic < object.nFaces; ic++ )
{
const Face& face = object.pFaces[ic];
if ( face.visible )
{
for ( int j = 0; j < 3; j++ )
{
int neighbourIndex = face.neighbourIndices[j];
Teraz sprawdź czy sąsiadujące ściany są widoczne dla tego obiektu. Jeżeli nie, to znaczy, że krawędź rzuca cień.
if ( neighbourIndex == -1 || object.pFaces[neighbourIndex].visible == false )
{
Następny segment kodu zwróci dwa wierzchołki aktualnej krawędzi, v1 i v2. Wtedy oblicza v3 oraz v4, które są wyświetlane wzdłuż wektora pomiędzy źródłem światła a pierwszą krawędzią.
const Point3f& v1 = object.pVertices[face.vertexIndices[j]];
const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]];
Point3f v3, v4;
v3.x = ( v1.x-lightPosition[0] )*INFINITY;
v3.y = ( v1.y-lightPosition[1] )*INFINITY;
v3.z = ( v1.z-lightPosition[2] )*INFINITY;
v4.x = ( v2.x-lightPosition[0] )*INFINITY;
v4.y = ( v2.y-lightPosition[1] )*INFINITY;
v4.z = ( v2.z-lightPosition[2] )*INFINITY;
Myślę, że następną sekcję zrozumiesz bez problemów. Po prostu rysuje czworobok zdefiniowany przez cztery punkty.
glBegin( GL_TRIANGLE_STRIP );
glVertex3f( v1.x, v1.y, v1.z );
glVertex3f( v1.x+v3.x, v1.y+v3.y, v1.z+v3.z );
glVertex3f( v2.x, v2.y, v2.z );
glVertex3f( v2.x+v4.x, v2.y+v4.y, v2.z+v4.z );
glEnd();
}
}
}
}
}
I w ten sposób, sekcja rzucająca cień jest gotowa. Ale jeszcze nie skończyliśmy! Co z drawGLScene()? Po kolei: czyszczenie bufora, ustawienie źródła światła, narysowanie sfery.
bool drawGLScene()
{
GLmatrix16f Minv;
GLvector4f wlp, lp;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -20.0f);
glLightfv(GL_LIGHT1, GL_POSITION, LightPos);
glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);
gluSphere(q, 1.5f, 32, 16);
Następnie musimy policzyć pozycję źródła światła względną do lokalnego układy współrzędnych obiektu. Komentarze dokładnie wyjaśniają każdy krok. Minv przechowuje macierz przekształceń obiektu, jakkolwiek jest ona odwrócona i z przeciwnym znakiem, więc właściwie jest to odwrotność macierzy przekształceń. lp to kopia pozycji światła przemnożona przez macierz. Stąd lp jest pozycją źródła w układzie współrzędnych obiektu.
glLoadIdentity();
glRotatef(-yrot, 0.0f, 1.0f, 0.0f);
glRotatef(-xrot, 1.0f, 0.0f, 0.0f);
glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);
glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
lp[0] = LightPos[0];
lp[1] = LightPos[1];
lp[2] = LightPos[2];
lp[3] = LightPos[3];
VMatMult(Minv, lp);
Oto kod rysujący pokój i obiekt. Wywołanie castShadow() rysuje cień obiektu.
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -20.0f);
DrawGLRoom();
glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);
glRotatef(xrot, 1.0f, 0.0f, 0.0f);
glRotatef(yrot, 0.0f, 1.0f, 0.0f);
drawObject(obj);
castShadow(obj, lp);
Poniższe linie narysują pomarańczową sferę w miejscu gdzie jest światło.
glColor4f(0.7f, 0.4f, 0.0f, 1.0f);
glDisable(GL_LIGHTING);
glDepthMask(GL_FALSE);
glTranslatef(lp[0], lp[1], lp[2]);
gluSphere(q, 0.2f, 16, 8);
glEnable(GL_LIGHTING);
glDepthMask(GL_TRUE);
Ostatni kawałek funkcji uaktualnia pozycję obiektu.
xrot += xspeed;
yrot += yspeed;
glFlush();
return TRUE;
}
Oto funkcja DrawGLRoom(), z które skorzystaliśmy przed chwilą.
void DrawGLRoom()
{
glBegin(GL_QUADS);
glNormal3f(0.0f, 1.0f, 0.0f);
glVertex3f(-10.0f,-10.0f,-20.0f);
glVertex3f(-10.0f,-10.0f, 20.0f);
glVertex3f( 10.0f,-10.0f, 20.0f);
glVertex3f( 10.0f,-10.0f,-20.0f);
glNormal3f(0.0f,-1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, 20.0f);
glVertex3f(-10.0f, 10.0f,-20.0f);
glVertex3f( 10.0f, 10.0f,-20.0f);
glVertex3f( 10.0f, 10.0f, 20.0f);
glNormal3f(0.0f, 0.0f, 1.0f);
glVertex3f(-10.0f, 10.0f,-20.0f);
glVertex3f(-10.0f,-10.0f,-20.0f);
glVertex3f( 10.0f,-10.0f,-20.0f);
glVertex3f( 10.0f, 10.0f,-20.0f);
glNormal3f(0.0f, 0.0f,-1.0f);
glVertex3f( 10.0f, 10.0f, 20.0f);
glVertex3f( 10.0f,-10.0f, 20.0f);
glVertex3f(-10.0f,-10.0f, 20.0f);
glVertex3f(-10.0f, 10.0f, 20.0f);
glNormal3f(1.0f, 0.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, 20.0f);
glVertex3f(-10.0f,-10.0f, 20.0f);
glVertex3f(-10.0f,-10.0f,-20.0f);
glVertex3f(-10.0f, 10.0f,-20.0f);
glNormal3f(-1.0f, 0.0f, 0.0f);
glVertex3f( 10.0f, 10.0f,-20.0f);
glVertex3f( 10.0f,-10.0f,-20.0f);
glVertex3f( 10.0f,-10.0f, 20.0f);
glVertex3f( 10.0f, 10.0f, 20.0f);
glEnd();
}
A oto kolejna, pomocnicza, funkcja. Mnoży macierz przez wektor
void VMatMult(GLmatrix16f M, GLvector4f v)
{
GLfloat res[4];
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[1]=res[1];
v[2]=res[2];
v[3]=res[3];
}
Funkcja ładująca obiekt jest bardzo prosta, wywołuje readObject() a potem ustawia połącznie każdej ściany z równaniem płaszczyzny.
int InitGLObjects()
{
if (!readObject("Data/Object2.txt", obj))
{
return FALSE;
}
setConnectivity(obj);
for ( int i=0;i < obj.nFaces;i++)
calculatePlane(obj, obj.pFaces[i]);
return TRUE;
}
Ostatecznie bardzo wygodna funkcja KillGLObjects() - tutaj umieść zwolnienie wszystkich obiektów.
void KillGLObjects()
{
killObject( obj );
}
Pozostałe funkcje nie wymagają wyjaśnień. Pominąłem podstawowy kod, tj. definicja zmiennych i przetwarzanie klawiatury. Są dobrze skomentowane we wcześniejszych lekcjach.
Kilka rzeczy o tym turorialu:
- Sfera nie zatrzymuje cieni wyświetlanych na ścianie. W rzeczywistości ona też powinna rzucać cień, ale widzenie jej na ścianie nie ma znacznia, jest ukryta. Jest tam tylko po to, żeby zobaczyć co dzieje się na zakrzywionych powierzchniach.
- Jeżeli framerate jest u Ciebie niski, spróbuj włączyć tryb pełnoekranowy albo ustawić głębie kolorów na pulpicie na 32bpp.
- Arseny L. pisze: Jeżeli masz problem z TNT2 w trybie okienkowym, upewnij się że ustawiona głębia kolorów jest inna niż 16bpp. W tym trybie bufor powielania (ang. stencil buffer) jest emulowany, co obniża wydajność. Na 32bpp nie ma z tym problemów (mam TNT2 Ultra i sprawdziłem).
Muszę przyznać, że to było długie zadanie napisanie tego tutoriala. Należy docenić pracę jaką Jeff wkłada w ten kurs. Mam nadzieję, że ta lekcja Ci się podobała. Wielkie podziękowania dla Banu, który napisał oryginalny kod! Jeżeli coś wymaga wyjaśnienia, napisz do mnie (Brett) na brettporter@yahoo.com.
Randy Ridge dodaje: żeby zobaczyć cień na mojej karcie musiałem ustawić bliższą płaszczyznę obcinania (ang. near clipping plane) na 0.001f zamiast na 0.1f w ReSizeGLScene(). Kod w tej lekcji został zmieniony i powinien działać na wszystkich kartach.