Lekcja 27. Cienie
Autor: Marcin 'Aklimx' Milewski
Oryginał: Shadows (Banu Octavian (Choko) & Brett Porter )
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson27.zip

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]

        // definicja nieskończoności do obliczania wektora rozmiaru cienia
#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.

        // struktura opisujaca wektor
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.

        // struktura płaszczyzny o równaniu ax + by + cz + d = 0
struct Plane
{
GLfloat a, b, c, d;
};

Struktura Face zawiera wszystkie informacje niezbędne do rzucenia cienia.

        // struktura opisująca ścianę obiektu
struct Face
{
int vertexIndices[3];         // indeks każdego wektora, który tworzy trójkąt w danej ścianie (ang. face)
Point3f normals[3];         // normalna do każdego wektora

Plane planeEquation;         // równanie płaszczyzny, która zawiera trójkąt
int neighbourIndices[3];         // indeksy ścian, które sąsiadują z obiektem
bool visible;         // czy ściana jest widoczna dla źródła światła?
};

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;         // zostanie przydzielona dynamicznia
int nFaces;
Face *pFaces;         // zostanie przydzielona dynamicznia
};

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;
}
        // Wczytaj wierzchołki
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 );
}
        // wczytaj ściany
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;         // na razie nie ma sąsiadów
for ( j = 0; j < 3; j++ )
{
fscanf( pInputFile, "%d", &pFace->vertexIndices[j] );
pFace->vertexIndices[j]--;         // pliki określają je z początkiem tablicy w 1, a my mam tablice indeksowane od 0
}
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];
        // sprawdź czy są sąsiadami - krawędź jest taka sama
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).

        // rysuj obiekt - proste rysowanie kazdej trójkątnej sciany
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 )
{
        // zmienne, zeby skrocic zapis :)
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 )
{
        // Determine Which Faces Are Visible By The Light.
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 );         // wyłącz oświetlenie
glDepthMask( GL_FALSE );         // wyłącz zapis do bufora głębi
glDepthFunc( GL_LEQUAL );
glEnable( GL_STENCIL_TEST );         // włacz bufor powielania
glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );         // nie rysuj w buforze koloru
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ą).

        // pierwszy przebieg. Zwieksz wartość bufora głębi (dodaj cień)
glprzedniFace( GL_CCW );
glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
doShadowPass( object, lightPosition );
        // drugi przebieg. Zmniejsz wartość bufora głębi (obetnij cień)
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:

pierwszy przebieg drugi przebieg

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 );         // włącz renderowanie do bufora koloru dla wszystkich składowych
        // narysuj prostokąt pokrywający cały ekran
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 )
{
        // przejdź przez każdą krawędź
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ń.

        // jeżeli nie ma sąsiadów lub ich sąsiadujące ściany nie są widoczne, to 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ą.

        // pobierz punkty na karwędzi
const Point3f& v1 = object.pVertices[face.vertexIndices[j]];
const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]];
        // oblicz dwa wierzchołki w odległości
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.

        // narysuj czworobok (jako pasek trójkątów)
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;
        // wyczyść bufor koloru, bufor głębi, bufor powielania
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

glLoadIdentity();         // zresetuj macierz modelowania

glTranslatef(0.0f, 0.0f, -20.0f);         // oddal 20 jednostek
glLightfv(GL_LIGHT1, GL_POSITION, LightPos);         // ustaw pozycje 1. światła
glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);         // ustaw pozycję sfery
gluSphere(q, 1.5f, 32, 16);         // narysuj sferę

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();         // zresetuj macierz
glRotatef(-yrot, 0.0f, 1.0f, 0.0f);         // obróć o -yrot względem osi Y
glRotatef(-xrot, 1.0f, 0.0f, 0.0f);         // obróć o -xrot względem osi X
glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);         // przesuń w przeciwną stronę względem osi o wartość ObjPos[] (X,Y,Z)
glGetFloatv(GL_MODELVIEW_MATRIX,Minv);         // pobierz macierz modelowania do Minv
lp[0] = LightPos[0];         // składowa X pozycji światła
lp[1] = LightPos[1];         // składowa Y pozycji światła
lp[2] = LightPos[2];         // składowa Z pozycji światła
lp[3] = LightPos[3];         // kierunek światła
VMatMult(Minv, lp);         // w tablicy 'lp' mamy obrócony wektor światła

Oto kod rysujący pokój i obiekt. Wywołanie castShadow() rysuje cień obiektu.

glLoadIdentity();         // zresetuj macierz modelowania
glTranslatef(0.0f, 0.0f, -20.0f);         // oddal o 29 jednostek
DrawGLRoom();         // narysuj pokój
glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);         // przesuń na pozycję obiektu
glRotatef(xrot, 1.0f, 0.0f, 0.0f);         // obróć wokół osi X
glRotatef(yrot, 0.0f, 1.0f, 0.0f);         // obróć wokół osi Y
drawObject(obj);         // narysuj załadowany obiekt
castShadow(obj, lp);         // rzucanie cienia bazujące na Silhouette

Poniższe linie narysują pomarańczową sferę w miejscu gdzie jest światło.

glColor4f(0.7f, 0.4f, 0.0f, 1.0f);         // ustaw kolor na pomarańczowy
glDisable(GL_LIGHTING);         // wylącz oświetlenie
glDepthMask(GL_FALSE);         // wyłącz maskę głębi
glTranslatef(lp[0], lp[1], lp[2]);         // przesuń do źródła światła
        // zauważ, że wciąż jesteśmy w lokalnym ukł. współrzędnych
gluSphere(q, 0.2f, 16, 8);         // narysuj sferę (reprezentacja światła)
glEnable(GL_LIGHTING);         // włącz światło
glDepthMask(GL_TRUE);         // włącz maskę głębi

Ostatni kawałek funkcji uaktualnia pozycję obiektu.

xrot += xspeed;         // zwiększ xrot o xspeed
yrot += yspeed;         // zwiększ yrot o yspeed
glFlush();
return TRUE;         // wszystko poszło gładko ;p
}

Oto funkcja DrawGLRoom(), z które skorzystaliśmy przed chwilą.

void DrawGLRoom()         // narysuj pokój ("pudełko")
{
glBegin(GL_QUADS);         // zacznij rysowanie czworokątów
        // podłoga
glNormal3f(0.0f, 1.0f, 0.0f);         // normalna w górę
glVertex3f(-10.0f,-10.0f,-20.0f);         // tylni lewy
glVertex3f(-10.0f,-10.0f, 20.0f);         // przedni lewy
glVertex3f( 10.0f,-10.0f, 20.0f);         // przedni prawy
glVertex3f( 10.0f,-10.0f,-20.0f);         // tylni prawy
        // sufit
glNormal3f(0.0f,-1.0f, 0.0f);         // normalna w dół
glVertex3f(-10.0f, 10.0f, 20.0f);         // przedni lewy
glVertex3f(-10.0f, 10.0f,-20.0f);         // tylni lewy
glVertex3f( 10.0f, 10.0f,-20.0f);         // tylni prawy
glVertex3f( 10.0f, 10.0f, 20.0f);         // przedni prawy
        // przednia ściana
glNormal3f(0.0f, 0.0f, 1.0f);         // normalna od obserwatora
glVertex3f(-10.0f, 10.0f,-20.0f);         // górny lewy
glVertex3f(-10.0f,-10.0f,-20.0f);         // dolny lewy
glVertex3f( 10.0f,-10.0f,-20.0f);         // dolny prawy
glVertex3f( 10.0f, 10.0f,-20.0f);         // górny prawy
        // tylna ściana
glNormal3f(0.0f, 0.0f,-1.0f);         // normalna w kierunku obserwatora
glVertex3f( 10.0f, 10.0f, 20.0f);         // górny prawy
glVertex3f( 10.0f,-10.0f, 20.0f);         // dolny prawy
glVertex3f(-10.0f,-10.0f, 20.0f);         // dolny lewy
glVertex3f(-10.0f, 10.0f, 20.0f);         // górny lewy
        // lewa ściana
glNormal3f(1.0f, 0.0f, 0.0f);         // normalna w prawo
glVertex3f(-10.0f, 10.0f, 20.0f);         // górny przedni
glVertex3f(-10.0f,-10.0f, 20.0f);         // dolny przedni
glVertex3f(-10.0f,-10.0f,-20.0f);         // dolny tylni
glVertex3f(-10.0f, 10.0f,-20.0f);         // górny tylni
        // prawa ściana
glNormal3f(-1.0f, 0.0f, 0.0f);         // normalna w lewo
glVertex3f( 10.0f, 10.0f,-20.0f);         // górny tylni
glVertex3f( 10.0f,-10.0f,-20.0f);         // dolny tylni
glVertex3f( 10.0f,-10.0f, 20.0f);         // dolny przedni
glVertex3f( 10.0f, 10.0f, 20.0f);         // górny przedni
glEnd();         // zakończ rysowanie czworokątów
}

A oto kolejna, pomocnicza, funkcja. Mnoży macierz przez wektor

void VMatMult(GLmatrix16f M, GLvector4f v)
{
GLfloat res[4];         // tu będą wyniki mnożenia
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];         // wyniki zapisz w v[]
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()         // inicjalizuje obiekty
{
if (!readObject("Data/Object2.txt", obj))         // wczytaj obiekt z piku do [i]obj[/i]
{
return FALSE;         // jeżeli wystąpi błąd, to zwróć FALSE
}
setConnectivity(obj);         // ustaw połącznie Face to Face
        // policz równanie płaszczyzny dla wszystkich ścian
for ( int i=0;i < obj.nFaces;i++)
calculatePlane(obj, obj.pFaces[i]);
return TRUE;         // zwróć 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:

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.